diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml new file mode 100644 index 000000000..07cd9183c --- /dev/null +++ b/.github/workflows/gradle.yml @@ -0,0 +1,31 @@ +# This workflow will build a Java project with Gradle +# For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-gradle + +name: Java CI with Gradle + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + +jobs: + build: + + runs-on: ubuntu-latest + strategy: + matrix: + java: [ '17' ] + max-parallel: 1 + + steps: + - uses: actions/checkout@v3 + - name: Set up Java + uses: actions/setup-java@v3 + with: + java-version: ${{ matrix.java }} + distribution: 'temurin' + - name: Grant execute permission for gradlew + run: chmod +x gradlew + - name: Build with Gradle + run: ./gradlew -x :structurizr-autolayout:test diff --git a/.gitignore b/.gitignore index 5547fea44..4a491ce38 100644 --- a/.gitignore +++ b/.gitignore @@ -23,9 +23,12 @@ build out bin **/bin/ +**/target/ # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml hs_err_pid* -# Structurizr workspace archives -*.json +structurizr-dsl/src/test/resources/dsl/spring-petclinic/.structurizr +structurizr-dsl/src/test/resources/dsl/spring-petclinic/workspace.json + +**/structurizr.properties \ No newline at end of file diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 2b4c188b2..000000000 --- a/.travis.yml +++ /dev/null @@ -1,4 +0,0 @@ -language: java -jdk: - - oraclejdk8 -install: true \ No newline at end of file diff --git a/README.md b/README.md index 790d9e711..d6ec8cb2d 100644 --- a/README.md +++ b/README.md @@ -1,82 +1,3 @@ -![Structurizr](docs/images/structurizr-banner.png) - # Structurizr for Java -This GitHub repository is a collection of tooling to help you visualise, document and explore the software architecture of a software system. In summary, it allows you to create a software architecture model based upon Simon Brown's [C4 model](https://c4model.com) using Java code, and then export that model to be visualised using tools such as: - -1. [Structurizr](https://structurizr.com): a web-based software as a service and on-premises product to render software architecture diagrams and supplementary Markdown/AsciiDoc documentation. -1. [PlantUML](docs/plantuml.md): a tool to create UML diagrams using a simple textual domain specific language. -1. [Graphviz](docs/graphviz-and-dot.md): a tool to render directed graphs using the DOT format. - -As an example, the following Java code can be used to create a software architecture model that describes a user using a software system. - -```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(); -} -``` - -If using [Structurizr](https://structurizr.com), the end-result, after adding some styling and positioning the diagram elements, is a system context diagram like this: - -![Getting Started with Structurizr for Java](docs/images/getting-started.png) - -You can see the live workspace at [https://structurizr.com/share/25441](https://structurizr.com/share/25441). - -[![Build Status](https://travis-ci.org/structurizr/java.svg?branch=master)](https://travis-ci.org/structurizr/java) - -## Table of contents - -* Introduction - * [Getting started](docs/getting-started.md) - * [About Structurizr and how it compares to other tooling](https://structurizr.com/help/about) - * [Basic concepts](https://structurizr.com/help/concepts) (workspaces, models, views and documentation) - * [C4 model](https://c4model.com) - * [Binaries](docs/binaries.md) - * [API Client](docs/api-client.md) - * [Usage patterns](docs/usage-patterns.md) -* Diagrams - * [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) - * [Enterprise Context diagram](docs/enterprise-context-diagram.md) - * [Styling elements](docs/styling-elements.md) - * [Styling relationships](docs/styling-relationships.md) - * [Filtered views](docs/filtered-views.md) -* 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) -* Extracting software architecture information from code - * [Component finder](docs/component-finder.md) - * [Structurizr annotations](docs/structurizr-annotations.md) - * [Type matchers](docs/type-matchers.md) - * [Spring component finder strategies](docs/spring-component-finder-strategies.md) - * [Supplementing the model from source code](docs/supplementing-from-source-code.md) - * [Components and supporting types](docs/supporting-types.md) - * [The Spring PetClinic example](docs/spring-petclinic.md) -* Exporting and visualising with other tools - * [PlantUML](docs/plantuml.md) - * [Graphviz and DOT](docs/graphviz-and-dot.md) -* Other - * [Client-side encryption](docs/client-side-encryption.md) - * [Corporate branding](docs/corporate-branding.md) - * [Building from source](docs/building.md) -* Related projects - * [cat-boot-structurizr](https://github.com/Catalysts/cat-boot/tree/master/cat-boot-structurizr): A way to apply dependency management to help modularise Structurizr code. - * [java-quickstart](https://github.com/structurizr/java-quickstart): A simple starting point for using Structurizr for Java - * [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 \ 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 diff --git a/build.gradle b/build.gradle index bae84007f..18adb8a7b 100644 --- a/build.gradle +++ b/build.gradle @@ -1,107 +1,96 @@ -task wrapper(type: Wrapper) { - gradleVersion = '4.0' -} +defaultTasks 'clean', 'compileJava', 'test' subprojects { proj -> - apply plugin: 'idea' - apply plugin: 'java' + apply plugin: 'java-library' apply plugin: 'maven-publish' - apply plugin: 'maven' apply plugin: 'signing' description = 'Structurizr' group = 'com.structurizr' - version = '1.0.0-RC4' repositories { mavenCentral() - mavenLocal() } - sourceSets { - main { - java { - srcDir 'src' - } + 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 { + excludeTags "IntegrationTest" } - test { - java { - srcDir 'test/unit' - } + } + + tasks.register("integrationTest", Test) { + useJUnitPlatform { + includeTags "IntegrationTest" } + mustRunAfter check } compileJava.options.encoding = 'UTF-8' compileTestJava.options.encoding = 'UTF-8' - sourceCompatibility = 1.8 - targetCompatibility = 1.8 + sourceCompatibility = 17 + targetCompatibility = 17 + + java { + withJavadocJar() + withSourcesJar() + } jar { manifest { attributes( - "Implementation-Title": "Structurizr for Java", - "Implementation-Version": version + "Implementation-Title": "Structurizr for Java", + "Implementation-Version": version ) } } - task sourcesJar(type: Jar) { - classifier = 'sources' - from sourceSets.main.allJava - } - - task javadocJar(type: Jar) { - classifier = 'javadoc' - from javadoc - } - - artifacts { - archives javadocJar, sourcesJar - } - - signing { - sign configurations.archives - } - - uploadArchives { + publishing { repositories { - mavenDeployer { - beforeDeployment { MavenDeployment deployment -> signing.signPom(deployment) } - - repository(url: "https://oss.sonatype.org/service/local/staging/deploy/maven2/") { - authentication(userName: ossrhUsername, password: ossrhPassword) + maven { + name = "ossrh" + url = "https://ossrh-staging-api.central.sonatype.com/service/local/staging/deploy/maven2/" + credentials { + username = findProperty('ossrhUsername') + password = findProperty('ossrhPassword') } + } + } - snapshotRepository(url: "https://oss.sonatype.org/content/repositories/snapshots/") { - authentication(userName: ossrhUsername, password: ossrhPassword) - } + publications { + mavenJava(MavenPublication) { + from components.java - pom.project { - name 'Structurizr for Java' - packaging 'jar' - description 'Structurizr for Java' - url 'https://github.com/structurizr/java' + pom { + name = 'Structurizr for Java' + description = 'Structurizr for Java' + 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' - url 'https://github.com/structurizr/java' + connection = 'scm:git:git://github.com/structurizr/java.git' + developerConnection = 'scm:git:git@github.com:structurizr/java.git' + url = 'https://github.com/structurizr/java' } licenses { license { - name 'The Apache License, Version 2.0' - url 'http://www.apache.org/licenses/LICENSE-2.0.txt' + name = 'The Apache License, Version 2.0' + url = 'http://www.apache.org/licenses/LICENSE-2.0.txt' } } developers { developer { - id "simon" - name "Simon Brown" - email "simon@structurizr.com" + id = "simon" + name = "Simon Brown" + email = "simon@structurizr.com" } } } @@ -109,44 +98,8 @@ subprojects { proj -> } } - def pomConfig = { - licenses { - license { - name "The Apache Software License, Version 2.0" - url "http://www.apache.org/licenses/LICENSE-2.0.txt" - distribution "repo" - } - } - developers { - developer { - id "simon" - name "Simon Brown" - email "simon@structurizr.com" - } - } - } - - publishing { - publications { - mavenJava(MavenPublication) { - from components.java - artifact sourcesJar - artifact javadocJar - pom.withXml { - def root = asNode() - root.appendNode('description', 'Visualise, document and explore your software architecture with the C4 model.') - root.appendNode('name', 'Structurizr for Java') - root.appendNode('url', 'https://github.com/structurizr/java') - root.children().last() + pomConfig - } - } - } - - repositories { - maven { - url "$buildDir/repo" - } - } + signing { + sign publishing.publications.mavenJava } } \ No newline at end of file diff --git a/changelog.md b/changelog.md new file mode 100644 index 000000000..396b73915 --- /dev/null +++ b/changelog.md @@ -0,0 +1,166 @@ +# 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`. +- structurizr-client: Adds branches and users information to the admin API response. + +## v5.0.1 (1st November 2025) + +- 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`. +- 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: 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 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 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 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 (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. +- 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). +- 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) + +- 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). +- 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. +- 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) + +- 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. +- 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) + +- 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. +- 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) + +- 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 `. +- 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. +- 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) + +- 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. +- 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` 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 (`!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). +- 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) + +- 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 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) + +- 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'). + +## 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. +- 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) + +- 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. +- 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). +- 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) + +- structurizr-core: Removes deprecated concepts - enterprise and software system/person location. +- structurizr-core: Adds `Workspace.trim()` to trim a workspace of unused elements (i.e. those not associated with any views). +- structurizr-core: Adds support for SVG image views (https://github.com/structurizr/java/issues/249). +- structurizr-core: View keys will be automatically generated if not specified. +- structurizr-client: Removes `StructurizrClient` (use `WorkspaceApiClient` instead). +- structurizr-client: Merges https://github.com/structurizr/java/pull/238 (fix: re-enable system properties for theme http client). +- structurizr-dsl: Removes `enterprise` keyword. +- structurizr-dsl: Adds `!decisions` as a synonym for `!adrs`. +- structurizr-dsl: Allows `!identifiers` to be used inside `model`. +- structurizr-dsl: Fixes https://github.com/structurizr/java/issues/233 (Implied relationships not configured correctly when DSL workspace extends a JSON workspace). +- structurizr-import: Adds support for importing decisions managed by Log4brains. +- structurizr-import: Adds support for importing decisions in MADR format. +- structurizr-import: Fixes https://github.com/structurizr/java/issues/251 (Importing docs fails on files without extension). +- structurizr-inspection: Initial version. \ No newline at end of file diff --git a/docs/api-client.md b/docs/api-client.md deleted file mode 100644 index adf556ea7..000000000 --- a/docs/api-client.md +++ /dev/null @@ -1,59 +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-ui), 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); -``` \ No newline at end of file diff --git a/docs/binaries.md b/docs/binaries.md deleted file mode 100644 index 4fa64d3e0..000000000 --- a/docs/binaries.md +++ /dev/null @@ -1,9 +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:1.0.0-RC4 | structurizr-core is the basic library that can used to create software architecture models. -com.structurizr:structurizr-spring:1.0.0-RC4 | structurizr-spring is a library that will help you create a software architecture model of your Spring-based application. It uses reflection to find components in your code that correspond to Java types annotated ```@Controller```, ```@RestController```, ```@Component```, ```@Service``` and ```@Repository```, plus those that extend ```JpaRepository```. -com.structurizr:structurizr-annotations:1.0.0-RC4 | structurizr-annotations is a very small library that allows you to add software architecture hints into your own code. -com.structurizr:structurizr-dot:1.0.0-RC4 | structurizr-dot is a library to export the view definitions to a DOT file, so they can be rendered with graphviz. \ No newline at end of file diff --git a/docs/building.md b/docs/building.md deleted file mode 100644 index f6f8b2402..000000000 --- a/docs/building.md +++ /dev/null @@ -1,17 +0,0 @@ -# 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 8)... - -``` -git clone https://github.com/structurizr/java.git -cd 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 diff --git a/docs/client-side-encryption.md b/docs/client-side-encryption.md deleted file mode 100644 index 644c48052..000000000 --- a/docs/client-side-encryption.md +++ /dev/null @@ -1,23 +0,0 @@ -# Client-side encryption - -> Note: this page describes a feature that is not available to use with Structurizr's Free Plan. - -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. - -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/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 diff --git a/docs/component-diagram.md b/docs/component-diagram.md deleted file mode 100644 index 3afab5660..000000000 --- a/docs/component-diagram.md +++ /dev/null @@ -1,39 +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 - -As an example, a Component diagram for the Web Application of a simplified, fictional Internet Banking System might look something like this. In summary, it shows the components that make up the Web Application, and the relationships between them. - -![An example Component diagram](images/component-diagram-1.png) - -With Structurizr for Java, you can create this diagram with code like the following: - -```java -Component homePageController = webApplication.addComponent("Home Page Controller", "Serves up the home page.", "Spring MVC Controller"); -Component signinController = webApplication.addComponent("Sign In Controller", "Allows users to sign in to the Internet Banking System.", "Spring MVC Controller"); -Component accountsSummaryController = webApplication.addComponent("Accounts Summary Controller", "Provides customers with an summary of their bank accounts.", "Spring MVC Controller"); -Component securityComponent = webApplication.addComponent("Security Component", "Provides functionality related to signing in, changing passwords, etc.", "Spring Bean"); -Component mainframeBankingSystemFacade = webApplication.addComponent("Mainframe Banking System Facade", "A facade onto the mainframe banking system.", "Spring Bean"); - -webApplication.getComponents().stream().filter(c -> "Spring MVC Controller".equals(c.getTechnology())).forEach(c -> customer.uses(c, "Uses", "HTTPS")); -signinController.uses(securityComponent, "Uses"); -accountsSummaryController.uses(mainframeBankingSystemFacade, "Uses"); -securityComponent.uses(database, "Reads from and writes to", "JDBC"); -mainframeBankingSystemFacade.uses(mainframeBankingSystem, "Uses", "XML/HTTPS"); - -ComponentView componentView = views.createComponentView(webApplication, "Components", "The components diagram for the Web Application"); -componentView.addAllContainers(); -componentView.addAllComponents(); -componentView.add(customer); -componentView.add(mainframeBankingSystem); -``` - -See [BigBankPlc.java](https://github.com/structurizr/java/blob/master/structurizr-examples/src/com/structurizr/example/BigBankPlc.java) for the full code, and [https://structurizr.com/share/36141#Components](https://structurizr.com/share/36141#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](component-finder.md), using static analysis and reflection techniques. \ No newline at end of file diff --git a/docs/component-finder.md b/docs/component-finder.md deleted file mode 100644 index f98743a50..000000000 --- a/docs/component-finder.md +++ /dev/null @@ -1,48 +0,0 @@ -# Component finder - -The Structurizr for Java library includes a component finder and a number of prebuilt pluggable strategies that allow you to extract components from a codebase. - -## Background - -The idea behind the [C4 model](https://c4model.com) and Structurizr is that there are a number of levels of abstraction sitting above the code. Although we write Java code using interfaces and classes in packages, it's often useful to think about how that code is organised into "components". In its simplest form, a "component" is just a grouping of classes and interfaces. - -If you reverse-engineer some Java code using a UML tool, you'll typically get a UML class diagram showing all of the classes and interfaces. The component diagram in the C4 model is about hiding some of this complexity and implementation detail. You can read more about this at [Components vs classes](https://structurizr.com/help/components-vs-classes). - -## Purpose - -The purpose of the [component finder](https://github.com/structurizr/java/blob/master/structurizr-core/src/com/structurizr/analysis/ComponentFinder.java) is to find components in your codebase. Since every codebase is different (i.e. code structure, naming conventions, frameworks used, etc), different pluggable component finder strategies allow you to customize the rules that you use to find components. -Some example rules that you might use to find components include: - -- All classes where the name ends with ```Component```. -- All classes where the name ends with ```Controller```. -- All classes that are annotated with the Spring ```@Repository``` annotation. -- All classes that inherit from ```AbstractComponent```. -- etc - -## Basic usage - -To use a component finder, simply create an instance of the ```ComponentFinder``` class and configure it as needed. - -```java -Container webApplication = mySoftwareSystem.addContainer("Web Application", "Description", "Apache Tomcat 7.x"); - -ComponentFinder componentFinder = new ComponentFinder( - webApplication, "com.mycompany.mysoftwaresystem", - ... a number of component finder strategies ...); -componentFinder.findComponents(); -``` - -In this case, we're going to find components and associate them with the ```webApplication``` container, and we're only going to find components that reside somewhere underneath the ```com.mycompany.mysoftwaresystem``` package to avoid accidentally finding components residing in frameworks that we might be using. - -We also need to plug in one or more component finder strategies, which actually implement the logic to find and extract components from a codebase. - -## Component finder strategies - -The are a number of component finder strategies already implemented in this GitHub repository and, since the code is open source, you can build your own too. Some of the component finder strategies work using static analysis and reflection techniques against the compiled version of the code (you will need this on your classpath), others by parsing the source code. - -Name | Dependency | Description | Extracted from ----- | ---------- | ----------- | -------------- -[TypeMatcherComponentFinderStrategy](type-matchers.md) | structurizr-core | A component finder strategy that uses type information to find components, based upon a number of pluggable TypeMatcher implementations (e.g. [NameSuffixTypeMatcher](https://github.com/structurizr/java/blob/master/structurizr-core/src/com/structurizr/analysis/NameSuffixTypeMatcher.java), [ImplementsInterfaceTypeMatcher](https://github.com/structurizr/java/blob/master/structurizr-core/src/com/structurizr/analysis/ImplementsInterfaceTypeMatcher.java), [RegexTypeMatcher](https://github.com/structurizr/java/blob/master/structurizr-core/src/com/structurizr/analysis/RegexTypeMatcher.java) and [AnnotationTypeMatcher](https://github.com/structurizr/java/blob/master/structurizr-core/src/com/structurizr/analysis/AnnotationTypeMatcher.java)). | Compiled bytecode -[SpringComponentFinderStrategy](spring-component-finder-strategies.md) | structurizr-spring | Finds types annotated ```@Controller```, ```@RestController```, ```@Component```, ```@Service``` and ```@Repository```, plus classes that extend ```JpaRepository```. | Compiled bytecode -[StructurizrAnnotationsComponentFinderStrategy](structurizr-annotations.md) | structurizr-core | Finds the Structurizr annotations ```@Component```, ```@UsedByPerson```, ```@UsedBySoftwareSystem```, ```@UsedByContainer```, ```@UsesSoftwareSystem```, ```@UsesContainer``` and ```@UsesComponent```. | Compiled bytecode -[SourceCodeComponentFinderStrategy](supplementing-from-source-code.md) | structurizr-core | This component finder strategy doesn't really find components, it instead extracts the top-level Javadoc comment from the code so that this can be added to existing component definitions. It also calculates the size of components, based upon the number of lines of source code. | Source code diff --git a/docs/container-diagram.md b/docs/container-diagram.md deleted file mode 100644 index a1536b79e..000000000 --- a/docs/container-diagram.md +++ /dev/null @@ -1,29 +0,0 @@ -# Container diagram - -Once you understand how your system fits in to the overall IT environment, a really useful next step is to illustrate the high-level technology choices 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 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 - -As an example, a Container diagram for a simplified, fictional Internet Banking System might look something like this. In summary, it shows that the Internet Banking System is made up a Web Application and a Database. It also shows the relationship between the Web Application and the Mainframe Banking System. - -![An example Container diagram](images/container-diagram-1.png) - -With Structurizr for Java, you can create this diagram with code like the following: - -```java -Container webApplication = internetBankingSystem.addContainer("Web Application", "Provides all of the Internet banking functionality to customers.", "Java and Spring MVC"); -Container database = internetBankingSystem.addContainer("Database", "Stores interesting data.", "Relational Database Schema"); - -customer.uses(webApplication, "HTTPS"); -webApplication.uses(database, "Reads from and writes to", "JDBC"); -webApplication.uses(mainframeBankingSystem, "Uses", "XML/HTTPS"); - -ContainerView containerView = views.createContainerView(internetBankingSystem, "Containers", "The container diagram for the Internet Banking System."); -containerView.add(customer); -containerView.addAllContainers(); -containerView.add(mainframeBankingSystem); -``` - -See [BigBankPlc.java](https://github.com/structurizr/java/blob/master/structurizr-examples/src/com/structurizr/example/BigBankPlc.java) for the full 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 36194ccd6..000000000 --- a/docs/corporate-branding.md +++ /dev/null @@ -1,43 +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). -- 5 foreground/background colour pairs. - -Here's an example diagram that hasn't been styled, aside from setting the shape of the ```Person``` elements. - -![Unbranded diagram](images/corporate-branding-1.png) - -And here's what the documentation looks like. - -![Unbranded documentation](images/corporate-branding-2.png) - -You can add branding to an existing workspace, as follows: - -```java -Branding branding = views.getConfiguration().getBranding(); -branding.setColor1(new ColorPair("#02172c", "#ffffff")); -branding.setColor2(new ColorPair("#08427b", "#ffffff")); -branding.setColor3(new ColorPair("#1168bd", "#ffffff")); -branding.setColor4(new ColorPair("#438dd5", "#ffffff")); -branding.setColor5(new ColorPair("#85bbf0", "#ffffff")); -branding.setLogo(ImageUtils.getImageAsDataUri(new File("./docs/images/structurizr-logo.png"))); -``` - -## Diagrams - -If no colours have been specified via diagram element styles, colour pairs 1-4 will be used when rendering people, software systems, containers and components respectively. Here's the same diagram, now with added branding. Notice that the logo is now also shown in the bottom left corner. - -![Branded diagram](images/corporate-branding-3.png) - -## Documentation - -With documentation, the colour pairs are mapped onto the navigation links at the top of the page, while the first colour pair is used when rendering hyperlinks. Again, the logo is shown. - -![Branded documentation](images/corporate-branding-4.png) - -See [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/deployment-diagram.md b/docs/deployment-diagram.md deleted file mode 100644 index 013c7192c..000000000 --- a/docs/deployment-diagram.md +++ /dev/null @@ -1,39 +0,0 @@ -# Deployment diagram - -A Deployment diagram allows you to illustrate how containers in the static model are mapped to infrastructure at deployment time. It's based upon the [UML deployment diagram](https://en.wikipedia.org/wiki/Deployment_diagram). - -> Note: this page describes a feature that is not available to use with Structurizr's Free Plan. You can, however, render Deployment diagrams using the [PlantUMLWriter](plantuml.md). - -## 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](images/deployment-diagram-1.png) - -With Structurizr for Java, you can create this diagram with code like the following: - -```java -DeploymentNode liveWebServer = model.addDeploymentNode("bigbank-web***", "A web server residing in the web server farm, accessed via F5 BIG-IP LTMs.", "Ubuntu 16.04 LTS", 8, MapUtils.create("Location=London")); -liveWebServer.addDeploymentNode("Apache Tomcat", "An open source Java EE web server.", "Apache Tomcat 8.x", 1, MapUtils.create("Xmx=512M", "Xms=1024M", "Java Version=8")) - .add(webApplication); - -DeploymentNode primaryDatabaseServer = model.addDeploymentNode("bigbank-db01", "The primary database server.", "Ubuntu 16.04 LTS", 1, MapUtils.create("Location=London")) - .addDeploymentNode("Oracle - Primary", "The primary, live database server.", "Oracle 12c"); -primaryDatabaseServer.add(database); - -DeploymentNode secondaryDatabaseServer = model.addDeploymentNode("bigbank-db02", "The secondary database server.", "Ubuntu 16.04 LTS", 1, MapUtils.create("Location=Reading")) - .addDeploymentNode("Oracle - Secondary", "A secondary, standby database server, used for failover purposes only.", "Oracle 12c"); -ContainerInstance secondaryDatabase = secondaryDatabaseServer.add(database); - -model.getRelationships().stream().filter(r -> r.getDestination().equals(secondaryDatabase)).forEach(r -> r.addTags("Failover")); -Relationship dataReplicationRelationship = primaryDatabaseServer.uses(secondaryDatabaseServer, "Replicates data to", ""); -secondaryDatabase.addTags("Failover"); - -DeploymentView liveDeploymentView = views.createDeploymentView(internetBankingSystem, "LiveDeployment", "An example live deployment scenario for the Internet Banking System."); -liveDeploymentView.add(liveWebServer); -liveDeploymentView.add(primaryDatabaseServer); -liveDeploymentView.add(secondaryDatabaseServer); -liveDeploymentView.add(dataReplicationRelationship); -``` - -See [BigBankPlc.java](https://github.com/structurizr/java/blob/master/structurizr-examples/src/com/structurizr/example/BigBankPlc.java) for the full 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 4a73222fb..000000000 --- a/docs/documentation-arc42.md +++ /dev/null @@ -1,36 +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. This particular example is rendered as follows: - -![Documentation based upon the arc42 template](images/documentation-arc42-1.png) - -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 6478a07b2..000000000 --- a/docs/documentation-automatic.md +++ /dev/null @@ -1,26 +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. This particular example is rendered as follows: - -![Documentation based upon the Structurizr template](images/documentation-automatic-1.png) - -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 49f8f70fd..000000000 --- a/docs/documentation-structurizr.md +++ /dev/null @@ -1,36 +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. This particular example is rendered as follows: - -![Documentation based upon the Structurizr template](images/documentation-structurizr-1.png) - -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 978611b24..000000000 --- a/docs/documentation-viewpoints-and-perspectives.md +++ /dev/null @@ -1,31 +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. This particular example is rendered as follows: - -![Documentation based upon the Viewpoints and Perspectives template](images/documentation-viewpoints-and-perspectives-1.png) - -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 3f4bc4db3..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, by specifying the section name (a String) and group (an integer, 1-5; this is used for colour coding section navigation buttons): - -```java -template.addSection(softwareSystem, "My custom section", 3, 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 deleted file mode 100644 index 2fbdc39dd..000000000 --- a/docs/dynamic-diagram.md +++ /dev/null @@ -1,33 +0,0 @@ -# Dynamic diagram - -In addition to the diagrams showing static structure, you can also create a simple Dynamic diagram. This 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. - -The Dynamic diagram in Structurizr is based upon a [UML communication diagram](https://en.wikipedia.org/wiki/Communication_diagram) (previously known as a "UML collaboration diagram"). This 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. - -> Note: this page describes a feature that is not available to use with Structurizr's Free Plan. You can, however, render Dynamic diagrams using the [PlantUMLWriter](plantuml.md). - -## 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](images/dynamic-diagram-1.png) - -With Structurizr for Java, you can create this diagram with code like the following: - -```java -DynamicView dynamicView = views.createDynamicView(webApplication, "SignIn", "Summarises how the sign in feature works."); -dynamicView.add(customer, "Requests /signin from", signinController); -dynamicView.add(customer, "Submits credentials to", signinController); -dynamicView.add(signinController, "Calls isAuthenticated() on", securityComponent); -dynamicView.add(securityComponent, "select * from users u where username = ?", database); -``` - -See [BigBankPlc.java](https://github.com/structurizr/java/blob/master/structurizr-examples/src/com/structurizr/example/BigBankPlc.java) for the full code, and [https://structurizr.com/share/36141#SignIn](https://structurizr.com/share/36141#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/enterprise-context-diagram.md b/docs/enterprise-context-diagram.md deleted file mode 100644 index 38906fe79..000000000 --- a/docs/enterprise-context-diagram.md +++ /dev/null @@ -1,33 +0,0 @@ -# Enterprise Context diagram - -An Enterprise Context diagram is really the same as the [System Context diagram](system-context-diagram.md), without a focus on a specific software system. It can help to provide a broader view of the people and software systems that are related to and reside within a given enterprise context (e.g. a business or organisation). - -## Example - -As an example, an Enterprise Context diagram for a simplified, fictional Internet Banking System might look something like this. In summary, it shows more than just the immediate relationships of the Internet Banking System. - -![An example Enterprise Context diagram](images/enterprise-context-diagram-1.png) - -With Structurizr for Java, you can create this diagram with code like the following: - -```java -Person customer = model.addPerson(Location.External, "Customer", "A customer of the bank."); - -SoftwareSystem internetBankingSystem = model.addSoftwareSystem(Location.Internal, "Internet Banking System", "Allows customers to view information about their bank accounts and make payments."); -customer.uses(internetBankingSystem, "Uses"); - -SoftwareSystem mainframeBankingSystem = model.addSoftwareSystem(Location.Internal, "Mainframe Banking System", "Stores all of the core banking information about customers, accounts, transactions, etc."); -internetBankingSystem.uses(mainframeBankingSystem, "Uses"); - -SoftwareSystem atm = model.addSoftwareSystem(Location.Internal, "ATM", "Allows customers to withdraw cash."); -atm.uses(mainframeBankingSystem, "Uses"); -customer.uses(atm, "Withdraws cash using"); - -Person bankStaff = model.addPerson(Location.Internal, "Bank Staff", "Staff within the bank."); -bankStaff.uses(mainframeBankingSystem, "Uses"); - -EnterpriseContextView enterpriseContextView = views.createEnterpriseContextView("EnterpriseContext", "The system context diagram for the Internet Banking System."); -enterpriseContextView.addAllElements(); -``` - -See [BigBankPlc.java](https://github.com/structurizr/java/blob/master/structurizr-examples/src/com/structurizr/example/BigBankPlc.java) for the full code, and [https://structurizr.com/share/36141#EnterpriseContext](https://structurizr.com/share/36141#EnterpriseContext) for the diagram. \ No newline at end of file diff --git a/docs/filtered-views.md b/docs/filtered-views.md deleted file mode 100644 index 2d0cd5d33..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. Enterprise Context, 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(); -EnterpriseContextView enterpriseContextView = views.createEnterpriseContextView("EnterpriseContext", "An example Enterprise Context diagram."); -enterpriseContextView.addAllElements(); - -views.createFilteredView(enterpriseContextView, "CurrentState", "The current context.", FilterMode.Exclude, FUTURE_STATE); -views.createFilteredView(enterpriseContextView, "FutureState", "The future state context 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/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 diff --git a/docs/getting-started.md b/docs/getting-started.md deleted file mode 100644 index d7652353e..000000000 --- a/docs/getting-started.md +++ /dev/null @@ -1,70 +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/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). - -For more examples, please see [structurizr-examples](https://github.com/structurizr/java/tree/master/structurizr-examples/src/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. - -Name | Description -------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- -com.structurizr:structurizr-core:1.0.0-RC4 | The core library that can used to create models and upload models to Structurizr. - -## 2. 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"); -``` - -## 3. 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(); -``` - -## 4. Add some colour - -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); -``` - -## 5. Upload to Structurizr - -Structurizr provides a web API to get and put workspaces. - -```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 [Workspaces](https://structurizr.com/help/workspaces) for information about finding your workspace ID, API key and secret. - -The result is a diagram like this (once you've dragged the boxes around). - -![Getting Started with Structurizr for Java](images/getting-started.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) \ No newline at end of file diff --git a/docs/graphviz-and-dot.md b/docs/graphviz-and-dot.md deleted file mode 100644 index ab28ed867..000000000 --- a/docs/graphviz-and-dot.md +++ /dev/null @@ -1,58 +0,0 @@ -# Graphviz and DOT - -Structurizr for Java also includes the ```structurizr-dot``` library, which in turn uses Cyrille Martraire's [dot-diagram library](https://github.com/LivingDocumentation/dot-diagram) to create DOT (graph description language) files that can be imported into the [Graphviz tool](http://www.graphviz.org). - -Simply create your software architecture model and views as usual, and use the [DotWriter](https://github.com/structurizr/java/blob/master/structurizr-dot/src/com/structurizr/io/dot/DotWriter.java) class to export the views. [For example](https://github.com/structurizr/java/blob/master/structurizr-dot/src/com/structurizr/io/dot/DotWriterExample.java): - -```java -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(); - -StringWriter stringWriter = new StringWriter(); -DotWriter dotWriter = new DotWriter(); -dotWriter.write(workspace, stringWriter); -System.out.println(stringWriter); -``` - -> You will need Graphviz installed. - -This code will generate and output a DOT diagram definition that looks like this: - -``` -digraph G { - graph [labelloc=top,label="Software System - System Context",fontname="Verdana",fontsize=12]; - edge [fontname="Verdana",fontsize=9,labelfontname="Verdana",labelfontsize=9]; - node [fontname="Verdana",fontsize=9,shape=record]; - c0 [label="User"] - c1 [label="Software System"] - // null - c0 -> c1 [label="Uses" , ]; -} -``` - -Importing this graph definition into Graphviz (or [GraphvizFiddle](https://stamm-wilbrandt.de/GraphvizFiddle/)) gives you the following image: - -![A simple Graphviz diagram](images/graphviz-getting-started.png) - -## Benefits of using Graphviz with Structurizr - -The key benefit of using Graphviz in conjunction with the Structurizr client library is that you can create diagrams from a __model__ of your software system. The model provides a set of rules that must be followed; related to elements, relationships, and how they are exposed using diagrams. This means: - -1. Rather than looking after a collection of disjointed Graphviz diagram definitions, you can create many Graphviz diagrams from a single model and keep them all up to date easily, especially if integrated with your continuous build server and build pipeline. -1. The naming of elements and the definition of relationships between elements _remains consistent across diagrams_. -1. The software architecture model at the component level can be created by extracting components from a codebase, using _static analysis and reflection techniques_. - -### Example - -Here is a Graphviz version of the Component diagram from the [Spring PetClinic example](https://structurizr.com/share/1#components). - -![](images/graphviz-spring-petclinic-components.png) \ No newline at end of file diff --git a/docs/images/component-diagram-1.png b/docs/images/component-diagram-1.png deleted file mode 100644 index 1b06da062..000000000 Binary files a/docs/images/component-diagram-1.png and /dev/null differ diff --git a/docs/images/container-diagram-1.png b/docs/images/container-diagram-1.png deleted file mode 100644 index 42a3fd50a..000000000 Binary files a/docs/images/container-diagram-1.png and /dev/null differ diff --git a/docs/images/corporate-branding-1.png b/docs/images/corporate-branding-1.png deleted file mode 100644 index c3403d89c..000000000 Binary files a/docs/images/corporate-branding-1.png and /dev/null differ diff --git a/docs/images/corporate-branding-2.png b/docs/images/corporate-branding-2.png deleted file mode 100644 index 90056fe94..000000000 Binary files a/docs/images/corporate-branding-2.png and /dev/null differ diff --git a/docs/images/corporate-branding-3.png b/docs/images/corporate-branding-3.png deleted file mode 100644 index 4256f303d..000000000 Binary files a/docs/images/corporate-branding-3.png and /dev/null differ diff --git a/docs/images/corporate-branding-4.png b/docs/images/corporate-branding-4.png deleted file mode 100644 index bd5e55952..000000000 Binary files a/docs/images/corporate-branding-4.png and /dev/null differ diff --git a/docs/images/deployment-diagram-1.png b/docs/images/deployment-diagram-1.png deleted file mode 100644 index 9409b4fdf..000000000 Binary files a/docs/images/deployment-diagram-1.png and /dev/null differ diff --git a/docs/images/documentation-1.png b/docs/images/documentation-1.png deleted file mode 100644 index 78f12d680..000000000 Binary files a/docs/images/documentation-1.png and /dev/null differ diff --git a/docs/images/documentation-2.png b/docs/images/documentation-2.png deleted file mode 100644 index d93baaf58..000000000 Binary files a/docs/images/documentation-2.png and /dev/null differ diff --git a/docs/images/documentation-3.png b/docs/images/documentation-3.png deleted file mode 100644 index c15564c1c..000000000 Binary files a/docs/images/documentation-3.png and /dev/null differ diff --git a/docs/images/documentation-arc42-1.png b/docs/images/documentation-arc42-1.png deleted file mode 100644 index 40bcf5cb1..000000000 Binary files a/docs/images/documentation-arc42-1.png and /dev/null differ diff --git a/docs/images/documentation-automatic-1.png b/docs/images/documentation-automatic-1.png deleted file mode 100644 index 033aee841..000000000 Binary files a/docs/images/documentation-automatic-1.png and /dev/null differ diff --git a/docs/images/documentation-structurizr-1.png b/docs/images/documentation-structurizr-1.png deleted file mode 100644 index 252d0887b..000000000 Binary files a/docs/images/documentation-structurizr-1.png and /dev/null differ diff --git a/docs/images/documentation-viewpoints-and-perspectives-1.png b/docs/images/documentation-viewpoints-and-perspectives-1.png deleted file mode 100644 index 7ddf5d068..000000000 Binary files a/docs/images/documentation-viewpoints-and-perspectives-1.png and /dev/null differ diff --git a/docs/images/dynamic-diagram-1.png b/docs/images/dynamic-diagram-1.png deleted file mode 100644 index 9bc32ecec..000000000 Binary files a/docs/images/dynamic-diagram-1.png and /dev/null differ diff --git a/docs/images/enterprise-context-diagram-1.png b/docs/images/enterprise-context-diagram-1.png deleted file mode 100644 index ce18a26a0..000000000 Binary files a/docs/images/enterprise-context-diagram-1.png and /dev/null differ diff --git a/docs/images/filtered-views-1.png b/docs/images/filtered-views-1.png deleted file mode 100644 index 2e8e8978a..000000000 Binary files a/docs/images/filtered-views-1.png and /dev/null differ diff --git a/docs/images/filtered-views-2.png b/docs/images/filtered-views-2.png deleted file mode 100644 index 5d1a8e192..000000000 Binary files a/docs/images/filtered-views-2.png and /dev/null differ diff --git a/docs/images/getting-started-diagram-key.png b/docs/images/getting-started-diagram-key.png deleted file mode 100644 index 250b700c2..000000000 Binary files a/docs/images/getting-started-diagram-key.png and /dev/null differ diff --git a/docs/images/getting-started.png b/docs/images/getting-started.png deleted file mode 100644 index 8140ce3aa..000000000 Binary files a/docs/images/getting-started.png and /dev/null differ diff --git a/docs/images/graphviz-getting-started.png b/docs/images/graphviz-getting-started.png deleted file mode 100644 index bd30bbdfb..000000000 Binary files a/docs/images/graphviz-getting-started.png and /dev/null differ diff --git a/docs/images/graphviz-spring-petclinic-components.png b/docs/images/graphviz-spring-petclinic-components.png deleted file mode 100644 index a8b00fdd4..000000000 Binary files a/docs/images/graphviz-spring-petclinic-components.png and /dev/null differ diff --git a/docs/images/plantuml-getting-started.png b/docs/images/plantuml-getting-started.png deleted file mode 100644 index 777f66e52..000000000 Binary files a/docs/images/plantuml-getting-started.png and /dev/null differ diff --git a/docs/images/plantuml-spring-petclinic-components.png b/docs/images/plantuml-spring-petclinic-components.png deleted file mode 100644 index a31458456..000000000 Binary files a/docs/images/plantuml-spring-petclinic-components.png and /dev/null differ diff --git a/docs/images/plantuml-spring-petclinic-containers.png b/docs/images/plantuml-spring-petclinic-containers.png deleted file mode 100644 index 24a52868a..000000000 Binary files a/docs/images/plantuml-spring-petclinic-containers.png and /dev/null differ diff --git a/docs/images/plantuml-spring-petclinic-deployment-development.png b/docs/images/plantuml-spring-petclinic-deployment-development.png deleted file mode 100644 index 318e579e7..000000000 Binary files a/docs/images/plantuml-spring-petclinic-deployment-development.png and /dev/null differ diff --git a/docs/images/plantuml-spring-petclinic-deployment-live.png b/docs/images/plantuml-spring-petclinic-deployment-live.png deleted file mode 100644 index 8948c85b4..000000000 Binary files a/docs/images/plantuml-spring-petclinic-deployment-live.png and /dev/null differ diff --git a/docs/images/plantuml-spring-petclinic-deployment-staging.png b/docs/images/plantuml-spring-petclinic-deployment-staging.png deleted file mode 100644 index 6afe83330..000000000 Binary files a/docs/images/plantuml-spring-petclinic-deployment-staging.png and /dev/null differ diff --git a/docs/images/plantuml-spring-petclinic-dynamic.png b/docs/images/plantuml-spring-petclinic-dynamic.png deleted file mode 100644 index 5877893b4..000000000 Binary files a/docs/images/plantuml-spring-petclinic-dynamic.png and /dev/null differ diff --git a/docs/images/plantuml-spring-petclinic-system-context.png b/docs/images/plantuml-spring-petclinic-system-context.png deleted file mode 100644 index 36134dd85..000000000 Binary files a/docs/images/plantuml-spring-petclinic-system-context.png and /dev/null differ diff --git a/docs/images/spring-petclinic-1.png b/docs/images/spring-petclinic-1.png deleted file mode 100644 index fc3864bd7..000000000 Binary files a/docs/images/spring-petclinic-1.png and /dev/null differ diff --git a/docs/images/spring-petclinic-plantuml.png b/docs/images/spring-petclinic-plantuml.png deleted file mode 100644 index 8abf38919..000000000 Binary files a/docs/images/spring-petclinic-plantuml.png and /dev/null differ diff --git a/docs/images/structurizr-annotations-1.png b/docs/images/structurizr-annotations-1.png deleted file mode 100644 index ab4250cc4..000000000 Binary files a/docs/images/structurizr-annotations-1.png and /dev/null differ diff --git a/docs/images/structurizr-banner.png b/docs/images/structurizr-banner.png deleted file mode 100644 index 8edc28cf1..000000000 Binary files a/docs/images/structurizr-banner.png and /dev/null differ diff --git a/docs/images/structurizr-logo.png b/docs/images/structurizr-logo.png deleted file mode 100644 index 9324ae8dc..000000000 Binary files a/docs/images/structurizr-logo.png and /dev/null differ diff --git a/docs/images/structurizr-overview.png b/docs/images/structurizr-overview.png deleted file mode 100644 index 4d2746502..000000000 Binary files a/docs/images/structurizr-overview.png and /dev/null differ diff --git a/docs/images/styling-elements-1.png b/docs/images/styling-elements-1.png deleted file mode 100644 index 661a3e857..000000000 Binary files a/docs/images/styling-elements-1.png and /dev/null differ diff --git a/docs/images/styling-elements-2.png b/docs/images/styling-elements-2.png deleted file mode 100644 index 27abd955d..000000000 Binary files a/docs/images/styling-elements-2.png and /dev/null differ diff --git a/docs/images/styling-elements-3.png b/docs/images/styling-elements-3.png deleted file mode 100644 index 9fe17c82c..000000000 Binary files a/docs/images/styling-elements-3.png and /dev/null differ diff --git a/docs/images/styling-elements-4.png b/docs/images/styling-elements-4.png deleted file mode 100644 index 58ef6d5d9..000000000 Binary files a/docs/images/styling-elements-4.png and /dev/null differ diff --git a/docs/images/styling-elements-5.png b/docs/images/styling-elements-5.png deleted file mode 100644 index 175900aa2..000000000 Binary files a/docs/images/styling-elements-5.png and /dev/null differ diff --git a/docs/images/styling-elements-6.png b/docs/images/styling-elements-6.png deleted file mode 100644 index 99be63edf..000000000 Binary files a/docs/images/styling-elements-6.png and /dev/null differ diff --git a/docs/images/styling-relationships-1.png b/docs/images/styling-relationships-1.png deleted file mode 100644 index 2e9643205..000000000 Binary files a/docs/images/styling-relationships-1.png and /dev/null differ diff --git a/docs/images/styling-relationships-2.png b/docs/images/styling-relationships-2.png deleted file mode 100644 index 2bbc6d600..000000000 Binary files a/docs/images/styling-relationships-2.png and /dev/null differ diff --git a/docs/images/styling-relationships-3.png b/docs/images/styling-relationships-3.png deleted file mode 100644 index ab842e5cb..000000000 Binary files a/docs/images/styling-relationships-3.png and /dev/null differ diff --git a/docs/images/styling-relationships-4.png b/docs/images/styling-relationships-4.png deleted file mode 100644 index 1bd7f7a6b..000000000 Binary files a/docs/images/styling-relationships-4.png and /dev/null differ diff --git a/docs/images/supporting-types-1.png b/docs/images/supporting-types-1.png deleted file mode 100644 index 868effabf..000000000 Binary files a/docs/images/supporting-types-1.png and /dev/null differ diff --git a/docs/images/supporting-types-2.png b/docs/images/supporting-types-2.png deleted file mode 100644 index 361ef7377..000000000 Binary files a/docs/images/supporting-types-2.png and /dev/null differ diff --git a/docs/images/supporting-types-3.png b/docs/images/supporting-types-3.png deleted file mode 100644 index 2fc019eea..000000000 Binary files a/docs/images/supporting-types-3.png and /dev/null differ diff --git a/docs/images/supporting-types-4.png b/docs/images/supporting-types-4.png deleted file mode 100644 index a493ecae1..000000000 Binary files a/docs/images/supporting-types-4.png and /dev/null differ diff --git a/docs/images/supporting-types-5.png b/docs/images/supporting-types-5.png deleted file mode 100644 index 1caa44f19..000000000 Binary files a/docs/images/supporting-types-5.png and /dev/null differ diff --git a/docs/images/supporting-types-6.gif b/docs/images/supporting-types-6.gif deleted file mode 100644 index f65070486..000000000 Binary files a/docs/images/supporting-types-6.gif and /dev/null differ diff --git a/docs/images/supporting-types-7.png b/docs/images/supporting-types-7.png deleted file mode 100644 index 7a1918dca..000000000 Binary files a/docs/images/supporting-types-7.png and /dev/null differ diff --git a/docs/images/supporting-types-8.png b/docs/images/supporting-types-8.png deleted file mode 100644 index 8288890eb..000000000 Binary files a/docs/images/supporting-types-8.png and /dev/null differ diff --git a/docs/images/system-context-diagram-1.png b/docs/images/system-context-diagram-1.png deleted file mode 100644 index a7603a6eb..000000000 Binary files a/docs/images/system-context-diagram-1.png and /dev/null differ diff --git a/docs/images/visualising-software-architecture.png b/docs/images/visualising-software-architecture.png deleted file mode 100644 index 155f4bfec..000000000 Binary files a/docs/images/visualising-software-architecture.png and /dev/null differ diff --git a/docs/plantuml.md b/docs/plantuml.md deleted file mode 100644 index f62511061..000000000 --- a/docs/plantuml.md +++ /dev/null @@ -1,98 +0,0 @@ -# PlantUML - -Structurizr for Java also includes a simple exporter that can create diagram definitions compatible with [PlantUML](http://www.plantuml.com). The following diagram types are supported: - -- Enterprise Context -- System Context -- Container -- Component -- Dynamic -- Deployment - -Simply create your software architecture model and views as usual, and use the [PlantUMLWriter](https://github.com/structurizr/java/blob/master/structurizr-core/src/com/structurizr/io/plantuml/PlantUMLWriter.java) class to export the views. [For example](https://github.com/structurizr/java/blob/master/structurizr-examples/src/com/structurizr/example/PlantUML.java): - -```java -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(); - -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); - -StringWriter stringWriter = new StringWriter(); -PlantUMLWriter plantUMLWriter = new PlantUMLWriter(); -plantUMLWriter.addSkinParam("rectangleFontColor", "#ffffff"); -plantUMLWriter.addSkinParam("rectangleStereotypeFontColor", "#ffffff"); -plantUMLWriter.write(workspace, stringWriter); -System.out.println(stringWriter.toString()); -``` - -This code will generate and output a PlantUML diagram definition that looks like this: - -``` -@startuml -title Software System - System Context -caption An example of a System Context diagram. - -skinparam { - shadowing false - arrowColor #707070 - actorBorderColor #707070 - componentBorderColor #707070 - rectangleBorderColor #707070 - noteBackgroundColor #ffffff - noteBorderColor #707070 - rectangleFontColor #ffffff - rectangleStereotypeFontColor #ffffff -} -rectangle 2 <> #1168bd [ - Software System - -- - My software system. -] -actor "User" <> as 1 #08427b -note right of 1 - A user of my software system. -end note -1 .[#707070].> 2 : Uses -@enduml -``` - -If you copy/paste this into [PlantUML online](http://www.plantuml.com/plantuml/), you will get something like this: - -![A simple PlantUML diagram](images/plantuml-getting-started.png) - -## Benefits of using PlantUML with Structurizr - -The key benefit of using PlantUML in conjunction with the Structurizr client library is that you can create diagrams from a __model__ of your software system. The model provides a set of rules that must be followed; related to elements, relationships, and how they are exposed using diagrams. This means: - -1. Rather than looking after a collection of disjointed PlantUML diagram definitions, you can create many PlantUML diagrams from a single model and keep them all up to date easily, especially if integrated with your continuous build server and build pipeline. -1. The naming of elements and the definition of relationships between elements _remains consistent across diagrams_. -1. The software architecture model at the component level can be created by extracting components from a codebase, using _static analysis and reflection techniques_. - -### Example - -Here are the PlantUML versions of the diagrams from the [Spring PetClinic example](https://structurizr.com/share/1). - -![](images/plantuml-spring-petclinic-system-context.png) - -![](images/plantuml-spring-petclinic-containers.png) - -![](images/plantuml-spring-petclinic-components.png) - -![](images/plantuml-spring-petclinic-dynamic.png) - -![](images/plantuml-spring-petclinic-deployment-development.png) - -![](images/plantuml-spring-petclinic-deployment-staging.png) - -![](images/plantuml-spring-petclinic-deployment-live.png) diff --git a/docs/spring-component-finder-strategies.md b/docs/spring-component-finder-strategies.md deleted file mode 100644 index 14fe2a2f3..000000000 --- a/docs/spring-component-finder-strategies.md +++ /dev/null @@ -1,31 +0,0 @@ -# Spring component finder strategies - -Included in the ```structurizr-spring``` library are a number of component finder strategies that help you identify components in Spring applications, including those built using Spring Boot. - -* __SpringMvcControllerComponentFinderStrategy__: A component finder strategy that finds Spring MVC controllers (classes annotated ```@Controller```). -* __SpringRestControllerComponentFinderStrategy__: A component finder strategy that finds Spring REST controllers (classes annotated ```@RestController```). -* __SpringServiceComponentFinderStrategy__: A component finder strategy that finds Spring services (classes annotated ```@Service```). -* __SpringComponentComponentFinderStrategy__: A component finder strategy that finds Spring components (classes annotated ```@Component```). -* __SpringRepositoryComponentFinderStrategy__: A component finder strategy for Spring repositories (classes annotated ```@Repository```, plus those that extend ```JpaRepository``` or ```CrudRepository```). -* __SpringComponentFinderStrategy__: A combined component finder strategy that uses all of the individual strategies listed above. - -## Spring naming conventions and interfaces vs implementation classes - -Some of the Spring annotations (e.g. ```@Component```, ```@Service``` and ```@Repository```) are typically used to annotate _implementation classes_. For example, a ```JdbcCustomerRepository``` class might be annotated ```@Repository```, rather than the ```CustomerRepository``` interface. This component finder strategy tries to refer to interface types rather than implementation classes, based upon the following assumptions: - -1. Having a component named ```CustomerRepository``` is preferable to ```JdbcCustomerRespository```. -2. Other types in the codebase will likely have a dependency on the interface type (e.g. via dependency injection) rather than being coupled to the implementation class. - -Given that a class can implement any number of interfaces, for a given implementation class, the component finder strategies will try to find the interface where the name of that interface is included in the name of the implementation class (i.e. ```*Interface```, ```Interface*``` and ```*Interface*```). For example, the following implementation classes will match an interface named ```CustomerRepository```: - -* ```JdbcCustomerRepository``` -* ```CustomerRepositoryImpl``` -* ```JdbcCustomerRepositoryImpl``` - -## Public vs non-public types - -By default, non-public types will be ignored so that, for example, you can hide repository implementations behind services, as described at [Whoops! Where did my architecture go](http://olivergierke.de/2013/01/whoops-where-did-my-architecture-go/). Use the ```setIncludePublicTypesOnly``` method to change this behaviour. - -## Example - -You can see an example of how to use the Spring component finder strategies in the [Spring PetClinic example](spring-petclinic.md). \ No newline at end of file diff --git a/docs/spring-petclinic.md b/docs/spring-petclinic.md deleted file mode 100644 index 92a15d940..000000000 --- a/docs/spring-petclinic.md +++ /dev/null @@ -1,221 +0,0 @@ -# The Spring PetClinic example - -This is a step-by-step guide to recreating the System Context, Container and Component diagrams from the [Spring PetClinic example](https://structurizr.com/share/1), which are based upon the original version of the application. It assumes that you have working Java, Maven and git installations plus a development environment to write code. The full source code for this example can be found in [SpringPetClinic.java](https://github.com/structurizr/java/blob/master/structurizr-examples/src/com/structurizr/example/SpringPetClinic.java). - -## 1. Clone and build the Spring PetClinic code - -First of all, we need to download a copy of the [Spring PetClinic source code](https://github.com/spring-projects/spring-petclinic/). Please note that this example was created with a specific version of the Spring PetClinic codebase, so please be sure to perform the ```git checkout``` step too. - -``` -git clone https://github.com/spring-projects/spring-petclinic.git -cd spring-petclinic -git checkout 95de1d9f8bf63560915331664b27a4a75ce1f1f6 -``` - -Next we need to run the build. - -``` -mvn -``` - -The Spring PetClinic is a sample application and includes three persistence implementations (JDBC, JPA and Spring Data) that all do the same thing. As this is unrealistic for most applications, let's make things easier by removing the JPA and Spring Data implementations. - -``` -rm -r target/spring-petclinic-1.0.0-SNAPSHOT/WEB-INF/classes/org/springframework/samples/petclinic/repository/jpa/ -rm -r target/spring-petclinic-1.0.0-SNAPSHOT/WEB-INF/classes/org/springframework/samples/petclinic/repository/springdatajpa/ -``` - -## 2. Create a model - -With the Spring PetClinic application built, we now need to create a software architecture model using the [extract and supplement](https://structurizr.com/help/extract-and-supplement) approach. We will do this by creating a simple Java program to create the model. The Maven, Gradle, etc dependencies you will need are as follows: - -Name | Description -------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- -com.structurizr:structurizr-core:1.0.0-RC4 | The core library that can used to create models and upload models to Structurizr. -com.structurizr:structurizr-spring:1.0.0-RC4 | The Spring component finder. - -First we need to create a little boilerplate code to create a workspace and a model. - -```java -public static void main(String[] args) throws Exception { - Workspace workspace = new Workspace("Spring PetClinic", - "This is a C4 representation of the Spring PetClinic sample app (https://github.com/spring-projects/spring-petclinic/)"); - Model model = workspace.getModel(); -``` - -## 3. People and software systems - -A system context diagram for the Spring PetClinic system would simply consist of a single type of user (a clinic employee) using the Spring PetClinic system. With Structurizr for Java, we can represent this in code as follows. - -```java -SoftwareSystem springPetClinic = model.addSoftwareSystem("Spring PetClinic", - "Allows employees to view and manage information regarding the veterinarians, the clients, and their pets."); -Person clinicEmployee = model.addPerson("Clinic Employee", "An employee of the clinic"); - -clinicEmployee.uses(springPetClinic, "Uses"); -``` - -### 4. Containers - -Stepping down to containers, the Spring PetClinic system is made up of a Java web application that uses a database to store data. If we make some assumptions about the deployment technology stack, we can represent this in code as follows. - -```java -Container webApplication = springPetClinic.addContainer("Web Application", - "Allows employees to view and manage information regarding the veterinarians, the clients, and their pets.", - "Java and Spring"); -Container relationalDatabase = springPetClinic.addContainer("Relational Database", - "Stores information regarding the veterinarians, the clients, and their pets.", - "Relational Database Schema"); - -clinicEmployee.uses(webApplication, "Uses", "HTTPS"); -webApplication.uses(relationalDatabase, "Reads from and writes to", "JDBC"); -``` - -## 5. Components - -At the next level of abstraction, we need to open up the web application to see the components inside it. Although we couldn't really get the two previous levels of abstraction from the codebase easily, we *can* get the components. All we need to do is [understand what a "component" means in the context of this codebase](https://structurizr.com/help/components-vs-classes). We can then use this information to help us find and extract them in order to populate the software architecture model. - -Spring MVC uses Java annotations (```@Controller```, ```@Service``` and ```@Repository```) to signify classes as being web controllers, services and repositories respectively. Assuming that we consider these to be our architecturally significant code elements, it's then a simple job of extracting these annotated classes (Spring Beans) from the codebase. - -```java -ComponentFinder componentFinder = new ComponentFinder( - webApplication, "org.springframework.samples.petclinic", - new SpringComponentFinderStrategy( - new ReferencedTypesSupportingTypesStrategy(false) - ), - new SourceCodeComponentFinderStrategy(new File(sourceRoot, "/src/main/java/"), 150)); - -componentFinder.findComponents(); -``` - -The ```SpringComponentFinderStrategy``` is a pre-built component finder strategy that understands how applications are built with Spring and how to identify Spring components, such as MVC controllers, REST controllers, services, repositories, JPA repositories, etc. The way that you identify supporting types (i.e. the Java classes and interfaces) that implement a component is also pluggable. Here, with the ```ReferencedTypesSupportingTypesStrategy```, we're looking for all types directly referenced by the component type(s). See [Components and supporting types](supporting-types.md) for more details about this. - -Once the components and their supporting types have been identified, the dependencies between components are also identified and extracted. - -In addition, the ```SourceCodeComponentFinderStrategy``` will parse the top-level Javadoc comment from the source file for each component type for inclusion in the model. It will also calculate the size of each component based upon the number of lines of source code across all supporting types. - -The final thing we need to do is connect the user to the web controllers, and the repositories to the database. This is easy to do since the software architecture model is represented in code. - -```java -webApplication.getComponents().stream() - .filter(c -> c.getTechnology().equals(SpringComponentFinderStrategy.SPRING_MVC_CONTROLLER)) - .forEach(c -> clinicEmployee.uses(c, "Uses", "HTTP")); - -webApplication.getComponents().stream() - .filter(c -> c.getTechnology().equals(SpringComponentFinderStrategy.SPRING_REPOSITORY)) - .forEach(c -> c.uses(relationalDatabase, "Reads from and writes to", "JDBC")); -``` - -## 6. Create some views - -With the software architecture model in place, we now need to create some views with which to visualise the model. Again, we can do this using code. First the context diagram, which includes all people and all software systems. - -```java -ViewSet viewSet = workspace.getViews(); -SystemContextView contextView = viewSet.createSystemContextView(springPetClinic, "context", "Context view for Spring PetClinic"); -contextView.addAllSoftwareSystems(); -contextView.addAllPeople(); -``` - -Next is the container diagram. - -```java -ContainerView containerView = viewSet.createContainerView(springPetClinic, "containers", "Container view for Spring PetClinic"); -containerView.addAllPeople(); -containerView.addAllSoftwareSystems(); -containerView.addAllContainers(); -``` - -And finally is the component diagram. - -```java -ComponentView componentView = viewSet.createComponentView(webApplication, "components", "The Components diagram for the Spring PetClinic web application."); -componentView.addAllComponents(); -componentView.addAllPeople(); -componentView.add(relationalDatabase); -``` - -## 7. Linking elements to external resources - -In order to create a set of maps for the Spring PetClinic system that reflect reality, we can link the components on the component diagram to the source code. This isn't necessary, but doing so means that we can [navigate from the diagrams to the code](https://structurizr.com/help/diagram-navigation). - -```java -for (Component component : webApplication.getComponents()) { - for (CodeElement codeElement : component.getCode()) { - String sourcePath = codeElement.getUrl(); - if (sourcePath != null) { - codeElement.setUrl(sourcePath.replace( - sourceRoot.toURI().toString(), - "https://github.com/spring-projects/spring-petclinic/tree/864580702f8ef4d2cdfd7fe4497fb8c9e86018d2/")); - } - } -} -``` - -Since we don't have a component model for the database, let's instead simply link the database element to the data definition language in GitHub. - -```java -relationalDatabase.setUrl("https://github.com/spring-projects/spring-petclinic/tree/master/src/main/resources/db/hsqldb"); -``` - -## 8. Styling the diagrams - -By default, Structurizr will render all of the elements as grey boxes. However, the elements and relationships can be styled. - -```java -springPetClinic.addTags("Spring PetClinic"); -webApplication.getComponents().stream().filter(c -> c.getTechnology().equals(SpringComponentFinderStrategy.SPRING_MVC_CONTROLLER)) - .forEach(c -> c.addTags("Spring MVC Controller")); -webApplication.getComponents().stream().filter(c -> c.getTechnology().equals(SpringComponentFinderStrategy.SPRING_SERVICE)) - .forEach(c -> c.addTags("Spring Service")); -webApplication.getComponents().stream().filter(c -> c.getTechnology().equals(SpringComponentFinderStrategy.SPRING_REPOSITORY)) - .forEach(c -> c.addTags("Spring Repository")); -relationalDatabase.addTags("Database"); - -Styles styles = viewSet.getConfiguration().getStyles(); -styles.addElementStyle("Spring PetClinic").background("#6CB33E").color("#ffffff"); -styles.addElementStyle(Tags.PERSON).background("#519823").color("#ffffff").shape(Shape.Person); -styles.addElementStyle(Tags.CONTAINER).background("#91D366").color("#ffffff"); -styles.addElementStyle("Database").shape(Shape.Cylinder); -styles.addElementStyle("Spring MVC Controller").background("#D4F3C0").color("#000000"); -styles.addElementStyle("Spring Service").background("#6CB33E").color("#000000"); -styles.addElementStyle("Spring Repository").background("#95D46C").color("#000000"); -``` - -## 9. Upload the model and views to Structurizr - -The code we've just seen simply creates an in-memory representation of the software architecture model, in this case as a collection of Java objects. The open source Structurizr for Java library also includes a way to export this model to an intermediate JSON representation, which can then be imported into some tooling that is able to visualise it. This is what Structurizr does. - -```java -StructurizrClient structurizrClient = new StructurizrClient("key", "secret"); -structurizrClient.putWorkspace(1234, workspace); -``` - -In order to upload your model to Structurizr using the web API, you'll need to [sign up](https://structurizr.com/signup) to get your own API key and secret. -Also, when you run the Structurizr program you just created, you'll need to ensure that the compiled version of the Spring PetClinic application is on your classpath; specifically these directories: - -``` -target/spring-petclinic-1.0.0-SNAPSHOT/WEB-INF/classes -target/spring-petclinic-1.0.0-SNAPSHOT/WEB-INF/lib -``` - -## 10. View the diagrams and layout the elements - -If you sign in to Structurizr and open the workspace you just uploaded, you'll see something like this. - -![The Spring PetClinic workspace](images/spring-petclinic-1.png) - -Structurizr doesn't do any automatic layout of the elements on your diagrams, so you will need to drag the boxes around to create a layout that you like. Here are the links to the live example diagrams: - -- [System Context diagram](https://structurizr.com/share/1#context) -- [Container diagram](https://structurizr.com/share/1#containers) -- [Component diagram](https://structurizr.com/share/1#components) - -## 11. Explore the model -Once you have a model of your software system, you can explore it using a number of different visualisations. For example: - -- [Static structure, rendered as a tree](https://structurizr.com/share/1/explore/tree) -- [Static structure, rendered as circles based upon size](https://structurizr.com/share/1/explore/size-circles) -- [Component dependencies](https://structurizr.com/share/1/explore/component-dependencies) -- [Component and code dependencies](https://structurizr.com/share/1/explore/component-and-code-dependencies) - diff --git a/docs/structurizr-annotations.md b/docs/structurizr-annotations.md deleted file mode 100644 index 29c814a7d..000000000 --- a/docs/structurizr-annotations.md +++ /dev/null @@ -1,106 +0,0 @@ -# Structurizr annotations - -Structurizr for Java includes 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, or they help supplement the software architecture model. - -The annotations can be found in the [structurizr-annotations](https://bintray.com/structurizr/maven/structurizr-java) artifact, which is a very small standalone JAR file containing only the Structurizr annotations. All annotations have a runtime retention policy, so they will be present in the compiled bytecode. - -## @Component - -A type-level annotation that can be used to signify that the annotated type (an interface or class) can be considered to be a "component". The properties are as follows: - -- description: The description of the component (optional). -- technology: The technology of component (optional). - -## @UsedBySoftwareSystem - -A type-level annotation that can be used to signify that the named software system uses the component on which this annotation is placed, creating a relationship from the software system to the component. The properties are as follows: - -- name: The name of the software system (required). -- description: The description of the relationship (optional). -- technology: The technology of relationship (optional). - -## @UsedByPerson - -A type-level annotation that can be used to signify that the named person uses the component on which this annotation is placed, creating a relationship form the person to the component. The properties are as follows: - -- name: The name of the person (required). -- description: The description of the relationship (optional). -- technology: The technology of relationship (optional). - -## @UsedByContainer - -A type-level annotation that can be used to signify that the named container uses the component on which this annotation is placed, creating a relationship from the container to the component. The properties are as follows: - -- name: The name of the container (required). -- description: The description of the relationship (optional). -- technology: The technology of relationship (optional). - -If the container resides in the same software system as the component, the simple name can be used to identify the container (e.g. "Database"). Otherwise, the full canonical name of the form "Software System/Container" must be used (e.g. "Some Other Software System/Database"). - -## @UsesSoftwareSystem - -A type-level annotation that can be used to signify that the component on which this annotation is placed has a relationship to the named software system, creating a relationship from the component to the software system. The properties are as follows: - -- name: The name of the software system (required). -- description: The description of the relationship (optional). -- technology: The technology of relationship (optional). - -## @UsesContainer - -A type-level annotation that can be used to signify that the component on which this annotation is placed has a relationship to the named container, creating a relationship from the component to the container. The properties are as follows: - -- name: The name of the container (required). -- description: The description of the relationship (optional). -- technology: The technology of relationship (optional). - -If the container resides in the same software system as the component, the simple name can be used to identify the container (e.g. "Database"). Otherwise, the full canonical name of the form "Software System/Container" must be used (e.g. "Some Other Software System/Database"). - -## @UsesComponent - -A field-level annotation that can be used to supplement the existing relationship (i.e. add a description and/or technology) between two components. - -When using the various component finder strategies, Structurizr for Java will identify components along with the relationships between those components. Since this is typically done using reflection against the compiled bytecode, you'll notice that the description and technology properties of the resulting relationships is always empty. The ```@UsesComponent``` annotation provides a simple way to ensure that such information is added into the model. - -The properties are as follows: - -- description: The description of the relationship (required). -- technology: The technology of relationship (optional). - -## Example - -Here are some examples of the annotations, which have been used to create the following diagram. - -![](images/structurizr-annotations-1.png) - -```java -@Component(description = "Serves HTML pages to users.", technology = "Java") -@UsedByPerson(name = "User", description = "Uses", technology = "HTTPS") -class HtmlController { - - @UsesComponent(description = "Gets data using") - private Repository repository = new JdbcRepository(); - -} -``` - -```java -@Component(description = "Provides access to data stored in the database.", technology = "Java and JPA") -public interface Repository { - - String getData(long id); - -} -``` - -```java -@UsesContainer(name = "Database", description = "Reads from", technology = "JDBC") -class JdbcRepository implements Repository { - - public String getData(long id) { - return "..."; - } - -} -``` - -See [StructurizrAnnotations.java](https://github.com/structurizr/java/blob/master/structurizr-examples/src/com/structurizr/example/StructurizrAnnotations.java) for the full source code illustrating how to use the various annotations in conjunction with the component finder. The resulting diagrams can be found at [https://structurizr.com/share/36571](https://structurizr.com/share/36571). \ No newline at end of file diff --git a/docs/styling-elements.md b/docs/styling-elements.md deleted file mode 100644 index d8e7674f3..000000000 --- a/docs/styling-elements.md +++ /dev/null @@ -1,80 +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) - -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 diff --git a/docs/styling-relationships.md b/docs/styling-relationships.md deleted file mode 100644 index 942a1c179..000000000 --- a/docs/styling-relationships.md +++ /dev/null @@ -1,51 +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) - - -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 diff --git a/docs/supplementing-from-source-code.md b/docs/supplementing-from-source-code.md deleted file mode 100644 index eb27fd451..000000000 --- a/docs/supplementing-from-source-code.md +++ /dev/null @@ -1,32 +0,0 @@ -# Supplementing the model from source code - -Most of the component finder strategies included in Structurizr for Java find components using reflection against the compiled bytecode. Some useful information exists in the source code though; including: - -* The type-level doc comment (```/** ... */```, typically extracted using the javadoc tool). This can be used to populate the description property of components. -* The number of lines of source code. This can be a used to calculate the "size" of a component. - -A pre-built [SourceCodeComponentFinderStrategy](https://github.com/structurizr/java/blob/master/structurizr-core/src/com/structurizr/analysis/SourceCodeComponentFinderStrategy.java) is provided to do this, which uses the standard ```javadoc``` tool. You will need to include ```JAVA_HOME/lib/tools.jar``` on your classpath. - -## Example - -Here's an example of how to use the ```SourceCodeComponentFinderStrategy```, taken from the [Spring PetClinic example](spring-petclinic.md). - -```java -File sourceRoot = new File("/some/path"); - -ComponentFinder componentFinder = new ComponentFinder( - webApplication, "org.springframework.samples.petclinic", - new SpringComponentFinderStrategy( - new ReferencedTypesSupportingTypesStrategy(false) - ), - new SourceCodeComponentFinderStrategy(new File(sourceRoot, "/src/main/java/"), 150)); - -componentFinder.findComponents(); -``` - -For every ```CodeElement``` that belongs to every ```Component``` in the ```Container``` passed to the ```ComponentFinder```, the ```SourceCodeComponentFinderStrategy``` will: - -* Set the description property, based on the type-level doc comment. The doc comment will be truncated if necessary (e.g. to 150 characters in the example above). -* Set the size property to be the number of lines of the file that the type was found in. - -Additionally, the description property of the ```Component``` will be set to be that of the primary ```CodeElement```, if a description has not been set on the ```Component``` already. \ No newline at end of file diff --git a/docs/supporting-types.md b/docs/supporting-types.md deleted file mode 100644 index ad1f1dd44..000000000 --- a/docs/supporting-types.md +++ /dev/null @@ -1,104 +0,0 @@ -# Components and supporting types - -In Structurizr, a [Component](https://github.com/structurizr/java/blob/master/structurizr-core/src/com/structurizr/model/Component.java) is described by a number of properties and can have a number of [CodeElement](https://github.com/structurizr/java/blob/master/structurizr-core/src/com/structurizr/model/CodeElement.java)s associated with it that represent the Java classes and interfaces that implement and/or support that component. - -Because each codebase is different, the mechanism to find a component's supporting types is pluggable via a number of strategies ([SupportingTypesStrategy](https://github.com/structurizr/java/blob/master/structurizr-core/src/com/structurizr/analysis/SupportingTypesStrategy.java)), which can be used in combination. Let's look at the [Spring PetClinic example](spring-petclinic.md) to see how the various supporting types strategies affect the software architecture model. - -## 1. No supporting types strategy - -This simple example uses the SpringComponentFinderStrategy with no supporting types strategy. - -```java -ComponentFinder componentFinder = new ComponentFinder( - webApplication, "org.springframework.samples.petclinic", - new SpringComponentFinderStrategy(), - new SourceCodeComponentFinderStrategy(new File(sourceRoot, "/src/main/java/"), 150)); - -componentFinder.findComponents(); -``` - -When executed against the compiled version of the Spring PetClinic codebase, each component identified will be made up of only the types found by the component finder strategy. For example, the ```VisitRepository``` component will be made up of an interface (```VisitRepository```) and the implementation class (```JdbcVisitRepositoryImpl```). You can visualise this using [Structurizr's tree exploration](https://structurizr.com/help/explorations). - -![](images/supporting-types-1.png) - -## 2. Referenced types in the same package - -Next, let's use the [ReferencedTypesInSamePackageSupportingTypesStrategy](https://github.com/structurizr/java/blob/master/structurizr-core/src/com/structurizr/analysis/ReferencedTypesInSamePackageSupportingTypesStrategy.java) to find all supporting types in the same package as the component type. This is particularly useful when each component neatly resides in its own Java package, and you aren't interested in any other code outside of this package. - -```java -ComponentFinder componentFinder = new ComponentFinder( - webApplication, "org.springframework.samples.petclinic", - new SpringComponentFinderStrategy( - new ReferencedTypesInSamePackageSupportingTypesStrategy() - ), - new SourceCodeComponentFinderStrategy(new File(sourceRoot, "/src/main/java/"), 150)); - -componentFinder.findComponents(); -``` - -This strategy finds all of the supporting types that are referenced by the types found by the component finder strategy. In real terms, we've now additionally picked up the ```JdbcVisitRowMapper``` class, since this is used by the ```JdbcVisitRepositoryImpl``` class. - -![](images/supporting-types-2.png) - -## 3. All referenced types - -Next, let's use the [ReferencedTypesSupportingTypesStrategy](https://github.com/structurizr/java/blob/master/structurizr-core/src/com/structurizr/analysis/ReferencedTypesSupportingTypesStrategy.java) to find all of the referenced types, irrespective of which package they reside in, provided that package sits somewhere underneath ```org.springframework.samples.petclinic```. - -```java -ComponentFinder componentFinder = new ComponentFinder( - webApplication, "org.springframework.samples.petclinic", - new SpringComponentFinderStrategy( - new ReferencedTypesSupportingTypesStrategy() - ), - new SourceCodeComponentFinderStrategy(new File(sourceRoot, "/src/main/java/"), 150)); - -componentFinder.findComponents(); -``` - -As the following image illustrates, we now have many more classes that are supporting the implementation of the ```VisitRepository``` component. - -![](images/supporting-types-3.png) - -This collection of classes may look confusing at first, but the ```JdbcVisitRepositoryImpl``` class references the ```Visit``` class, which in turn references the ```Pet``` class, which in turn references the ```Owner``` class, etc. The Structurizr tree exploration shows that these classes are shared between the ```VisitRepository``` and other components by rendering their names in grey. - -## 4. Directly referenced types only - -Of course, the ```JdbcVisitRepositoryImpl``` class may not actually use all of these classes, but they are certainly available to it. This is one of the drawbacks of using static analysis based upon compiled bytecode. You can modify this behaviour and find only those types directly referenced by the component by passing ```false``` to the constructor of the [ReferencedTypesSupportingTypesStrategy](https://github.com/structurizr/java/blob/master/structurizr-core/src/com/structurizr/analysis/ReferencedTypesSupportingTypesStrategy.java). - -![](images/supporting-types-4.png) - -## Excluding types - -The Structurizr ```ComponentFinder``` will also allow you to exclude types from the component scanning process using one or more regular expressions. If we wanted to exclude the ```BaseEntity``` and ```NamedEntity``` classes in one of the previous examples, we could add the following line of code before calling ```componentFinder.findComponents()```. - -``` -componentFinder.exclude("org\\.springframework\\.samples\\.petclinic\\.model\\..*Entity"); -``` - -The result is as follows. - -![](images/supporting-types-5.png) - -## Type erasure - -The implementation of the various supporting types strategies above use reflection based upon Java bytecode. For this reason, the results are subject to the rules around Java's [type erasure](https://docs.oracle.com/javase/tutorial/java/generics/erasure.html). For example, if you have a component that references ```Collection``` in method signatures, you may find ```Customer``` missing from the list of supporting types. - -## Visualising shared code - -The easiest way to analyse the component-code relationships is to visualise them. Structurizr has a number of built-in explorations that can help with this. - -### Component size - -[Structurizr's component size exploration](https://structurizr.com/share/1/explore/size-circles) renders components and code elements as a collection of nested circles, where the size of each circle is based upon the number of lines of code. Shared code elements are rendered using a different style, and hovering the mouse over a shared code element will highlight all other occurrences. This allows you to easily see where code elements (interfaces and classes) are shared between components. - -![](images/supporting-types-6.gif) - -### Component and code dependencies - -The [component and code dependency exploration](https://structurizr.com/share/1/explore/component-and-code-dependencies) renders a force-directed graph of the components and code elements, along with all of the relationships between them. Shared code elements are rendered in grey. - -![](images/supporting-types-7.png) - -Clicking any node will allow you to easily see which other nodes are directly connected to it. - -![](images/supporting-types-8.png) diff --git a/docs/system-context-diagram.md b/docs/system-context-diagram.md deleted file mode 100644 index 1e97731a9..000000000 --- a/docs/system-context-diagram.md +++ /dev/null @@ -1,28 +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 - -As an example, a System Context diagram for a simplified, fictional Internet Banking System might look something like this. In summary, it shows that customers of the bank use the Internet Banking System, which itself uses the internal Mainframe Banking System. - -![An example System Context diagram](images/system-context-diagram-1.png) - -With Structurizr for Java, you can create this diagram with code like the following: - -```java -Person customer = model.addPerson(Location.External, "Customer", "A customer of the bank."); - -SoftwareSystem internetBankingSystem = model.addSoftwareSystem(Location.Internal, "Internet Banking System", "Allows customers to view information about their bank accounts and make payments."); -customer.uses(internetBankingSystem, "Uses"); - -SoftwareSystem mainframeBankingSystem = model.addSoftwareSystem(Location.Internal, "Mainframe Banking System", "Stores all of the core banking information about customers, accounts, transactions, etc."); -internetBankingSystem.uses(mainframeBankingSystem, "Uses"); - -SystemContextView systemContextView = views.createSystemContextView(internetBankingSystem, "SystemContext", "The system context diagram for the Internet Banking System."); -systemContextView.addNearestNeighbours(internetBankingSystem); -``` - -See [BigBankPlc.java](https://github.com/structurizr/java/blob/master/structurizr-examples/src/com/structurizr/example/BigBankPlc.java) for the full 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/type-matchers.md b/docs/type-matchers.md deleted file mode 100644 index 5082cc3d1..000000000 --- a/docs/type-matchers.md +++ /dev/null @@ -1,65 +0,0 @@ -# Type matchers - -Structurizr for Java includes a number "type matchers", which when used in conjunction with the [TypeMatcherComponentFinderStrategy](https://github.com/structurizr/java/blob/master/structurizr-core/src/com/structurizr/analysis/TypeMatcherComponentFinderStrategy.java), provides a simple way to find components in your codebase based upon specific types. The following pre-built type matchers are included. - -## NameSuffixTypeMatcher - -Matches types where the (simple) name of the type ends with the specified suffix. For example, to find all types named ```*Controller```: - -```java -new NameSuffixTypeMatcher("Controller", "", ""); -``` - -## RegexTypeMatcher - -Matches types using a regex against the fully qualified type name. For example, to find all types with a fully qualified name of ```*.web.*Controller```: - -```java -new RegexTypeMatcher(".*\.web\..*Controller", "", ""); -``` - -## AnnotationTypeMatcher - -Matches types based upon the presence of a type-level annotation. For example, to find all types that are annotated with the Java EE ```@Stateless``` annotation: - -```java -new AnnotationTypeMatcher(javax.ejb.Stateless.class, "", ""); -``` - -## ImplementsInterfaceTypeMatcher - -Matches types where the type implements the specified interface. For example, to find all types that implement the ```Repository``` interface: - -```java -new ImplementsInterfaceTypeMatcher(Repository.class, "", ""); -``` - -## ExtendsClassTypeMatcher - -Matches types where the type extends the specified class. For example, to find all types that extend the ```AbstractComponent``` class: - -```java -new ExtendsClassTypeMatcher(AbstractComponent.class, "", ""); -``` - -## Example - -Here is an example of how you might use the type matchers in conjunction with the component finder: - -```java -ComponentFinder componentFinder = new ComponentFinder( - someContainer, - "com.mycompany.myapp", - new TypeMatcherComponentFinderStrategy( - new NameSuffixTypeMatcher("Controller", "Controller description", "Controller technology"), - new NameSuffixTypeMatcher("Repository", "Repository description", "Repository technology") - ) -); -componentFinder.findComponents(); -``` - -The description and technology properties specified on the type matchers will be used to set the corresponding properties on the ```Component``` instances that are created when matching types are found. - -## Writing your own type matchers - -You can write your own type matchers by implementing the [TypeMatcher](https://github.com/structurizr/java/blob/master/structurizr-core/src/com/structurizr/analysis/TypeMatcher.java) interface, or by extending the [AbstractTypeMatcher](https://github.com/structurizr/java/blob/master/structurizr-core/src/com/structurizr/analysis/AbstractTypeMatcher.java) class. \ No newline at end of file diff --git a/docs/usage-patterns.md b/docs/usage-patterns.md deleted file mode 100644 index aead1b358..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 [Cat Boot Structurizr](https://github.com/Catalysts/cat-boot/tree/master/cat-boot-structurizr). - -## 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-core/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. \ No newline at end of file diff --git a/gradle.properties b/gradle.properties index fe55ae08c..8e1b695b3 100644 --- a/gradle.properties +++ b/gradle.properties @@ -5,3 +5,4 @@ signing.secretKeyRingFile=/some/path ossrhUsername=username ossrhPassword=password +version=5.0.3 \ No newline at end of file diff --git a/gradle/gradle.iml b/gradle/gradle.iml deleted file mode 100644 index d2f85ef06..000000000 --- a/gradle/gradle.iml +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - - - - - - - \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index ee739ada8..3994438e2 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,5 @@ -#Mon Jul 03 19:06:47 CEST 2017 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.4-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-4.0-bin.zip 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 727dee502..4226e5c13 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,9 +1,11 @@ -rootProject.name = 'structurizr' +rootProject.name = 'structurizr-java' -include 'structurizr-annotations' +include 'structurizr-annotation' +include 'structurizr-autolayout' +include 'structurizr-client' +include 'structurizr-component' include 'structurizr-core' -include 'structurizr-dot' -include 'structurizr-examples' -include 'structurizr-javaee' -include 'structurizr-spring' - +include 'structurizr-dsl' +include 'structurizr-export' +include 'structurizr-import' +include 'structurizr-inspection' \ No newline at end of file diff --git a/structurizr-annotation/README.md b/structurizr-annotation/README.md new file mode 100644 index 000000000..d57047c20 --- /dev/null +++ b/structurizr-annotation/README.md @@ -0,0 +1,10 @@ +# 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) + +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/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/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-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-annotations/src/com/structurizr/annotation/Component.java b/structurizr-annotations/src/com/structurizr/annotation/Component.java deleted file mode 100644 index c53c86ff7..000000000 --- a/structurizr-annotations/src/com/structurizr/annotation/Component.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.structurizr.annotation; - -import java.lang.annotation.*; - -/** - * A type-level annotation that can be used to signify that the annotated type - * (an interface or class) can be considered to be a "component". - */ -@Documented -@Target(ElementType.TYPE) -@Retention(RetentionPolicy.RUNTIME) -public @interface Component { - - String description() default ""; - String technology() default ""; - -} diff --git a/structurizr-annotations/src/com/structurizr/annotation/UsedByContainer.java b/structurizr-annotations/src/com/structurizr/annotation/UsedByContainer.java deleted file mode 100644 index 2af4686bb..000000000 --- a/structurizr-annotations/src/com/structurizr/annotation/UsedByContainer.java +++ /dev/null @@ -1,20 +0,0 @@ -package com.structurizr.annotation; - -import java.lang.annotation.*; - -/** - * A type-level annotation that can be used to signify that the named - * container uses the component on which this annotation is placed, - * creating a relationship from the container to the component. - */ -@Documented -@Target({ElementType.TYPE}) -@Retention(RetentionPolicy.RUNTIME) -@Repeatable(UsedByContainers.class) -public @interface UsedByContainer { - - String name(); - String description() default ""; - String technology() default ""; - -} diff --git a/structurizr-annotations/src/com/structurizr/annotation/UsedByContainers.java b/structurizr-annotations/src/com/structurizr/annotation/UsedByContainers.java deleted file mode 100644 index 3d580b27d..000000000 --- a/structurizr-annotations/src/com/structurizr/annotation/UsedByContainers.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.structurizr.annotation; - -import java.lang.annotation.*; - -/** - * A wrapper for multiple @UsedByContainer annotations. - */ -@Documented -@Target({ElementType.TYPE}) -@Retention(RetentionPolicy.RUNTIME) -public @interface UsedByContainers { - - UsedByContainer[] value(); - -} diff --git a/structurizr-annotations/src/com/structurizr/annotation/UsedByPeople.java b/structurizr-annotations/src/com/structurizr/annotation/UsedByPeople.java deleted file mode 100644 index 87138f3ac..000000000 --- a/structurizr-annotations/src/com/structurizr/annotation/UsedByPeople.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.structurizr.annotation; - -import java.lang.annotation.*; - -/** - * A wrapper for multiple @UsedByPerson annotations. - */ -@Documented -@Target({ElementType.TYPE}) -@Retention(RetentionPolicy.RUNTIME) -public @interface UsedByPeople { - - UsedByPerson[] value(); - -} diff --git a/structurizr-annotations/src/com/structurizr/annotation/UsedByPerson.java b/structurizr-annotations/src/com/structurizr/annotation/UsedByPerson.java deleted file mode 100644 index 551625a34..000000000 --- a/structurizr-annotations/src/com/structurizr/annotation/UsedByPerson.java +++ /dev/null @@ -1,20 +0,0 @@ -package com.structurizr.annotation; - -import java.lang.annotation.*; - -/** - * A type-level annotation that can be used to signify that the named - * person uses the component on which this annotation is placed, - * creating a relationship from the person to the component. - */ -@Documented -@Target({ElementType.TYPE}) -@Retention(RetentionPolicy.RUNTIME) -@Repeatable(UsedByPeople.class) -public @interface UsedByPerson { - - String name(); - String description() default ""; - String technology() default ""; - -} diff --git a/structurizr-annotations/src/com/structurizr/annotation/UsedBySoftwareSystem.java b/structurizr-annotations/src/com/structurizr/annotation/UsedBySoftwareSystem.java deleted file mode 100644 index 464e7b811..000000000 --- a/structurizr-annotations/src/com/structurizr/annotation/UsedBySoftwareSystem.java +++ /dev/null @@ -1,20 +0,0 @@ -package com.structurizr.annotation; - -import java.lang.annotation.*; - -/** - * A type-level annotation that can be used to signify that the named - * software system uses the component on which this annotation is placed, - * creating a relationship from the software system to the component. - */ -@Documented -@Target({ElementType.TYPE}) -@Retention(RetentionPolicy.RUNTIME) -@Repeatable(UsedBySoftwareSystems.class) -public @interface UsedBySoftwareSystem { - - String name(); - String description() default ""; - String technology() default ""; - -} diff --git a/structurizr-annotations/src/com/structurizr/annotation/UsedBySoftwareSystems.java b/structurizr-annotations/src/com/structurizr/annotation/UsedBySoftwareSystems.java deleted file mode 100644 index 137f31b41..000000000 --- a/structurizr-annotations/src/com/structurizr/annotation/UsedBySoftwareSystems.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.structurizr.annotation; - -import java.lang.annotation.*; - -/** - * A wrapper for multiple @UsedBySoftwareSystem annotations. - */ -@Documented -@Target({ElementType.TYPE}) -@Retention(RetentionPolicy.RUNTIME) -public @interface UsedBySoftwareSystems { - - UsedBySoftwareSystem[] value(); - -} diff --git a/structurizr-annotations/src/com/structurizr/annotation/UsesComponent.java b/structurizr-annotations/src/com/structurizr/annotation/UsesComponent.java deleted file mode 100644 index 4d55dbb96..000000000 --- a/structurizr-annotations/src/com/structurizr/annotation/UsesComponent.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.structurizr.annotation; - -import java.lang.annotation.*; - -/** - * A field-level annotation that can be used to supplement the existing relationship - * (i.e. add a description and/or technology) between two components. - */ -@Documented -@Target({ElementType.FIELD}) -@Retention(RetentionPolicy.RUNTIME) -public @interface UsesComponent { - - String description(); - String technology() default ""; - -} diff --git a/structurizr-annotations/src/com/structurizr/annotation/UsesContainer.java b/structurizr-annotations/src/com/structurizr/annotation/UsesContainer.java deleted file mode 100644 index 53df48342..000000000 --- a/structurizr-annotations/src/com/structurizr/annotation/UsesContainer.java +++ /dev/null @@ -1,20 +0,0 @@ -package com.structurizr.annotation; - -import java.lang.annotation.*; - -/** - * A type-level annotation that can be used to signify that the component - * on which this annotation is placed has a relationship to the named container, - * creating a relationship from the component to the container. - */ -@Documented -@Target({ElementType.TYPE}) -@Retention(RetentionPolicy.RUNTIME) -@Repeatable(UsesContainers.class) -public @interface UsesContainer { - - String name(); - String description() default ""; - String technology() default ""; - -} diff --git a/structurizr-annotations/src/com/structurizr/annotation/UsesContainers.java b/structurizr-annotations/src/com/structurizr/annotation/UsesContainers.java deleted file mode 100644 index 646c22c87..000000000 --- a/structurizr-annotations/src/com/structurizr/annotation/UsesContainers.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.structurizr.annotation; - -import java.lang.annotation.*; - -/** - * A wrapper for multiple @UsesContainer annotations. - */ -@Documented -@Target({ElementType.TYPE}) -@Retention(RetentionPolicy.RUNTIME) -public @interface UsesContainers { - - UsesContainer[] value(); - -} diff --git a/structurizr-annotations/src/com/structurizr/annotation/UsesSoftwareSystem.java b/structurizr-annotations/src/com/structurizr/annotation/UsesSoftwareSystem.java deleted file mode 100644 index a2a00065d..000000000 --- a/structurizr-annotations/src/com/structurizr/annotation/UsesSoftwareSystem.java +++ /dev/null @@ -1,20 +0,0 @@ -package com.structurizr.annotation; - -import java.lang.annotation.*; - -/** - * A type-level annotation that can be used to signify that the component - * on which this annotation is placed has a relationship to the named software system, - * creating a relationship from the component to the software system. - */ -@Documented -@Target({ElementType.TYPE}) -@Retention(RetentionPolicy.RUNTIME) -@Repeatable(UsesSoftwareSystems.class) -public @interface UsesSoftwareSystem { - - String name(); - String description() default ""; - String technology() default ""; - -} diff --git a/structurizr-annotations/src/com/structurizr/annotation/UsesSoftwareSystems.java b/structurizr-annotations/src/com/structurizr/annotation/UsesSoftwareSystems.java deleted file mode 100644 index b64124624..000000000 --- a/structurizr-annotations/src/com/structurizr/annotation/UsesSoftwareSystems.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.structurizr.annotation; - -import java.lang.annotation.*; - -/** - * A wrapper for multiple @UsesSoftwareSystem annotations. - */ -@Documented -@Target({ElementType.TYPE}) -@Retention(RetentionPolicy.RUNTIME) -public @interface UsesSoftwareSystems { - - UsesSoftwareSystem[] value(); - -} diff --git a/structurizr-autolayout/README.md b/structurizr-autolayout/README.md new file mode 100644 index 000000000..92e58e3e6 --- /dev/null +++ b/structurizr-autolayout/README.md @@ -0,0 +1,26 @@ +# structurizr-autolayout + +[![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), +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-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. +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-autolayout/build.gradle b/structurizr-autolayout/build.gradle new file mode 100644 index 000000000..23decccd6 --- /dev/null +++ b/structurizr-autolayout/build.gradle @@ -0,0 +1,9 @@ +dependencies { + + api project(':structurizr-export') + + testImplementation project(':structurizr-client') + +} + +description = 'Automatic layout facilities for Structurizr views' \ No newline at end of file diff --git a/structurizr-autolayout/src/main/java/com/structurizr/autolayout/graphviz/Constants.java b/structurizr-autolayout/src/main/java/com/structurizr/autolayout/graphviz/Constants.java new file mode 100644 index 000000000..34cf8efc5 --- /dev/null +++ b/structurizr-autolayout/src/main/java/com/structurizr/autolayout/graphviz/Constants.java @@ -0,0 +1,17 @@ +package com.structurizr.autolayout.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-autolayout/src/main/java/com/structurizr/autolayout/graphviz/DOTDiagram.java b/structurizr-autolayout/src/main/java/com/structurizr/autolayout/graphviz/DOTDiagram.java new file mode 100644 index 000000000..0e2cc9e7b --- /dev/null +++ b/structurizr-autolayout/src/main/java/com/structurizr/autolayout/graphviz/DOTDiagram.java @@ -0,0 +1,17 @@ +package com.structurizr.autolayout.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-autolayout/src/main/java/com/structurizr/autolayout/graphviz/DOTExporter.java b/structurizr-autolayout/src/main/java/com/structurizr/autolayout/graphviz/DOTExporter.java new file mode 100644 index 000000000..783ce7967 --- /dev/null +++ b/structurizr-autolayout/src/main/java/com/structurizr/autolayout/graphviz/DOTExporter.java @@ -0,0 +1,223 @@ +package com.structurizr.autolayout.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.ElementStyle; +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 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; + private final double rankSeparation; + private final double nodeSeparation; + + private int groupId = 1; + + DOTExporter(RankDirection rankDirection, double rankSeparation, double nodeSeparation) { + this.rankDirection = rankDirection != null ? rankDirection : RankDirection.TopBottom; + this.rankSeparation = rankSeparation / Constants.STRUCTURIZR_DPI; + this.nodeSeparation = nodeSeparation / Constants.STRUCTURIZR_DPI; + } + + void setLocale(Locale locale) { + this.locale = locale; + } + + @Override + protected void writeHeader(ModelView view, IndentingWriter writer) { + 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 startGroupBoundary(ModelView view, String group, IndentingWriter writer) { + writer.writeLine("subgraph \"cluster_group_" + (groupId++) + "\" {"); + writer.indent(); + writer.writeLine("margin=" + Integer.parseInt(getViewOrViewSetProperty(view, GROUP_PADDING_PROPERTY_NAME, DEFAULT_CLUSTER_INTERNAL_PADDING))); + } + + @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=" + Integer.parseInt(getViewOrViewSetProperty(view, BOUNDARY_PADDING_PROPERTY_NAME, DEFAULT_CLUSTER_INTERNAL_PADDING))); + } + + @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=" + Integer.parseInt(getViewOrViewSetProperty(view, BOUNDARY_PADDING_PROPERTY_NAME, DEFAULT_CLUSTER_INTERNAL_PADDING))); + } + + @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=" + Integer.parseInt(getViewOrViewSetProperty(view, DEPLOYMENT_NODE_PADDING_PROPERTY_NAME, DEFAULT_CLUSTER_INTERNAL_PADDING))); + } + + @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 = StyleUtils.findElementStyle(view, element); + + writer.writeLine(String.format(locale, "%s [width=%f,height=%f,fixedsize=true,id=%s,label=\"%s: %s\"]", + element.getId(), + 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()) + )); + } + + @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 String escape(String s) { + if (StringUtils.isNullOrEmpty(s)) { + return s; + } else { + return s.replaceAll("\"", "\\\\\""); + } + } + + 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; + } + +} \ No newline at end of file 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 new file mode 100644 index 000000000..efd3c5c7e --- /dev/null +++ b/structurizr-autolayout/src/main/java/com/structurizr/autolayout/graphviz/GraphvizAutomaticLayout.java @@ -0,0 +1,244 @@ +package com.structurizr.autolayout.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 = 300; + private double nodeSeparation = 300; + + 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(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; + } + + 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 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(view.getAutomaticLayout()).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(view.getAutomaticLayout()).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(view.getAutomaticLayout()).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(view.getAutomaticLayout()).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(view.getAutomaticLayout()).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(view.getAutomaticLayout()).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(view.getAutomaticLayout()).export(view); + writeFile(diagram); + runGraphviz(view); + createSVGReader().parseAndApplyLayout(view); + } + + public void apply(Workspace workspace) throws Exception { + for (CustomView view : workspace.getViews().getCustomViews()) { + if (view.getAutomaticLayout() != null && view.getAutomaticLayout().getImplementation() == AutomaticLayout.Implementation.Graphviz) { + apply(view); + } + } + + for (SystemLandscapeView view : workspace.getViews().getSystemLandscapeViews()) { + if (view.getAutomaticLayout() != null && view.getAutomaticLayout().getImplementation() == AutomaticLayout.Implementation.Graphviz) { + apply(view); + } + } + + for (SystemContextView view : workspace.getViews().getSystemContextViews()) { + if (view.getAutomaticLayout() != null && view.getAutomaticLayout().getImplementation() == AutomaticLayout.Implementation.Graphviz) { + apply(view); + } + } + + for (ContainerView view : workspace.getViews().getContainerViews()) { + if (view.getAutomaticLayout() != null && view.getAutomaticLayout().getImplementation() == AutomaticLayout.Implementation.Graphviz) { + apply(view); + } + } + + for (ComponentView view : workspace.getViews().getComponentViews()) { + if (view.getAutomaticLayout() != null && view.getAutomaticLayout().getImplementation() == AutomaticLayout.Implementation.Graphviz) { + apply(view); + } + } + + for (DynamicView view : workspace.getViews().getDynamicViews()) { + if (view.getAutomaticLayout() != null && view.getAutomaticLayout().getImplementation() == AutomaticLayout.Implementation.Graphviz) { + apply(view); + } + } + + for (DeploymentView view : workspace.getViews().getDeploymentViews()) { + if (view.getAutomaticLayout() != null && view.getAutomaticLayout().getImplementation() == AutomaticLayout.Implementation.Graphviz) { + apply(view); + } + } + } + +} \ No newline at end of file diff --git a/structurizr-autolayout/src/main/java/com/structurizr/autolayout/graphviz/RankDirection.java b/structurizr-autolayout/src/main/java/com/structurizr/autolayout/graphviz/RankDirection.java new file mode 100644 index 000000000..e3c4e366c --- /dev/null +++ b/structurizr-autolayout/src/main/java/com/structurizr/autolayout/graphviz/RankDirection.java @@ -0,0 +1,23 @@ +package com.structurizr.autolayout.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-autolayout/src/main/java/com/structurizr/autolayout/graphviz/SVGReader.java b/structurizr-autolayout/src/main/java/com/structurizr/autolayout/graphviz/SVGReader.java new file mode 100644 index 000000000..adc73c6d0 --- /dev/null +++ b/structurizr-autolayout/src/main/java/com/structurizr/autolayout/graphviz/SVGReader.java @@ -0,0 +1,203 @@ +package com.structurizr.autolayout.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 = StyleUtils.findElementStyle(view, 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 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-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/DOTExporterTests.java b/structurizr-autolayout/src/test/java/com/structurizr/autolayout/graphviz/DOTExporterTests.java new file mode 100644 index 000000000..3bc79c14a --- /dev/null +++ b/structurizr-autolayout/src/test/java/com/structurizr/autolayout/graphviz/DOTExporterTests.java @@ -0,0 +1,674 @@ +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; +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 { + 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 + public void test_writeSystemLandscapeView() { + Workspace workspace = new Workspace("Name", ""); + CustomElement box = workspace.getModel().addCustomElement("Box"); + Person user = workspace.getModel().addPerson("User", ""); + SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Software System", ""); + user.uses(softwareSystem, "Uses"); + + SystemLandscapeView view = workspace.getViews().createSystemLandscapeView("SystemLandscape", ""); + view.addAllElements(); + view.add(box); + + 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 [] + + 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 + public void test_writeSystemLandscapeViewWithGroupedElements() { + 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); + + 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=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 + 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", "/"); + + 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_writeSystemLandscapeViewInGermanLocale() { + // 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", ""); + SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Software System", ""); + user.uses(softwareSystem, "Uses"); + + SystemLandscapeView view = workspace.getViews().createSystemLandscapeView("SystemLandscape", ""); + view.addAllElements(); + view.add(box); + + 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 [] + + 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 + public void test_writeSystemContextView() { + Workspace workspace = new Workspace("Name", ""); + CustomElement box = workspace.getModel().addCustomElement("Box"); + 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(); + view.add(box); + + 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 [] + + 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 + public void test_writeSystemContextViewWithGroupedElements() { + 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); + + 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=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 + public void test_writeContainerViewWithGroupedElementsInASingleSoftwareSystem() { + 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 { + 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 + public void test_writeContainerViewWithGroupedElementsInMultipleSoftwareSystems() { + 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 { + 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 + 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", ""); + 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 { + 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_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 + 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(); + + 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.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=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] + + }""", 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 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 new file mode 100644 index 000000000..c499674a4 --- /dev/null +++ b/structurizr-autolayout/src/test/java/com/structurizr/autolayout/graphviz/GraphvizAutomaticLayoutTests.java @@ -0,0 +1,57 @@ +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; +import com.structurizr.view.AutomaticLayout; +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 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"); + + 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()); + + 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()); + 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-autolayout/src/test/java/com/structurizr/autolayout/graphviz/SVGReaderTests.java b/structurizr-autolayout/src/test/java/com/structurizr/autolayout/graphviz/SVGReaderTests.java new file mode 100644 index 000000000..c74a59937 --- /dev/null +++ b/structurizr-autolayout/src/test/java/com/structurizr/autolayout/graphviz/SVGReaderTests.java @@ -0,0 +1,63 @@ +package com.structurizr.autolayout.graphviz; + +import com.structurizr.Workspace; +import com.structurizr.autolayout.graphviz.SVGReader; +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", ""); + 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); + + return workspace; + } + +} \ No newline at end of file diff --git a/structurizr-autolayout/src/test/resources/graphviz/SystemContext.dot b/structurizr-autolayout/src/test/resources/graphviz/SystemContext.dot new file mode 100644 index 000000000..3766cb71a --- /dev/null +++ b/structurizr-autolayout/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-autolayout/src/test/resources/graphviz/SystemContext.dot.svg b/structurizr-autolayout/src/test/resources/graphviz/SystemContext.dot.svg new file mode 100644 index 000000000..216272c76 --- /dev/null +++ b/structurizr-autolayout/src/test/resources/graphviz/SystemContext.dot.svg @@ -0,0 +1,35 @@ + + + + + + +%3 + + +cluster_enterprise + + + + +2 + +2: Software System + + + +1 + +1: User + + + +1->2 + + + + + diff --git a/structurizr-autolayout/src/test/resources/structurizr-54915-workspace.json b/structurizr-autolayout/src/test/resources/structurizr-54915-workspace.json new file mode 100644 index 000000000..28e62ac94 --- /dev/null +++ b/structurizr-autolayout/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-client/README.md b/structurizr-client/README.md new file mode 100644 index 000000000..ea9734085 --- /dev/null +++ b/structurizr-client/README.md @@ -0,0 +1,11 @@ +# 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) + +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-client/build.gradle b/structurizr-client/build.gradle new file mode 100644 index 000000000..fd7c55622 --- /dev/null +++ b/structurizr-client/build.gradle @@ -0,0 +1,9 @@ +dependencies { + + api project(':structurizr-core') + + 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' + +} \ No newline at end of file diff --git a/structurizr-client/src/main/java/com/structurizr/api/AbstractApiClient.java b/structurizr-client/src/main/java/com/structurizr/api/AbstractApiClient.java new file mode 100644 index 000000000..df6086c1b --- /dev/null +++ b/structurizr-client/src/main/java/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/main/java/com/structurizr/api/AdminApiClient.java b/structurizr-client/src/main/java/com/structurizr/api/AdminApiClient.java new file mode 100644 index 000000000..55a9c79db --- /dev/null +++ b/structurizr-client/src/main/java/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 admin API client. + * + * @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/main/java/com/structurizr/api/ApiResponse.java b/structurizr-client/src/main/java/com/structurizr/api/ApiResponse.java new file mode 100644 index 000000000..b2849f027 --- /dev/null +++ b/structurizr-client/src/main/java/com/structurizr/api/ApiResponse.java @@ -0,0 +1,49 @@ +package com.structurizr.api; + +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; + +/** + * Represents a response returned by the Structurizr API. + */ +final class ApiResponse { + + private boolean success; + private String message; + private Long revision; + + ApiResponse() { + } + + public boolean isSuccess() { + return success; + } + + void setSuccess(boolean success) { + this.success = success; + } + + String getMessage() { + return message; + } + + void setMessage(String message) { + this.message = message; + } + + public Long getRevision() { + return revision; + } + + void setRevision(Long revision) { + this.revision = revision; + } + + static ApiResponse parse(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, ApiResponse.class); + } + +} \ No newline at end of file diff --git a/structurizr-client/src/main/java/com/structurizr/api/HashBasedMessageAuthenticationCode.java b/structurizr-client/src/main/java/com/structurizr/api/HashBasedMessageAuthenticationCode.java new file mode 100644 index 000000000..34f9ef077 --- /dev/null +++ b/structurizr-client/src/main/java/com/structurizr/api/HashBasedMessageAuthenticationCode.java @@ -0,0 +1,28 @@ +package com.structurizr.api; + +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; +import javax.xml.bind.DatatypeConverter; + +/** + * Uses a secret to create a message hash. + */ +final class HashBasedMessageAuthenticationCode { + + private static final String HMAC_SHA256_ALGORITHM = "HmacSHA256"; + + private String secret; + + HashBasedMessageAuthenticationCode(String secret) { + this.secret = secret; + } + + String generate(String content) throws Exception { + SecretKeySpec signingKey = new SecretKeySpec(secret.getBytes(), HMAC_SHA256_ALGORITHM); + Mac mac = Mac.getInstance(HMAC_SHA256_ALGORITHM); + mac.init(signingKey); + byte[] rawHmac = mac.doFinal(content.getBytes()); + return DatatypeConverter.printHexBinary(rawHmac).toLowerCase(); + } + +} \ No newline at end of file diff --git a/structurizr-core/src/com/structurizr/api/HmacAuthorizationHeader.java b/structurizr-client/src/main/java/com/structurizr/api/HmacAuthorizationHeader.java similarity index 91% rename from structurizr-core/src/com/structurizr/api/HmacAuthorizationHeader.java rename to structurizr-client/src/main/java/com/structurizr/api/HmacAuthorizationHeader.java index 103576d37..85ad204ad 100644 --- a/structurizr-core/src/com/structurizr/api/HmacAuthorizationHeader.java +++ b/structurizr-client/src/main/java/com/structurizr/api/HmacAuthorizationHeader.java @@ -2,6 +2,9 @@ import java.util.Base64; +/** + * Represents the header used for authorization purposes. + */ final class HmacAuthorizationHeader { private String apiKey; @@ -31,4 +34,4 @@ static HmacAuthorizationHeader parse(String s) { return new HmacAuthorizationHeader(apiKey, hmac); } -} +} \ No newline at end of file diff --git a/structurizr-core/src/com/structurizr/api/HmacContent.java b/structurizr-client/src/main/java/com/structurizr/api/HmacContent.java similarity index 86% rename from structurizr-core/src/com/structurizr/api/HmacContent.java rename to structurizr-client/src/main/java/com/structurizr/api/HmacContent.java index 4dd9430e1..380e43e3b 100644 --- a/structurizr-core/src/com/structurizr/api/HmacContent.java +++ b/structurizr-client/src/main/java/com/structurizr/api/HmacContent.java @@ -1,5 +1,8 @@ package com.structurizr.api; +/** + * Wraps up and combines a number of separate strings. + */ final class HmacContent { private String[] strings; @@ -19,4 +22,4 @@ public String toString() { return buf.toString(); } -} +} \ No newline at end of file diff --git a/structurizr-client/src/main/java/com/structurizr/api/HttpHeaders.java b/structurizr-client/src/main/java/com/structurizr/api/HttpHeaders.java new file mode 100644 index 000000000..b107fdea6 --- /dev/null +++ b/structurizr-client/src/main/java/com/structurizr/api/HttpHeaders.java @@ -0,0 +1,14 @@ +package com.structurizr.api; + +/** + * Constants representing the HTTP header names used in the Structurizr API. + */ +final class HttpHeaders { + + static final String USER_AGENT = "User-Agent"; + static final String AUTHORIZATION = "X-Authorization"; + static final String CONTENT_TYPE = "Content-Type"; + static final String CONTENT_MD5 = "Content-MD5"; + static final String NONCE = "Nonce"; + +} \ No newline at end of file diff --git a/structurizr-core/src/com/structurizr/api/Md5Digest.java b/structurizr-client/src/main/java/com/structurizr/api/Md5Digest.java similarity index 91% rename from structurizr-core/src/com/structurizr/api/Md5Digest.java rename to structurizr-client/src/main/java/com/structurizr/api/Md5Digest.java index c72c8e292..26a3a7ac8 100644 --- a/structurizr-core/src/com/structurizr/api/Md5Digest.java +++ b/structurizr-client/src/main/java/com/structurizr/api/Md5Digest.java @@ -3,6 +3,9 @@ import javax.xml.bind.DatatypeConverter; import java.security.MessageDigest; +/** + * Creates an MD5 digest of content. + */ final class Md5Digest { private static final String ALGORITHM = "MD5"; diff --git a/structurizr-core/src/com/structurizr/api/StructurizrClientException.java b/structurizr-client/src/main/java/com/structurizr/api/StructurizrClientException.java similarity index 81% rename from structurizr-core/src/com/structurizr/api/StructurizrClientException.java rename to structurizr-client/src/main/java/com/structurizr/api/StructurizrClientException.java index 8d504b225..254b98c4a 100644 --- a/structurizr-core/src/com/structurizr/api/StructurizrClientException.java +++ b/structurizr-client/src/main/java/com/structurizr/api/StructurizrClientException.java @@ -1,5 +1,8 @@ package com.structurizr.api; +/** + * Thrown by the StructurizrClient when something goes wrong. + */ public final class StructurizrClientException extends Exception { private static final long serialVersionUID = 1L; diff --git a/structurizr-client/src/main/java/com/structurizr/api/WorkspaceApiClient.java b/structurizr-client/src/main/java/com/structurizr/api/WorkspaceApiClient.java new file mode 100644 index 000000000..f45d0bdb6 --- /dev/null +++ b/structurizr-client/src/main/java/com/structurizr/api/WorkspaceApiClient.java @@ -0,0 +1,474 @@ +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.ParseException; +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; + +/** + * 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 MAIN_BRANCH = "main"; + + private String user; + + private String apiKey; + private String apiSecret; + private String branch = ""; + + private EncryptionStrategy encryptionStrategy; + + private IdGenerator idGenerator = null; + private boolean mergeFromRemote = true; + private File workspaceArchiveLocation = new File("."); + + protected WorkspaceApiClient() { + } + + /** + * 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; + } + + protected 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; + } + + protected 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; + } + + 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. + * + * @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)) { + String json = EntityUtils.toString(response.getEntity()); + debugResponse(response, json); + + ApiResponse apiResponse = ApiResponse.parse(json); + + 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 { + 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."); + } + + try (CloseableHttpClient httpClient = HttpClients.createSystem()) { + log.info("Getting workspace with ID " + workspaceId); + + HttpGet httpGet; + 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); + } + + addHeaders(httpGet, "", ""); + debugRequest(httpGet, null); + + try (CloseableHttpResponse response = httpClient.execute(httpGet)) { + String json = EntityUtils.toString(response.getEntity()); + debugResponse(response, json); + + if (response.getCode() == HttpStatus.SC_OK) { + archiveWorkspace(workspaceId, json); + + return 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()); + + HttpPut httpPut; + 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); + } + + 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()); + debugResponse(response, json); + + if (response.getCode() != HttpStatus.SC_OK) { + 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("Request"); + log.debug("HTTP method: " + httpRequest.getMethod()); + log.debug("Path: " + httpRequest.getPath()); + Header[] headers = httpRequest.getHeaders(); + for (Header header : headers) { + 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, 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 { + 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 + "-" + (StringUtils.isNullOrEmpty(branch) ? "" : (branch + "-")) + 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/main/java/com/structurizr/api/WorkspaceMetadata.java b/structurizr-client/src/main/java/com/structurizr/api/WorkspaceMetadata.java new file mode 100644 index 000000000..c6c5e1bba --- /dev/null +++ b/structurizr-client/src/main/java/com/structurizr/api/WorkspaceMetadata.java @@ -0,0 +1,102 @@ +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; + + private String[] branches; + + private WorkspaceUsers users; + + 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; + } + + 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 diff --git a/structurizr-client/src/main/java/com/structurizr/api/Workspaces.java b/structurizr-client/src/main/java/com/structurizr/api/Workspaces.java new file mode 100644 index 000000000..bf578ec89 --- /dev/null +++ b/structurizr-client/src/main/java/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 diff --git a/structurizr-core/src/com/structurizr/encryption/AesEncryptionStrategy.java b/structurizr-client/src/main/java/com/structurizr/encryption/AesEncryptionStrategy.java similarity index 97% rename from structurizr-core/src/com/structurizr/encryption/AesEncryptionStrategy.java rename to structurizr-client/src/main/java/com/structurizr/encryption/AesEncryptionStrategy.java index de7176e73..4f43b7862 100644 --- a/structurizr-core/src/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 { @@ -104,4 +105,4 @@ public String getIv() { return iv; } -} +} \ No newline at end of file diff --git a/structurizr-core/src/com/structurizr/encryption/EncryptedWorkspace.java b/structurizr-client/src/main/java/com/structurizr/encryption/EncryptedWorkspace.java similarity index 90% rename from structurizr-core/src/com/structurizr/encryption/EncryptedWorkspace.java rename to structurizr-client/src/main/java/com/structurizr/encryption/EncryptedWorkspace.java index 1df4e3c92..7a62ebb94 100644 --- a/structurizr-core/src/com/structurizr/encryption/EncryptedWorkspace.java +++ b/structurizr-client/src/main/java/com/structurizr/encryption/EncryptedWorkspace.java @@ -31,6 +31,9 @@ public final class EncryptedWorkspace extends AbstractWorkspace { * @throws Exception if an error occurs while creating the encrypted workspace */ public EncryptedWorkspace(Workspace workspace, EncryptionStrategy encryptionStrategy) throws Exception { + setConfiguration(workspace.getConfiguration()); + workspace.clearConfiguration(); + JsonWriter jsonWriter = new JsonWriter(false); StringWriter stringWriter = new StringWriter(); jsonWriter.write(workspace, stringWriter); @@ -39,6 +42,9 @@ public EncryptedWorkspace(Workspace workspace, EncryptionStrategy encryptionStra } public EncryptedWorkspace(Workspace workspace, String plaintext, EncryptionStrategy encryptionStrategy) throws Exception { + setConfiguration(workspace.getConfiguration()); + workspace.clearConfiguration(); + init(workspace, plaintext, encryptionStrategy); } @@ -49,8 +55,8 @@ private void init(Workspace workspace, String plaintext, EncryptionStrategy encr setName(workspace.getName()); setDescription(workspace.getDescription()); setVersion(workspace.getVersion()); - setThumbnail(workspace.getThumbnail()); - setApi(workspace.getApi()); + setLastModifiedUser(workspace.getLastModifiedUser()); + setLastModifiedAgent(workspace.getLastModifiedAgent()); this.plaintext = plaintext; this.ciphertext = encryptionStrategy.encrypt(plaintext); @@ -96,4 +102,4 @@ public void setEncryptionStrategy(EncryptionStrategy encryptionStrategy) { this.encryptionStrategy = encryptionStrategy; } -} +} \ No newline at end of file diff --git a/structurizr-core/src/com/structurizr/encryption/EncryptionLocation.java b/structurizr-client/src/main/java/com/structurizr/encryption/EncryptionLocation.java similarity index 97% rename from structurizr-core/src/com/structurizr/encryption/EncryptionLocation.java rename to structurizr-client/src/main/java/com/structurizr/encryption/EncryptionLocation.java index 99deb8531..7b8ad611f 100644 --- a/structurizr-core/src/com/structurizr/encryption/EncryptionLocation.java +++ b/structurizr-client/src/main/java/com/structurizr/encryption/EncryptionLocation.java @@ -5,4 +5,4 @@ public enum EncryptionLocation { Client, Server -} +} \ No newline at end of file diff --git a/structurizr-core/src/com/structurizr/encryption/EncryptionStrategy.java b/structurizr-client/src/main/java/com/structurizr/encryption/EncryptionStrategy.java similarity index 99% rename from structurizr-core/src/com/structurizr/encryption/EncryptionStrategy.java rename to structurizr-client/src/main/java/com/structurizr/encryption/EncryptionStrategy.java index 414a0b408..085afffef 100644 --- a/structurizr-core/src/com/structurizr/encryption/EncryptionStrategy.java +++ b/structurizr-client/src/main/java/com/structurizr/encryption/EncryptionStrategy.java @@ -44,5 +44,4 @@ public void setLocation(EncryptionLocation location) { public abstract String decrypt(String ciphertext) throws Exception; -} - +} \ No newline at end of file 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..b82a75a65 --- /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 HttpClientException("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 HttpClientException("The content from " + url + " could not be loaded: HTTP status=" + httpStatus); + } + } catch (Exception ioe) { + throw new HttpClientException("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/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 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..337668bee --- /dev/null +++ b/structurizr-client/src/main/java/com/structurizr/http/RemoteContent.java @@ -0,0 +1,39 @@ +package com.structurizr.http; + +/** + * Wrapper for remote content loaded via HTTP. + */ +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; + 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-core/src/com/structurizr/io/WorkspaceReader.java b/structurizr-client/src/main/java/com/structurizr/io/WorkspaceReader.java similarity index 100% rename from structurizr-core/src/com/structurizr/io/WorkspaceReader.java rename to structurizr-client/src/main/java/com/structurizr/io/WorkspaceReader.java diff --git a/structurizr-core/src/com/structurizr/io/WorkspaceReaderException.java b/structurizr-client/src/main/java/com/structurizr/io/WorkspaceReaderException.java similarity index 100% rename from structurizr-core/src/com/structurizr/io/WorkspaceReaderException.java rename to structurizr-client/src/main/java/com/structurizr/io/WorkspaceReaderException.java diff --git a/structurizr-core/src/com/structurizr/io/WorkspaceWriter.java b/structurizr-client/src/main/java/com/structurizr/io/WorkspaceWriter.java similarity index 100% rename from structurizr-core/src/com/structurizr/io/WorkspaceWriter.java rename to structurizr-client/src/main/java/com/structurizr/io/WorkspaceWriter.java diff --git a/structurizr-core/src/com/structurizr/io/WorkspaceWriterException.java b/structurizr-client/src/main/java/com/structurizr/io/WorkspaceWriterException.java similarity index 100% rename from structurizr-core/src/com/structurizr/io/WorkspaceWriterException.java rename to structurizr-client/src/main/java/com/structurizr/io/WorkspaceWriterException.java diff --git a/structurizr-client/src/main/java/com/structurizr/io/json/AbstractJsonReader.java b/structurizr-client/src/main/java/com/structurizr/io/json/AbstractJsonReader.java new file mode 100644 index 000000000..89a099ed7 --- /dev/null +++ b/structurizr-client/src/main/java/com/structurizr/io/json/AbstractJsonReader.java @@ -0,0 +1,17 @@ +package com.structurizr.io.json; + +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; + +class AbstractJsonReader { + + ObjectMapper createObjectMapper() { + ObjectMapper objectMapper = new ObjectMapper(); + objectMapper.enable(DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT); + objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + objectMapper.configure(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_AS_NULL, true); + + return objectMapper; + } + +} \ No newline at end of file diff --git a/structurizr-client/src/main/java/com/structurizr/io/json/AbstractJsonWriter.java b/structurizr-client/src/main/java/com/structurizr/io/json/AbstractJsonWriter.java new file mode 100644 index 000000000..104b4155e --- /dev/null +++ b/structurizr-client/src/main/java/com/structurizr/io/json/AbstractJsonWriter.java @@ -0,0 +1,35 @@ +package com.structurizr.io.json; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.databind.MapperFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.databind.json.JsonMapper; + +import java.text.SimpleDateFormat; +import java.util.TimeZone; + +class AbstractJsonWriter { + + private static final String ISO_DATE_TIME_FORMAT = "yyyy-MM-dd'T'HH:mm:ss'Z'"; + + ObjectMapper createObjectMapper(boolean indentOutput) { + ObjectMapper objectMapper = JsonMapper + .builder() + .enable(MapperFeature.SORT_PROPERTIES_ALPHABETICALLY).build(); + + if (indentOutput) { + objectMapper.enable(SerializationFeature.INDENT_OUTPUT); + } + + objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); + objectMapper.setSerializationInclusion(JsonInclude.Include.NON_EMPTY); + + SimpleDateFormat sdf = new SimpleDateFormat(AbstractJsonWriter.ISO_DATE_TIME_FORMAT); + sdf.setTimeZone(TimeZone.getTimeZone("UTC")); + objectMapper.setDateFormat(sdf); + + return objectMapper; + } + +} \ No newline at end of file diff --git a/structurizr-client/src/main/java/com/structurizr/io/json/EncryptedJsonReader.java b/structurizr-client/src/main/java/com/structurizr/io/json/EncryptedJsonReader.java new file mode 100644 index 000000000..c26786891 --- /dev/null +++ b/structurizr-client/src/main/java/com/structurizr/io/json/EncryptedJsonReader.java @@ -0,0 +1,32 @@ +package com.structurizr.io.json; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.structurizr.encryption.EncryptedWorkspace; +import com.structurizr.io.WorkspaceReaderException; + +import java.io.IOException; +import java.io.Reader; + +public final class EncryptedJsonReader extends AbstractJsonReader { + + public EncryptedJsonReader() { + } + + /** + * Reads and parses a workspace definition from a JSON document. + * + * @param reader a Reader on top of the workspace definition + * @return a Workspace object + * @throws WorkspaceReaderException if something goes wrong + */ + public EncryptedWorkspace read(Reader reader) throws WorkspaceReaderException { + try { + ObjectMapper objectMapper = createObjectMapper(); + + return objectMapper.readValue(reader, EncryptedWorkspace.class); + } catch (IOException ioe) { + throw new WorkspaceReaderException("Could not read JSON", ioe); + } + } + +} \ No newline at end of file diff --git a/structurizr-client/src/main/java/com/structurizr/io/json/EncryptedJsonWriter.java b/structurizr-client/src/main/java/com/structurizr/io/json/EncryptedJsonWriter.java new file mode 100644 index 000000000..972294112 --- /dev/null +++ b/structurizr-client/src/main/java/com/structurizr/io/json/EncryptedJsonWriter.java @@ -0,0 +1,40 @@ +package com.structurizr.io.json; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.structurizr.encryption.EncryptedWorkspace; +import com.structurizr.io.WorkspaceWriterException; + +import java.io.Writer; + +public final class EncryptedJsonWriter extends AbstractJsonWriter { + + private boolean indentOutput = true; + + public EncryptedJsonWriter(boolean indentOutput) { + this.indentOutput = indentOutput; + } + + /** + * Writes an encrypted workspace definition as a JSON string to the specified Writer object. + * + * @param workspace the Workspace object to write + * @param writer the Writer object to write the workspace to + * @throws WorkspaceWriterException if something goes wrong + */ + public void write(EncryptedWorkspace workspace, Writer writer) throws WorkspaceWriterException { + if (workspace == null) { + throw new IllegalArgumentException("EncryptedWorkspace cannot be null."); + } + if (writer == null) { + throw new IllegalArgumentException("Writer cannot be null."); + } + + try { + ObjectMapper objectMapper = createObjectMapper(indentOutput); + writer.write(objectMapper.writeValueAsString(workspace)); + } catch (Exception e) { + throw new WorkspaceWriterException("Could not write as JSON", e); + } + } + +} \ No newline at end of file diff --git a/structurizr-client/src/main/java/com/structurizr/io/json/JsonReader.java b/structurizr-client/src/main/java/com/structurizr/io/json/JsonReader.java new file mode 100644 index 000000000..3cca175a8 --- /dev/null +++ b/structurizr-client/src/main/java/com/structurizr/io/json/JsonReader.java @@ -0,0 +1,54 @@ +package com.structurizr.io.json; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.structurizr.Workspace; +import com.structurizr.io.WorkspaceReader; +import com.structurizr.io.WorkspaceReaderException; +import com.structurizr.model.IdGenerator; + +import java.io.IOException; +import java.io.Reader; + +/** + * Reads a workspace definition as JSON. + */ +public final class JsonReader extends AbstractJsonReader implements WorkspaceReader { + + private IdGenerator idGenerator = null; + + /** + * 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; + } + + /** + * Reads and parses a workspace definition from a JSON document. + * + * @param reader a Reader on top of the workspace definition + * @return a Workspace object + * @throws WorkspaceReaderException if something goes wrong + */ + public Workspace read(Reader reader) throws WorkspaceReaderException { + try { + ObjectMapper objectMapper = createObjectMapper(); + + Workspace workspace = objectMapper.readValue(reader, Workspace.class); + + if (idGenerator != null) { + workspace.getModel().setIdGenerator(idGenerator); + } + + workspace.hydrate(); + + return workspace; + } catch (IOException ioe) { + ioe.printStackTrace(); + throw new WorkspaceReaderException("Could not read JSON", ioe); + } + } + +} \ No newline at end of file diff --git a/structurizr-client/src/main/java/com/structurizr/io/json/JsonWriter.java b/structurizr-client/src/main/java/com/structurizr/io/json/JsonWriter.java new file mode 100644 index 000000000..c8e946355 --- /dev/null +++ b/structurizr-client/src/main/java/com/structurizr/io/json/JsonWriter.java @@ -0,0 +1,45 @@ +package com.structurizr.io.json; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.structurizr.Workspace; +import com.structurizr.io.WorkspaceWriter; +import com.structurizr.io.WorkspaceWriterException; + +import java.io.IOException; +import java.io.Writer; + +/** + * Writes a workspace definition as a JSON string. + */ +public final class JsonWriter extends AbstractJsonWriter implements WorkspaceWriter { + + private boolean indentOutput = true; + + public JsonWriter(boolean indentOutput) { + this.indentOutput = indentOutput; + } + + /** + * Writes a workspace definition as a JSON string to the specified Writer object. + * + * @param workspace the Workspace object to write + * @param writer the Writer object to write the workspace to + * @throws WorkspaceWriterException if something goes wrong + */ + public void write(Workspace workspace, Writer writer) throws WorkspaceWriterException { + if (workspace == null) { + throw new IllegalArgumentException("Workspace cannot be null."); + } + if (writer == null) { + throw new IllegalArgumentException("Writer cannot be null."); + } + + try { + ObjectMapper objectMapper = createObjectMapper(indentOutput); + writer.write(objectMapper.writeValueAsString(workspace)); + } catch (IOException ioe) { + throw new WorkspaceWriterException("Could not write as JSON", ioe); + } + } + +} \ No newline at end of file diff --git a/structurizr-client/src/main/java/com/structurizr/util/WorkspaceUtils.java b/structurizr-client/src/main/java/com/structurizr/util/WorkspaceUtils.java new file mode 100644 index 000000000..ba3243b04 --- /dev/null +++ b/structurizr-client/src/main/java/com/structurizr/util/WorkspaceUtils.java @@ -0,0 +1,107 @@ +package com.structurizr.util; + +import com.structurizr.Workspace; +import com.structurizr.io.json.JsonReader; +import com.structurizr.io.json.JsonWriter; + +import java.io.*; +import java.nio.charset.StandardCharsets; + +/** + * Some utility methods related to workspaces. + */ +public final class WorkspaceUtils { + + /** + * Loads a workspace from a JSON definition saved as a file. + * + * @param file a File representing the JSON definition + * @return a Workspace object + * @throws Exception if something goes wrong + */ + public static Workspace loadWorkspaceFromJson(File file) throws Exception { + if (file == null) { + throw new IllegalArgumentException("The path to a JSON file must be specified."); + } else if (!file.exists()) { + throw new IllegalArgumentException("The specified JSON file does not exist."); + } + + return new JsonReader().read(new FileReader(file)); + } + + /** + * Saves a workspace to a JSON definition as a file. + * + * @param workspace a Workspace object + * @param file a File representing the JSON definition + * @throws Exception if something goes wrong + */ + public static void saveWorkspaceToJson(Workspace workspace, File file) throws Exception { + if (workspace == null) { + throw new IllegalArgumentException("A workspace must be provided."); + } else if (file == null) { + throw new IllegalArgumentException("The path to a JSON file must be specified."); + } + + OutputStreamWriter writer = new OutputStreamWriter(new FileOutputStream(file), StandardCharsets.UTF_8); + new JsonWriter(true).write(workspace, writer); + writer.flush(); + writer.close(); + } + + /** + * Prints a workspace as JSON to stdout - useful for debugging purposes. + * + * @param workspace the workspace to print + */ + public static void printWorkspaceAsJson(Workspace workspace) { + if (workspace == null) { + throw new IllegalArgumentException("A workspace must be provided."); + } + + try { + System.out.println(toJson(workspace, true)); + } catch (Exception e) { + e.printStackTrace(); + } + } + + /** + * Serializes the specified workspace to a JSON string. + * + * @param workspace a Workspace instance + * @param indentOutput whether to indent the output (prettify) + * @return a JSON string + * @throws Exception if something goes wrong + */ + public static String toJson(Workspace workspace, boolean indentOutput) throws Exception { + if (workspace == null) { + throw new IllegalArgumentException("A workspace must be provided."); + } + + JsonWriter jsonWriter = new JsonWriter(indentOutput); + StringWriter stringWriter = new StringWriter(); + jsonWriter.write(workspace, stringWriter); + stringWriter.flush(); + stringWriter.close(); + + return stringWriter.toString(); + } + + /** + * Converts the specified JSON string to a Workspace instance. + * + * @param json the JSON definition of the workspace + * @return a Workspace instance + * @throws Exception if the JSON can not be deserialized + */ + public static Workspace fromJson(String json) throws Exception { + if (json == null || json.trim().length() == 0) { + throw new IllegalArgumentException("A JSON string must be provided."); + } + + StringReader stringReader = new StringReader(json); + return new JsonReader().read(stringReader); + } + +} \ 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 new file mode 100644 index 000000000..869c7bd5f --- /dev/null +++ b/structurizr-client/src/main/java/com/structurizr/view/ThemeUtils.java @@ -0,0 +1,182 @@ +package com.structurizr.view; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.databind.DeserializationFeature; +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.util.ImageUtils; +import com.structurizr.util.StringUtils; +import com.structurizr.util.Url; + +import java.io.*; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; + +/** + * Some utility methods for exporting themes to JSON. + */ +public final class ThemeUtils { + + 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. + * + * @param workspace a Workspace object + * @param file a File representing the JSON definition + * @throws Exception if something goes wrong + */ + public static void toJson(Workspace workspace, File file) throws Exception { + if (workspace == null) { + throw new IllegalArgumentException("A workspace must be provided."); + } else if (file == null) { + throw new IllegalArgumentException("The path to a file must be specified."); + } + + OutputStreamWriter writer = new OutputStreamWriter(new FileOutputStream(file), StandardCharsets.UTF_8); + write(workspace, writer); + } + + /** + * Serializes the theme (element and relationship styles) in the specified workspace to a JSON string. + * + * @param workspace a Workspace instance + * @return a JSON string + * @throws Exception if something goes wrong + */ + public static String toJson(Workspace workspace) throws Exception { + if (workspace == null) { + throw new IllegalArgumentException("A workspace must be provided."); + } + + StringWriter writer = new StringWriter(); + write(workspace, writer); + + return writer.toString(); + } + + /** + * 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. + * + * @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 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 { + 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)) { + RemoteContent remoteContent = httpClient.get(themeLocation); + 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); + + 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); + } else { + throw new RuntimeException(String.format("%s - expected content type of %s, actual content type is %s", themeLocation, RemoteContent.CONTENT_TYPE_JSON, remoteContent.getContentType())); + } + } + } + } + + /** + * 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 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 { + try { + ObjectMapper objectMapper = new ObjectMapper(); + objectMapper.enable(SerializationFeature.INDENT_OUTPUT); + objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); + objectMapper.setSerializationInclusion(JsonInclude.Include.NON_EMPTY); + objectMapper.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS); + + 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); + } + + writer.flush(); + writer.close(); + } + +} \ No newline at end of file diff --git a/structurizr-client/src/test/java/com/structurizr/api/ApiResponseTests.java b/structurizr-client/src/test/java/com/structurizr/api/ApiResponseTests.java new file mode 100644 index 000000000..d3546f95a --- /dev/null +++ b/structurizr-client/src/test/java/com/structurizr/api/ApiResponseTests.java @@ -0,0 +1,15 @@ +package com.structurizr.api; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class ApiResponseTests { + + @Test + void parse_createsAnApiErrorObjectWithTheSpecifiedErrorMessage() throws Exception { + ApiResponse apiResponse = ApiResponse.parse("{\"message\": \"Hello\"}"); + assertEquals("Hello", apiResponse.getMessage()); + } + +} diff --git a/structurizr-client/src/test/java/com/structurizr/api/BackwardsCompatibilityTests.java b/structurizr-client/src/test/java/com/structurizr/api/BackwardsCompatibilityTests.java new file mode 100644 index 000000000..58933c9c0 --- /dev/null +++ b/structurizr-client/src/test/java/com/structurizr/api/BackwardsCompatibilityTests.java @@ -0,0 +1,41 @@ +package com.structurizr.api; + +import com.structurizr.Workspace; +import com.structurizr.documentation.Format; +import com.structurizr.documentation.Section; +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("./src/test/resources/backwardsCompatibility"); + + @Test + void test() throws Exception { + for (File file : PATH_TO_WORKSPACE_FILES.listFiles(f -> f.getName().endsWith(".json"))) { + WorkspaceUtils.loadWorkspaceFromJson(file); + } + } + + @Test + void documentation() throws Exception { + Workspace workspace = new Workspace("Name", "Description"); + workspace.getDocumentation().addSection(new Section(Format.Markdown, "## Heading 1")); + + assertEquals(""" + {"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-core/test/unit/com/structurizr/api/HashBasedMessageAuthenticationCodeTests.java b/structurizr-client/src/test/java/com/structurizr/api/HashBasedMessageAuthenticationCodeTests.java similarity index 78% rename from structurizr-core/test/unit/com/structurizr/api/HashBasedMessageAuthenticationCodeTests.java rename to structurizr-client/src/test/java/com/structurizr/api/HashBasedMessageAuthenticationCodeTests.java index 4ac054a91..539d7a733 100644 --- a/structurizr-core/test/unit/com/structurizr/api/HashBasedMessageAuthenticationCodeTests.java +++ b/structurizr-client/src/test/java/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 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-core/test/unit/com/structurizr/api/HmacAuthorizationHeaderTests.java b/structurizr-client/src/test/java/com/structurizr/api/HmacAuthorizationHeaderTests.java similarity index 84% rename from structurizr-core/test/unit/com/structurizr/api/HmacAuthorizationHeaderTests.java rename to structurizr-client/src/test/java/com/structurizr/api/HmacAuthorizationHeaderTests.java index abf23d95c..86c79a6c6 100644 --- a/structurizr-core/test/unit/com/structurizr/api/HmacAuthorizationHeaderTests.java +++ b/structurizr-client/src/test/java/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 format() { header = new HmacAuthorizationHeader("apiKey", "f7bc83f430538424b13298e6aa6fb143ef4d59a14946175997479dbc2d1a3cd8"); assertEquals("apiKey:ZjdiYzgzZjQzMDUzODQyNGIxMzI5OGU2YWE2ZmIxNDNlZjRkNTlhMTQ5NDYxNzU5OTc0NzlkYmMyZDFhM2NkOA==", header.format()); } @Test - public void test_parse() { + void parse() { header = HmacAuthorizationHeader.parse("apiKey:ZjdiYzgzZjQzMDUzODQyNGIxMzI5OGU2YWE2ZmIxNDNlZjRkNTlhMTQ5NDYxNzU5OTc0NzlkYmMyZDFhM2NkOA=="); assertEquals("apiKey", header.getApiKey()); assertEquals("f7bc83f430538424b13298e6aa6fb143ef4d59a14946175997479dbc2d1a3cd8", header.getHmac()); diff --git a/structurizr-client/src/test/java/com/structurizr/api/HmacContentTests.java b/structurizr-client/src/test/java/com/structurizr/api/HmacContentTests.java new file mode 100644 index 000000000..ba81a6b96 --- /dev/null +++ b/structurizr-client/src/test/java/com/structurizr/api/HmacContentTests.java @@ -0,0 +1,20 @@ +package com.structurizr.api; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class HmacContentTests { + + + @Test + void toString_WhenThereAreNoStrings() { + assertEquals("", new HmacContent().toString()); + } + + @Test + void toString_WhenThereAreSomeStrings() { + assertEquals("String1\nString2\nString3\n", new HmacContent("String1", "String2", "String3").toString()); + } + +} diff --git a/structurizr-client/src/test/java/com/structurizr/api/Md5DigestTests.java b/structurizr-client/src/test/java/com/structurizr/api/Md5DigestTests.java new file mode 100644 index 000000000..220270830 --- /dev/null +++ b/structurizr-client/src/test/java/com/structurizr/api/Md5DigestTests.java @@ -0,0 +1,22 @@ +package com.structurizr.api; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class Md5DigestTests { + + private Md5Digest md5 = new Md5Digest(); + + @Test + void generate_TreatsNullAsEmptyContent() throws Exception { + assertEquals(md5.generate(null), md5.generate("")); + } + + @Test + void generate() throws Exception { + assertEquals("ed076287532e86365e841e92bfc50d8c", md5.generate("Hello World!")); + assertEquals("d41d8cd98f00b204e9800998ecf8427e", md5.generate("")); + } + +} \ No newline at end of file diff --git a/structurizr-client/src/test/java/com/structurizr/api/WorkspaceApiClientIntegrationTests.java b/structurizr-client/src/test/java/com/structurizr/api/WorkspaceApiClientIntegrationTests.java new file mode 100644 index 000000000..3b4628c6c --- /dev/null +++ b/structurizr-client/src/test/java/com/structurizr/api/WorkspaceApiClientIntegrationTests.java @@ -0,0 +1,124 @@ +package com.structurizr.api; + +import com.structurizr.Workspace; +import com.structurizr.encryption.AesEncryptionStrategy; +import com.structurizr.encryption.EncryptedWorkspace; +import com.structurizr.io.json.EncryptedJsonReader; +import com.structurizr.io.json.JsonReader; +import com.structurizr.model.Person; +import com.structurizr.model.SoftwareSystem; +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; +import java.io.FileReader; + +import static org.junit.jupiter.api.Assertions.*; + +public class WorkspaceApiClientIntegrationTests { + + private WorkspaceApiClient client; + private final File workspaceArchiveLocation = new File(System.getProperty("java.io.tmpdir"), "structurizr"); + + @BeforeEach + void setUp() { + client = new WorkspaceApiClient("81ace434-94a1-486f-a786-37bbeaa44e08", "a8673e21-7b6f-4f52-be65-adb7248be86b"); + client.setWorkspaceArchiveLocation(workspaceArchiveLocation); + workspaceArchiveLocation.mkdirs(); + clearWorkspaceArchive(); + assertEquals(0, workspaceArchiveLocation.listFiles().length); + client.setMergeFromRemote(false); + } + + @AfterEach + void tearDown() { + clearWorkspaceArchive(); + workspaceArchiveLocation.delete(); + } + + private void clearWorkspaceArchive() { + if (workspaceArchiveLocation.listFiles() != null) { + for (File file : workspaceArchiveLocation.listFiles()) { + file.delete(); + } + } + } + + private File getArchivedWorkspace() { + return workspaceArchiveLocation.listFiles()[0]; + } + + @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"); + Person person = workspace.getModel().addPerson("Person", "Description"); + person.uses(softwareSystem, "Uses"); + SystemContextView systemContextView = workspace.getViews().createSystemContextView(softwareSystem, "SystemContext", "Description"); + systemContextView.addAllElements(); + + client.putWorkspace(20081, workspace); + + workspace = client.getWorkspace(20081); + assertNotNull(workspace.getModel().getSoftwareSystemWithName("Software System")); + assertNotNull(workspace.getModel().getPersonWithName("Person")); + assertEquals(1, workspace.getModel().getRelationships().size()); + assertEquals(1, workspace.getViews().getSystemContextViews().size()); + + // and check the archive version is readable + Workspace archivedWorkspace = new JsonReader().read(new FileReader(getArchivedWorkspace())); + assertEquals(20081, archivedWorkspace.getId()); + assertEquals("Structurizr client library tests - without encryption", archivedWorkspace.getName()); + assertEquals(1, archivedWorkspace.getModel().getSoftwareSystems().size()); + + assertEquals(1, workspaceArchiveLocation.listFiles().length); + } + + @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"); + SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Software System", "Description"); + Person person = workspace.getModel().addPerson("Person", "Description"); + person.uses(softwareSystem, "Uses"); + SystemContextView systemContextView = workspace.getViews().createSystemContextView(softwareSystem, "SystemContext", "Description"); + systemContextView.addAllElements(); + + client.putWorkspace(20081, workspace); + + workspace = client.getWorkspace(20081); + assertNotNull(workspace.getModel().getSoftwareSystemWithName("Software System")); + assertNotNull(workspace.getModel().getPersonWithName("Person")); + assertEquals(1, workspace.getModel().getRelationships().size()); + assertEquals(1, workspace.getViews().getSystemContextViews().size()); + + // and check the archive version is readable + EncryptedWorkspace archivedWorkspace = new EncryptedJsonReader().read(new FileReader(getArchivedWorkspace())); + assertEquals(20081, archivedWorkspace.getId()); + assertEquals("Structurizr client library tests - with encryption", archivedWorkspace.getName()); + assertTrue(archivedWorkspace.getEncryptionStrategy() instanceof AesEncryptionStrategy); + + assertEquals(1, workspaceArchiveLocation.listFiles().length); + } + + @Test + @Tag("IntegrationTest") + void lockWorkspace() throws Exception { + client.unlockWorkspace(20081); + assertTrue(client.lockWorkspace(20081)); + } + + + @Test + @Tag("IntegrationTest") + void unlockWorkspace() throws Exception { + client.lockWorkspace(20081); + assertTrue(client.unlockWorkspace(20081)); + } + +} \ No newline at end of file diff --git a/structurizr-client/src/test/java/com/structurizr/api/WorkspaceApiClientTests.java b/structurizr-client/src/test/java/com/structurizr/api/WorkspaceApiClientTests.java new file mode 100644 index 000000000..0e2aeca51 --- /dev/null +++ b/structurizr-client/src/test/java/com/structurizr/api/WorkspaceApiClientTests.java @@ -0,0 +1,166 @@ +package com.structurizr.api; + +import com.structurizr.Workspace; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +public class WorkspaceApiClientTests { + + private WorkspaceApiClient client; + + @Test + void construction_WithTwoParameters() { + client = new WorkspaceApiClient("key", "secret"); + assertEquals("https://api.structurizr.com", client.getUrl()); + assertEquals("key", client.getApiKey()); + assertEquals("secret", client.getApiSecret()); + } + + @Test + void construction_WithThreeParameters() { + 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() { + 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 { + client = new WorkspaceApiClient(null, "secret"); + fail(); + } catch (IllegalArgumentException iae) { + assertEquals("The API key must not be null or empty.", iae.getMessage()); + } + } + + @Test + void construction_ThrowsAnException_WhenAnEmptyApiKeyIsUsed() { + try { + client = new WorkspaceApiClient(" ", "secret"); + fail(); + } catch (IllegalArgumentException iae) { + assertEquals("The API key must not be null or empty.", iae.getMessage()); + } + } + + @Test + void construction_ThrowsAnException_WhenANullApiSecretIsUsed() { + try { + client = new WorkspaceApiClient("key", null); + fail(); + } catch (IllegalArgumentException iae) { + assertEquals("The API secret must not be null or empty.", iae.getMessage()); + } + } + + @Test + void construction_ThrowsAnException_WhenAnEmptyApiSecretIsUsed() { + try { + client = new WorkspaceApiClient("key", " "); + fail(); + } catch (IllegalArgumentException iae) { + assertEquals("The API secret must not be null or empty.", iae.getMessage()); + } + } + + @Test + void construction_ThrowsAnException_WhenANullApiUrlIsUsed() { + try { + client = new WorkspaceApiClient(null, "key", "secret"); + fail(); + } catch (IllegalArgumentException iae) { + assertEquals("The API URL must not be null or empty.", iae.getMessage()); + } + } + + @Test + void construction_ThrowsAnException_WhenAnEmptyApiUrlIsUsed() { + try { + client = new WorkspaceApiClient(" ", "key", "secret"); + fail(); + } catch (IllegalArgumentException iae) { + assertEquals("The API URL must not be null or empty.", iae.getMessage()); + } + } + + @Test + void getWorkspace_ThrowsAnException_WhenTheWorkspaceIdIsNotValid() throws Exception { + try { + client = new WorkspaceApiClient("key", "secret"); + client.getWorkspace(0); + fail(); + } catch (IllegalArgumentException iae) { + assertEquals("The workspace ID must be a positive integer.", iae.getMessage()); + } + } + + @Test + void putWorkspace_ThrowsAnException_WhenTheWorkspaceIdIsNotValid() throws Exception { + try { + 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()); + } + } + + @Test + void putWorkspace_ThrowsAnException_WhenANullWorkspaceIsSpecified() throws Exception { + try { + client = new WorkspaceApiClient("key", "secret"); + client.putWorkspace(1234, null); + fail(); + } catch (IllegalArgumentException iae) { + assertEquals("The workspace must not be null.", iae.getMessage()); + } + } + + @Test + void getAgent() { + client = new WorkspaceApiClient("key", "secret"); + assertTrue(client.getAgent().startsWith("structurizr-java/")); + } + + @Test + void setAgent() { + client = new WorkspaceApiClient("key", "secret"); + client.setAgent("new_agent"); + assertEquals("new_agent", client.getAgent()); + } + + @Test + void setAgent_ThrowsAnException_WhenPassedNull() { + client = new WorkspaceApiClient("key", "secret"); + + try { + client.setAgent(null); + fail(); + } catch (Exception e) { + assertEquals("An agent must be provided.", e.getMessage()); + } + } + + @Test + void setAgent_ThrowsAnException_WhenPassedAnEmptyString() { + client = new WorkspaceApiClient("key", "secret"); + + try { + client.setAgent(" "); + fail(); + } catch (Exception e) { + assertEquals("An agent must be provided.", e.getMessage()); + } + } + +} \ No newline at end of file diff --git a/structurizr-client/src/test/java/com/structurizr/api/WorkspaceRulesValidationTests.java b/structurizr-client/src/test/java/com/structurizr/api/WorkspaceRulesValidationTests.java new file mode 100644 index 000000000..6ff8ebe84 --- /dev/null +++ b/structurizr-client/src/test/java/com/structurizr/api/WorkspaceRulesValidationTests.java @@ -0,0 +1,170 @@ +package com.structurizr.api; + +import com.structurizr.WorkspaceValidationException; +import com.structurizr.util.WorkspaceUtils; +import org.junit.jupiter.api.Test; + +import java.io.File; + +import static org.junit.jupiter.api.Assertions.*; + +public class WorkspaceRulesValidationTests { + + private static final File PATH_TO_WORKSPACE_FILES = new File("./src/test/resources/workspaceValidation"); + + @Test + void exceptionThrown_WhenViewKeysAreNotUnique() throws Exception { + try { + WorkspaceUtils.loadWorkspaceFromJson(new File(PATH_TO_WORKSPACE_FILES, "ViewKeysAreNotUnique.json")); + fail(); + } catch (WorkspaceValidationException we) { + assertEquals("A view with the key key already exists.", we.getMessage()); + } + } + + @Test + void exceptionThrown_WhenPeopleAndSoftwareSystemNamesAreNotUnique() throws Exception { + try { + WorkspaceUtils.loadWorkspaceFromJson(new File(PATH_TO_WORKSPACE_FILES, "PeopleAndSoftwareSystemNamesAreNotUnique.json")); + fail(); + } catch (WorkspaceValidationException we) { + assertEquals("A person or software system named \"Name\" already exists.", we.getMessage()); + } + } + + @Test + void exceptionThrown_WhenContainerNamesAreNotUnique() throws Exception { + try { + WorkspaceUtils.loadWorkspaceFromJson(new File(PATH_TO_WORKSPACE_FILES, "ContainerNamesAreNotUnique.json")); + fail(); + } catch (WorkspaceValidationException we) { + assertEquals("A container named \"Container\" already exists within \"Software System\".", we.getMessage()); + } + } + + @Test + void exceptionThrown_WhenComponentNamesAreNotUnique() throws Exception { + try { + WorkspaceUtils.loadWorkspaceFromJson(new File(PATH_TO_WORKSPACE_FILES, "ComponentNamesAreNotUnique.json")); + fail(); + } catch (WorkspaceValidationException we) { + assertEquals("A component named \"Component\" already exists within \"Container\".", we.getMessage()); + } + } + + @Test + void exceptionThrown_WhenTopLevelDeploymentNodeNamesAreNotUnique() throws Exception { + try { + WorkspaceUtils.loadWorkspaceFromJson(new File(PATH_TO_WORKSPACE_FILES, "TopLevelDeploymentNodeNamesAreNotUnique.json")); + fail(); + } catch (WorkspaceValidationException we) { + assertEquals("A top-level deployment node named \"Deployment Node\" already exists for the environment named \"Default\".", we.getMessage()); + } + } + + @Test + void exceptionNotThrown_WhenTopLevelDeploymentNodeNamesAreNotUniqueButTheyExistInDifferentEnvironments() throws Exception { + WorkspaceUtils.loadWorkspaceFromJson(new File(PATH_TO_WORKSPACE_FILES, "TopLevelDeploymentNodeNamesAreNotUniqueButTheyExistInDifferentEnvironments.json")); + } + + @Test + void exceptionThrown_WhenChildDeploymentNodeNamesAreNotUnique() throws Exception { + try { + WorkspaceUtils.loadWorkspaceFromJson(new File(PATH_TO_WORKSPACE_FILES, "ChildDeploymentNodeNamesAreNotUnique.json")); + fail(); + } catch (WorkspaceValidationException we) { + assertEquals("A deployment node named \"Child\" already exists within \"Deployment Node\".", we.getMessage()); + } + } + + @Test + void exceptionThrown_WhenRelationshipDescriptionsAreNotUnique() throws Exception { + try { + WorkspaceUtils.loadWorkspaceFromJson(new File(PATH_TO_WORKSPACE_FILES, "RelationshipDescriptionsAreNotUnique.json")); + fail(); + } catch (WorkspaceValidationException we) { + assertEquals("A relationship with the description \"Uses\" already exists between \"User\" and \"Software System\".", we.getMessage()); + } + } + + @Test + void exceptionThrown_WhenSoftwareSystemAssociatedWithSystemContextViewIsMissingFromTheModel() throws Exception { + try { + WorkspaceUtils.loadWorkspaceFromJson(new File(PATH_TO_WORKSPACE_FILES, "SoftwareSystemAssociatedWithSystemContextViewIsMissingFromTheModel.json")); + fail(); + } catch (WorkspaceValidationException we) { + assertEquals("The system context view with key \"SystemContext\" is associated with a software system (id=2), but that element does not exist in the model.", we.getMessage()); + } + } + + @Test + void exceptionThrown_WhenSoftwareSystemAssociatedWithContainerViewIsMissingFromTheModel() throws Exception { + try { + WorkspaceUtils.loadWorkspaceFromJson(new File(PATH_TO_WORKSPACE_FILES, "SoftwareSystemAssociatedWithContainerViewIsMissingFromTheModel.json")); + fail(); + } catch (WorkspaceValidationException we) { + assertEquals("The container view with key \"Containers\" is associated with a software system (id=2), but that element does not exist in the model.", we.getMessage()); + } + } + + @Test + void exceptionThrown_WhenContainerAssociatedWithComponentViewIsMissingFromTheModel() throws Exception { + try { + WorkspaceUtils.loadWorkspaceFromJson(new File(PATH_TO_WORKSPACE_FILES, "ContainerAssociatedWithComponentViewIsMissingFromTheModel.json")); + fail(); + } catch (WorkspaceValidationException we) { + assertEquals("The component view with key \"Components\" is associated with a container (id=3), but that element does not exist in the model.", we.getMessage()); + } + } + + @Test + void exceptionThrown_WhenElementAssociatedWithDynamicViewIsMissingFromTheModel() throws Exception { + try { + WorkspaceUtils.loadWorkspaceFromJson(new File(PATH_TO_WORKSPACE_FILES, "ElementAssociatedWithDynamicViewIsMissingFromTheModel.json")); + fail(); + } catch (WorkspaceValidationException we) { + assertEquals("The dynamic view with key \"Dynamic\" is associated with an element (id=2), but that element does not exist in the model.", we.getMessage()); + } + } + + @Test + void exceptionThrown_WhenSoftwareSystemAssociatedWithDeploymentViewIsMissingFromTheModel() throws Exception { + try { + WorkspaceUtils.loadWorkspaceFromJson(new File(PATH_TO_WORKSPACE_FILES, "SoftwareSystemAssociatedWithDeploymentViewIsMissingFromTheModel.json")); + fail(); + } catch (WorkspaceValidationException we) { + assertEquals("The deployment view with key \"Deployment\" is associated with a software system (id=2), but that element does not exist in the model.", we.getMessage()); + } + } + + @Test + void exceptionThrown_WhenViewAssociatedWithFilteredViewIsMissingFromTheWorkspace() throws Exception { + try { + WorkspaceUtils.loadWorkspaceFromJson(new File(PATH_TO_WORKSPACE_FILES, "ViewAssociatedWithFilteredViewIsMissingFromTheWorkspace.json")); + fail(); + } catch (WorkspaceValidationException we) { + assertEquals("The filtered view with key \"Filtered\" is based upon a view (key=SystemContext), but that view does not exist in the workspace.", we.getMessage()); + } + } + + @Test + void exceptionThrown_WhenElementReferencedByViewIsMissingFromTheModel() throws Exception { + try { + WorkspaceUtils.loadWorkspaceFromJson(new File(PATH_TO_WORKSPACE_FILES, "ElementReferencedByViewIsMissingFromTheModel.json")); + fail(); + } catch (WorkspaceValidationException we) { + assertEquals("The view with key \"SystemLandscape\" references an element (id=2), but that element does not exist in the model.", we.getMessage()); + } + } + + @Test + void exceptionThrown_WhenRelationshipReferencedByViewIsMissingFromTheModel() throws Exception { + try { + WorkspaceUtils.loadWorkspaceFromJson(new File(PATH_TO_WORKSPACE_FILES, "RelationshipReferencedByViewIsMissingFromTheModel.json")); + fail(); + } catch (WorkspaceValidationException we) { + assertEquals("The view with key \"SystemLandscape\" references a relationship (id=4), but that relationship does not exist in the model.", we.getMessage()); + } + } + +} \ No newline at end of file diff --git a/structurizr-client/src/test/java/com/structurizr/encryption/AesEncryptionStrategyTests.java b/structurizr-client/src/test/java/com/structurizr/encryption/AesEncryptionStrategyTests.java new file mode 100644 index 000000000..33171b4b3 --- /dev/null +++ b/structurizr-client/src/test/java/com/structurizr/encryption/AesEncryptionStrategyTests.java @@ -0,0 +1,100 @@ +package com.structurizr.encryption; + +import org.junit.jupiter.api.Test; + +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"); + assertEquals("Hello world", strategy.decrypt(ciphertext)); + } + + @Test + 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"); + assertNotEquals("Hello world", strategy.decrypt(ciphertext)); + } catch (Exception e) { + // this is okay + } + } + + @Test + void decrypt_doesNotDecryptTheCiphertext_WhenTheIncorrectIterationCountIsUsed() throws Exception { + 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"); + assertNotEquals("Hello world", strategy.decrypt(ciphertext)); + } catch (Exception e) { + // this is okay + } + } + + @Test + void decrypt_doesNotDecryptTheCiphertext_WhenTheIncorrectSaltIsUsed() throws Exception { + 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"); + assertNotEquals("Hello world", strategy.decrypt(ciphertext)); + } catch (Exception e) { + // this is okay + } + } + + @Test + void decrypt_doesNotDecryptTheCiphertext_WhenTheIncorrectIvIsUsed() throws Exception { + 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"); + assertNotEquals("Hello world", strategy.decrypt(ciphertext)); + } catch (Exception e) { + // this is okay + } + } + + @Test + void decrypt_doesNotDecryptTheCiphertext_WhenTheIncorrectPassphraseIsUsed() throws Exception { + 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"); + assertNotEquals("Hello world", strategy.decrypt(ciphertext)); + } catch (Exception e) { + // this is okay + } + } + +} \ No newline at end of file diff --git a/structurizr-client/src/test/java/com/structurizr/encryption/EncryptedWorkspaceTests.java b/structurizr-client/src/test/java/com/structurizr/encryption/EncryptedWorkspaceTests.java new file mode 100644 index 000000000..f52794996 --- /dev/null +++ b/structurizr-client/src/test/java/com/structurizr/encryption/EncryptedWorkspaceTests.java @@ -0,0 +1,108 @@ +package com.structurizr.encryption; + +import com.structurizr.Workspace; +import com.structurizr.configuration.Role; +import com.structurizr.io.json.JsonWriter; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.io.StringWriter; + +import static org.junit.jupiter.api.Assertions.*; + +public class EncryptedWorkspaceTests { + + private EncryptedWorkspace encryptedWorkspace; + private Workspace workspace; + private EncryptionStrategy encryptionStrategy; + + @BeforeEach + public void setUp() throws Exception { + workspace = new Workspace("Name", "Description"); + workspace.setVersion("1.2.3"); + workspace.setId(1234); + workspace.getConfiguration().addUser("user@domain.com", Role.ReadOnly); + workspace.setLastModifiedUser("simon"); + workspace.setLastModifiedAgent("structurizr-java"); + + encryptionStrategy = new MockEncryptionStrategy(); + } + + @Test + void construction_WhenTwoParametersAreSpecified() throws Exception { + encryptedWorkspace = new EncryptedWorkspace(workspace, encryptionStrategy); + + assertEquals("Name", encryptedWorkspace.getName()); + assertEquals("Description", encryptedWorkspace.getDescription()); + assertEquals("1.2.3", encryptedWorkspace.getVersion()); + assertEquals("simon", encryptedWorkspace.getLastModifiedUser()); + assertEquals("structurizr-java", encryptedWorkspace.getLastModifiedAgent()); + assertEquals(1234, encryptedWorkspace.getId()); + assertEquals("user@domain.com", encryptedWorkspace.getConfiguration().getUsers().iterator().next().getUsername()); + assertNotNull(workspace.getConfiguration()); + assertTrue(workspace.getConfiguration().getUsers().isEmpty()); + + assertSame(workspace, encryptedWorkspace.getWorkspace()); + assertSame(encryptionStrategy, encryptedWorkspace.getEncryptionStrategy()); + + JsonWriter jsonWriter = new JsonWriter(false); + StringWriter stringWriter = new StringWriter(); + jsonWriter.write(workspace, stringWriter); + + assertEquals(stringWriter.toString(), encryptedWorkspace.getPlaintext()); + assertEquals(encryptionStrategy.encrypt(stringWriter.toString()), encryptedWorkspace.getCiphertext()); + } + + @Test + void construction_WhenThreeParametersAreSpecified() throws Exception { + JsonWriter jsonWriter = new JsonWriter(false); + StringWriter stringWriter = new StringWriter(); + jsonWriter.write(workspace, stringWriter); + + encryptedWorkspace = new EncryptedWorkspace(workspace, stringWriter.toString(), encryptionStrategy); + + assertEquals("Name", encryptedWorkspace.getName()); + assertEquals("Description", encryptedWorkspace.getDescription()); + assertEquals("1.2.3", encryptedWorkspace.getVersion()); + assertEquals("simon", encryptedWorkspace.getLastModifiedUser()); + assertEquals("structurizr-java", encryptedWorkspace.getLastModifiedAgent()); + assertEquals(1234, encryptedWorkspace.getId()); + + assertSame(workspace, encryptedWorkspace.getWorkspace()); + assertSame(encryptionStrategy, encryptedWorkspace.getEncryptionStrategy()); + + assertEquals(stringWriter.toString(), encryptedWorkspace.getPlaintext()); + assertEquals(encryptionStrategy.encrypt(stringWriter.toString()), encryptedWorkspace.getCiphertext()); + } + + @Test + void getPlaintext_ReturnsTheDecryptedVersionOfTheCiphertext() throws Exception { + encryptedWorkspace = new EncryptedWorkspace(workspace, encryptionStrategy); + String cipherText = encryptedWorkspace.getCiphertext(); + + encryptedWorkspace = new EncryptedWorkspace(); + encryptedWorkspace.setEncryptionStrategy(encryptionStrategy); + encryptedWorkspace.setCiphertext(cipherText); + + assertEquals(new StringBuilder(cipherText).reverse().toString(), encryptedWorkspace.getPlaintext()); + } + + @Test + void getWorkspace_ReturnsTheWorkspace_WhenACipherextIsSpecified() throws Exception { + JsonWriter jsonWriter = new JsonWriter(false); + StringWriter stringWriter = new StringWriter(); + jsonWriter.write(workspace, stringWriter); + String expected = stringWriter.toString(); + + encryptedWorkspace = new EncryptedWorkspace(); + encryptedWorkspace.setEncryptionStrategy(encryptionStrategy); + encryptedWorkspace.setCiphertext(encryptionStrategy.encrypt(expected)); + + workspace = encryptedWorkspace.getWorkspace(); + assertEquals("Name", workspace.getName()); + stringWriter = new StringWriter(); + jsonWriter.write(workspace, stringWriter); + assertEquals(expected, stringWriter.toString()); + } + +} \ No newline at end of file diff --git a/structurizr-core/test/unit/com/structurizr/encryption/MockEncryptionStrategy.java b/structurizr-client/src/test/java/com/structurizr/encryption/MockEncryptionStrategy.java similarity index 100% rename from structurizr-core/test/unit/com/structurizr/encryption/MockEncryptionStrategy.java rename to structurizr-client/src/test/java/com/structurizr/encryption/MockEncryptionStrategy.java 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-client/src/test/java/com/structurizr/io/json/EncryptedJsonTests.java b/structurizr-client/src/test/java/com/structurizr/io/json/EncryptedJsonTests.java new file mode 100644 index 000000000..c598cd06c --- /dev/null +++ b/structurizr-client/src/test/java/com/structurizr/io/json/EncryptedJsonTests.java @@ -0,0 +1,39 @@ +package com.structurizr.io.json; + +import com.structurizr.Workspace; +import com.structurizr.encryption.AesEncryptionStrategy; +import com.structurizr.encryption.EncryptedWorkspace; +import org.junit.jupiter.api.Test; + +import java.io.StringReader; +import java.io.StringWriter; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class EncryptedJsonTests { + + @Test + void write_and_read() throws Exception { + final Workspace workspace1 = new Workspace("Name", "Description"); + + // output the model as JSON + EncryptedJsonWriter jsonWriter = new EncryptedJsonWriter(true); + AesEncryptionStrategy encryptionStrategy = new AesEncryptionStrategy("password"); + final EncryptedWorkspace encryptedWorkspace1 = new EncryptedWorkspace(workspace1, encryptionStrategy); + StringWriter stringWriter = new StringWriter(); + jsonWriter.write(encryptedWorkspace1, stringWriter); + + // and read it back again + EncryptedJsonReader jsonReader = new EncryptedJsonReader(); + StringReader stringReader = new StringReader(stringWriter.toString()); + final EncryptedWorkspace encryptedWorkspace2 = jsonReader.read(stringReader); + assertEquals("Name", encryptedWorkspace2.getName()); + assertEquals("Description", encryptedWorkspace2.getDescription()); + + encryptedWorkspace2.getEncryptionStrategy().setPassphrase(encryptionStrategy.getPassphrase()); + final Workspace workspace2 = encryptedWorkspace2.getWorkspace(); + assertEquals("Name", workspace2.getName()); + assertEquals("Description", workspace2.getDescription()); + } + +} \ No newline at end of file diff --git a/structurizr-client/src/test/java/com/structurizr/io/json/EncryptedJsonWriterTests.java b/structurizr-client/src/test/java/com/structurizr/io/json/EncryptedJsonWriterTests.java new file mode 100644 index 000000000..74b40614c --- /dev/null +++ b/structurizr-client/src/test/java/com/structurizr/io/json/EncryptedJsonWriterTests.java @@ -0,0 +1,39 @@ +package com.structurizr.io.json; + +import com.structurizr.Workspace; +import com.structurizr.encryption.AesEncryptionStrategy; +import com.structurizr.encryption.EncryptedWorkspace; +import org.junit.jupiter.api.Test; + +import java.io.StringWriter; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; + +public class EncryptedJsonWriterTests { + + @Test + void write_ThrowsAnIllegalArgumentException_WhenANullEncryptedWorkspaceIsSpecified() throws Exception { + try { + EncryptedJsonWriter writer = new EncryptedJsonWriter(true); + writer.write(null, new StringWriter()); + fail(); + } catch (IllegalArgumentException e) { + assertEquals("EncryptedWorkspace cannot be null.", e.getMessage()); + } + } + + @Test + void write_ThrowsAnIllegalArgumentException_WhenANullWriterIsSpecified() throws Exception { + try { + EncryptedJsonWriter writer = new EncryptedJsonWriter(true); + Workspace workspace = new Workspace("Name", "Description"); + EncryptedWorkspace encryptedWorkspace = new EncryptedWorkspace(workspace, new AesEncryptionStrategy("password")); + writer.write(encryptedWorkspace, null); + fail(); + } catch (IllegalArgumentException e) { + assertEquals("Writer cannot be null.", e.getMessage()); + } + } + +} diff --git a/structurizr-client/src/test/java/com/structurizr/io/json/JsonTests.java b/structurizr-client/src/test/java/com/structurizr/io/json/JsonTests.java new file mode 100644 index 000000000..f394c1ace --- /dev/null +++ b/structurizr-client/src/test/java/com/structurizr/io/json/JsonTests.java @@ -0,0 +1,93 @@ +package com.structurizr.io.json; + +import com.structurizr.Workspace; +import com.structurizr.model.*; +import org.junit.jupiter.api.Test; + +import java.io.StringReader; +import java.io.StringWriter; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +public class JsonTests { + + @Test + void write_and_read() throws Exception { + final Workspace workspace1 = new Workspace("Name", "Description"); + + // output the model as JSON + JsonWriter jsonWriter = new JsonWriter(true); + StringWriter stringWriter = new StringWriter(); + jsonWriter.write(workspace1, stringWriter); + + // and read it back again + JsonReader jsonReader = new JsonReader(); + StringReader stringReader = new StringReader(stringWriter.toString()); + final Workspace workspace2 = jsonReader.read(stringReader); + assertEquals("Name", workspace2.getName()); + assertEquals("Description", workspace2.getDescription()); + } + + @Test + void backwardsCompatibilityOfRenamingEnterpriseContextViewsToSystemLandscapeViews() throws Exception { + Workspace workspace = new Workspace("Name", "Description"); + workspace.getViews().createSystemLandscapeView("key", "description"); + + JsonWriter jsonWriter = new JsonWriter(false); + StringWriter stringWriter = new StringWriter(); + jsonWriter.write(workspace, stringWriter); + String workspaceAsJson = stringWriter.toString(); + workspaceAsJson = workspaceAsJson.replaceAll("systemLandscapeViews", "enterpriseContextViews"); + + JsonReader jsonReader = new JsonReader(); + StringReader stringReader = new StringReader(workspaceAsJson); + workspace = jsonReader.read(stringReader); + assertEquals(1, workspace.getViews().getSystemLandscapeViews().size()); + } + + @Test + void write_and_read_withCustomIdGenerator() throws Exception { + Workspace workspace1 = new Workspace("Name", "Description"); + workspace1.getModel().setIdGenerator(new CustomIdGenerator()); + Person user = workspace1.getModel().addPerson("User"); + SoftwareSystem softwareSystem = workspace1.getModel().addSoftwareSystem("Software System"); + user.uses(softwareSystem, "Uses"); + + // output the model as JSON + JsonWriter jsonWriter = new JsonWriter(true); + StringWriter stringWriter = new StringWriter(); + jsonWriter.write(workspace1, stringWriter); + + // and read it back again + JsonReader jsonReader = new JsonReader(); + jsonReader.setIdGenerator(new CustomIdGenerator()); + StringReader stringReader = new StringReader(stringWriter.toString()); + + Workspace workspace2 = jsonReader.read(stringReader); + assertEquals(user.getId(), workspace2.getModel().getPersonWithName("User").getId()); + assertNotNull(workspace2.getModel().getElement(user.getId())); + } + + class CustomIdGenerator implements IdGenerator { + + @Override + public String generateId(Element element) { + return UUID.randomUUID().toString(); + } + + @Override + public String generateId(Relationship relationship) { + return UUID.randomUUID().toString(); + } + + @Override + public void found(String id) { + + } + + } + + +} \ No newline at end of file diff --git a/structurizr-client/src/test/java/com/structurizr/io/json/JsonWriterTests.java b/structurizr-client/src/test/java/com/structurizr/io/json/JsonWriterTests.java new file mode 100644 index 000000000..4db62acb8 --- /dev/null +++ b/structurizr-client/src/test/java/com/structurizr/io/json/JsonWriterTests.java @@ -0,0 +1,36 @@ +package com.structurizr.io.json; + +import com.structurizr.Workspace; +import org.junit.jupiter.api.Test; + +import java.io.StringWriter; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; + +public class JsonWriterTests { + + @Test + void write_ThrowsAnIllegalArgumentException_WhenANullWorkspaceIsSpecified() throws Exception { + try { + JsonWriter writer = new JsonWriter(true); + writer.write(null, new StringWriter()); + fail(); + } catch (IllegalArgumentException e) { + assertEquals("Workspace cannot be null.", e.getMessage()); + } + } + + @Test + void write_ThrowsAnIllegalArgumentException_WhenANullWriterIsSpecified() throws Exception { + try { + JsonWriter writer = new JsonWriter(true); + Workspace workspace = new Workspace("Name", "Description"); + writer.write(workspace, null); + fail(); + } catch (IllegalArgumentException e) { + assertEquals("Writer cannot be null.", e.getMessage()); + } + } + +} 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/java/com/structurizr/util/WorkspaceUtilsTests.java b/structurizr-client/src/test/java/com/structurizr/util/WorkspaceUtilsTests.java new file mode 100644 index 000000000..e1258825a --- /dev/null +++ b/structurizr-client/src/test/java/com/structurizr/util/WorkspaceUtilsTests.java @@ -0,0 +1,174 @@ +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; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; + +public class WorkspaceUtilsTests { + + @Test + void loadWorkspaceFromJson_ThrowsAnException_WhenANullFileIsSpecified() { + try { + WorkspaceUtils.loadWorkspaceFromJson(null); + fail(); + } catch (Exception e) { + assertEquals("The path to a JSON file must be specified.", e.getMessage()); + } + } + + @Test + void loadWorkspaceFromJson_ThrowsAnException_WhenTheFileDoesNotExist() { + try { + WorkspaceUtils.loadWorkspaceFromJson(new File("test/unit/com/structurizr/util/other-workspace.json")); + fail(); + } catch (Exception e) { + assertEquals("The specified JSON file does not exist.", e.getMessage()); + } + } + + @Test + void saveWorkspaceToJson_ThrowsAnException_WhenANullWorkspaceIsSpecified() { + try { + WorkspaceUtils.saveWorkspaceToJson(null, null); + fail(); + } catch (Exception e) { + assertEquals("A workspace must be provided.", e.getMessage()); + } + } + + @Test + void saveWorkspaceToJson_ThrowsAnException_WhenANullFileIsSpecified() { + try { + WorkspaceUtils.saveWorkspaceToJson(new Workspace("Name", "Description"), null); + fail(); + } catch (Exception e) { + assertEquals("The path to a JSON file must be specified.", e.getMessage()); + } + } + + @Test + void saveWorkspaceToJson_and_loadWorkspaceFromJson() throws Exception { + File file = new File("build/workspace-utils.json"); + Workspace workspace = new Workspace("Name", "Description"); + WorkspaceUtils.saveWorkspaceToJson(workspace, file); + + workspace = WorkspaceUtils.loadWorkspaceFromJson(file); + assertEquals("Name", workspace.getName()); + } + + @Test + void toJson_ThrowsAnException_WhenANullWorkspaceIsProvided() throws Exception { + try { + WorkspaceUtils.toJson(null, true); + fail(); + } catch (IllegalArgumentException iae) { + assertEquals("A workspace must be provided.", iae.getMessage()); + } + } + + @Test + void toJson() throws Exception { + Workspace workspace = new Workspace("Name", "Description"); + String indentedOutput = WorkspaceUtils.toJson(workspace, true); + String unindentedOutput = WorkspaceUtils.toJson(workspace, false); + + assertEquals(""" + { + "configuration" : { }, + "description" : "Description", + "documentation" : { }, + "id" : 0, + "model" : { }, + "name" : "Name", + "views" : { + "configuration" : { + "branding" : { }, + "styles" : { }, + "terminology" : { } + } + } + }""", indentedOutput); + + assertEquals(""" + {"configuration":{},"description":"Description","documentation":{},"id":0,"model":{},"name":"Name","views":{"configuration":{"branding":{},"styles":{},"terminology":{}}}}""", unindentedOutput); + } + + @Test + void fromJson_ThrowsAnException_WhenANullJsonStringIsProvided() throws Exception { + try { + WorkspaceUtils.fromJson(null); + fail(); + } catch (IllegalArgumentException iae) { + assertEquals("A JSON string must be provided.", iae.getMessage()); + } + } + + @Test + void fromJson_ThrowsAnException_WhenAnEmptyJsonStringIsProvided() throws Exception { + try { + WorkspaceUtils.fromJson(" "); + fail(); + } catch (IllegalArgumentException iae) { + assertEquals("A JSON string must be provided.", iae.getMessage()); + } + } + + @Test + 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()); + } + + @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 + } + + @Test + void toJson_IsDeterministic() throws Exception { + File file = new File("./src/test/resources/structurizr-36141-workspace.json"); + Workspace workspace = WorkspaceUtils.loadWorkspaceFromJson(file); + + final String expectedJson = WorkspaceUtils.toJson(workspace, true); + + // serialize and deserialize many times ... the JSON should remain the same + for (int i = 0; i < 100; i++) { + String actualJson = WorkspaceUtils.toJson(workspace, true); + assertEquals(expectedJson, actualJson); + workspace = WorkspaceUtils.fromJson(actualJson); + } + + } + +} \ 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 new file mode 100644 index 000000000..9d61853f2 --- /dev/null +++ b/structurizr-client/src/test/java/com/structurizr/view/ThemeUtilsTests.java @@ -0,0 +1,190 @@ +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; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.Collection; + +import static org.junit.jupiter.api.Assertions.*; + +public class ThemeUtilsTests { + + @Test + void loadThemes_DoesNothingWhenNoThemesAreDefined() throws Exception { + Workspace workspace = new Workspace("Name", "Description"); + ThemeUtils.loadThemes(workspace); + + // there should still be zero styles in the workspace + assertEquals(0, workspace.getViews().getConfiguration().getStyles().getElements().size()); + } + + @Test + @Tag("IntegrationTest") + 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"); + workspace.getViews().getConfiguration().setThemes("https://static.structurizr.com/themes/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://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"); + assertEquals("{\n" + + " \"name\" : \"Name\",\n" + + " \"description\" : \"Description\"\n" + + "}", ThemeUtils.toJson(workspace)); + + 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" + + " \"elements\" : [ {\n" + + " \"tag\" : \"Element\",\n" + + " \"background\" : \"#ff0000\"\n" + + " } ],\n" + + " \"relationships\" : [ {\n" + + " \"tag\" : \"Relationship\",\n" + + " \"color\" : \"#ff0000\"\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)); + } + + @Test + void findElementStyle_WithThemes() { + Workspace workspace = new Workspace("Name", "Description"); + SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Name"); + workspace.getViews().getConfiguration().getStyles().addElementStyle("Element").shape(Shape.RoundedBox); + + // theme 1 + 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(new Theme(elementStyles, relationshipStyles)); + + // theme 2 + elementStyles = new ArrayList<>(); + relationshipStyles = new ArrayList<>(); + elementStyles.add(new ElementStyle("Element").background("#ff0000")); + workspace.getViews().getConfiguration().getStyles().addStylesFromTheme(new Theme(elementStyles, relationshipStyles)); + + ElementStyle style = workspace.getViews().getConfiguration().getStyles().findElementStyle(softwareSystem); + 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()); + assertEquals(Shape.RoundedBox, style.getShape()); // from workspace + assertNull(style.getIcon()); + assertEquals(Border.Solid, style.getBorder()); + assertEquals("#b20000", style.getStroke()); + assertEquals(Integer.valueOf(100), style.getOpacity()); + assertEquals(true, style.getMetadata()); + assertEquals(true, style.getDescription()); + } + + @Test + void findRelationshipStyle_WithThemes() { + Workspace workspace = new Workspace("Name", "Description"); + SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Name"); + Relationship relationship = softwareSystem.uses(softwareSystem, "Uses"); + workspace.getViews().getConfiguration().getStyles().addRelationshipStyle("Relationship").dashed(false); + + // theme 1 + Collection elementStyles = new ArrayList<>(); + Collection relationshipStyles = new ArrayList<>(); + relationshipStyles.add(new RelationshipStyle("Relationship").color("#ff0000").thickness(4)); + 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(new Theme(elementStyles, relationshipStyles)); + + RelationshipStyle style = workspace.getViews().getConfiguration().getStyles().findRelationshipStyle(relationship); + assertEquals(Integer.valueOf(4), style.getThickness()); // from theme 1 + assertEquals("#0000ff", style.getColor()); // from theme 2 + assertFalse(style.getDashed()); // from workspace + 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 + @Tag("IntegrationTest") + 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://static.structurizr.com/themes/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://static.structurizr.com/themes/amazon-web-services-2020.04.30/alexa-for-business.png", style.getIcon()); + } + +} \ No newline at end of file diff --git a/structurizr-client/src/test/resources/backwardsCompatibility/structurizr-31-workspace.json b/structurizr-client/src/test/resources/backwardsCompatibility/structurizr-31-workspace.json new file mode 100644 index 000000000..1d02a645b --- /dev/null +++ b/structurizr-client/src/test/resources/backwardsCompatibility/structurizr-31-workspace.json @@ -0,0 +1,389 @@ +{ + "id": 31, + "name": "Financial Risk System", + "description": "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", + "revision": 3, + "lastModifiedDate": "2019-07-23T06:42:29Z", + "lastModifiedUser": "", + "lastModifiedAgent": "structurizr-web/1602", + "model": { + "people": [ + { + "id": "2", + "tags": "Element,Person", + "name": "Business User", + "description": "A regular business user.", + "relationships": [ + { + "id": "3", + "tags": "Relationship,Synchronous", + "sourceId": "2", + "destinationId": "1", + "description": "Views reports using", + "interactionStyle": "Synchronous" + } + ], + "location": "Unspecified" + }, + { + "id": "4", + "tags": "Element,Person", + "name": "Configuration User", + "description": "A regular business user who can also configure the parameters used in the risk calculations.", + "relationships": [ + { + "id": "5", + "tags": "Relationship,Synchronous", + "sourceId": "4", + "destinationId": "1", + "description": "Configures parameters using", + "interactionStyle": "Synchronous" + } + ], + "location": "Unspecified" + } + ], + "softwareSystems": [ + { + "id": "17", + "tags": "Element,Software System", + "name": "Active Directory", + "description": "The bank's authentication and authorisation system.", + "location": "Unspecified" + }, + { + "id": "15", + "tags": "Element,Software System", + "name": "Central Monitoring Service", + "description": "The bank's central monitoring and alerting dashboard.", + "location": "Unspecified" + }, + { + "id": "12", + "tags": "Element,Software System", + "name": "E-mail system", + "description": "The bank's Microsoft Exchange system.", + "relationships": [ + { + "id": "14", + "tags": "Relationship,Asynchronous", + "sourceId": "12", + "destinationId": "2", + "description": "Sends a notification that a report is ready to", + "technology": "E-mail message", + "interactionStyle": "Asynchronous" + } + ], + "location": "Unspecified" + }, + { + "id": "1", + "tags": "Element,Software System,Risk System", + "name": "Financial Risk System", + "description": "Calculates the bank's exposure to risk for product X.", + "relationships": [ + { + "id": "7", + "tags": "Relationship,Synchronous", + "sourceId": "1", + "destinationId": "6", + "description": "Gets trade data from", + "interactionStyle": "Synchronous" + }, + { + "id": "11", + "tags": "Relationship,Synchronous,Future State", + "sourceId": "1", + "destinationId": "10", + "description": "Gets counterparty data from", + "interactionStyle": "Synchronous" + }, + { + "id": "13", + "tags": "Relationship,Synchronous", + "sourceId": "1", + "destinationId": "12", + "description": "Sends a notification that a report is ready to", + "interactionStyle": "Synchronous" + }, + { + "id": "18", + "tags": "Relationship,Synchronous", + "sourceId": "1", + "destinationId": "17", + "description": "Uses for user authentication and authorisation", + "interactionStyle": "Synchronous" + }, + { + "id": "9", + "tags": "Relationship,Synchronous", + "sourceId": "1", + "destinationId": "8", + "description": "Gets counterparty data from", + "interactionStyle": "Synchronous" + }, + { + "id": "16", + "tags": "Relationship,Asynchronous,Alert", + "sourceId": "1", + "destinationId": "15", + "description": "Sends critical failure alerts to", + "technology": "SNMP", + "interactionStyle": "Asynchronous" + } + ], + "location": "Unspecified" + }, + { + "id": "8", + "tags": "Element,Software System", + "name": "Reference Data System", + "description": "Manages reference data for all counterparties the bank interacts with.", + "location": "Unspecified" + }, + { + "id": "10", + "tags": "Element,Software System,Future State", + "name": "Reference Data System v2.0", + "description": "Manages reference data for all counterparties the bank interacts with.", + "location": "Unspecified" + }, + { + "id": "6", + "tags": "Element,Software System", + "name": "Trade Data System", + "description": "The system of record for trades of type X.", + "location": "Unspecified" + } + ], + "customElements": [], + "deploymentNodes": [] + }, + "documentation": { + "sections": [ + { + "elementId": "1", + "type": "Context", + "title": "Context", + "order": 1, + "format": "AsciiDoc", + "content": "== Context\n\nA global investment bank based in London, New York and Singapore trades (buys and sells) financial products with other banks (counterparties). When share prices on the stock markets move up or down, the bank either makes money or loses it. At the end of the working day, the bank needs to gain a view of how much risk they are exposed to (e.g. of losing money) by running some calculations on the data held about their trades. The bank has an existing Trade Data System (TDS) and Reference Data System (RDS) but need a new Risk System.\n\nimage::https://structurizr.com/share/31/images/Context.png[]\n\n=== Trade Data System\n\nThe Trade Data System maintains a store of all trades made by the bank. It is already configured to generate a file-based XML export of trade data at the close of business (5pm) in New York. The export includes the following information for every trade made by the bank:\n\n* Trade ID\n* Date\n* Current trade value in US dollars\n* Counterparty ID\n\n=== Reference Data System\n\nThe Reference Data System maintains all of the reference data needed by the bank. This includes information about counterparties; each of which represents an individual, a bank, etc. A file-based XML export is also available and includes basic information about each counterparty. A new organisation-wide reference data system is due for completion in the next 3 months, with the current system eventually being decommissioned." + }, + { + "elementId": "1", + "type": "Functional Overview", + "title": "Functional Overview", + "order": 2, + "format": "Markdown", + "content": "## Functional Overview\n\nThe high-level functional requirements for the new Risk System are as follows.\n\n![Functional overview](images/functional-overview.png)\n\n1. Import trade data from the Trade Data System.\n2. Import counterparty data from the Reference Data System.\n3. Join the two sets of data together, enriching the trade data with information about the counterparty.\n4. For each counterparty, calculate the risk that the bank is exposed to.\n5. Generate a report that can be imported into Microsoft Excel containing the risk figures for all counterparties known by the bank.\n6. Distribute the report to the business users before the start of the next trading day (9am) in Singapore.\n7. Provide a way for a subset of the business users to configure and maintain the external parameters used by the risk calculations.\n" + }, + { + "elementId": "1", + "type": "Quality Attributes", + "title": "Quality Attributes", + "order": 3, + "format": "Markdown", + "content": "## Quality Attributes\n\nThe quality attributes for the new Financial Risk System are as follows.\n\n### Performance\n\n- Risk reports must be generated before 9am the following business day in Singapore.\n \n### Scalability\n- The system must be able to cope with trade volumes for the next 5 years.\n- The Trade Data System export includes approximately 5000 trades now and it is anticipated that there will be an additional 10 trades per day.\n- The Reference Data System counterparty export includes approximately 20,000 counterparties and growth will be negligible.\n- There are 40-50 business users around the world that need access to the report.\n\n### Availability\n\n- Risk reports should be available to users 24x7, but a small amount of downtime (less than 30 minutes per day) can be tolerated.\n\n### Failover\n\n- Manual failover is sufficient for all system components, provided that the availability targets can be met.\n\n### Security\n\n- This system must follow bank policy that states system access is restricted to authenticated and authorised users only.\n- Reports must only be distributed to authorised users.\n- Only a subset of the authorised users are permitted to modify the parameters used in the risk calculations.\n- Although desirable, there are no single sign-on requirements (e.g. integration with Active Directory, LDAP, etc).\n- All access to the system and reports will be within the confines of the bank's global network.\n\n### Audit\n\n- The following events must be recorded in the system audit logs:\n - Report generation.\n - Modification of risk calculation parameters.\n- It must be possible to understand the input data that was used in calculating risk.\n\n### Fault Tolerance and Resilience\n\n- The system should take appropriate steps to recover from an error if possible, but all errors should be logged.\n- Errors preventing a counterparty risk calculation being completed should be logged and the process should continue.\n\n### Internationalization and Localization\n\n- All user interfaces will be presented in English only.\n- All reports will be presented in English only.\n- All trading values and risk figures will be presented in US dollars only.\n\n### Monitoring and Management\n\n- A Simple Network Management Protocol (SNMP) trap should be sent to the bank's Central Monitoring Service in the following circumstances:\n - When there is a fatal error with a system component.\n - When reports have not been generated before 9am Singapore time.\n\n### Data Retention and Archiving\n\n- Input files used in the risk calculation process must be retained for 1 year.\n\n### Interoperability\n\n- Interfaces with existing data systems should conform to and use existing data formats." + } + ], + "images": [ + { + "name": "images/functional-overview.png", + "content": "iVBORw0KGgoAAAANSUhEUgAAAs4AAAGcCAYAAADJQiZNAABUuElEQVR42u3dB3xUVfr/cQkdQVAQ7G13YUUFbLgWFLDioiCCuooC/qXp/hRQpOiqFEWw0aQpRUG6IKAUJUhCC0hAEiCUYAIhBAMECcgIUZ5/nkPmzp3MTMqQDDOTz/u1z8vXkmTKyT3nfnPnnHPPEQAAAAD5OocmAAAAAAjOAAAAAMEZAAAAIDgDAAAABGcAAACA4AwAAAAQnAEAAACCMwAAAACCMwAAAEBwBgAAAAjOAAAAAMEZAAAAIDgDAAAABGcAAACA4AwAAACA4AwAAAAQnAEAAACCMwAAAEBwBgAAAAjOAAAAAMEZAAAAIDgDAAAABGcAAAAABGcAAACA4AwAAAAQnAEAAACCMwAAAEBwDjunTp2S33//XY4dO0ZRYVHHjx+nYwNFTPsV4wsVLqW5R/MPCM5e/fHHH7J8+XJ5//33pX379nL77bfLJZdcIpUrV5ZSpUrJOeecQ1FhVREREXLeeefJ5ZdfLvfcc4906tRJPv74Y1m7dq38+eefDApALtovtH9oP9H+ov1G+4/2I+1PjCtUuJXmH81Bmoc0F2k+0pykeUlzE8G5hMnIyJAxY8bIfffdJxUrVszzwKlUqZKce+65FBUWldfxrqVB4NFHH5WvvvqKq9Mo0fSqm/YD7Q/aL/LqN9qvGF+ocCnNPXldONTjXfOT5ijNUwTnMKZXDFq3bi3ly5d3C8f169eXl19+WcaOHWv+mkpJSZHMzEw+qkBY+uuvv+TIkSOSlJQkP/zwg4wcOdJcRatdu7bb4FilShV54YUXZPv27TQaSoxt27aZ416Pf3t/0P6h/UT7i/Yb7T/aj7Q/AeFG88/Ro0dNHtJcpPlIc5LmJXuo1jzVpk0bk68IzmFk1apV5q8j5y+6dOnS8uCDD8rkyZPl4MGD9BAgx969e2XUqFHmozn71I4nnnhCtm7dSgMhbOnxrce5feqF9gPtD9ovAJymuUnz0wMPPGDylLO/3H///SZvEZxD2K+//irt2rVz+xi6T58+sm/fPo58IB87duyQzp07W5/QlC1bVl5//XWzeAQIF3o89+zZ0xzfzitoetzr8Q8gb6mpqSZXOacz6dVozV2avwjOIWbGjBlSrVo184usUKGCvPXWW3L48GGOcsCPgbFr167WlThdFBUdHU3DIOTpcazHs/OTFT3O9XgHUDiar/73v/+ZvKX96fzzz5eZM2cSnEOBrvbUwc95lblZs2aya9cujmrgDK1fv15uvfVWa7rTu+++yzoAhCQ9bvX4dX7MrMe1Ht8AzozmLc1dzgymeSwcduEI2+B86NAha26mftymc9MAFB3dlqtv377W4hBdbFsStyZC6NLjVY9b58fKejyzDSNQtDR/Oaf53XHHHSafEZyDjK78rFu3rvklXXnllbJx40aOXKCYLF682JoK1aRJE7O7ABDs9DjV41WPWz1+lyxZQqMAxURzmOYx7W+azzSnEZyDxP79++Waa64xv5wbbriBOWpAAMTFxZnN8Z1XFNj3GcFMj089TvV41eM2Pj6eRgGKmeYxzWXa7/72t7+ZvEZwDoIrCA0aNLDmqbEAEAic5ORk64pC8+bNJSsri0ZB0NHj8t///rf1iaQetwACQ3OZc32M5rVQ/IQybIKzLvDQ/Zj1l1GnTh05cOAARygQYHrDiBo1aph++NJLL9EgCDovvviiOT71OOWGPkDgaT7TnKb9UHNbqC0sD5vgPHDgQPNLqFmzJlcQgLNI7xrlXAgSTlsQIfTp1qTOBePhfnczIJhpTrvwwgtNf9RdbQjOAbZy5UqzlZCuimaBB3D26Spq542GfvnlFxoEZ50eh84bM7DLEnD2aV7T3Kb5TXMcwTlATp48ae2goVsJAQgOzm2+dD4pcLY9/PDD5nhs06YNjQEECb3ToHOnDc1zBOcAGDx4sGn02rVry4kTJzgKgSCht1l1blM3d+5cGgRnjR5/zm3nwu32v0Ao09ym+U3755AhQwjOxe3gwYNSuXJl0+Dff/89RyAQZD799FPTP3WLSHbZwNmgx51zi1I9HgEEF81v2j81z2muIzgXI70Xuja2fgQHIPj89ddf1urpL7/8kgZBwOlx59xtSY9HAMHHOZVKcx3BuZjo3n/Oj4FXr17NUQcEqUmTJpl+eu2114bctkMIbXq86XGnx98XX3xBgwBBatWqVdZ0qmDf2zlkg/OYMWNMIzdu3JgjDghi+lH5FVdcYfrrsmXLaBAETGRkpHWjE6YKAcHtnnvuMf1V8x3BuRg4b5c6ZcoUjjYgyL311lumv7Zv357GQMC0a9fOHHdvv/02jQEEucmTJ5v+eueddxKci9quXbusieS///47RxsQ5Hbu3Gn6bJUqVeT48eM0CIqdHmfOxeOJiYk0CBDkNM85+6zmPIJzERo5cqRp2P/85z8caUCIuPnmm9kBBwGjN1fQ4+2WW26hMYAQoblO+63mPIJzEWrVqpVp2PHjx3OUASGiV69ept/27t2bxkDAjjf9L4DQ8Pnnn5t++/jjjxOci9IFF1xgGpZb+QKhw3kF8LbbbqMxUOz0ONPjTY87AKFBc5322+rVqxOci8revXtNo9asWZMjDAghv/32m+m75557LtvSoVjp8VWpUiVzvOlxByB0XHjhhabvat4jOBcB5/ZCjRo14ugCQkytWrVM/92zZw+NgWKjx5ceZxdddBGNAYQYzXfafzXvEZyLwKhRo0yDvvDCCxxdQIi5++67Tf9dunQpjYFi88MPP5jjTPeFBRBaNN9p/9W8R3AuAv369TMNqvvCAggtTz31lOm/06ZNozFQbKZOncrOS0CI0ttua//VvEdwLgKvvfaaadAPPviAowsIMR07djT9d9y4cTQGis3YsWPNcabHG4DQMmTIENN/Ne8RnItA586dQ+KWjAA89ejRw/Tfjz76iMZAsfnwww/NcabHG4DQMnr0aNN/Ne8RnItAhw4dTINOmDCBowsIMbqHs/bfQYMG0RgoNu+99x57hgMhSu/Rof33+eefJzgTnAGCM8EZBGcABGeCMwCCMwjOAAjOBGcABGcQnAEQnAnOAAjOIDgDIDgTnAEQnEFwBkBwJjgDIDgDBGeA4ExwJjgDBGeA4AwQnAnOBGeA4AwQnAEQnAnOAAjOIDgHoyyHQ7KysiSLpgDBmeAcjo5vnSNtW7SVTp06FaLaSsvOn8juszIyHpVv+reTJvXqSY+pPxfbs5zc+6N0a9tW2vpog7ZtW2bXKzJ8VpSkOxiMCM4gOJ/txLpbhnZ6Nntcymf87tZHhk2cKRsSDxXt06dvlH5tbzJto9Vq+E8cLGfeqhI15lVp0dbHOTr731tkn7+79x8qyxJ+pbkIzgTnQDiy/jNroCtMlSrdWNZmnjoLL3idNC4VUewD85H1nxaqLSbF7C26P2aSV8rg/oNNLUs+zkhHcAbBuQCD1kZ5tFTpQo3jjw34Torm7/7d0juivNtj3zJkNQfLGTsqo5pULfDv897uk2VfkV3QckjUxOEyePBgGTIpSsLl+hDBmeB85iFt6xxp0aSFtNWrq7ZqUq+s1RnrN3k019dbStOWvSX+bATn4xvlmYgyxR+cN7n+oChd/1G3v/K7deuW/Zf+vR6D1sDFO4o8tI+IPci5g+AMgnOhxsbcY5brCmVLueGcUm7jVrtJRfDJnS20l27wgnw1b5Esi+MKaFEE5wktLrB+Vy3cPk3oln0u6mRdSHJW2RvfkV1ZRfPcztBe/t5P5DDBmeBMcM7b1gkdrI44Lu73oDw5BCo4txrr/cTiSN0g/VrYrgZE3CALkk8W6XOP2PAb5w6CMwjOhRsbx+YVhg/Lwv6PW2NM+ftGSWbAnhv+BudS5Z6WeK+B2CHrZw10C8/Vun9XBHPMXc9d8bEiOEYIzgTncLdpdIcCXPXMkszMjOxyWP8/OWGDREdHS1xyrvlzjsOSGLfWfC0yMlKiY+IkNSOfD38cByQhNlZisysuLlHMt5/cnH9wzjwgcTHR5rmiY2IlMTXjzIJzngH9sExoW8P63pv7/uDjvRTk/We3p8Mhe6NGWo/3QXSSOBxZfj4ewRkokcE5v4sKtnE04txePq5QZkl6YvzpcTS7Yp1jsLeH+/VH68pnu7E/iS4N9By2Cv54WY7M7PNKpnVuycrYLbFmPI+TjCx/x/vc5yuRjNRdEhvjej2ZWXmfj2IjF8m8efNMRWa/lrzXtxT8/RYoOJd9NM/pkYfWf+l2EWdJqveLOPqeY/Q1ZZ83IiOzz9WJqR7TMHSBp8OxW4a2qGZdcU7ObrcsPx+P4ExwJjhb6XLj6cEyu5POjt0ko9tW9zK/7bAsHNbD95yszp/INi+DwdZ5H3p+f/bz9B3WP485zg5ZM/FNr89TrunLsizxSDEEZ5FTe7+3Pv40VwUcp9yv7hT0/Tvb08v3TUv4/Yzak+AMEJwLGshOpq6T3k0qex1juo/9wXb10SEzO93i9fvut72Ggj/eaeuHPmRd6YyNGudjbU0hx/uc8VUfY2p2WB5qW8hof/xpcYcKdj7KqQGz13v+IVHI91vQ39OGfMb2hb0b2qbgbHF/+wlL5JUmlXyu0xm+aKtH++eu8zrPsEJxYR6P4ExwJjh7GahzlzM4bxrdxu3fI+o1lnr13BewlLt3kOyzP/eUF/NdBKFB1f3kcFRmdrs135+bllCw8FyY4KzPPcV21Xlc3G+2dizE+88jOH+w9qDf7UlwBgjO7gPcOmtOctkbB8kB+5e2TveYB527qnafkRP+Dssn9arkeQ4o3ON5nn+8L0r3Y7zP43zl9hzlnpa1Ga6Aun5Ch3x/ZuCyFD/br2iD8/GtrqvOlR4b73oOLwtHmzSp73meiU7JMziXvSlnrnMhH4/gTHAmOOcxEHUfM1Nisv+aX6tb4zi2SceIslann21bLJKZvFZ616vkccVDr97aH6/HxOWS7sgSR+Zumdnvcbev2U8OSd/0dBv45sftNR8VZjkOuP1cRJV3ChQqCxecs9trgpf2KvT7zzIfTyZFua5uvDp/S/Z7zzj9EaIf7UlwBgjOdpmpG+T9Z1wLzu63735x6hd5JaKc9bXnhnwnqTkf0f+66Wu3P+zfzAmLjuwxKyPpRytIfRC504xZGTpo+fF43oKzXkGeHR0j0ZExkp7l53if63ylixgX5/zckeSVbl9zri05ufdbt088h0duzbnielTWTHnTtiAv548PP99vUQVnOe6agqNz150L+rZOecm2k8oM1xSTrMzs9/Gq7VOC1Tn/rNNkXFM1dDrPNv0958w1KezjEZwJzgRnbwNR9qDyRe6PuGxbx7Wb6rnjxPFNn3sMCGsGt3aF5tmeP7O034NeAu0+ead0BeuxVmTkHlwcblcnvk7Mf4u3Qgdnb+3lx/u3/7u5em1fHOjn4xGcgZIVnE9fAWziVt6uglZs6b4f/96l79kC9XLPp9g5x3oc/dg+y8tz2xc0+/t49vG0SufJuXZ08HO8t71GDdu5x8gjaz6yfuaN+b+YCxlL+zyQ5/nom24XW1fCV2Y/nt/tV2TBeaNbcHZecV4/+CHrfe/y+CGHtXtG7k9xfS0O9O/xCM4EZ4KzWye9qe93Xr7BIcmJiZKc7L5YICvLIZkZu2VK7wdyDQgH5P0m553+C7fSy146pPt8YmenPL5zuverJzYnbR9hdZ26JTDBudDv3/O53dvev8cjOAMlLzgXpG512wFD5yvXymeffocVpnSM3pblJTjbxj9/H88aT80it1O5wqef473tNXr9Odv0g9Pzs4/K6Bbnu85HXlLukYQF0r9PHxkwdqFknEn7FXNwdqTvlsTEZElNt180yhKHI1OS1k/zsW7I9dy5d17x7/EIzgRngnOuLYh8d5D0hBUycdgAs4eo3vHPY06Zc0CwDVyVn5vsfWXucc9dNexB0+x12aKFR9n3pS5IZy6a4FzI959vcPbv8QjOQMkKzg1a9pYxE8fImDHuNWxwX3mmXjmv6yfsQdFMP2jyqJextKnbOGOFQ6/B2f/Hc46nXq8M+zve217jByv2530+05+xnWvc5gvnEXL9br9iCs5uV+odByR63iTp162ztPDxCURBg7N/j0dwJjgTnAswp+6ozLR91OVzMYZzQCjQXqCHrcHJV3DOr+4v8uDsvkm9aweMQr7/fIOzf49HcAZKVnDOey9lhyy0jSOu8dB9HCvUOOMjOPv7eFZw9jKW+T3e+5hO4js4uy7kFOwGIGfQfkUUnO1tY7/4dGTr1z4XnvsTnP17PIIzwZngnG9wti/g0Orcf7Qsio6RuIREs5jgyKYvfV5xrtZ9rvf5X16e0z5YVB8wQ+LiYiUmxl4xEhsbl/3vWgme+4CeaXA+tNIaRHQRxW5/338+wdnfxyM4AyUsOOczZp3cOcfLDZ5swS9ni9HYXOOo/vf0OBonCfa9+vMLzoV8vIIG50KN915fYx7tZzsf5XUDkKysrDNvvyIKzmvef8i2sPyXnH/d7bZgsVzTjjJ21kKJyW6j03teH7XuRVCw4Ozv4xGcCc4E53wGatd8Lx1EvG0D57w7oTUgnNxldUh7AHUb8JO+9ZjjXKCQm7lLFunG9YvWmFXZRRecs9yu3tw8YLn/7z/P4Oz/4xGcAYKz+/e6piHoRQrnThHOT/PMGOLwPobsjF5sbgCyIuHXfIOzv49X0OBcqPG+sMHZvubGx/lo58zXXPOVHZn+t18RBOeTSUusCzg6x3pZ+unvtc8J14WWHn8A+LypmPfg7P/jEZwJzgTnfAbqw9bKWu+3Ct1tDUpmHpvj9HZsc53hMNfemE72kOqau2a7C1b5F3LdgOS0KNtuHPZ9lgsSnB+f5GsxoUOihrZ322PUOVj59/49n9v1Wv1/PIIzQHD29b32ubD2bTXbeRn3Tv36o3Xhwu0qpI9Q6u/j5RWc/R7vCx2cs+Qb2+4cA5flnhe9T96vV9ntdfjdfgUJzj5vuZ390pN/dFsYevMA1x1sj6z/1MunCy57l37o4+uu59Y53o4zfjyCM8GZ4JzPQJ3ldhejHlNX59zO1CF745Z53FXJuZVd7vlrIyJ3np6ykXVYltpCau7ntH9EVe7ed2SDc7WvY598M7id236bhd3HWQesiV99JRMnTrRqWP++8miuRTbuu3X49/5zP/djug9oRuYZPR7BGSA4uw9wPqYh2KadOe+K5wxMe+MWuIUztwsbvkKpn4+XZ3D2d7wvdHDWG4pMd9vHeWz0TnFkZYkjY5dMsN0x0dpVyt/2K0BwNueDwRPkK9t5aOKYYdK7U0v3m2Ll+mNC96J2hvUyDXrJ2uTTn1Y6MlJl6cQ33H5W50Uftj23dQU9+xy4OGGv6N3K/X88gjPBOcytt92hLq/g7Bx8vQ3UR9b7s4jDfeqDc8DKd+GBY5d1AxDXXfU8d5woyB7O3gJ8fvXc8B885mT79/7Fbb9m+1UTvx+P4AyUiOCc13ic+6qtdTMl/aQs9aT1pZ3f/M/rnVpz3/nO4SN05j5f+PN4+QVnv8b7/ILzEe/tt2Zo63wX+dnvNOhX++URnO07dRTkrof23+Vphwv3GLbdPnLfQfD07iL+Px7BmeAc1uydf5zP4Lw53+3otn7zrtfO1LL7eEnOTHXrgLcMcT6GQ6LGdPX6c0NmLZXRnW72/pxZ+2RKn1Zef04XMCxOPFLwc5BtH1BfVa9eY+k+eIJsSPb9uP6+/6WD3Qdr50Dv3+MRnIHwD86bC7Q9qHUlM2cBl7eP1Peu+cLjlsrO6jL8B88pBrZQ6u18UdjHc0558LYdnd/jve01fuFtut7J7dYam9ztt2mW93G3wbODJD795Bm/37x+T1NsvyevFXGDNG37ikxatN7342Zuk34tqnoNtZNikrJf70gvtzXPbrKkJW7vw5rW4+fjEZwJziiozH0SGxMt0dHRZuVtaobDbWBITc+QjPRUyXC4X7PVTdZjoiMlMjJSomPiJL2AnS8zfZft52LNSt+sUHz/GRmSnp4uGRmZ7q/fz8cjOANhHJyL3FFJjFtrxtHIyGiJjUuUDEcwPV6Ax3vHYUmI1cdPlYS4OElOP35W3q//siQ1cYM5b0TrbkyJ7jfSytRzRvY5R88fbu2X5cg+n6Rnn4syJNPtnOLn4xGcCc4ACM4gOAMIfgRngjMAgjMIzgAIzgRnAARnEJwBEJwJzgAIziA4AyA4E5wBEJxBcAZAcCY4AyA4g+BMYwAEZ4IzwRkgOAMEZ4DgTHAmOAMEZ4DgDBCcCc4E56CV5ZD01FRJzq7U7ErPfUMPgOAMgnOJ58hIl9TkZHOeSE1Nz3XDDYDgTHAO+8C8T74Z1kNuOKeU5605SzeWAWMXyj4HzQSCMwjOJdne2AXySosbvd5Suumz/WRx7F4aCQRngnN4O5m60u0e9z4r4gaZFnckoK/tePJKGdx/sKllycf5ZRGcAYLzWZElUUPb53+eyK6bus+QzIC+NodETRwugwcPliGTooRrPARngjPBuRgdkKFNqrquLpd9VIbPWiixcQmSELdWZg7r4TYgRpR/QeIdpwL26o6s/9R67hGxB/l1EZwBgvNZcGjNR27ngpbdh8rimFhJSEiQmMjZ0rvFeW5fbzfp5wC+uqMyKuc8Vv7eT+Qwvy6CM8GZ4Fxcjm/90hroKrb8RPZ5maaWlb5OXqlX0fq+NxanBC44b/rMFZw3/MYvjOAMEJwDziETWlxgjcUfLPN2DsiSNRNfdV1kqfKO7AtgcHa+voqPjQrw1W4QnAnOJcrOma9ZA92bKw4W6GrD/UNWm0EyMyNTMjO1fH8wlpX99Yzs78u9biQrc5/ERkfKokWLZNG8ebIoepOkO9wH4UyHQ/ZGjXQN1tFJ4vC1ACXzgMTFREt0dHbFxEpiaob31+PIeT32n4uNMT8XE5sgmfaHdxyQBOtrcZKayeIXgjMIziXQyV3ySkQ5KxD7vqJ7WN6vV9laG7Mi49TpMVfPE17OA/ZgnpmR4eVckiWpiRskUs8T2TVv3iKJS/w115juyD4v7JahLapZV5yTsx8n6wzOE+b843zNOf+SnhgvMfpz0TGSkOo+ZTE9eZv1tbjEVBbUE5wJzuFs/dA2ruC8bL/vb8zcLL1bNJUWTVrIuLXZ33dyu3SMKGsNkGszvUzfOL7Rmjtd/j7nVYCjsnTMqz7nUI9dlnT6Z49slMalIrx+37SE390G3DUT3/T6feWavizLEo/ker8Pma9V7T5ZYhaN8FwIWfZRWZZ8SLbOetfrY74xewsHDcEZBOeS5cg6ayzX4Hwgj2/d9NWb0rRFC2nZfby54rxzykuuiy7DV3v/mdFtPKbkHUn8UV5pUsnrOHxj51Gy2+E+pueu8zrPsM11Ltx5wn7+GbJoqQx95gKPn2s15AfJyIiXfk2qeHytTINeEp95iuOG4ExwDkf2K8kaGqdGb5WCXlhdM7R1ntM3fo1yPXarnAHTPkBq1W/SxGMnjxFrD+YZnD9Y67wyflRmdrs134Uq0xKO2AboDgVa3JLn4yWySJHgDIJzSeK+Fuax/pNlW3oBJ0QccoXusjd6m75xwHWVOvsctDbjlNtFF+dFlSZN6ruNw865zL6Cc9mbnHOdC3+e0Od/JqLMGZ0n3IM7CM4E57Bx6tcfvQbUZ7v1k4mzFpqPnXzNxDi5c3oeg0SWfGMbrL5OPpn9b7uld0T5nL/ye8kG6+OuLNm6dIQVoCs9Nl4ycz4qS4r60HqMV+dvEUdmhhXsk77p6Qr95Z6W+XF7zUeBWY4DMrPf417n2uUOzm98FSUZ2T90fO9K6wq6s+7rO1kS0zMkIz3Z7fFaDf+JA4fgDIJziRL1vmdALdv0Gek/bIKZapfqM0g7ZGanWj4vPJxM+tZ1Huky10xzSFr4P9unfD9Z55asjG0ytO1N1tfGxf1mpgNmZrqmakSc20u2mSmCDr/PE7mDs56vViT+qmcliRrT1eNTyqnRO7OfL0OSYr+2Ar/5I4CrzgRngnN4OrT+S6/7N7vtz9nyFZm0aFOucJzrSoF9kDi+2Rp49MqA+WjviOsqws0Dlnu8jjX9Hzw9gFV6WXblhOPjmz53DZJuiwP3yTulK1jPrXPpPAZre3DPGaztwbnrVPdpF78ufc/6WuXnJru/V9sVEIIzwRkE55LngEzodEue54mIeo2le/8JEp/qHo7dP310Hz83TXCNyR/EnJ4uuD7nk0mdBvhj7rH90ErrYk/X2Tty/tHX4kD/zhP24Kzno21un8Luk371XFNIvnCbOuj6VFWfbwPBmeBMcA5j2X99R8+bJN3atsxzYNSP2nbZtqPbahv0BtpWWh+yDZRWQD3i/vFb3zHfmMUZ9jEpKytLHA7Xwg63XTVs29Edt13tPr1Y0dNJ244hztfgDM7ergbYQ7rH1ne2gZTgTHAGwbmkSk9YIRMGd5dH65XL81zxxvwdXi+kuE/XOCDvNznP2urUGVDta28qtOwl82O2mk8GbSeK7P85xJGV5RGcXetp/D9P2Md7z5+zPZeXre/s5xiCM8GZ4FxS6G23k7dlB+mp8n63Zz0GxJv7/mB966lfv7euVruma7imaegVg2Xpp6y/7r/pdrHXRYHPdntfZkeuz7Wzhu/gbP93rRYtWnhUk3plPa5yOAc1/Thvd1bBnovgTHAGwRlerrdkpEtC7Ar5aswQeaZJRY9xfUnqSet7l/Zu6DFdwz5N4+YBP9gujMzxGsbrt3hGhoyZKRty7azhKzj7e56wj/dvzP/F53N52/qO4ExwJjiHd0LO2U4uQzLzWsXgOCBLR3d1mye2wbrq7BpEzL/rQGG7unB6vrLdYVmY66YquWvg/C2FDs751f25grO3AY/gTHAGwRleTwLW9qN5+TVhmXSsV94VRMf+7HV8bTX29Bhq/8RS5yvbHUpYIq80aeBzTK/Q8h3Z5uU8lFdwLuh5wj7e535dvp6L4ExwJjiXBLadK6p1/y6fvScdMqFtDa/THJK+cS3keHftQTluu9vfG4t/8ZHZMyU5Ya3MmjhEOrZo6jGALUg+WeDgXH3ADImLi5WYGHvFSGxsXPa/ayVIRpb7oOZtwCM4E5xBcIYne8B1js2+HN/0pY/5zK7F4WVvHCQZ2SOwc6cO8wmgj8fLTE82dyYc3Kezx0J213kr/+BcmPOEfbz3vGMtwZngTHAuuY5vtnaS0HlnB/L59oU5H7V5zA+27fGpO1HM7PeA14EjKz1eZs2alV1LJd3LDVHsK5ytvTwLEJx9BtnMXebmKvMWrbGej+BMcAbBGf4H54F57fef7dRe1/S93GOlc+6yTuGbGjnVCsLu3+eQTZHzs88TX8myuNxTMrJkr23nCtcc4/yDc2HOEwRngjPBGT4cdbuNapdJq30PhunrrIHEfarG6cFsrm27IdfVgLluV7Hte0bnXolsBrmot/IMzm4fmdmmg+iikniH5wAV1e9Bj58lOBOcQXBGIa+x2BZO63i7Nt13IFwzur3XqRrqZNIcr2tc3K9iu3ZrqvTYl14+CT1q7ZSh43ju4KzTAx1neJ4gOBOcCc7wyb5NkPOOTPE5O13oqmXdwzh63gi3j8jsizisUJzrcbwNOCdtCz7KNNC7NR1y/dGfHi/v2/bndC4esYfZx4Z8J6kZrmFqjW1f0XL3viMb0nO2EnLsk28Gt7PtBDLIYx9ngjPBGQRnFJT7DVA07A5ftClnp4sss79+csIKGdrtPtdaGLeF4bZQnLOLhnUjEyv8Ornv+9xl7A+uKRRZmbJp3gfWFe3KzzkXpB+V0S3Oty7sLE7Ya63b8ec8QXAmOBOckYcsWTq4dYEXT5g9Lb381W6/BXfuvZh9DYin7xz4qDRpXM73HZeOrPOY12ZdFXDskt71KuXaR7Sex2v+2rbhvj04HyY4E5xBcEaBnNz7o/vd/PKpHrO3eH2crbZbcJ++sZXnOpiTuXfVyA7qLVo09bjfgH1sz30HQWthuh/niaIKztwAheBMcA5jSdHT5Jk89uXUqwcDJnrOTbazf+TlM1xm7ZMJ3e73+TyPDZiRa661wyPYuw1k2Y83pU8rr49VrmlHWZx4xH1Qy5mrV/nZ8XlecR6XR3Bul+vjRxCcQXAuETJ3Z4+3z+V9s6xn+8myhF99PoT9brV5hcu9a77weldb51Xl2bHuz3E8aYlbsHe7OFLI84R9vP8ij1018tyOzrnLFAjOBOdwliWpiRskcp4u4Jsn8+bNk0WR0ea2244C/LQVnHXOWmreK68zUrdJTHSkLFq0KOd5YiQ5w/ezODIyJD09XTIyMr3u/pGZvss8XmRkpETHxHrcWAUEZxCcUURnisx9Eps93upibx2/581bJNGxcZKaUYAzxSFXcL6pRz67OWUdloTYmOxxfVH2ueL088TEJfk+H5nphenZ5wrdYjWL8wTBmeBMcA5itr/QPW5XDRCcQXCGuK7I+lokDhCcCc5hLT1hg8TExbpNv/DcMB4gOIPgXGJlHZDY6FiJjfzCmqfseXMsgOBMcA57R2WUfaU1gyEIziA4I5cjtpticYEFBGeCM8HZdgvU+AwWQ4DgDIIzbME5162vB8xmgTUIzgTnEkr3es7MzDCL9gCCMwjO8HKmEEdmZvZ5IsPaXxkgOBOcARCcQXAGQHAmOAMgOIPgDIDgTHAGQHAGwRkAwZngDIDgDIIzAIIzwZngDBCcAYIzQHAmOBOcAYIzQHAGCM4E5zPUpUsX06CjR4/m6AJCTPfu3U3//fjjj2kMFJuPPvrIHGd6vAEILaNGjTL9t2vXrgTnotCzZ0/ToEOGDOHoAkLMCy+8YPrvZ599RmOg2IwbN84cZx07dqQxgBAzePBg03817xGci0D//v1Ng/7vf//j6AJCzJNPPmn67/Tp02kMFJtp06aZ4+ypp56iMYAQ8+abb5r+q3mP4FwExo4daxpU5zoDCC133nmn6b/Lli2jMVBs9PjS4+yuu+6iMYAQ41zLpnmP4FwEfvzxR9Ogd9xxB0cXEGJq1Khh+m9qaiqNgWKjx5ceZxdeeCGNAYQYzXfaf5cvX05wLgr79u0zDVq9enWOLiCEHDp0yPTdKlWq0BgodpUrVzbHmx53AELHBRdcYPpuWloawbmo1KxZ0zTq9u3bOcKAEPHtt9/y8TkCxjktSI87AKFBc53221q1agXtawzJ4OxcYDRmzBiOMiBE9OjRg4W9CBjnAiM97gCEBt1qWPut5jyCcxFyLhB8/PHHOcqAEFGvXj3Tb3WdAlDcnAsE9bgDEBo01wXzwsCQDc579uyRUqVKScWKFeXIkSMcaUCQ27JlixkMzz//fDlx4gQNgmL3xx9/SLVq1cxxp8cfgOCmeU5zneY7zXkE5yLWpEkTMyDqrRkBBLdevXqZ/tq5c2caAwHTqVMnbr0NhIjPP//c9NemTZsG9esM2eA8ceJE08C33XYbRxsQxPTK38UXX2z666pVq2gQBMzKlSvNcafHnx6HAIKX5jntr5rvCM7F4Pfffzd7dGojR0ZGcsQBQcq52OPGG2+kMRBwDRo0MMefHocAgtPSpUutvdc13xGci8l7771nGlqnbQAIPidPnpSrrrrK9NNZs2bRIAg4Pe70+NPjUI9HAMGncePGpp9qrgt2IR2cdSK5LjbSxp47dy5HHhBkBg8ebPpn3bp15a+//qJBEHB63F177bXmOBwyZAgNAgQZzW/OxeOhsOHDOaHe4J9++qlp8CuuuCLoL+8DJYmuij733HNN//z+++9pEJw1evzpcajHY0pKCg0CBAnNbZrftH9qngsFIR+c9WrCzTffbBr9v//9L0chEAROnTolDz30kOmXTzzxBA2Cs65NmzbmeNTjUo9PAGef5jbtl5rjQuVTyXPCoeE3bNgg5cqVM43/9ddfcyQCZ9mgQYNMf6xRo4bs27ePBsFZl5qaao5HPS7ff/99GgQ4y2bPnm36o+Y3zXGh4pxw+QUMHz7c/AKqVq0qCQkJHJHAWaK73JQpU8ZsYr9o0SIaBEFj4cKF5rjU45PdmICzR3Oa5jXNbSNGjAip135OOP0iWrdubc13Zh4bEHh61aBKlSqmH/bt25cGQdDp06ePOT7PO++8kLrKBYQLzWeXX3656Yc6hSrUhFVwPn78uNx1113WKv60tDSOUCBA9LbGtWrVMv3v6aefZh4pgpIel3p86nGqxyu34wYCR3OZ5jPtf5rXHA4HwflsO3z4sNxwww3ml3LNNddIYmIiRypQzFavXi0XXHCB6XcPPvgg++UiqOnxqcepHq963OrxC6B47dy50+Qy7Xea0zSvhaJzwvGXc+DAAWnYsKH55dSsWVOioqI4YoFiMm3aNKlUqZLpby1btgzJKwgoefQ41eNVj1s9fqdPn06jAMVk+fLlJo9pf9N8dvDgwZB9L+eE6y/p2LFj1nZYpUuXlgEDBnADBqCIg0fnzp1NH9Pq2LGj/PnnnzQMQoYer3rcOo9hPZ75ww8oOpq7NH9pDnNuB6n5LJSdE86/MB0U33zzTYmIiDC/sNtvv102btzIkQycoR9++EHq1Klj+lWFChVkzJgxNApClh6/ehzr8azHtR7fAM6M5q1//etfpl9pDtM8Fg4XV84pCb88vWvUJZdcYl19fumll2Tv3r0c1UAh6RZCekMT5xU6XeTx888/0zAIeXocOxctOW/cw9amQOFpvtKc5bzKrPkrnP4YPaek/CIzMzOle/fuZv9O54bb+rHc1q1bOcqBfKxZs8Zs9+j89EZvXaw3kWARIMKJHs96XDtvFa/Hu26XFRMTQ+MA+dA81alTJ+uGdJq3evToYfJXODmnpP1iN2/eLE8++aQVALRuueUWGTZsmFnxCeD0ll2bNm2SgQMHSu3ata2+Ur58eenatSv7pCOs7dmzxxznerw7j33tB9oftF+w1SJwmuYmzU+ao5x9RfOV5izNW+HonJL6y96+fbu54lytWjXrl62lm3K3bdvW3DJ4zpw55uO75ORkOXTokFk08scff1BUWJTue6470Pzyyy8SGxtrdhXo16+fPP7449atiZ118cUXS69evbh9NkoUPd71uNfj394ftH9oP9H+ov1G+4/2I+1P2q8YX6hwKc09mn80B2ke0lyk+eiZZ56Ryy67zK1faJ7SXKX5KpydU9IHRj0wZs6caeazXXjhhW4HAUWV5NI7cHbo0EEWL17Mbhko0fT4136g/UH7BeMDRZ0uzU2anzRHaZ4qCc5hSHTRj9/i4uJk3Lhx8uqrr0rz5s3NJt1XXnml2SRfV13rR3cUFQ5VsWJFc+Xs6quvlgYNGkirVq3M7Yi/+OILbhwE5EH7h/YT7S/ab7T/aD/S/qT9ivGFCpfS3KP5R3OQ5iHNRZqPNCdpXiqJ05YIzgAAAADBGQAAACA4AwAAAARnAACAwjhx4kTY7RsMgjPCXEZGBvucAgACShds6k4P7du3pzFAcEZo0MCst9ksW7asZGVl0SAAgICYN2+eCc668wNAcEZI0KvNOnBVrVqVxgAABMzKlSvN+ef222+nMUBwRmjQ22/qwHXNNdfQGACAgNm6das5/9SpU4fGAMEZoWHt2rVm4Lr11ltpDABAwOzfv9+cf2rWrEljgOCM0KC3pdWB64EHHqAxAAABoztq6PlH19gABGeEhGnTppmB68knn6QxAAABpbeI1nPQ8ePHaQwQnBH8xowZYwatTp060RgAgICqVauWOQelpaXRGCA4I/gNHjzYDFo9e/akMQAAAVW7dm1zDtq2bRuNAYIzgl/fvn3NoDVw4EAaAwAQULfccos5B61bt47GAMEZwe+ll14yg9aIESNoDABAQDVu3Nicg3788UcaAwRnBL8OHTqYQWvChAk0BgAgoB5++GFzDvr2229pDBCcEfyeeOIJM2hNnz6dxgAABFSbNm3MOWjmzJk0BgjOCH7Nmzc3g9aCBQtoDABAQLVr186cgyZOnEhjgOCM4NekSRMzaEVGRtIYAICAevHFF8056NNPP6UxQHBG8GvYsKEZtGJiYmgMAEBAvfbaa+YcNGTIEBoDBGcEv+uvv94MWvHx8TQGACCg3n77bXMO0v8CBGcEvauvvtoMWrt27aIxAAABpVeauQkXCM4IGc7bne7fv5/GAAAE1MiRI805SOc6AwRnBL3KlSubQSszM5PGAAAElO6moeeg9u3b0xggOCP4RUREmEHrzz//pDEAAAE1Y8YMcw7SewoABGcEtT/++MMMWOXLl6cxAAABN2fOHHMeeuyxx2gMEJwR3I4ePWoGLJ2uAQBAoM2fP9+chx555BEaAwRnBLfDhw+bAatatWo0BgAg4BYuXGjOQ82aNaMxQHBGcDtw4IAZsGrUqEFjAAACbsmSJeY8dP/999MYIDgjuKWlpZkB66KLLqIxAAABFxkZac5DTZs2pTFAcEZwS0lJMQPWpZdeSmMAAAIuKirKnIfuvvtuGgMEZwS35ORkM2BdeeWVNAYAIOBWrVplzkN33HEHjQGCM4JbYmKiGbD+9re/0RgAgIBbu3atOQ81bNiQxgDBGcFt27ZtZsCqU6cOjQEACLjY2FhzHrrppptoDBCcEdw2b95sBqzrrruOxgAABNzPP/9szkP16tWjMUBwBgMWAAC+cAEHBGeEDD4iAwCcTfHx8eY8dP3119MYIDgjuG3YsMEMWDfeeCONAQAIuI0bN5rzUIMGDWgMEJwR3OLi4syAdcMNN9AYAICAW79+vTkP3XzzzTQGCM4Iblu2bDEDVt26dWkMAEDAxcTEmPPQbbfdRmOA4Izgtn37djNg1a5dm8YAAATcypUrzXnozjvvpDFAcEZw27VrlxmwrrnmGhoDABBwy5cv55bbIDgjNHDLbQDA2RQZGWnOQ02bNqUxQHBGcNu7d68ZsC699FIaAwAQcEuWLDHnoQceeIDGAMEZwW3//v1mwKpVqxaNAQAIuO+++86ch5o1a0ZjgOCM4HbgwAEzYNWoUYPGAAAE3Pz588156JFHHqExQHBGcDt8+LAZsKpVq0ZjAAACbs6cOeY89Nhjj9EYIDgjuB09etQMWJUrV6YxAAABN336dHMeevLJJ2kMEJwR3E6cOGEGrHLlytEYAICAmzBhgjkPdejQgcYAwRnBr3Tp0mbQ+vPPP2kMAEBAjRw50pyDXnrpJRoDBGcEvypVqphBKzMzk8YAAATU4MGDzTmoZ8+eNAYIzgh+uhWdDlq6NR0AAIH09ttvm3OQ/hcgOCPoXX311WbQ0ttvAwAQSHqlWc9BeuUZIDgj6F133XVm0IqPj6cxAAAB1blzZ3MOGj16NI0BgjOC3+23324GrVWrVtEYAICAeuqpp8w5aOrUqTQGCM4Ifg8++KAZtBYtWkRjAAAC6uGHHzbnoG+//ZbGAMEZwe+JJ54wg5ZuQg8AQCDdeeed5hy0YsUKGgMEZwS/jh07mkFr7NixNAYAIKCuv/56cw6Ki4ujMUBwRvB77bXXzKA1ZMgQGgMAEFCXXnqpOQft2bOHxgDBGcFv4MCBZtDq06cPjQEACKjy5cubc9Dx48dpDBCcEfzGjBljBi2dsgEAQKAcPXrUnH8qVapEY4DgjNAwe/ZsM3A99thjNAYAIGCSkpLM+efKK6+kMUBwRmiIiooyA1ejRo1oDABAwKxbt86cf26++WYaAwRnhIYtW7aYgeuf//wnjQEACJiFCxea84/eTwAgOCMk/Prrr2bgqlGjBo0BAAiY8ePHm/NPu3btaAwQnBEa/vrrLyldurSUKlVKTp48SYMAAAJiwIAB7OoEgjNCj3Mfzd27d9MYAICA6NKlizn3jBw5ksYAwRmho2HDhmbwWr16NY0BAAiIRx991Jx75syZQ2OA4IzQ0bJlSzN46dZ0AAAEgu6moeeetWvX0hggOCN0vPTSS2bwGj58OI0BAAiImjVrmnNPamoqjQGCM0LHoEGDzOD12muv0RgAgGJ37Ngxc97RW26fOnWKBgHBGaFj+vTpZgB7/PHHaQwAQLGLj4835506derQGCA4I7To/DIdwG688UYaAwBQ7ObPn2/OO82aNaMxQHBGaDlw4IAZwKpVq0ZjAACK3dChQ81558UXX6QxQHBG6KlSpYoZxDIyMmgMAECx+u9//2vOOR999BGNAYIzQk/9+vXNILZu3ToaAwBQrJo2bWrOOQsXLqQxQHBG6HnqqafMIPbFF1/QGACAYnXxxRebc05SUhKNAYIzQk+/fv3MINarVy8aAwBQbA4fPmzON5UqVWIrOhCcEZpmzZplBjK9BSoAAMVl9erV5nxz00030RggOCM0bd682Qxkf//732kMAECxGTNmjDnfPPfcczQGCM4ITSdOnJBy5cpJRESEHD16lAYBABSLjh07muCsW9IBBGeELP3YTAezFStW0BgAAM41IDgDXAUAAJwNfLoJgjPCxujRo5l3BgAoNj/99JM5z/zzn/+kMUBwRmjTm5/ogFa3bl0aAwBQ5D755BNznunQoQONAYIzQpt+hFaxYkUpVaqUHDx4kAYBABSpVq1ameA8YcIEGgMEZ4S+xo0bm0Htm2++oTEAAEXqwgsvNOeYxMREGgMEZ4S+t956ywxqr776Ko0BACgyCQkJ5vyit9sGCM4ICz/88IMZ2G699VYaAwBQZHTHJj2/PP300zQGCM4ID7///ruUL1/ebBXEPGcAQFF54IEHTHCeMmUKjQGCMxjcAADw5tixY9ZFmQMHDtAgIDgjfPBxWnj666+/JCsri6LCovR4RuiYN2+eOa/861//ojFAcEZ42bFjhxngqlevLn/++ScNEkLBeP369TJ8+HB58cUX5d5775W//e1vZhV7hQoVzO+UosKp9LjW41uP8/vuu09eeuklc/xrPyBYB5dnnnnG/M7ee+89GgMEZ4QfvauTDnJLliyhMYLY8ePHZerUqdKyZUupVq1aniFD9+cuXbo0RYVF6fGc1/Gu/UH7hfYP7Sc4u+NU5cqVze/ll19+oUFAcEb46devnxnk2rdvT2MEId3W6f/9v/9nnYycddVVV5krOwMGDJBp06ZJTEyM+d6UlBQzr5Ciwqn0uNbjW49zPd71uNfjX/uBvV9oP3nhhRfM9yLwZsyYwTQNEJwR3pzTNapWrSp//PEHDRIk4uPjpU2bNm5X226++WYZMmSIbNy4kTBFUTml/WHw4MGmf9g/ddH+o/0IgdO8eXPT/sOGDaMxQHBG+HKecGbOnEljnGWZmZnSvXt38xG1/k7KlStnPg3QK22EJIrKu9asWWP6i/Yb7T/aj3r06GH6FYrX7t27zU4auqMGu2mA4IywNnLkSHOSadq0KY1xFi1dutTcaUt/F3oC0ikaesWMQERRhSvtN9p/tB9pf7rkkkskMjKSQaYYvfnmm+zSBIIzSoYjR47Iueeeaz7e3L59Ow0SYLorgN4C3Tkt45ZbbpFly5YRgCjqDEv7kfMTNe1fb7/9NrtwFAPdNtD5R390dDQNAoIzwp8uqNFBTz/WRODoKnTnvEA9sb/++uvy66+/EnooqohK+1PPnj2tP0y1v7H7RtHSHU20ba+77joaAwRnlAy6H6pza6ejR4/SIAGQkZEhd9xxh2n3Cy64QL755huCDkUVU82dO9f0M+1v2u+0/6Fo3HTTTaZdP/vsMxoDBGeUHI0aNTKDn95REMVLFys5TzaXXXaZWdREuKGo4l88qP1N+532PxYNnjmdO67tWatWLXZmAsEZJYvzVqlXXnkldxIsRidOnDALMbWt9U5oLACkqMBVXFycXHPNNdaCaO2P8N9DDz1k2nLgwIE0BgjOKFlOnTolderUMYOgzllD8Xj22WetKzSxsbGEGYoKcGm/0/6n/fC5555jUPKTbpPpvPHMoUOHaBAQnFHyfP7552YgvPbaa1l9Xoztq7uYREVFEWIo6izV8uXLpVKlSqY/jh8/nsHJDw8++KBpv759+9IYIDijZDp58qRcffXVZjD86quvaJAitGXLFqlQoYJp21GjRhFeKOosl/ZD7Y/aL7du3cogVQirV682bXfeeedxtRkEZ5RsevVFB0SdtsFV56LjXHypNwggtFBUcNR//vMf0y+1f6Lg7rnnHtNueuMTgOCMEk03s9dFa3yEWXQmTZpk2vPCCy+UXbt2EVgoKkhK+6P2S+2fX3zxBYNVAejWmdpeNWrUkN9++40GAcEZcG5or7eqPXbsGA1yBvRmC86FSKNHjyasUFSQTtnQfupwOBi08rmw4lxEPmLECBoEBGdA6Q4bDRs2NIPjO++8Q4OcAT25aDs2aNCAkEJRQVr169cnDBbAsGHDTDvVrl3bhGiA4AzkWLFihbUDREpKCg3iB11sefnll5t2/PLLLwkoFBWkpdM0tJ9eccUVpt/CU1pamlkMqO00f/58GgQEZyC31q1bm0FS/4vC+/rrr62Flunp6QQUigrS0v6pV1G1v86ZM4fBywvnQspHHnmExgDBGfBGrzTr5vY6WC5atIgGKaQWLVqYthswYADhhKKCvPr372/6a8uWLRm8clm6dKlpG937Ojk5mQYBwRnw5cMPP7RuD83CmYLTvU3LlCkjpUuXNns4E0woKrhL+2lERISULVuWvYltjh49au3vP2jQIBoEBGcgL7oA5IYbbjCD5quvvkqDFND06dNNm919992EEooKkdL+qv12xowZDGI5XnzxRdMmN910EwsCQXAGCuKnn34yV0/1asyqVatokALo3LmzOdn873//I5BQVIiU9lftt9p/IRIZGSmlSpWScuXKSVxcHA0CgjNQUH379rW2IdK9iZE350Kj77//nkBCUSFSS5Yssca5kk6nq1x22WXWOg2A4AwUwokTJ+T66683g2iXLl1okDzoHxbaTjpXcv/+/QQSigqR0i3X9NM17b8l/QKBLpLUdrjjjjuYogGCM+CPn3/+WcqXL8+WTQVoJ22jf/zjH4QRigqx0n6r/XfTpk0ldgxz3k2xWrVq7KIBgjNwJoYPH24G1PPPP1/27NlDg3ihC4u0jZo1a0YQoagQK+232n9nzpxZIscvXdPivEBSUtsABGegSDVv3twMqrfddpuZwgF3I0eONO3z/PPPE0QoKsSqQ4cOpv9qPy5p9P0773aqu2kABGegCBw8eFCuvPJKM7h26tSJBslF9zrVtnn55ZcJIhQVYvV///d/JXLP4j///FOaNm1qzWvm1uMgOANFKDY2VipUqGAG2c8//5wGsXHuQKL/JYhQVGhVnz59TP994403StS45dyv+aKLLpLU1FQGchCcgaI2adIkM9DqHp9RUVE0SI7XXnvNtMs777xDEKGoEKu3337b9F/txyXFJ598Yt6zXgxZvXo1gzgIzkBx6datmxlwq1evLjt27KBBCM4URXAOIfPmzTM3t9IbnegdTwGCM1CM/vrrL3nkkUes7dd0/jPBmeBMUQTn4LdixQqpWLEiNzkBwRkIpGPHjsmNN95oBt+GDRua/09wJjhTFME5eOk+1bpPM4u8QXAGzoJ9+/bJ1VdfbQbh+++/v0RvU0dwpiiCczDbtm2bWQSo77N169bmk0OA4AwEWGJiotSqVcsMxo8//niJvU0rwZmiCM7BavPmzdY4XdIvcoDgDJx1ervpqlWrmkG5ZcuWJXJQJjhTFME5GG3cuFFq1Khh3l/jxo3l999/56QFgjNwtq1du9YKzw8//LA4HA6CM0VRBOezaN26dXL++eeb93bPPfcQmkFwBoJJTEyMnHfeeWaQvu+++0rUIE1wpiiCczBZtWqVNR7rleaSvoAbBGcgKK1Zs8a68qxXOI4ePUpwpiiK4BxAy5Ytk8qVK5v31Lx58xL3CSAIzkBI0Tl1NWvWNIP27bffLhkZGQRniqIIzgEwfvx4KVu2rHk/Tz/9dIldsA2CMxBStm/fLldccYUZvGvXrh32dxgkOFMUwfls0u3lnOOQ872w5RwIzkAISUlJkQYNGphBXBeo6MeHBGeKogjORUunxDnv5qpXm/WqM0BwBkKQLkjRLeqcA/q4ceMIzhRFEZyLyO7du6VevXrm9VevXl2WL1/OiQcEZyCUnTp1Snr16mV9hNi9e/ew+wiR4ExRBOdAi4qKsm5s8s9//tPckAogOANhYuLEiVKuXDlrr+dDhw4RnCmKIjgX0p9//mnGmtKlS1t3Azx8+DAnGRCcgXATHR1t3cXqsssuM1dMCM4URRGcC0bXjtx9993m9UZERMgbb7xhgjRAcAbClM7Ju+OOO8zAr1dM9KQV6gM/wZmiCM7Fbe7cuXLBBReY13rJJZeE9YJrgOAM2GhQfvPNN80VEz0JNGrUSPbs2UNwpiiK4JyL3sDkxRdftNaJ6E1N9HUDBGeghPnxxx/NlRM9GeiVFL2iQnCmKIrgfNqKFSukTp065vWVL19ehg4dyokDIDijJDt48KC1B6lW27Ztzb8RnCmKKqnBOTMz01xlLlWqlHltdevWNXdlBUBwBozhw4dLpUqVzEniwgsvlGnTphGcKYoqccH522+/lcsvv9za//6tt96SEydOcJIACM6AO92HtGnTptbVZ70SravICc4URYV7cNbxz/7pW8OGDSU+Pp4TA0BwBvL22WefSbVq1czJ49xzz5WBAwfKH3/8QXCmiq/SUiQhO6RszC4NKwk7kySNdiE4B4DeMrt3795mDrO+jvPOO08++eSTsLtRFEBwBorRvn37pE2bNtbVl2uuuSZoFw+Ge3DeMOd9ad2stTz33HN5VrdeA2XinKWyM83LY8wYJM2aNZM23T+XJL9eR5osGt5DWrd++Awew1tg3ixfvttV6p5TyjrWnFUq4k7p/eF0iU8hXBKci57uLvT5559bC6R1PnOHDh1k//79nAAAgjPgH91544YbbrDCzH333Sfr168nOAewlg581CNU5lWlyjwkMzfucX+MD1qYr5W7e4gk+vU6kuSDu6qc4WO417747+ShUhH5v6eIa2Vs1C8BbfM9G7+Vt3u9bWpurrakQjs4nzp1SmbMmCG1a9e2jrHbbrtN1q1bx4APEJyBorky8+mnn1qb/+uVmdatW0tCQgLBOQC1/MOnrBN8s9bPer3a3Oyu69zCZkS5thKdkm4L3/eafy9Tb4DfwXlYs2pWcD7zK87bZGBOEHeG/fcmTJfIqNWyOmqxjH+3a57vp7jrl6XvW889KHIHATeIg7NOLUtKSipQYF6wYIHceOON1u/2H//4h1kIrV8DQHAGitShQ4fk9ddfl4oVK1p3HtSPNnft2lWgn58wYYJZgENw9i84lyrbSqLT8gh7a79yu4I7KHKX9bW0lCTZuXNndqX4+TqKNjjvWfWp9TrLPzxANnt5X2kJ30unuuWt7+sxc1PggvPyT7y2IxVcwfnjjz826zDymo+clZUlU6ZMcfvk7LLLLpNx48aZrwEgOAPFSuc/d+3a1WzV5AzQTz31lPz88895/tzMmTPl4osvls2bNxOc/QnOZR6SJUl5X3VdNew/VjjoOjHGPYimpZnyCKhJmyVywRzz8fWMyZNlxoLlkpBS2OCcIkk7k8yVv5S0/N/T2vEvWa/z1W+3+/y+HYv6W993zzsLzVxr5/MkJfn+IyAtSf9Q8Hwt+b/X7MdPSZG4+a4rzv0WbJCUlDSv874T1kWbq5hakVHrZKeP+din/3DJfj3Of0tKkKjIxebnFkeuliT760zZJqutr0VJfFIawdkL7e/6c7fccovXr+uiv5EjR8pVV13lFpj1JiZ6N0AABGcgoPRK8/PPPy/lypVzTSVo1kyWLVvm9fu3bt1qvqd69ery008/EZz9CM7L8gnO9iu5p4NmzmPkBOpyd9unaiTJ1x929Tmv+KO5GwoWnNN+ktfvOtf62WrdZ+e7I4Z93naPuVt8f29StLzSrJE0u6uZfLx4qxzYFyPPRpSxFg96/UNiz4/WlXfXay3ge036Ue70Me963Oo9tvnZS+SVuyp5/b4uH33t8YeFc6pMlS6jZPH0QV7npc/duENWTejr9TF7TFxJcLbp0aOH1Tbt27d3+9r27dvllVdekapVq1rfo3f/Gz9+vJw8eZKBGyA4A2eX7vXcvXt385Gp80R13XXXyejRo81VHyf9WNQZsnXLp6ioKIJzEV9xXmQLpP0WbPF4jAr/doXe5TkLBq3f2V13eexuMWjx9ryDc0p2aK5b0fr+Sm2GyrYCvCf7lWR9X2MXrHK/6lrA9+ht+sbW+a7Hbv7ewsK91zyCc7+ctvhl1WdedwGxV5Uu7juP2Oep+1vj1u0p8cFZp2R07NjRrV0+/PBDOXbsmEyaNEnuvvtut681atRIvv76a7aWAwjOQPDROdADBgywtnfSqly5srkqvXLlSvM99erVs76mc6UXLVpEcC7MFWdfC+TSkmTp5H5uYfTbnekej+EKvT/LKxGn/4gp2+hlWRb/izX9YNWcQVYwrPDvodYVW4/gnLJGXqlbwfW7bjeqwAsP07d+4zWgtun8ugyfMF2i1sVLko9pD/vWfmZ7zs9d0x9yXv+XnV2LvyZt3FfI95pmpoFssIXvrpNXSkrSztPBPv0n6RRR1vraE+9Mk/ikFHOFfevySW7vqcfcTT6Dc/cx82VnSpqkxH9nXUF31t3dR8m6hJ2yM2GjjO/V3PZHwA8lOjjrH946JSz3MXPPPfe4/dGuY84LL7yQ79QxAARnICjox6HTp0/3uPqje0HXrVvX7d/0CvSsWbMIzgXcVSOi7p1y11132eo671dH527y+hhW6E1yTWeo33ue51XdXqfvIBlRoaP8lOYenCv8e5Qk7fvJIzQXdsHgjqUj871y2+jhTjJixvJc4XibvFW3kver8HtWyOOlSlvTUrb59V4PyB7b4sCPbYsD475+wzYVxvOx9qydZL0nDfVpXn6H7ce6T7vYanvMSk+Mcn+vtmknJTk465zk5s2b+96CsVQpufPOO82+zPZPuQAQnIGQsmPHDnNXrksvvdTnSU8XF06cOJHgXIDgXKB9nCPu9JgLnVdwNldAP/xS1sXvdJubrAsJdRpOWq4rzlp33VnO7aqv37tspGyTBZNHSOfWD+f5nsrc0FN+sl1tty+C7GP7I2GH7UqxFVAL/V5z76qxw1oAOf65GnnPr87+Hmc7aRBfk5b/dJs9Xp/LFZydfwiU1OCsQbhp06Y+jw2d9rVixQoGW4DgDIQPnWPoDLi+rhgNHz6c4JxXcI64Vrq/+6GZ0+le70q3zm3cg2au/Zo9p2qkyJeda3pdKNem81vmDoTuu024B2d71e89u+huu71xdXaQHitv5Xo/WvW6z7ZN9ZjldmU3Jdc0DQ22cxPSrTBbuPfqKzgnyQfNXAvOytz1kFkM616N3KbLOEOys/0jKr0sP6cV5LkIziojI0P+9a9/5fvH4tVXXy2pqakMtADBGQgPU6dOlYiI/O8SN3DgQIKzn/s471n7lTXHNveVTc/grJUo09/tmufvo8+UlfkG58LfJCQtZzu5nT7nMTuvRn/9YQdXEM1+/6453q7XY/5d36ttmoZrbrY/79V3cM6rDbztlLEsV3C2L84kOOcfnPXr8+fPl/fee0+efvpps0bCvouPva699lrz/QAIzkBI0xNfmTJlvJ7s9G6EOhf6xRdflFGjRkl0dLS5UyHB2Z/t6NJcUwncQqav4OxaWLhx9RKZMPwdedZ2xdRZU8wCO/fQ+MRH82T5dNfWafb5wfmWbeeK87pMy2fruhQZ1vp8r38MbPjS9QlGn8XbZc/SwbbdNmJ9LqLM/70WIDhHXCsTI6MkcnGkLLZqsfmv7hajtXrjzgK1P8G5cNvR6SJB3dpS94V/6623pFWrVub22Trl66abbpLffvuNQRcgOAOhaenSpVK+fHmz0r1hw4Zmhw2929f3339vbqRSFAjOBzz2azbBzrZ9We7glpYQbe7kOGHC15Lg5SYh9t0cToc5+64aAzx22vC1WM5r7Vlh7SShc5fz275uercbvc8P/uV7a+6y7kQx/vUmXtup8O81/6kaee1wsnbBTJk8ebJ8u3orwTkAt9x20gWEGzZskC1btjDwAgRnIDTpzVD0Y/lTp04V23MQnL0vJLSHsNzBzb6P8sjVnvsD/zK/p8/gbJ9ukB43y21njJGrfynU7bvNIr4RC31vW5fwvRUcc19FN3Oan7vQyx7KX7hdxS78e3UPsx9H/eL5h0l2PTlipddt9pztYQ/JBOfiD84ACM4ACM5+B2e3wJcruO1bO8m1q8n1HWXuOldgS0qIlrda18t14w3fdw5c+sFTbrtfbC7Ae7LfpETrhnZDJDpnp4u0tBSzh/GCyYPc9kX2tghxR67H8RY+C/9e3cPsv3Wv5p1Jp39mx3dur6n3hKXW9nFxUV9ZITf3bh8EZ4IzQHAGQHAOsuC8avTzrqkTvWd77CNs31XDOR/adTe9h9y2mXPfscI1TUEfw/1GJ64bjJiA99EPBVog+PXbjxR4oZ3Z2s3b1AjbLbh9z7Uu7Hs9PQ0k9w1anH+IrP3yVY/Xl3s/ar1zYIqPXU0SCc4EZ4DgDIDgXEzBOWd6gLV7RB7fu2fVp153oXA+htuuDmmbZVjne3yG1X/3/tw2/1jDZ4PT28Llmgphwt/ST9yeNzolvUDvbcOCcfJ43XJ57knde7jn3GR7ze/VNP+76xXqvZ5+v7mDvT3Uxi0e6bY3tL3avzfb46qys/31luR5XXH+OI/g/MRHPxKcARCcARCcz2btjF8jixfMkRkzZphFbTPmLJaNO1MC+BrSJH7dMpkzWRfwTc55DQvMbbdTCvDzVnCOuFa+it9XpO81ZedOSUhIkJ07k7zs/pEk66IWy5w5c7JrgURGrZOdKRxPBGeA4AyA4EwFY9muyHrcrpoiOAMgOAMgOJf0Sli9TBZHRbpNv7AvhqQIzgAIzgAIzpQuVLyritu8Ys87BVIEZwAEZwAEZ4KzW3Au/3BPid6ZTrsQnAEQnAEQnKncpXs9JyXtNIv2aA+CMwCCMwCCM0URnAEQnAEQnCmKIjgDBGcABGeKogjOAMEZAMGZoiiCM0BwBkBwpiiK4AyA4AwQnCmKIjgDIDgDBGeCM0URnAEQnAEQnCmK4AyA4AyA4ExRBGcGM4DgDIDgTFEUwRkgOAMgOFMURXAGCM4ACM4URRGcARCcAYIzRVEEZwAEZ4DgTFEUwRkAwRkAwZmiCM4ACM4ACM4URXAGQHAGQHCmKIrgDBCcARCcKYoiOAMEZwAEZ4qiCM4ACM4AwZmiKIIzAIIzEOb69u1rTrx9+vQhiFBUiJX2W+2/b7zxBoMZQHAGUNwGDRpkTrwvv/wyQYSiQqz+7//+z/Tf999/n8EMIDgDKG4jR440J97nn3+eIEJRIVYdOnQw/ffTTz9lMAMIzgCK28yZM82Jt1mzZgQRigqxeuihh0z/nTVrFoMZQHAGUNw2bdpkTrz/+Mc/CCIUFWL197//3fRf7ccACM4Aitnx48fNibdMmTKSlpZGGKGoECntr9pvS5UqJQ6Hg8EMIDgDCIQ6deqY8LxkyRICCUWFSC1evNj0W+2/AAjOAAKkS5cu5gT85ptvEkgoKkRKt6DTfqv9FwDBGUCAzJgxw5yAGzVqRCChqBAp7a/ab3WBLwCCM4AAycjIkLJly0pERIRs3ryZUEJRQV7aT7W/ar/V/guA4AwggB577DFz9apfv34EE4oK8tJ+qv21VatWDF4AwRlAoM2dO9fali49PZ1wQlFBWto/tZ9qf9V+C4DgDCDATp48KVdeeaU5GU+aNImAQlFBWhMnTjT9VPtrVlYWgxdAcAZwNuhte/WEXL9+fQIKRQVp1atXj9tsAwRnAGeb3kThoosuMiflkSNHElIoKshqxIgRpn9qP+WmJwDBGcBZNnnyZHNirlGjhiQmJhJWKCpISvuj9kvtn9pPARCcAQSBe+65x5ycn3zySQILRQVJPfHEE6ZfNm7cmEEKIDgDCBYJCQlSsWJFc5LWj4YJLRQVHFM0tF9u27aNQQogOAMIJs6V+5UqVZJly5YRXijqLJX2P+cfsrrjDQCCM4Ag1L59e3Oyrlmzpvz000+EGIoKcGm/0/6n/bBDhw4MSgDBGUCwOnHihNx///3mpH311VfLpk2bCDMUFaDS/nbVVVeZ/qf9UPdaB0BwBhDEjh49Krfccos5eV9yySWyatUqQg1FFXNpP9P+pv1O+5/2QwAEZwAh4PDhw9KoUSNzEq9WrZrMnj2bcENRxVTav7SfaX/Tfqf9DwDBGUAI0ZsttGjRwpzMS5UqJa+++qrs37+foENRRVTan3r06GH6l/azli1bcpMTgOAMIFT99ddf0r9/f4mIiDAn9gYNGsjSpUsJPRR1hqX9SPuT9ivtXwMGDDD9DQDBGUCIW758uVx++eXW1ed27dqxcJCi/FwAqP3HeZVZ+5X2LwAEZwBh5NixY9KzZ08pU6aMOeGXLVtWnn32WRYPUlQBF/9pf9F+o/1H+9Hrr79u+hUAgjOAMLV161b5z3/+Y03f0Kpfv76899577P1MUbn2ZNZ+of3D2Ve032j/0bt1AiA4AyghduzYIZ07d5aqVataoUDrsssukyeffFLeeecdmTJlirnSFh8fL0lJSZKenk6gosKm9HjW41qPbz3O9XjX416Pf+0H9n6h/aRLly6m3wAgOAMooXQXgJkzZ0rr1q2levXqbmGBokpyaX/QfqH9448//mCwAAjOAOBy6tQp+fnnn2XUqFHy8ssvy4MPPih16tSRiy++WCpXrmwtiKKocCg9nvW41uNbj3M93vW41+Nf+4H2BwAgOAMAAAAEZwAAAIDgDAAAABCcAQAAAIIzAAAAQHAGAAAACM4AAAAAwRkAAAAAwRkAAAAgOAMAAAAEZwAAAIDgDAAAABCcAQAAAIIzAAAAQHAGAAAAQHAGAAAACM4AAAAAwRkAAAAgOAMAAAAEZwAAAIDgDAAAABCcAQAAAIIzAAAAAIIzAAAAQHAGAAAACM4AAAAAwRkAAAAgOAMAAAAEZwAAAIDgDAAAAIDgDAAAAPjl/wNKhOOAfZScygAAAABJRU5ErkJggg==", + "type": "image/png" + }, + { + "name": "codingthearchitecture.png", + "content": "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", + "type": "image/png" + } + ], + "decisions": [] + }, + "views": { + "systemContextViews": [ + { + "softwareSystemId": "1", + "description": "An example System Context diagram for the Financial Risk System architecture kata.", + "key": "Context", + "paperSize": "A4_Landscape", + "animations": [ + { + "order": 1, + "elements": [ + "1", + "12", + "2", + "15", + "4", + "6", + "17", + "8", + "10" + ], + "relationships": [ + "11", + "13", + "3", + "14", + "16", + "5", + "18", + "7", + "9" + ] + } + ], + "enterpriseBoundaryVisible": true, + "elements": [ + { + "id": "1", + "x": 1428, + "y": 909 + }, + { + "id": "12", + "x": 66, + "y": 909 + }, + { + "id": "2", + "x": 116, + "y": 32 + }, + { + "id": "15", + "x": 66, + "y": 1637 + }, + { + "id": "4", + "x": 2841, + "y": 32 + }, + { + "id": "6", + "x": 1075, + "y": 1927 + }, + { + "id": "17", + "x": 2791, + "y": 909 + }, + { + "id": "8", + "x": 2085, + "y": 1927 + }, + { + "id": "10", + "x": 2791, + "y": 1637 + } + ], + "relationships": [ + { + "id": "18" + }, + { + "id": "16" + }, + { + "id": "3" + }, + { + "id": "14" + }, + { + "id": "13" + }, + { + "id": "5" + }, + { + "id": "11" + }, + { + "id": "7" + }, + { + "id": "9" + } + ] + } + ], + "configuration": { + "branding": {}, + "styles": { + "elements": [ + { + "tag": "Element", + "color": "#ffffff", + "fontSize": 34 + }, + { + "tag": "Risk System", + "background": "#550000", + "color": "#ffffff" + }, + { + "tag": "Software System", + "width": 650, + "height": 400, + "background": "#801515", + "shape": "RoundedBox" + }, + { + "tag": "Person", + "width": 550, + "background": "#d46a6a", + "shape": "Person" + }, + { + "tag": "Future State", + "border": "Dashed", + "opacity": 30 + } + ], + "relationships": [ + { + "tag": "Relationship", + "thickness": 4, + "fontSize": 32, + "width": 400, + "dashed": false + }, + { + "tag": "Synchronous", + "dashed": false + }, + { + "tag": "Asynchronous", + "dashed": true + }, + { + "tag": "Alert", + "color": "#ff0000" + }, + { + "tag": "Future State", + "dashed": true, + "opacity": 30 + } + ] + }, + "terminology": {}, + "lastSavedView": "Context", + "themes": [] + }, + "customViews": [], + "systemLandscapeViews": [], + "containerViews": [], + "componentViews": [], + "dynamicViews": [], + "deploymentViews": [], + "filteredViews": [] + }, + "properties": {} +} \ No newline at end of file diff --git a/structurizr-client/src/test/resources/backwardsCompatibility/structurizr-36141-workspace.json b/structurizr-client/src/test/resources/backwardsCompatibility/structurizr-36141-workspace.json new file mode 100644 index 000000000..92083dfc8 --- /dev/null +++ b/structurizr-client/src/test/resources/backwardsCompatibility/structurizr-36141-workspace.json @@ -0,0 +1,2010 @@ +{ + "id": 36141, + "name": "Big Bank plc - Internet Banking System", + "description": "The software architecture of the Big Bank plc Internet Banking System.", + "revision": 51, + "lastModifiedDate": "2022-01-31T08:53:06Z", + "lastModifiedUser": "", + "lastModifiedAgent": "structurizr-cli/1.17.0", + "properties": { + "structurizr.dsl": "d29ya3NwYWNlIGV4dGVuZHMgLi4vbW9kZWwuZHNsIHsKICAgIG5hbWUgIkJpZyBCYW5rIHBsYyAtIEludGVybmV0IEJhbmtpbmcgU3lzdGVtIgogICAgZGVzY3JpcHRpb24gIlRoZSBzb2Z0d2FyZSBhcmNoaXRlY3R1cmUgb2YgdGhlIEJpZyBCYW5rIHBsYyBJbnRlcm5ldCBCYW5raW5nIFN5c3RlbS4iCgogICAgbW9kZWwgewogICAgICAgICFyZWYgaW50ZXJuZXRiYW5raW5nc3lzdGVtIHsKICAgICAgICAgICAgc2luZ2xlUGFnZUFwcGxpY2F0aW9uID0gY29udGFpbmVyICJTaW5nbGUtUGFnZSBBcHBsaWNhdGlvbiIgIlByb3ZpZGVzIGFsbCBvZiB0aGUgSW50ZXJuZXQgYmFua2luZyBmdW5jdGlvbmFsaXR5IHRvIGN1c3RvbWVycyB2aWEgdGhlaXIgd2ViIGJyb3dzZXIuIiAiSmF2YVNjcmlwdCBhbmQgQW5ndWxhciIgIldlYiBCcm93c2VyIgogICAgICAgICAgICBtb2JpbGVBcHAgPSBjb250YWluZXIgIk1vYmlsZSBBcHAiICJQcm92aWRlcyBhIGxpbWl0ZWQgc3Vic2V0IG9mIHRoZSBJbnRlcm5ldCBiYW5raW5nIGZ1bmN0aW9uYWxpdHkgdG8gY3VzdG9tZXJzIHZpYSB0aGVpciBtb2JpbGUgZGV2aWNlLiIgIlhhbWFyaW4iICJNb2JpbGUgQXBwIgogICAgICAgICAgICB3ZWJBcHBsaWNhdGlvbiA9IGNvbnRhaW5lciAiV2ViIEFwcGxpY2F0aW9uIiAiRGVsaXZlcnMgdGhlIHN0YXRpYyBjb250ZW50IGFuZCB0aGUgSW50ZXJuZXQgYmFua2luZyBzaW5nbGUgcGFnZSBhcHBsaWNhdGlvbi4iICJKYXZhIGFuZCBTcHJpbmcgTVZDIgogICAgICAgICAgICBhcGlBcHBsaWNhdGlvbiA9IGNvbnRhaW5lciAiQVBJIEFwcGxpY2F0aW9uIiAiUHJvdmlkZXMgSW50ZXJuZXQgYmFua2luZyBmdW5jdGlvbmFsaXR5IHZpYSBhIEpTT04vSFRUUFMgQVBJLiIgIkphdmEgYW5kIFNwcmluZyBNVkMiIHsKICAgICAgICAgICAgICAgIHNpZ25pbkNvbnRyb2xsZXIgPSBjb21wb25lbnQgIlNpZ24gSW4gQ29udHJvbGxlciIgIkFsbG93cyB1c2VycyB0byBzaWduIGluIHRvIHRoZSBJbnRlcm5ldCBCYW5raW5nIFN5c3RlbS4iICJTcHJpbmcgTVZDIFJlc3QgQ29udHJvbGxlciIKICAgICAgICAgICAgICAgIGFjY291bnRzU3VtbWFyeUNvbnRyb2xsZXIgPSBjb21wb25lbnQgIkFjY291bnRzIFN1bW1hcnkgQ29udHJvbGxlciIgIlByb3ZpZGVzIGN1c3RvbWVycyB3aXRoIGEgc3VtbWFyeSBvZiB0aGVpciBiYW5rIGFjY291bnRzLiIgIlNwcmluZyBNVkMgUmVzdCBDb250cm9sbGVyIgogICAgICAgICAgICAgICAgcmVzZXRQYXNzd29yZENvbnRyb2xsZXIgPSBjb21wb25lbnQgIlJlc2V0IFBhc3N3b3JkIENvbnRyb2xsZXIiICJBbGxvd3MgdXNlcnMgdG8gcmVzZXQgdGhlaXIgcGFzc3dvcmRzIHdpdGggYSBzaW5nbGUgdXNlIFVSTC4iICJTcHJpbmcgTVZDIFJlc3QgQ29udHJvbGxlciIKICAgICAgICAgICAgICAgIHNlY3VyaXR5Q29tcG9uZW50ID0gY29tcG9uZW50ICJTZWN1cml0eSBDb21wb25lbnQiICJQcm92aWRlcyBmdW5jdGlvbmFsaXR5IHJlbGF0ZWQgdG8gc2lnbmluZyBpbiwgY2hhbmdpbmcgcGFzc3dvcmRzLCBldGMuIiAiU3ByaW5nIEJlYW4iCiAgICAgICAgICAgICAgICBtYWluZnJhbWVCYW5raW5nU3lzdGVtRmFjYWRlID0gY29tcG9uZW50ICJNYWluZnJhbWUgQmFua2luZyBTeXN0ZW0gRmFjYWRlIiAiQSBmYWNhZGUgb250byB0aGUgbWFpbmZyYW1lIGJhbmtpbmcgc3lzdGVtLiIgIlNwcmluZyBCZWFuIgogICAgICAgICAgICAgICAgZW1haWxDb21wb25lbnQgPSBjb21wb25lbnQgIkUtbWFpbCBDb21wb25lbnQiICJTZW5kcyBlLW1haWxzIHRvIHVzZXJzLiIgIlNwcmluZyBCZWFuIgogICAgICAgICAgICB9CiAgICAgICAgICAgIGRhdGFiYXNlID0gY29udGFpbmVyICJEYXRhYmFzZSIgIlN0b3JlcyB1c2VyIHJlZ2lzdHJhdGlvbiBpbmZvcm1hdGlvbiwgaGFzaGVkIGF1dGhlbnRpY2F0aW9uIGNyZWRlbnRpYWxzLCBhY2Nlc3MgbG9ncywgZXRjLiIgIk9yYWNsZSBEYXRhYmFzZSBTY2hlbWEiICJEYXRhYmFzZSIKCiAgICAgICAgICAgICFkb2NzIGRvY3MKICAgICAgICAgICAgIWFkcnMgYWRycwogICAgICAgIH0KCiAgICAgICAgIyByZWxhdGlvbnNoaXBzIHRvL2Zyb20gY29udGFpbmVycwogICAgICAgIGN1c3RvbWVyIC0+IHdlYkFwcGxpY2F0aW9uICJWaXNpdHMgYmlnYmFuay5jb20vaWIgdXNpbmciICJIVFRQUyIKICAgICAgICBjdXN0b21lciAtPiBzaW5nbGVQYWdlQXBwbGljYXRpb24gIlZpZXdzIGFjY291bnQgYmFsYW5jZXMsIGFuZCBtYWtlcyBwYXltZW50cyB1c2luZyIKICAgICAgICBjdXN0b21lciAtPiBtb2JpbGVBcHAgIlZpZXdzIGFjY291bnQgYmFsYW5jZXMsIGFuZCBtYWtlcyBwYXltZW50cyB1c2luZyIKICAgICAgICB3ZWJBcHBsaWNhdGlvbiAtPiBzaW5nbGVQYWdlQXBwbGljYXRpb24gIkRlbGl2ZXJzIHRvIHRoZSBjdXN0b21lcidzIHdlYiBicm93c2VyIgoKICAgICAgICAjIHJlbGF0aW9uc2hpcHMgdG8vZnJvbSBjb21wb25lbnRzCiAgICAgICAgc2luZ2xlUGFnZUFwcGxpY2F0aW9uIC0+IHNpZ25pbkNvbnRyb2xsZXIgIk1ha2VzIEFQSSBjYWxscyB0byIgIkpTT04vSFRUUFMiCiAgICAgICAgc2luZ2xlUGFnZUFwcGxpY2F0aW9uIC0+IGFjY291bnRzU3VtbWFyeUNvbnRyb2xsZXIgIk1ha2VzIEFQSSBjYWxscyB0byIgIkpTT04vSFRUUFMiCiAgICAgICAgc2luZ2xlUGFnZUFwcGxpY2F0aW9uIC0+IHJlc2V0UGFzc3dvcmRDb250cm9sbGVyICJNYWtlcyBBUEkgY2FsbHMgdG8iICJKU09OL0hUVFBTIgogICAgICAgIG1vYmlsZUFwcCAtPiBzaWduaW5Db250cm9sbGVyICJNYWtlcyBBUEkgY2FsbHMgdG8iICJKU09OL0hUVFBTIgogICAgICAgIG1vYmlsZUFwcCAtPiBhY2NvdW50c1N1bW1hcnlDb250cm9sbGVyICJNYWtlcyBBUEkgY2FsbHMgdG8iICJKU09OL0hUVFBTIgogICAgICAgIG1vYmlsZUFwcCAtPiByZXNldFBhc3N3b3JkQ29udHJvbGxlciAiTWFrZXMgQVBJIGNhbGxzIHRvIiAiSlNPTi9IVFRQUyIKICAgICAgICBzaWduaW5Db250cm9sbGVyIC0+IHNlY3VyaXR5Q29tcG9uZW50ICJVc2VzIgogICAgICAgIGFjY291bnRzU3VtbWFyeUNvbnRyb2xsZXIgLT4gbWFpbmZyYW1lQmFua2luZ1N5c3RlbUZhY2FkZSAiVXNlcyIKICAgICAgICByZXNldFBhc3N3b3JkQ29udHJvbGxlciAtPiBzZWN1cml0eUNvbXBvbmVudCAiVXNlcyIKICAgICAgICByZXNldFBhc3N3b3JkQ29udHJvbGxlciAtPiBlbWFpbENvbXBvbmVudCAiVXNlcyIKICAgICAgICBzZWN1cml0eUNvbXBvbmVudCAtPiBkYXRhYmFzZSAiUmVhZHMgZnJvbSBhbmQgd3JpdGVzIHRvIiAiSkRCQyIKICAgICAgICBtYWluZnJhbWVCYW5raW5nU3lzdGVtRmFjYWRlIC0+IG1haW5mcmFtZSAiTWFrZXMgQVBJIGNhbGxzIHRvIiAiWE1ML0hUVFBTIgogICAgICAgIGVtYWlsQ29tcG9uZW50IC0+IGVtYWlsICJTZW5kcyBlLW1haWwgdXNpbmciCgogICAgICAgIGRlcGxveW1lbnRFbnZpcm9ubWVudCAiRGV2ZWxvcG1lbnQiIHsKICAgICAgICAgICAgZGVwbG95bWVudE5vZGUgIkRldmVsb3BlciBMYXB0b3AiICIiICJNaWNyb3NvZnQgV2luZG93cyAxMCBvciBBcHBsZSBtYWNPUyIgewogICAgICAgICAgICAgICAgZGVwbG95bWVudE5vZGUgIldlYiBCcm93c2VyIiAiIiAiQ2hyb21lLCBGaXJlZm94LCBTYWZhcmksIG9yIEVkZ2UiIHsKICAgICAgICAgICAgICAgICAgICBkZXZlbG9wZXJTaW5nbGVQYWdlQXBwbGljYXRpb25JbnN0YW5jZSA9IGNvbnRhaW5lckluc3RhbmNlIHNpbmdsZVBhZ2VBcHBsaWNhdGlvbgogICAgICAgICAgICAgICAgfQogICAgICAgICAgICAgICAgZGVwbG95bWVudE5vZGUgIkRvY2tlciBDb250YWluZXIgLSBXZWIgU2VydmVyIiAiIiAiRG9ja2VyIiB7CiAgICAgICAgICAgICAgICAgICAgZGVwbG95bWVudE5vZGUgIkFwYWNoZSBUb21jYXQiICIiICJBcGFjaGUgVG9tY2F0IDgueCIgewogICAgICAgICAgICAgICAgICAgICAgICBkZXZlbG9wZXJXZWJBcHBsaWNhdGlvbkluc3RhbmNlID0gY29udGFpbmVySW5zdGFuY2Ugd2ViQXBwbGljYXRpb24KICAgICAgICAgICAgICAgICAgICAgICAgZGV2ZWxvcGVyQXBpQXBwbGljYXRpb25JbnN0YW5jZSA9IGNvbnRhaW5lckluc3RhbmNlIGFwaUFwcGxpY2F0aW9uCiAgICAgICAgICAgICAgICAgICAgfQogICAgICAgICAgICAgICAgfQogICAgICAgICAgICAgICAgZGVwbG95bWVudE5vZGUgIkRvY2tlciBDb250YWluZXIgLSBEYXRhYmFzZSBTZXJ2ZXIiICIiICJEb2NrZXIiIHsKICAgICAgICAgICAgICAgICAgICBkZXBsb3ltZW50Tm9kZSAiRGF0YWJhc2UgU2VydmVyIiAiIiAiT3JhY2xlIDEyYyIgewogICAgICAgICAgICAgICAgICAgICAgICBkZXZlbG9wZXJEYXRhYmFzZUluc3RhbmNlID0gY29udGFpbmVySW5zdGFuY2UgZGF0YWJhc2UKICAgICAgICAgICAgICAgICAgICB9CiAgICAgICAgICAgICAgICB9CiAgICAgICAgICAgIH0KICAgICAgICAgICAgZGVwbG95bWVudE5vZGUgIkJpZyBCYW5rIHBsYyIgIiIgIkJpZyBCYW5rIHBsYyBkYXRhIGNlbnRlciIgIiIgewogICAgICAgICAgICAgICAgZGVwbG95bWVudE5vZGUgImJpZ2JhbmstZGV2MDAxIiAiIiAiIiAiIiB7CiAgICAgICAgICAgICAgICAgICAgc29mdHdhcmVTeXN0ZW1JbnN0YW5jZSBtYWluZnJhbWUKICAgICAgICAgICAgICAgIH0KICAgICAgICAgICAgfQogICAgICAgIH0KCiAgICAgICAgZGVwbG95bWVudEVudmlyb25tZW50ICJMaXZlIiB7CiAgICAgICAgICAgIGRlcGxveW1lbnROb2RlICJDdXN0b21lcidzIG1vYmlsZSBkZXZpY2UiICIiICJBcHBsZSBpT1Mgb3IgQW5kcm9pZCIgewogICAgICAgICAgICAgICAgbGl2ZU1vYmlsZUFwcEluc3RhbmNlID0gY29udGFpbmVySW5zdGFuY2UgbW9iaWxlQXBwCiAgICAgICAgICAgIH0KICAgICAgICAgICAgZGVwbG95bWVudE5vZGUgIkN1c3RvbWVyJ3MgY29tcHV0ZXIiICIiICJNaWNyb3NvZnQgV2luZG93cyBvciBBcHBsZSBtYWNPUyIgewogICAgICAgICAgICAgICAgZGVwbG95bWVudE5vZGUgIldlYiBCcm93c2VyIiAiIiAiQ2hyb21lLCBGaXJlZm94LCBTYWZhcmksIG9yIEVkZ2UiIHsKICAgICAgICAgICAgICAgICAgICBsaXZlU2luZ2xlUGFnZUFwcGxpY2F0aW9uSW5zdGFuY2UgPSBjb250YWluZXJJbnN0YW5jZSBzaW5nbGVQYWdlQXBwbGljYXRpb24KICAgICAgICAgICAgICAgIH0KICAgICAgICAgICAgfQoKICAgICAgICAgICAgZGVwbG95bWVudE5vZGUgIkJpZyBCYW5rIHBsYyIgIiIgIkJpZyBCYW5rIHBsYyBkYXRhIGNlbnRlciIgewogICAgICAgICAgICAgICAgZGVwbG95bWVudE5vZGUgImJpZ2Jhbmstd2ViKioqIiAiIiAiVWJ1bnR1IDE2LjA0IExUUyIgIiIgNCB7CiAgICAgICAgICAgICAgICAgICAgZGVwbG95bWVudE5vZGUgIkFwYWNoZSBUb21jYXQiICIiICJBcGFjaGUgVG9tY2F0IDgueCIgewogICAgICAgICAgICAgICAgICAgICAgICBsaXZlV2ViQXBwbGljYXRpb25JbnN0YW5jZSA9IGNvbnRhaW5lckluc3RhbmNlIHdlYkFwcGxpY2F0aW9uCiAgICAgICAgICAgICAgICAgICAgfQogICAgICAgICAgICAgICAgfQogICAgICAgICAgICAgICAgZGVwbG95bWVudE5vZGUgImJpZ2JhbmstYXBpKioqIiAiIiAiVWJ1bnR1IDE2LjA0IExUUyIgIiIgOCB7CiAgICAgICAgICAgICAgICAgICAgZGVwbG95bWVudE5vZGUgIkFwYWNoZSBUb21jYXQiICIiICJBcGFjaGUgVG9tY2F0IDgueCIgewogICAgICAgICAgICAgICAgICAgICAgICBsaXZlQXBpQXBwbGljYXRpb25JbnN0YW5jZSA9IGNvbnRhaW5lckluc3RhbmNlIGFwaUFwcGxpY2F0aW9uCiAgICAgICAgICAgICAgICAgICAgfQogICAgICAgICAgICAgICAgfQoKICAgICAgICAgICAgICAgIGRlcGxveW1lbnROb2RlICJiaWdiYW5rLWRiMDEiICIiICJVYnVudHUgMTYuMDQgTFRTIiB7CiAgICAgICAgICAgICAgICAgICAgcHJpbWFyeURhdGFiYXNlU2VydmVyID0gZGVwbG95bWVudE5vZGUgIk9yYWNsZSAtIFByaW1hcnkiICIiICJPcmFjbGUgMTJjIiB7CiAgICAgICAgICAgICAgICAgICAgICAgIGxpdmVQcmltYXJ5RGF0YWJhc2VJbnN0YW5jZSA9IGNvbnRhaW5lckluc3RhbmNlIGRhdGFiYXNlCiAgICAgICAgICAgICAgICAgICAgfQogICAgICAgICAgICAgICAgfQogICAgICAgICAgICAgICAgZGVwbG95bWVudE5vZGUgImJpZ2JhbmstZGIwMiIgIiIgIlVidW50dSAxNi4wNCBMVFMiICJGYWlsb3ZlciIgewogICAgICAgICAgICAgICAgICAgIHNlY29uZGFyeURhdGFiYXNlU2VydmVyID0gZGVwbG95bWVudE5vZGUgIk9yYWNsZSAtIFNlY29uZGFyeSIgIiIgIk9yYWNsZSAxMmMiICJGYWlsb3ZlciIgewogICAgICAgICAgICAgICAgICAgICAgICBsaXZlU2Vjb25kYXJ5RGF0YWJhc2VJbnN0YW5jZSA9IGNvbnRhaW5lckluc3RhbmNlIGRhdGFiYXNlICJGYWlsb3ZlciIKICAgICAgICAgICAgICAgICAgICB9CiAgICAgICAgICAgICAgICB9CiAgICAgICAgICAgICAgICBkZXBsb3ltZW50Tm9kZSAiYmlnYmFuay1wcm9kMDAxIiAiIiAiIiAiIiB7CiAgICAgICAgICAgICAgICAgICAgc29mdHdhcmVTeXN0ZW1JbnN0YW5jZSBtYWluZnJhbWUKICAgICAgICAgICAgICAgIH0KICAgICAgICAgICAgfQoKICAgICAgICAgICAgcHJpbWFyeURhdGFiYXNlU2VydmVyIC0+IHNlY29uZGFyeURhdGFiYXNlU2VydmVyICJSZXBsaWNhdGVzIGRhdGEgdG8iCiAgICAgICAgfQogICAgfQoKICAgIHZpZXdzIHsKICAgICAgICBzeXN0ZW1jb250ZXh0IGludGVybmV0QmFua2luZ1N5c3RlbSAiU3lzdGVtQ29udGV4dCIgewogICAgICAgICAgICBpbmNsdWRlICoKICAgICAgICAgICAgYW5pbWF0aW9uIHsKICAgICAgICAgICAgICAgIGludGVybmV0QmFua2luZ1N5c3RlbQogICAgICAgICAgICAgICAgY3VzdG9tZXIKICAgICAgICAgICAgICAgIG1haW5mcmFtZQogICAgICAgICAgICAgICAgZW1haWwKICAgICAgICAgICAgfQogICAgICAgIH0KCiAgICAgICAgY29udGFpbmVyIGludGVybmV0QmFua2luZ1N5c3RlbSAiQ29udGFpbmVycyIgewogICAgICAgICAgICBpbmNsdWRlICoKICAgICAgICAgICAgYW5pbWF0aW9uIHsKICAgICAgICAgICAgICAgIGN1c3RvbWVyIG1haW5mcmFtZSBlbWFpbAogICAgICAgICAgICAgICAgd2ViQXBwbGljYXRpb24KICAgICAgICAgICAgICAgIHNpbmdsZVBhZ2VBcHBsaWNhdGlvbgogICAgICAgICAgICAgICAgbW9iaWxlQXBwCiAgICAgICAgICAgICAgICBhcGlBcHBsaWNhdGlvbgogICAgICAgICAgICAgICAgZGF0YWJhc2UKICAgICAgICAgICAgfQogICAgICAgIH0KCiAgICAgICAgY29tcG9uZW50IGFwaUFwcGxpY2F0aW9uICJDb21wb25lbnRzIiB7CiAgICAgICAgICAgIGluY2x1ZGUgKgogICAgICAgICAgICBhbmltYXRpb24gewogICAgICAgICAgICAgICAgc2luZ2xlUGFnZUFwcGxpY2F0aW9uIG1vYmlsZUFwcCBkYXRhYmFzZSBlbWFpbCBtYWluZnJhbWUKICAgICAgICAgICAgICAgIHNpZ25pbkNvbnRyb2xsZXIgc2VjdXJpdHlDb21wb25lbnQKICAgICAgICAgICAgICAgIGFjY291bnRzU3VtbWFyeUNvbnRyb2xsZXIgbWFpbmZyYW1lQmFua2luZ1N5c3RlbUZhY2FkZQogICAgICAgICAgICAgICAgcmVzZXRQYXNzd29yZENvbnRyb2xsZXIgZW1haWxDb21wb25lbnQKICAgICAgICAgICAgfQogICAgICAgIH0KCiAgICAgICAgZHluYW1pYyBhcGlBcHBsaWNhdGlvbiAiU2lnbkluIiAiU3VtbWFyaXNlcyBob3cgdGhlIHNpZ24gaW4gZmVhdHVyZSB3b3JrcyBpbiB0aGUgc2luZ2xlLXBhZ2UgYXBwbGljYXRpb24uIiB7CiAgICAgICAgICAgIHNpbmdsZVBhZ2VBcHBsaWNhdGlvbiAtPiBzaWduaW5Db250cm9sbGVyICJTdWJtaXRzIGNyZWRlbnRpYWxzIHRvIgogICAgICAgICAgICBzaWduaW5Db250cm9sbGVyIC0+IHNlY3VyaXR5Q29tcG9uZW50ICJWYWxpZGF0ZXMgY3JlZGVudGlhbHMgdXNpbmciCiAgICAgICAgICAgIHNlY3VyaXR5Q29tcG9uZW50IC0+IGRhdGFiYXNlICJzZWxlY3QgKiBmcm9tIHVzZXJzIHdoZXJlIHVzZXJuYW1lID0gPyIKICAgICAgICAgICAgZGF0YWJhc2UgLT4gc2VjdXJpdHlDb21wb25lbnQgIlJldHVybnMgdXNlciBkYXRhIHRvIgogICAgICAgICAgICBzZWN1cml0eUNvbXBvbmVudCAtPiBzaWduaW5Db250cm9sbGVyICJSZXR1cm5zIHRydWUgaWYgdGhlIGhhc2hlZCBwYXNzd29yZCBtYXRjaGVzIgogICAgICAgICAgICBzaWduaW5Db250cm9sbGVyIC0+IHNpbmdsZVBhZ2VBcHBsaWNhdGlvbiAiU2VuZHMgYmFjayBhbiBhdXRoZW50aWNhdGlvbiB0b2tlbiB0byIKICAgICAgICB9CgogICAgICAgIGRlcGxveW1lbnQgaW50ZXJuZXRCYW5raW5nU3lzdGVtICJEZXZlbG9wbWVudCIgIkRldmVsb3BtZW50RGVwbG95bWVudCIgewogICAgICAgICAgICBpbmNsdWRlICoKICAgICAgICAgICAgYW5pbWF0aW9uIHsKICAgICAgICAgICAgICAgIGRldmVsb3BlclNpbmdsZVBhZ2VBcHBsaWNhdGlvbkluc3RhbmNlCiAgICAgICAgICAgICAgICBkZXZlbG9wZXJXZWJBcHBsaWNhdGlvbkluc3RhbmNlIGRldmVsb3BlckFwaUFwcGxpY2F0aW9uSW5zdGFuY2UKICAgICAgICAgICAgICAgIGRldmVsb3BlckRhdGFiYXNlSW5zdGFuY2UKICAgICAgICAgICAgfQogICAgICAgIH0KCiAgICAgICAgZGVwbG95bWVudCBpbnRlcm5ldEJhbmtpbmdTeXN0ZW0gIkxpdmUiICJMaXZlRGVwbG95bWVudCIgewogICAgICAgICAgICBpbmNsdWRlICoKICAgICAgICAgICAgYW5pbWF0aW9uIHsKICAgICAgICAgICAgICAgIGxpdmVTaW5nbGVQYWdlQXBwbGljYXRpb25JbnN0YW5jZQogICAgICAgICAgICAgICAgbGl2ZU1vYmlsZUFwcEluc3RhbmNlCiAgICAgICAgICAgICAgICBsaXZlV2ViQXBwbGljYXRpb25JbnN0YW5jZSBsaXZlQXBpQXBwbGljYXRpb25JbnN0YW5jZQogICAgICAgICAgICAgICAgbGl2ZVByaW1hcnlEYXRhYmFzZUluc3RhbmNlCiAgICAgICAgICAgICAgICBsaXZlU2Vjb25kYXJ5RGF0YWJhc2VJbnN0YW5jZQogICAgICAgICAgICB9CiAgICAgICAgfQoKICAgICAgICBzdHlsZXMgewogICAgICAgICAgICBlbGVtZW50ICJQZXJzb24iIHsKICAgICAgICAgICAgICAgIGNvbG9yICNmZmZmZmYKICAgICAgICAgICAgICAgIGZvbnRTaXplIDIyCiAgICAgICAgICAgICAgICBzaGFwZSBQZXJzb24KICAgICAgICAgICAgfQogICAgICAgICAgICBlbGVtZW50ICJDdXN0b21lciIgewogICAgICAgICAgICAgICAgYmFja2dyb3VuZCAjMDg0MjdiCiAgICAgICAgICAgIH0KICAgICAgICAgICAgZWxlbWVudCAiQmFuayBTdGFmZiIgewogICAgICAgICAgICAgICAgYmFja2dyb3VuZCAjOTk5OTk5CiAgICAgICAgICAgIH0KICAgICAgICAgICAgZWxlbWVudCAiU29mdHdhcmUgU3lzdGVtIiB7CiAgICAgICAgICAgICAgICBiYWNrZ3JvdW5kICMxMTY4YmQKICAgICAgICAgICAgICAgIGNvbG9yICNmZmZmZmYKICAgICAgICAgICAgfQogICAgICAgICAgICBlbGVtZW50ICJFeGlzdGluZyBTeXN0ZW0iIHsKICAgICAgICAgICAgICAgIGJhY2tncm91bmQgIzk5OTk5OQogICAgICAgICAgICAgICAgY29sb3IgI2ZmZmZmZgogICAgICAgICAgICB9CiAgICAgICAgICAgIGVsZW1lbnQgIkNvbnRhaW5lciIgewogICAgICAgICAgICAgICAgYmFja2dyb3VuZCAjNDM4ZGQ1CiAgICAgICAgICAgICAgICBjb2xvciAjZmZmZmZmCiAgICAgICAgICAgIH0KICAgICAgICAgICAgZWxlbWVudCAiV2ViIEJyb3dzZXIiIHsKICAgICAgICAgICAgICAgIHNoYXBlIFdlYkJyb3dzZXIKICAgICAgICAgICAgfQogICAgICAgICAgICBlbGVtZW50ICJNb2JpbGUgQXBwIiB7CiAgICAgICAgICAgICAgICBzaGFwZSBNb2JpbGVEZXZpY2VMYW5kc2NhcGUKICAgICAgICAgICAgfQogICAgICAgICAgICBlbGVtZW50ICJEYXRhYmFzZSIgewogICAgICAgICAgICAgICAgc2hhcGUgQ3lsaW5kZXIKICAgICAgICAgICAgfQogICAgICAgICAgICBlbGVtZW50ICJDb21wb25lbnQiIHsKICAgICAgICAgICAgICAgIGJhY2tncm91bmQgIzg1YmJmMAogICAgICAgICAgICAgICAgY29sb3IgIzAwMDAwMAogICAgICAgICAgICB9CiAgICAgICAgICAgIGVsZW1lbnQgIkZhaWxvdmVyIiB7CiAgICAgICAgICAgICAgICBvcGFjaXR5IDI1CiAgICAgICAgICAgIH0KICAgICAgICB9CiAgICB9Cgp9Cg==" + }, + "model": { + "enterprise": { + "name": "Big Bank plc" + }, + "people": [ + { + "id": "3", + "tags": "Element,Person,Bank Staff", + "name": "Back Office Staff", + "description": "Administration and support staff within the bank.", + "relationships": [ + { + "id": "16", + "tags": "Relationship", + "sourceId": "3", + "destinationId": "4", + "description": "Uses", + "defaultTags": [ + "Relationship" + ] + } + ], + "location": "Internal", + "defaultTags": [ + "Element", + "Person" + ] + }, + { + "id": "2", + "tags": "Element,Person,Bank Staff", + "name": "Customer Service Staff", + "description": "Customer service staff within the bank.", + "relationships": [ + { + "id": "13", + "tags": "Relationship", + "sourceId": "2", + "destinationId": "4", + "description": "Uses", + "defaultTags": [ + "Relationship" + ] + } + ], + "location": "Internal", + "defaultTags": [ + "Element", + "Person" + ] + }, + { + "id": "1", + "tags": "Element,Person,Customer", + "name": "Personal Banking Customer", + "description": "A customer of the bank, with personal bank accounts.", + "relationships": [ + { + "id": "30", + "tags": "Relationship", + "sourceId": "1", + "destinationId": "18", + "description": "Views account balances, and makes payments using", + "defaultTags": [ + "Relationship" + ] + }, + { + "id": "14", + "tags": "Relationship", + "sourceId": "1", + "destinationId": "6", + "description": "Withdraws cash using", + "defaultTags": [ + "Relationship" + ] + }, + { + "id": "12", + "tags": "Relationship", + "sourceId": "1", + "destinationId": "2", + "description": "Asks questions to", + "technology": "Telephone", + "defaultTags": [ + "Relationship" + ] + }, + { + "id": "8", + "tags": "Relationship", + "sourceId": "1", + "destinationId": "7", + "description": "Views account balances, and makes payments using", + "defaultTags": [ + "Relationship" + ] + }, + { + "id": "28", + "tags": "Relationship", + "sourceId": "1", + "destinationId": "19", + "description": "Visits bigbank.com/ib using", + "technology": "HTTPS", + "defaultTags": [ + "Relationship" + ] + }, + { + "id": "29", + "tags": "Relationship", + "sourceId": "1", + "destinationId": "17", + "description": "Views account balances, and makes payments using", + "defaultTags": [ + "Relationship" + ] + } + ], + "location": "External", + "defaultTags": [ + "Element", + "Person" + ] + } + ], + "softwareSystems": [ + { + "id": "6", + "tags": "Element,Software System,Existing System", + "name": "ATM", + "description": "Allows customers to withdraw cash.", + "relationships": [ + { + "id": "15", + "tags": "Relationship", + "sourceId": "6", + "destinationId": "4", + "description": "Uses", + "defaultTags": [ + "Relationship" + ] + } + ], + "location": "Internal", + "defaultTags": [ + "Element", + "Software System" + ] + }, + { + "id": "5", + "tags": "Element,Software System,Existing System", + "name": "E-mail System", + "description": "The internal Microsoft Exchange e-mail system.", + "relationships": [ + { + "id": "11", + "tags": "Relationship", + "sourceId": "5", + "destinationId": "1", + "description": "Sends e-mails to", + "defaultTags": [ + "Relationship" + ] + } + ], + "location": "Internal", + "defaultTags": [ + "Element", + "Software System" + ] + }, + { + "id": "7", + "tags": "Element,Software System", + "name": "Internet Banking System", + "description": "Allows customers to view information about their bank accounts, and make payments.", + "relationships": [ + { + "id": "10", + "tags": "Relationship", + "sourceId": "7", + "destinationId": "5", + "description": "Sends e-mail using", + "defaultTags": [ + "Relationship" + ] + }, + { + "id": "9", + "tags": "Relationship", + "sourceId": "7", + "destinationId": "4", + "description": "Gets account information from, and makes payments using", + "defaultTags": [ + "Relationship" + ] + } + ], + "location": "Internal", + "containers": [ + { + "id": "20", + "tags": "Element,Container", + "name": "API Application", + "description": "Provides Internet banking functionality via a JSON/HTTPS API.", + "relationships": [ + { + "id": "45", + "tags": "Relationship", + "sourceId": "20", + "destinationId": "27", + "description": "Reads from and writes to", + "technology": "JDBC", + "defaultTags": [ + "Relationship" + ] + }, + { + "id": "49", + "tags": "Relationship", + "sourceId": "20", + "destinationId": "5", + "description": "Sends e-mail using", + "defaultTags": [ + "Relationship" + ] + }, + { + "id": "47", + "tags": "Relationship", + "sourceId": "20", + "destinationId": "4", + "description": "Makes API calls to", + "technology": "XML/HTTPS", + "defaultTags": [ + "Relationship" + ] + } + ], + "technology": "Java and Spring MVC", + "components": [ + { + "id": "22", + "tags": "Element,Component", + "name": "Accounts Summary Controller", + "description": "Provides customers with a summary of their bank accounts.", + "relationships": [ + { + "id": "41", + "tags": "Relationship", + "sourceId": "22", + "destinationId": "25", + "description": "Uses", + "defaultTags": [ + "Relationship" + ] + } + ], + "technology": "Spring MVC Rest Controller", + "size": 0, + "defaultTags": [ + "Element", + "Component" + ] + }, + { + "id": "26", + "tags": "Element,Component", + "name": "E-mail Component", + "description": "Sends e-mails to users.", + "relationships": [ + { + "id": "48", + "tags": "Relationship", + "sourceId": "26", + "destinationId": "5", + "description": "Sends e-mail using", + "defaultTags": [ + "Relationship" + ] + } + ], + "technology": "Spring Bean", + "size": 0, + "defaultTags": [ + "Element", + "Component" + ] + }, + { + "id": "25", + "tags": "Element,Component", + "name": "Mainframe Banking System Facade", + "description": "A facade onto the mainframe banking system.", + "relationships": [ + { + "id": "46", + "tags": "Relationship", + "sourceId": "25", + "destinationId": "4", + "description": "Makes API calls to", + "technology": "XML/HTTPS", + "defaultTags": [ + "Relationship" + ] + } + ], + "technology": "Spring Bean", + "size": 0, + "defaultTags": [ + "Element", + "Component" + ] + }, + { + "id": "23", + "tags": "Element,Component", + "name": "Reset Password Controller", + "description": "Allows users to reset their passwords with a single use URL.", + "relationships": [ + { + "id": "42", + "tags": "Relationship", + "sourceId": "23", + "destinationId": "24", + "description": "Uses", + "defaultTags": [ + "Relationship" + ] + }, + { + "id": "43", + "tags": "Relationship", + "sourceId": "23", + "destinationId": "26", + "description": "Uses", + "defaultTags": [ + "Relationship" + ] + } + ], + "technology": "Spring MVC Rest Controller", + "size": 0, + "defaultTags": [ + "Element", + "Component" + ] + }, + { + "id": "24", + "tags": "Element,Component", + "name": "Security Component", + "description": "Provides functionality related to signing in, changing passwords, etc.", + "relationships": [ + { + "id": "44", + "tags": "Relationship", + "sourceId": "24", + "destinationId": "27", + "description": "Reads from and writes to", + "technology": "JDBC", + "defaultTags": [ + "Relationship" + ] + } + ], + "technology": "Spring Bean", + "size": 0, + "defaultTags": [ + "Element", + "Component" + ] + }, + { + "id": "21", + "tags": "Element,Component", + "name": "Sign In Controller", + "description": "Allows users to sign in to the Internet Banking System.", + "relationships": [ + { + "id": "40", + "tags": "Relationship", + "sourceId": "21", + "destinationId": "24", + "description": "Uses", + "defaultTags": [ + "Relationship" + ] + } + ], + "technology": "Spring MVC Rest Controller", + "size": 0, + "defaultTags": [ + "Element", + "Component" + ] + } + ], + "defaultTags": [ + "Element", + "Container" + ] + }, + { + "id": "27", + "tags": "Element,Container,Database", + "name": "Database", + "description": "Stores user registration information, hashed authentication credentials, access logs, etc.", + "technology": "Oracle Database Schema", + "defaultTags": [ + "Element", + "Container" + ] + }, + { + "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": "37", + "tags": "Relationship", + "sourceId": "18", + "destinationId": "20", + "description": "Makes API calls to", + "technology": "JSON/HTTPS", + "defaultTags": [ + "Relationship" + ] + }, + { + "id": "38", + "tags": "Relationship", + "sourceId": "18", + "destinationId": "22", + "description": "Makes API calls to", + "technology": "JSON/HTTPS", + "defaultTags": [ + "Relationship" + ] + }, + { + "id": "39", + "tags": "Relationship", + "sourceId": "18", + "destinationId": "23", + "description": "Makes API calls to", + "technology": "JSON/HTTPS", + "defaultTags": [ + "Relationship" + ] + }, + { + "id": "36", + "tags": "Relationship", + "sourceId": "18", + "destinationId": "21", + "description": "Makes API calls to", + "technology": "JSON/HTTPS", + "defaultTags": [ + "Relationship" + ] + } + ], + "technology": "Xamarin", + "defaultTags": [ + "Element", + "Container" + ] + }, + { + "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": "33", + "tags": "Relationship", + "sourceId": "17", + "destinationId": "20", + "description": "Makes API calls to", + "technology": "JSON/HTTPS", + "defaultTags": [ + "Relationship" + ] + }, + { + "id": "34", + "tags": "Relationship", + "sourceId": "17", + "destinationId": "22", + "description": "Makes API calls to", + "technology": "JSON/HTTPS", + "defaultTags": [ + "Relationship" + ] + }, + { + "id": "35", + "tags": "Relationship", + "sourceId": "17", + "destinationId": "23", + "description": "Makes API calls to", + "technology": "JSON/HTTPS", + "defaultTags": [ + "Relationship" + ] + }, + { + "id": "32", + "tags": "Relationship", + "sourceId": "17", + "destinationId": "21", + "description": "Makes API calls to", + "technology": "JSON/HTTPS", + "defaultTags": [ + "Relationship" + ] + } + ], + "technology": "JavaScript and Angular", + "defaultTags": [ + "Element", + "Container" + ] + }, + { + "id": "19", + "tags": "Element,Container", + "name": "Web Application", + "description": "Delivers the static content and the Internet banking single page application.", + "relationships": [ + { + "id": "31", + "tags": "Relationship", + "sourceId": "19", + "destinationId": "17", + "description": "Delivers to the customer's web browser", + "defaultTags": [ + "Relationship" + ] + } + ], + "technology": "Java and Spring MVC", + "defaultTags": [ + "Element", + "Container" + ] + } + ], + "defaultTags": [ + "Element", + "Software System" + ] + }, + { + "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", + "defaultTags": [ + "Element", + "Software System" + ] + } + ], + "deploymentNodes": [ + { + "id": "63", + "tags": "Element,Deployment Node", + "name": "Big Bank plc", + "environment": "Development", + "technology": "Big Bank plc data center", + "instances": 1, + "children": [ + { + "id": "64", + "tags": "Element,Deployment Node", + "name": "bigbank-dev001", + "environment": "Development", + "instances": 1, + "softwareSystemInstances": [ + { + "id": "65", + "tags": "Software System Instance", + "environment": "Development", + "deploymentGroups": [ + "Default" + ], + "instanceId": 1, + "softwareSystemId": "4" + } + ], + "children": [], + "containerInstances": [], + "infrastructureNodes": [] + } + ], + "softwareSystemInstances": [], + "containerInstances": [], + "infrastructureNodes": [] + }, + { + "id": "50", + "tags": "Element,Deployment Node", + "name": "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", + "environment": "Development", + "technology": "Docker", + "instances": 1, + "children": [ + { + "id": "60", + "tags": "Element,Deployment Node", + "name": "Database Server", + "environment": "Development", + "technology": "Oracle 12c", + "instances": 1, + "containerInstances": [ + { + "id": "61", + "tags": "Container Instance", + "environment": "Development", + "deploymentGroups": [ + "Default" + ], + "instanceId": 1, + "containerId": "27" + } + ], + "children": [], + "softwareSystemInstances": [], + "infrastructureNodes": [] + } + ], + "softwareSystemInstances": [], + "containerInstances": [], + "infrastructureNodes": [] + }, + { + "id": "53", + "tags": "Element,Deployment Node", + "name": "Docker Container - Web Server", + "environment": "Development", + "technology": "Docker", + "instances": 1, + "children": [ + { + "id": "54", + "tags": "Element,Deployment Node", + "name": "Apache Tomcat", + "environment": "Development", + "technology": "Apache Tomcat 8.x", + "instances": 1, + "containerInstances": [ + { + "id": "57", + "tags": "Container Instance", + "relationships": [ + { + "id": "66", + "sourceId": "57", + "destinationId": "65", + "description": "Makes API calls to", + "technology": "XML/HTTPS", + "linkedRelationshipId": "47" + }, + { + "id": "62", + "sourceId": "57", + "destinationId": "61", + "description": "Reads from and writes to", + "technology": "JDBC", + "linkedRelationshipId": "45" + } + ], + "environment": "Development", + "deploymentGroups": [ + "Default" + ], + "instanceId": 1, + "containerId": "20" + }, + { + "id": "55", + "tags": "Container Instance", + "relationships": [ + { + "id": "56", + "sourceId": "55", + "destinationId": "52", + "description": "Delivers to the customer's web browser", + "linkedRelationshipId": "31" + } + ], + "environment": "Development", + "deploymentGroups": [ + "Default" + ], + "instanceId": 1, + "containerId": "19" + } + ], + "children": [], + "softwareSystemInstances": [], + "infrastructureNodes": [] + } + ], + "softwareSystemInstances": [], + "containerInstances": [], + "infrastructureNodes": [] + }, + { + "id": "51", + "tags": "Element,Deployment Node", + "name": "Web Browser", + "environment": "Development", + "technology": "Chrome, Firefox, Safari, or Edge", + "instances": 1, + "containerInstances": [ + { + "id": "52", + "tags": "Container Instance", + "relationships": [ + { + "id": "58", + "sourceId": "52", + "destinationId": "57", + "description": "Makes API calls to", + "technology": "JSON/HTTPS", + "linkedRelationshipId": "33" + } + ], + "environment": "Development", + "deploymentGroups": [ + "Default" + ], + "instanceId": 1, + "containerId": "17" + } + ], + "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": "77", + "tags": "Element,Deployment Node", + "name": "bigbank-api***", + "environment": "Live", + "technology": "Ubuntu 16.04 LTS", + "instances": 8, + "children": [ + { + "id": "78", + "tags": "Element,Deployment Node", + "name": "Apache Tomcat", + "environment": "Live", + "technology": "Apache Tomcat 8.x", + "instances": 1, + "containerInstances": [ + { + "id": "79", + "tags": "Container Instance", + "relationships": [ + { + "id": "89", + "sourceId": "79", + "destinationId": "88", + "description": "Reads from and writes to", + "technology": "JDBC", + "linkedRelationshipId": "45" + }, + { + "id": "85", + "sourceId": "79", + "destinationId": "84", + "description": "Reads from and writes to", + "technology": "JDBC", + "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": "20" + } + ], + "children": [], + "softwareSystemInstances": [], + "infrastructureNodes": [] + } + ], + "softwareSystemInstances": [], + "containerInstances": [], + "infrastructureNodes": [] + }, + { + "id": "82", + "tags": "Element,Deployment Node", + "name": "bigbank-db01", + "environment": "Live", + "technology": "Ubuntu 16.04 LTS", + "instances": 1, + "children": [ + { + "id": "83", + "tags": "Element,Deployment Node", + "name": "Oracle - Primary", + "relationships": [ + { + "id": "93", + "tags": "Relationship", + "sourceId": "83", + "destinationId": "87", + "description": "Replicates data to", + "defaultTags": [ + "Relationship" + ] + } + ], + "environment": "Live", + "technology": "Oracle 12c", + "instances": 1, + "containerInstances": [ + { + "id": "84", + "tags": "Container Instance", + "environment": "Live", + "deploymentGroups": [ + "Default" + ], + "instanceId": 1, + "containerId": "27" + } + ], + "children": [], + "softwareSystemInstances": [], + "infrastructureNodes": [] + } + ], + "softwareSystemInstances": [], + "containerInstances": [], + "infrastructureNodes": [] + }, + { + "id": "86", + "tags": "Element,Deployment Node,Failover", + "name": "bigbank-db02", + "environment": "Live", + "technology": "Ubuntu 16.04 LTS", + "instances": 1, + "children": [ + { + "id": "87", + "tags": "Element,Deployment Node,Failover", + "name": "Oracle - Secondary", + "environment": "Live", + "technology": "Oracle 12c", + "instances": 1, + "containerInstances": [ + { + "id": "88", + "tags": "Container Instance", + "environment": "Live", + "deploymentGroups": [ + "Default" + ], + "instanceId": 1, + "containerId": "27" + } + ], + "children": [], + "softwareSystemInstances": [], + "infrastructureNodes": [] + } + ], + "softwareSystemInstances": [], + "containerInstances": [], + "infrastructureNodes": [] + }, + { + "id": "90", + "tags": "Element,Deployment Node", + "name": "bigbank-prod001", + "environment": "Live", + "instances": 1, + "softwareSystemInstances": [ + { + "id": "91", + "tags": "Software System Instance", + "environment": "Live", + "deploymentGroups": [ + "Default" + ], + "instanceId": 1, + "softwareSystemId": "4" + } + ], + "children": [], + "containerInstances": [], + "infrastructureNodes": [] + }, + { + "id": "73", + "tags": "Element,Deployment Node", + "name": "bigbank-web***", + "environment": "Live", + "technology": "Ubuntu 16.04 LTS", + "instances": 4, + "children": [ + { + "id": "74", + "tags": "Element,Deployment Node", + "name": "Apache Tomcat", + "environment": "Live", + "technology": "Apache Tomcat 8.x", + "instances": 1, + "containerInstances": [ + { + "id": "75", + "tags": "Container Instance", + "relationships": [ + { + "id": "76", + "sourceId": "75", + "destinationId": "71", + "description": "Delivers to the customer's web browser", + "linkedRelationshipId": "31" + } + ], + "environment": "Live", + "deploymentGroups": [ + "Default" + ], + "instanceId": 1, + "containerId": "19" + } + ], + "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": "80", + "sourceId": "71", + "destinationId": "79", + "description": "Makes API calls to", + "technology": "JSON/HTTPS", + "linkedRelationshipId": "33" + } + ], + "environment": "Live", + "deploymentGroups": [ + "Default" + ], + "instanceId": 1, + "containerId": "17" + } + ], + "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": "81", + "sourceId": "68", + "destinationId": "79", + "description": "Makes API calls to", + "technology": "JSON/HTTPS", + "linkedRelationshipId": "37" + } + ], + "environment": "Live", + "deploymentGroups": [ + "Default" + ], + "instanceId": 1, + "containerId": "18" + } + ], + "children": [], + "softwareSystemInstances": [], + "infrastructureNodes": [] + } + ], + "customElements": [] + }, + "documentation": { + "sections": [ + { + "elementId": "7", + "title": "Development Environment", + "order": 3, + "format": "AsciiDoc", + "content": "== Development Environment\n\nHere is some information about how to set up a development environment for the Internet Banking System...\n\nimage::embed:DevelopmentDeployment[]" + }, + { + "elementId": "7", + "title": "Deployment", + "order": 4, + "format": "AsciiDoc", + "content": "== Deployment\n\nHere is some information about the live deployment environment for the Internet Banking System...\n\nimage::embed:LiveDeployment[]" + }, + { + "elementId": "7", + "title": "Context", + "order": 1, + "format": "Markdown", + "content": "## Context\n\nHere is some context about the Internet Banking System...\n\n![](embed:SystemContext)\n\n### Internet Banking System\n...\n\n### Mainframe Banking System\n...\n" + }, + { + "elementId": "7", + "title": "Software Architecture", + "order": 2, + "format": "Markdown", + "content": "## Software Architecture\n\nHere is some information about the software architecture of the Internet Banking System...\n\n![](embed:Containers)\n\n### Web Application\n...\n\n### API Application\n...\n\nHere is some information about the API Application...\n\n![](embed:Components)\n\n### Sign in process\n\nHere is some information about the Sign In Controller, including how the sign in process works...\n\n![](embed:SignIn)" + } + ], + "decisions": [ + { + "elementId": "7", + "id": "1", + "date": "2020-06-05T00:00:00Z", + "title": "Record architecture decisions", + "status": "Accepted", + "content": "# 1. Record architecture decisions\n\nDate: 2020-06-05\n\n## Status\n\nAccepted\n\n## Context\n\nWe need to record the architectural decisions made on this project.\n\n## Decision\n\nWe 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)\n\n## Consequences\n\nSee Michael Nygard's article, linked above.", + "format": "Markdown" + } + ], + "images": [] + }, + "views": { + "systemContextViews": [ + { + "softwareSystemId": "7", + "key": "SystemContext", + "paperSize": "A5_Landscape", + "animations": [ + { + "order": 1, + "elements": [ + "7" + ], + "relationships": [] + }, + { + "order": 2, + "elements": [ + "1" + ], + "relationships": [ + "8" + ] + }, + { + "order": 3, + "elements": [ + "4" + ], + "relationships": [ + "9" + ] + }, + { + "order": 4, + "elements": [ + "5" + ], + "relationships": [ + "11", + "10" + ] + } + ], + "enterpriseBoundaryVisible": true, + "elements": [ + { + "id": "1", + "x": 632, + "y": 69 + }, + { + "id": "4", + "x": 607, + "y": 1259 + }, + { + "id": "5", + "x": 1422, + "y": 714 + }, + { + "id": "7", + "x": 607, + "y": 714 + } + ], + "relationships": [ + { + "id": "11" + }, + { + "id": "8" + }, + { + "id": "10" + }, + { + "id": "9" + } + ] + } + ], + "containerViews": [ + { + "softwareSystemId": "7", + "key": "Containers", + "paperSize": "A5_Landscape", + "animations": [ + { + "order": 1, + "elements": [ + "1", + "4", + "5" + ], + "relationships": [ + "11" + ] + }, + { + "order": 2, + "elements": [ + "19" + ], + "relationships": [ + "28" + ] + }, + { + "order": 3, + "elements": [ + "17" + ], + "relationships": [ + "29", + "31" + ] + }, + { + "order": 4, + "elements": [ + "18" + ], + "relationships": [ + "30" + ] + }, + { + "order": 5, + "elements": [ + "20" + ], + "relationships": [ + "33", + "47", + "37", + "49" + ] + }, + { + "order": 6, + "elements": [ + "27" + ], + "relationships": [ + "45" + ] + } + ], + "externalSoftwareSystemBoundariesVisible": true, + "elements": [ + { + "id": "1", + "x": 1056, + "y": 24 + }, + { + "id": "4", + "x": 2012, + "y": 1214 + }, + { + "id": "27", + "x": 37, + "y": 1214 + }, + { + "id": "5", + "x": 2012, + "y": 664 + }, + { + "id": "17", + "x": 780, + "y": 664 + }, + { + "id": "18", + "x": 1283, + "y": 664 + }, + { + "id": "19", + "x": 37, + "y": 664 + }, + { + "id": "20", + "x": 1031, + "y": 1214 + } + ], + "relationships": [ + { + "id": "29" + }, + { + "id": "28" + }, + { + "id": "37" + }, + { + "id": "11" + }, + { + "id": "33" + }, + { + "id": "31" + }, + { + "id": "45" + }, + { + "id": "30" + }, + { + "id": "47" + }, + { + "id": "49" + } + ] + } + ], + "componentViews": [ + { + "key": "Components", + "paperSize": "A5_Landscape", + "animations": [ + { + "order": 1, + "elements": [ + "4", + "27", + "5", + "17", + "18" + ], + "relationships": [] + }, + { + "order": 2, + "elements": [ + "24", + "21" + ], + "relationships": [ + "44", + "36", + "40", + "32" + ] + }, + { + "order": 3, + "elements": [ + "22", + "25" + ], + "relationships": [ + "34", + "46", + "38", + "41" + ] + }, + { + "order": 4, + "elements": [ + "23", + "26" + ], + "relationships": [ + "35", + "48", + "39", + "42", + "43" + ] + } + ], + "containerId": "20", + "externalContainerBoundariesVisible": true, + "elements": [ + { + "id": "22", + "x": 1925, + "y": 436 + }, + { + "id": "23", + "x": 1015, + "y": 436 + }, + { + "id": "24", + "x": 105, + "y": 817 + }, + { + "id": "25", + "x": 1925, + "y": 817 + }, + { + "id": "26", + "x": 1015, + "y": 817 + }, + { + "id": "4", + "x": 1925, + "y": 1307 + }, + { + "id": "5", + "x": 1015, + "y": 1307 + }, + { + "id": "27", + "x": 105, + "y": 1307 + }, + { + "id": "17", + "x": 560, + "y": 10 + }, + { + "id": "18", + "x": 1470, + "y": 11 + }, + { + "id": "21", + "x": 105, + "y": 436 + } + ], + "relationships": [ + { + "id": "40", + "position": 55 + }, + { + "id": "41", + "position": 50 + }, + { + "id": "42" + }, + { + "id": "43" + }, + { + "id": "32", + "position": 35 + }, + { + "id": "36", + "position": 85 + }, + { + "id": "35", + "position": 45 + }, + { + "id": "34", + "position": 85 + }, + { + "id": "44", + "position": 60 + }, + { + "id": "46" + }, + { + "id": "48" + }, + { + "id": "38", + "position": 40 + }, + { + "id": "39", + "position": 40 + } + ] + } + ], + "dynamicViews": [ + { + "description": "Summarises how the sign in feature works in the single-page application.", + "key": "SignIn", + "paperSize": "A5_Landscape", + "elementId": "20", + "externalBoundariesVisible": true, + "relationships": [ + { + "id": "32", + "description": "Submits credentials to", + "order": "1", + "response": false, + "vertices": [ + { + "x": 1238, + "y": 236 + } + ], + "routing": "Curved", + "position": 50 + }, + { + "id": "40", + "description": "Validates credentials using", + "order": "2", + "response": false, + "vertices": [ + { + "x": 2065, + "y": 845 + } + ], + "routing": "Curved" + }, + { + "id": "44", + "description": "select * from users where username = ?", + "order": "3", + "response": false, + "vertices": [ + { + "x": 1218, + "y": 1416 + } + ], + "routing": "Curved" + }, + { + "id": "44", + "description": "Returns user data to", + "order": "4", + "response": true, + "vertices": [ + { + "x": 1240, + "y": 1220 + } + ], + "routing": "Curved" + }, + { + "id": "40", + "description": "Returns true if the hashed password matches", + "order": "5", + "response": true, + "vertices": [ + { + "x": 1828, + "y": 841 + } + ], + "routing": "Curved" + }, + { + "id": "32", + "description": "Sends back an authentication token to", + "order": "6", + "response": true, + "vertices": [ + { + "x": 1210, + "y": 450 + } + ], + "routing": "Curved" + } + ], + "elements": [ + { + "id": "24", + "x": 1720, + "y": 1182 + }, + { + "id": "27", + "x": 290, + "y": 1182 + }, + { + "id": "17", + "x": 290, + "y": 192 + }, + { + "id": "21", + "x": 1720, + "y": 192 + } + ] + } + ], + "deploymentViews": [ + { + "softwareSystemId": "7", + "key": "LiveDeployment", + "paperSize": "A4_Landscape", + "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" + ] + } + ], + "elements": [ + { + "id": "88", + "x": 2584, + "y": 184 + }, + { + "id": "77", + "x": 0, + "y": 0 + }, + { + "id": "67", + "x": 0, + "y": 0 + }, + { + "id": "78", + "x": 0, + "y": 0 + }, + { + "id": "68", + "x": 424, + "y": 1071 + }, + { + "id": "79", + "x": 1504, + "y": 1071 + }, + { + "id": "69", + "x": 0, + "y": 0 + }, + { + "id": "90", + "x": 0, + "y": 0 + }, + { + "id": "91", + "x": 2584, + "y": 1959 + }, + { + "id": "70", + "x": 0, + "y": 0 + }, + { + "id": "71", + "x": 424, + "y": 184 + }, + { + "id": "82", + "x": 0, + "y": 0 + }, + { + "id": "83", + "x": 0, + "y": 0 + }, + { + "id": "72", + "x": 0, + "y": 0 + }, + { + "id": "84", + "x": 2584, + "y": 1071 + }, + { + "id": "73", + "x": 0, + "y": 0 + }, + { + "id": "74", + "x": 0, + "y": 0 + }, + { + "id": "86", + "x": 0, + "y": 0 + }, + { + "id": "75", + "x": 1504, + "y": 184 + }, + { + "id": "87", + "x": 0, + "y": 0 + } + ], + "relationships": [ + { + "id": "93" + }, + { + "id": "80" + }, + { + "id": "81" + }, + { + "id": "92" + }, + { + "id": "76" + }, + { + "id": "85" + }, + { + "id": "89" + } + ] + }, + { + "softwareSystemId": "7", + "key": "DevelopmentDeployment", + "paperSize": "A5_Landscape", + "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" + ] + } + ], + "elements": [ + { + "id": "55", + "x": 989, + "y": 176 + }, + { + "id": "57", + "x": 989, + "y": 516 + }, + { + "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": "52", + "x": 152, + "y": 346 + }, + { + "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": 1827, + "y": 1236 + } + ], + "relationships": [ + { + "id": "62", + "position": 50 + }, + { + "id": "56" + }, + { + "id": "66" + }, + { + "id": "58" + } + ] + } + ], + "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 + } + ], + "relationships": [] + }, + "terminology": {}, + "lastSavedView": "LiveDeployment", + "themes": [] + }, + "customViews": [], + "systemLandscapeViews": [], + "filteredViews": [] + } +} \ No newline at end of file diff --git a/structurizr-client/src/test/resources/backwardsCompatibility/structurizr-39459-workspace.json b/structurizr-client/src/test/resources/backwardsCompatibility/structurizr-39459-workspace.json new file mode 100644 index 000000000..d681a67ac --- /dev/null +++ b/structurizr-client/src/test/resources/backwardsCompatibility/structurizr-39459-workspace.json @@ -0,0 +1,278 @@ +{ + "id": 39459, + "name": "adr-tools", + "description": "A description of the adr-tools command line utility.", + "revision": 9, + "lastModifiedDate": "2021-10-17T15:02:07Z", + "lastModifiedUser": "", + "lastModifiedAgent": "structurizr-web/2539/diagram", + "model": { + "people": [ + { + "id": "1", + "tags": "Element,Person", + "name": "User", + "description": "Somebody on a software development team.", + "relationships": [ + { + "id": "5", + "tags": "Relationship,Synchronous", + "sourceId": "1", + "destinationId": "3", + "description": "Manages ADRs using", + "interactionStyle": "Synchronous" + }, + { + "id": "7", + "tags": "Relationship,Synchronous", + "sourceId": "1", + "destinationId": "2", + "description": "Manages ADRs using", + "interactionStyle": "Synchronous" + } + ], + "location": "Unspecified" + } + ], + "softwareSystems": [ + { + "id": "2", + "tags": "Element,Software System", + "url": "https://github.com/npryce/adr-tools", + "name": "adr-tools", + "description": "A command-line tool for working with Architecture Decision Records (ADRs).", + "location": "Unspecified", + "containers": [ + { + "id": "3", + "tags": "Element,Container", + "url": "https://github.com/npryce/adr-tools/tree/master/src", + "name": "adr", + "description": "A command-line tool for working with Architecture Decision Records (ADRs).", + "relationships": [ + { + "id": "6", + "tags": "Relationship,Synchronous", + "sourceId": "3", + "destinationId": "4", + "description": "Reads from and writes to", + "interactionStyle": "Synchronous" + } + ], + "technology": "Shell Scripts" + }, + { + "id": "4", + "tags": "Element,Container,File System", + "name": "File System", + "description": "Stores ADRs, templates, etc.", + "technology": "File System" + } + ] + } + ], + "customElements": [], + "deploymentNodes": [] + }, + "documentation": { + "decisions": [ + { + "elementId": "2", + "id": "8", + "date": "2017-02-21T00:00:00Z", + "title": "Use ISO 8601 Format for Dates", + "status": "Accepted", + "content": "# 8. Use ISO 8601 Format for Dates\n\nDate: 2017-02-21\n\n## Status\n\nAccepted\n\n## Context\n\n`adr-tools` seeks to communicate the history of architectural decisions of a\nproject. An important component of the history is the time at which a decision\nwas made.\n\nTo communicate effectively, `adr-tools` should present information as\nunambiguously as possible. That means that culture-neutral data formats should\nbe preferred over culture-specific formats.\n\nExisting `adr-tools` deployments format dates as `dd/mm/yyyy` by default. That\nformatting is common formatting in the United Kingdom (where the `adr-tools`\nproject was originally written), but is easily confused with the `mm/dd/yyyy`\nformat preferred in the United States.\n\nThe default date format may be overridden by setting `ADR_DATE` in `config.sh`.\n\n## Decision\n\n`adr-tools` will use the ISO 8601 format for dates: `yyyy-mm-dd`\n\n## Consequences\n\nDates are displayed in a standard, culture-neutral format.\n\nThe UK-style and ISO 8601 formats can be distinguished by their separator\ncharacter. The UK-style dates used a slash (`/`), while the ISO dates use a\nhyphen (`-`).\n\nPrior to this decision, `adr-tools` was deployed using the UK format for dates.\nAfter adopting the ISO 8601 format, existing deployments of `adr-tools` must do\none of the following:\n\n * Accept mixed formatting of dates within their documentation library.\n * Update existing documents to use ISO 8601 dates by running `adr upgrade-repository`\n\n---\nThis Architecture Decision Record (ADR) was written by Nat Pryce as a part of [adr-tools](https://github.com/npryce/adr-tools), and is reproduced here under the [Creative Commons Attribution 4.0 International (CC BY 4.0) license](https://creativecommons.org/licenses/by/4.0/).", + "format": "Markdown" + }, + { + "elementId": "2", + "id": "3", + "date": "2016-02-12T00:00:00Z", + "title": "Single command with subcommands", + "status": "Accepted", + "content": "# 3. Single command with subcommands\n\nDate: 2016-02-12\n\n## Status\n\nAccepted\n\n## Context\n\nThe tool provides a number of related commands to create\nand manipulate architecture decision records.\n\nHow can the user find out about the commands that are available?\n\n## Decision\n\nThe tool defines a single command, called `adr`.\n\nThe first argument to `adr` (the subcommand) specifies the\naction to perform. Further arguments are interpreted by the\nsubcommand.\n\nRunning `adr` without any arguments lists the available\nsubcommands.\n\nSubcommands are implemented as scripts in the same\ndirectory as the `adr` script. E.g. the subcommand `new` is\nimplemented as the script `adr-new`, the subcommand `help`\nas the script `adr-help` and so on.\n\nHelper scripts that are part of the implementation but not\nsubcommands follow a different naming convention, so that\nsubcommands can be listed by filtering and transforming script\nfile names.\n\n## Consequences\n\nUsers can more easily explore the capabilities of the tool.\n\nUsers are already used to this style of command-line tool. For\nexample, Git works this way.\n\nEach subcommand can be implemented in the most appropriate\nlanguage.\n\n---\nThis Architecture Decision Record (ADR) was written by Nat Pryce as a part of [adr-tools](https://github.com/npryce/adr-tools), and is reproduced here under the [Creative Commons Attribution 4.0 International (CC BY 4.0) license](https://creativecommons.org/licenses/by/4.0/).\n", + "format": "Markdown" + }, + { + "elementId": "2", + "id": "4", + "date": "2016-02-12T00:00:00Z", + "title": "Markdown format", + "status": "Accepted", + "content": "# 4. Markdown format\n\nDate: 2016-02-12\n\n## Status\n\nAccepted\n\n## Context\n\nThe decision records must be stored in a plain text format:\n\n* This works well with version control systems.\n\n* It allows the tool to modify the status of records and insert\n hyperlinks when one decision supercedes another.\n\n* Decisions can be read in the terminal, IDE, version control\n browser, etc.\n\nPeople will want to use some formatting: lists, code examples,\nand so on.\n\nPeople will want to view the decision records in a more readable\nformat than plain text, and maybe print them out.\n\n\n## Decision\n\nRecord architecture decisions in [Markdown format](https://daringfireball.net/projects/markdown/).\n\n## Consequences\n\nDecisions can be read in the terminal.\n\nDecisions will be formatted nicely and hyperlinked by the\nbrowsers of project hosting sites like GitHub and Bitbucket.\n\nTools like [Pandoc](http://pandoc.org/) can be used to convert\nthe decision records into HTML or PDF.\n\n---\nThis Architecture Decision Record (ADR) was written by Nat Pryce as a part of [adr-tools](https://github.com/npryce/adr-tools), and is reproduced here under the [Creative Commons Attribution 4.0 International (CC BY 4.0) license](https://creativecommons.org/licenses/by/4.0/).", + "format": "Markdown" + }, + { + "elementId": "2", + "id": "2", + "date": "2016-02-12T00:00:00Z", + "title": "Implement as shell scripts", + "status": "Accepted", + "content": "# 2. Implement as shell scripts\n\nDate: 2016-02-12\n\n## Status\n\nAccepted\n\n## Context\n\nADRs are plain text files stored in a subdirectory of the project.\n\nThe tool needs to create new files and apply small edits to\nthe Status section of existing files.\n\n## Decision\n\nThe tool is implemented as shell scripts that use standard Unix\ntools -- grep, sed, awk, etc.\n\n## Consequences\n\nThe tool won't support Windows. Being plain text files, ADRs can\nbe created by hand and edited in any text editor. This tool just\nmakes the process more convenient.\n\nDevelopment will have to cope with differences between Unix\nvariants, particularly Linux and MacOS X.\n\n---\nThis Architecture Decision Record (ADR) was written by Nat Pryce as a part of [adr-tools](https://github.com/npryce/adr-tools), and is reproduced here under the [Creative Commons Attribution 4.0 International (CC BY 4.0) license](https://creativecommons.org/licenses/by/4.0/).", + "format": "Markdown" + }, + { + "elementId": "2", + "id": "6", + "date": "2016-02-16T00:00:00Z", + "title": "Packaging and distribution in other version control repositories", + "status": "Accepted", + "content": "# 6. Packaging and distribution in other version control repositories\n\nDate: 2016-02-16\n\n## Status\n\nAccepted\n\n## Context\n\nUsers want to install adr-tools with their preferred package\nmanager. For example, Ubuntu users use `apt`, RedHat users use\n`yum` and Mac OS X users use [Homebrew](http://brew.sh).\n\nThe developers of `adr-tools` don't know how, nor have permissions,\nto use all these packaging and distribution systems. Therefore packaging\nand distribution must be done by \"downstream\" parties.\n\nThe developers of the tool should not favour any one particular\npackaging and distribution solution.\n\n## Decision\n\nThe `adr-tools` project will not contain any packaging or\ndistribution scripts and config.\n\nPackaging and distribution will be managed by other projects in\nseparate version control repositories.\n\n## Consequences\n\nThe git repo of this project will be simpler.\n\nEventually, users will not have to use Git to get the software.\n\nWe will have to tag releases in the `adr-tools` repository so that\npackaging projects know what can be published and how it should be\nidentified.\n\nWe will document how users can install the software in this\nproject's README file.\n\n---\nThis Architecture Decision Record (ADR) was written by Nat Pryce as a part of [adr-tools](https://github.com/npryce/adr-tools), and is reproduced here under the [Creative Commons Attribution 4.0 International (CC BY 4.0) license](https://creativecommons.org/licenses/by/4.0/).", + "format": "Markdown" + }, + { + "elementId": "2", + "id": "1", + "date": "2016-02-12T00:00:00Z", + "title": "Record architecture decisions", + "status": "Accepted", + "content": "# 1. Record architecture decisions\n\nDate: 2016-02-12\n\n## Status\n\nAccepted\n\n## Context\n\nWe need to record the architectural decisions made on this project.\n\n## Decision\n\nWe 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)\n\n## Consequences\n\nSee Michael Nygard's article, linked above.\n\n---\nThis Architecture Decision Record (ADR) was written by Nat Pryce as a part of [adr-tools](https://github.com/npryce/adr-tools), and is reproduced here under the [Creative Commons Attribution 4.0 International (CC BY 4.0) license](https://creativecommons.org/licenses/by/4.0/).", + "format": "Markdown" + }, + { + "elementId": "2", + "id": "5", + "date": "2016-02-13T00:00:00Z", + "title": "Help comments", + "status": "Accepted", + "content": "# 5. Help comments\n\nDate: 2016-02-13\n\n## Status\n\nAccepted\n\n## Context\n\nThe tool will have a `help` subcommand to provide documentation\nfor users.\n\nIt's nice to have usage documentation in the script files\nthemselves, in comments. When reading the code, that's the first\nplace to look for information about how to run a script.\n\n## Decision\n\nWrite usage documentation in comments in the source file.\n\nDistinguish between documentation comments and normal comments.\nDocumentation comments have two hash characters at the start of\nthe line.\n\nThe `adr help` command can parse comments out from the script\nusing the standard Unix tools `grep` and `cut`.\n\n## Consequences\n\nNo need to maintain help text in a separate file.\n\nHelp text can easily be kept up to date as the script is edited.\n\nThere's no automated check that the help text is up to date. The\ntests do not work well as documentation for users, and the help\ntext is not easily cross-checked against the code.\n\nThis won't work if any subcommands are not implemented as scripts\nthat use '#' as a comment character.\n\n---\nThis Architecture Decision Record (ADR) was written by Nat Pryce as a part of [adr-tools](https://github.com/npryce/adr-tools), and is reproduced here under the [Creative Commons Attribution 4.0 International (CC BY 4.0) license](https://creativecommons.org/licenses/by/4.0/).", + "format": "Markdown" + }, + { + "elementId": "2", + "id": "7", + "date": "2016-12-17T00:00:00Z", + "title": "Invoke adr-config executable to get configuration", + "status": "Accepted", + "content": "# 7. Invoke adr-config executable to get configuration\n\nDate: 2016-12-17\n\n## Status\n\nAccepted\n\n## Context\n\nPackagers (e.g. Homebrew developers) want to configure adr-tools to match the conventions of their installation. \n\nCurrently, this is done by sourcing a file `config.sh`, which should sit beside the `adr` executable.\n\nThis name is too common.\n\nThe `config.sh` file is not executable, and so doesn't belong in a bin directory.\n\n## Decision\n\nReplace `config.sh` with an executable, named `adr-config` that outputs configuration.\n\nEach script in ADR Tools will eval the output of `adr-config` to configure itself.\n\n## Consequences\n\nConfiguration within ADR Tools is a little more complicated.\n\nPackagers 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.\n\nTo 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.)\n\n---\nThis Architecture Decision Record (ADR) was written by Nat Pryce as a part of [adr-tools](https://github.com/npryce/adr-tools), and is reproduced here under the [Creative Commons Attribution 4.0 International (CC BY 4.0) license](https://creativecommons.org/licenses/by/4.0/).", + "format": "Markdown" + } + ], + "sections": [], + "images": [] + }, + "views": { + "systemContextViews": [ + { + "softwareSystemId": "2", + "description": "The system context diagram for adr-tools.", + "key": "SystemContext", + "paperSize": "A5_Landscape", + "animations": [ + { + "order": 1, + "elements": [ + "1", + "2" + ], + "relationships": [ + "7" + ] + } + ], + "enterpriseBoundaryVisible": true, + "elements": [ + { + "id": "1", + "x": 1040, + "y": 224 + }, + { + "id": "2", + "x": 1015, + "y": 1104 + } + ], + "relationships": [ + { + "id": "7" + } + ] + } + ], + "containerViews": [ + { + "softwareSystemId": "2", + "description": "The container diagram for adr-tools.", + "key": "Containers", + "paperSize": "A5_Landscape", + "dimensions": { + "width": 2480, + "height": 1748 + }, + "externalSoftwareSystemBoundariesVisible": false, + "elements": [ + { + "id": "1", + "x": 1040, + "y": 75 + }, + { + "id": "3", + "x": 1015, + "y": 713 + }, + { + "id": "4", + "x": 1015, + "y": 1251 + } + ], + "relationships": [ + { + "id": "5", + "routing": "Orthogonal" + }, + { + "id": "6", + "routing": "Orthogonal" + } + ], + "animations": [] + } + ], + "configuration": { + "branding": {}, + "styles": { + "elements": [ + { + "tag": "Element", + "color": "#ffffff", + "shape": "RoundedBox" + }, + { + "tag": "Software System", + "background": "#18adad", + "color": "#ffffff" + }, + { + "tag": "Person", + "background": "#008282", + "color": "#ffffff", + "shape": "Person" + }, + { + "tag": "Container", + "background": "#6dbfbf" + }, + { + "tag": "File System", + "shape": "Folder" + } + ], + "relationships": [] + }, + "terminology": {}, + "lastSavedView": "Containers", + "themes": [] + }, + "customViews": [], + "systemLandscapeViews": [], + "componentViews": [], + "dynamicViews": [], + "deploymentViews": [], + "filteredViews": [] + }, + "properties": {} +} \ No newline at end of file diff --git a/structurizr-client/src/test/resources/backwardsCompatibility/views-without-order.json b/structurizr-client/src/test/resources/backwardsCompatibility/views-without-order.json new file mode 100644 index 000000000..c84331f31 --- /dev/null +++ b/structurizr-client/src/test/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-client/src/test/resources/logo.png b/structurizr-client/src/test/resources/logo.png new file mode 100644 index 000000000..763d19bf5 Binary files /dev/null and b/structurizr-client/src/test/resources/logo.png differ diff --git a/structurizr-client/src/test/resources/structurizr-36141-workspace.json b/structurizr-client/src/test/resources/structurizr-36141-workspace.json new file mode 100644 index 000000000..e3f877256 --- /dev/null +++ b/structurizr-client/src/test/resources/structurizr-36141-workspace.json @@ -0,0 +1,1876 @@ +{ + "id": 36141, + "name": "Big Bank plc - Internet Banking System", + "description": "The software architecture of the Big Bank plc Internet Banking System.", + "revision": 71, + "lastModifiedDate": "2023-11-11T09:04:13Z", + "lastModifiedUser": "", + "lastModifiedAgent": "structurizr-cloud/diagram-editor/543191c0-d8a4-4bfd-ab86-95f219ce6988", + "properties": { + "structurizr.dsl": "Ly8gaHR0cHM6Ly9zdHJ1Y3R1cml6ci5jb20vc2hhcmUvMzYxNDEKCndvcmtzcGFjZSBleHRlbmRzIC4uL21vZGVsLmRzbCB7CiAgICBuYW1lICJCaWcgQmFuayBwbGMgLSBJbnRlcm5ldCBCYW5raW5nIFN5c3RlbSIKICAgIGRlc2NyaXB0aW9uICJUaGUgc29mdHdhcmUgYXJjaGl0ZWN0dXJlIG9mIHRoZSBCaWcgQmFuayBwbGMgSW50ZXJuZXQgQmFua2luZyBTeXN0ZW0uIgoKICAgIG1vZGVsIHsKICAgICAgICAhcmVmIGludGVybmV0YmFua2luZ3N5c3RlbSB7CiAgICAgICAgICAgIHNpbmdsZVBhZ2VBcHBsaWNhdGlvbiA9IGNvbnRhaW5lciAiU2luZ2xlLVBhZ2UgQXBwbGljYXRpb24iICJQcm92aWRlcyBhbGwgb2YgdGhlIEludGVybmV0IGJhbmtpbmcgZnVuY3Rpb25hbGl0eSB0byBjdXN0b21lcnMgdmlhIHRoZWlyIHdlYiBicm93c2VyLiIgIkphdmFTY3JpcHQgYW5kIEFuZ3VsYXIiICJXZWIgQnJvd3NlciIKICAgICAgICAgICAgbW9iaWxlQXBwID0gY29udGFpbmVyICJNb2JpbGUgQXBwIiAiUHJvdmlkZXMgYSBsaW1pdGVkIHN1YnNldCBvZiB0aGUgSW50ZXJuZXQgYmFua2luZyBmdW5jdGlvbmFsaXR5IHRvIGN1c3RvbWVycyB2aWEgdGhlaXIgbW9iaWxlIGRldmljZS4iICJYYW1hcmluIiAiTW9iaWxlIEFwcCIKICAgICAgICAgICAgd2ViQXBwbGljYXRpb24gPSBjb250YWluZXIgIldlYiBBcHBsaWNhdGlvbiIgIkRlbGl2ZXJzIHRoZSBzdGF0aWMgY29udGVudCBhbmQgdGhlIEludGVybmV0IGJhbmtpbmcgc2luZ2xlIHBhZ2UgYXBwbGljYXRpb24uIiAiSmF2YSBhbmQgU3ByaW5nIE1WQyIKICAgICAgICAgICAgYXBpQXBwbGljYXRpb24gPSBjb250YWluZXIgIkFQSSBBcHBsaWNhdGlvbiIgIlByb3ZpZGVzIEludGVybmV0IGJhbmtpbmcgZnVuY3Rpb25hbGl0eSB2aWEgYSBKU09OL0hUVFBTIEFQSS4iICJKYXZhIGFuZCBTcHJpbmcgTVZDIiB7CiAgICAgICAgICAgICAgICBzaWduaW5Db250cm9sbGVyID0gY29tcG9uZW50ICJTaWduIEluIENvbnRyb2xsZXIiICJBbGxvd3MgdXNlcnMgdG8gc2lnbiBpbiB0byB0aGUgSW50ZXJuZXQgQmFua2luZyBTeXN0ZW0uIiAiU3ByaW5nIE1WQyBSZXN0IENvbnRyb2xsZXIiCiAgICAgICAgICAgICAgICBhY2NvdW50c1N1bW1hcnlDb250cm9sbGVyID0gY29tcG9uZW50ICJBY2NvdW50cyBTdW1tYXJ5IENvbnRyb2xsZXIiICJQcm92aWRlcyBjdXN0b21lcnMgd2l0aCBhIHN1bW1hcnkgb2YgdGhlaXIgYmFuayBhY2NvdW50cy4iICJTcHJpbmcgTVZDIFJlc3QgQ29udHJvbGxlciIKICAgICAgICAgICAgICAgIHJlc2V0UGFzc3dvcmRDb250cm9sbGVyID0gY29tcG9uZW50ICJSZXNldCBQYXNzd29yZCBDb250cm9sbGVyIiAiQWxsb3dzIHVzZXJzIHRvIHJlc2V0IHRoZWlyIHBhc3N3b3JkcyB3aXRoIGEgc2luZ2xlIHVzZSBVUkwuIiAiU3ByaW5nIE1WQyBSZXN0IENvbnRyb2xsZXIiCiAgICAgICAgICAgICAgICBzZWN1cml0eUNvbXBvbmVudCA9IGNvbXBvbmVudCAiU2VjdXJpdHkgQ29tcG9uZW50IiAiUHJvdmlkZXMgZnVuY3Rpb25hbGl0eSByZWxhdGVkIHRvIHNpZ25pbmcgaW4sIGNoYW5naW5nIHBhc3N3b3JkcywgZXRjLiIgIlNwcmluZyBCZWFuIgogICAgICAgICAgICAgICAgbWFpbmZyYW1lQmFua2luZ1N5c3RlbUZhY2FkZSA9IGNvbXBvbmVudCAiTWFpbmZyYW1lIEJhbmtpbmcgU3lzdGVtIEZhY2FkZSIgIkEgZmFjYWRlIG9udG8gdGhlIG1haW5mcmFtZSBiYW5raW5nIHN5c3RlbS4iICJTcHJpbmcgQmVhbiIKICAgICAgICAgICAgICAgIGVtYWlsQ29tcG9uZW50ID0gY29tcG9uZW50ICJFLW1haWwgQ29tcG9uZW50IiAiU2VuZHMgZS1tYWlscyB0byB1c2Vycy4iICJTcHJpbmcgQmVhbiIKICAgICAgICAgICAgfQogICAgICAgICAgICBkYXRhYmFzZSA9IGNvbnRhaW5lciAiRGF0YWJhc2UiICJTdG9yZXMgdXNlciByZWdpc3RyYXRpb24gaW5mb3JtYXRpb24sIGhhc2hlZCBhdXRoZW50aWNhdGlvbiBjcmVkZW50aWFscywgYWNjZXNzIGxvZ3MsIGV0Yy4iICJPcmFjbGUgRGF0YWJhc2UgU2NoZW1hIiAiRGF0YWJhc2UiCiAgICAgICAgfQoKICAgICAgICAjIHJlbGF0aW9uc2hpcHMgdG8vZnJvbSBjb250YWluZXJzCiAgICAgICAgY3VzdG9tZXIgLT4gd2ViQXBwbGljYXRpb24gIlZpc2l0cyBiaWdiYW5rLmNvbS9pYiB1c2luZyIgIkhUVFBTIgogICAgICAgIGN1c3RvbWVyIC0+IHNpbmdsZVBhZ2VBcHBsaWNhdGlvbiAiVmlld3MgYWNjb3VudCBiYWxhbmNlcywgYW5kIG1ha2VzIHBheW1lbnRzIHVzaW5nIgogICAgICAgIGN1c3RvbWVyIC0+IG1vYmlsZUFwcCAiVmlld3MgYWNjb3VudCBiYWxhbmNlcywgYW5kIG1ha2VzIHBheW1lbnRzIHVzaW5nIgogICAgICAgIHdlYkFwcGxpY2F0aW9uIC0+IHNpbmdsZVBhZ2VBcHBsaWNhdGlvbiAiRGVsaXZlcnMgdG8gdGhlIGN1c3RvbWVyJ3Mgd2ViIGJyb3dzZXIiCgogICAgICAgICMgcmVsYXRpb25zaGlwcyB0by9mcm9tIGNvbXBvbmVudHMKICAgICAgICBzaW5nbGVQYWdlQXBwbGljYXRpb24gLT4gc2lnbmluQ29udHJvbGxlciAiTWFrZXMgQVBJIGNhbGxzIHRvIiAiSlNPTi9IVFRQUyIKICAgICAgICBzaW5nbGVQYWdlQXBwbGljYXRpb24gLT4gYWNjb3VudHNTdW1tYXJ5Q29udHJvbGxlciAiTWFrZXMgQVBJIGNhbGxzIHRvIiAiSlNPTi9IVFRQUyIKICAgICAgICBzaW5nbGVQYWdlQXBwbGljYXRpb24gLT4gcmVzZXRQYXNzd29yZENvbnRyb2xsZXIgIk1ha2VzIEFQSSBjYWxscyB0byIgIkpTT04vSFRUUFMiCiAgICAgICAgbW9iaWxlQXBwIC0+IHNpZ25pbkNvbnRyb2xsZXIgIk1ha2VzIEFQSSBjYWxscyB0byIgIkpTT04vSFRUUFMiCiAgICAgICAgbW9iaWxlQXBwIC0+IGFjY291bnRzU3VtbWFyeUNvbnRyb2xsZXIgIk1ha2VzIEFQSSBjYWxscyB0byIgIkpTT04vSFRUUFMiCiAgICAgICAgbW9iaWxlQXBwIC0+IHJlc2V0UGFzc3dvcmRDb250cm9sbGVyICJNYWtlcyBBUEkgY2FsbHMgdG8iICJKU09OL0hUVFBTIgogICAgICAgIHNpZ25pbkNvbnRyb2xsZXIgLT4gc2VjdXJpdHlDb21wb25lbnQgIlVzZXMiCiAgICAgICAgYWNjb3VudHNTdW1tYXJ5Q29udHJvbGxlciAtPiBtYWluZnJhbWVCYW5raW5nU3lzdGVtRmFjYWRlICJVc2VzIgogICAgICAgIHJlc2V0UGFzc3dvcmRDb250cm9sbGVyIC0+IHNlY3VyaXR5Q29tcG9uZW50ICJVc2VzIgogICAgICAgIHJlc2V0UGFzc3dvcmRDb250cm9sbGVyIC0+IGVtYWlsQ29tcG9uZW50ICJVc2VzIgogICAgICAgIHNlY3VyaXR5Q29tcG9uZW50IC0+IGRhdGFiYXNlICJSZWFkcyBmcm9tIGFuZCB3cml0ZXMgdG8iICJTUUwvVENQIgogICAgICAgIG1haW5mcmFtZUJhbmtpbmdTeXN0ZW1GYWNhZGUgLT4gbWFpbmZyYW1lICJNYWtlcyBBUEkgY2FsbHMgdG8iICJYTUwvSFRUUFMiCiAgICAgICAgZW1haWxDb21wb25lbnQgLT4gZW1haWwgIlNlbmRzIGUtbWFpbCB1c2luZyIKCiAgICAgICAgZGVwbG95bWVudEVudmlyb25tZW50ICJEZXZlbG9wbWVudCIgewogICAgICAgICAgICBkZXBsb3ltZW50Tm9kZSAiRGV2ZWxvcGVyIExhcHRvcCIgIiIgIk1pY3Jvc29mdCBXaW5kb3dzIDEwIG9yIEFwcGxlIG1hY09TIiB7CiAgICAgICAgICAgICAgICBkZXBsb3ltZW50Tm9kZSAiV2ViIEJyb3dzZXIiICIiICJDaHJvbWUsIEZpcmVmb3gsIFNhZmFyaSwgb3IgRWRnZSIgewogICAgICAgICAgICAgICAgICAgIGRldmVsb3BlclNpbmdsZVBhZ2VBcHBsaWNhdGlvbkluc3RhbmNlID0gY29udGFpbmVySW5zdGFuY2Ugc2luZ2xlUGFnZUFwcGxpY2F0aW9uCiAgICAgICAgICAgICAgICB9CiAgICAgICAgICAgICAgICBkZXBsb3ltZW50Tm9kZSAiRG9ja2VyIENvbnRhaW5lciAtIFdlYiBTZXJ2ZXIiICIiICJEb2NrZXIiIHsKICAgICAgICAgICAgICAgICAgICBkZXBsb3ltZW50Tm9kZSAiQXBhY2hlIFRvbWNhdCIgIiIgIkFwYWNoZSBUb21jYXQgOC54IiB7CiAgICAgICAgICAgICAgICAgICAgICAgIGRldmVsb3BlcldlYkFwcGxpY2F0aW9uSW5zdGFuY2UgPSBjb250YWluZXJJbnN0YW5jZSB3ZWJBcHBsaWNhdGlvbgogICAgICAgICAgICAgICAgICAgICAgICBkZXZlbG9wZXJBcGlBcHBsaWNhdGlvbkluc3RhbmNlID0gY29udGFpbmVySW5zdGFuY2UgYXBpQXBwbGljYXRpb24KICAgICAgICAgICAgICAgICAgICB9CiAgICAgICAgICAgICAgICB9CiAgICAgICAgICAgICAgICBkZXBsb3ltZW50Tm9kZSAiRG9ja2VyIENvbnRhaW5lciAtIERhdGFiYXNlIFNlcnZlciIgIiIgIkRvY2tlciIgewogICAgICAgICAgICAgICAgICAgIGRlcGxveW1lbnROb2RlICJEYXRhYmFzZSBTZXJ2ZXIiICIiICJPcmFjbGUgMTJjIiB7CiAgICAgICAgICAgICAgICAgICAgICAgIGRldmVsb3BlckRhdGFiYXNlSW5zdGFuY2UgPSBjb250YWluZXJJbnN0YW5jZSBkYXRhYmFzZQogICAgICAgICAgICAgICAgICAgIH0KICAgICAgICAgICAgICAgIH0KICAgICAgICAgICAgfQogICAgICAgICAgICBkZXBsb3ltZW50Tm9kZSAiQmlnIEJhbmsgcGxjIiAiIiAiQmlnIEJhbmsgcGxjIGRhdGEgY2VudGVyIiAiIiB7CiAgICAgICAgICAgICAgICBkZXBsb3ltZW50Tm9kZSAiYmlnYmFuay1kZXYwMDEiICIiICIiICIiIHsKICAgICAgICAgICAgICAgICAgICBzb2Z0d2FyZVN5c3RlbUluc3RhbmNlIG1haW5mcmFtZQogICAgICAgICAgICAgICAgfQogICAgICAgICAgICB9CiAgICAgICAgfQoKICAgICAgICBkZXBsb3ltZW50RW52aXJvbm1lbnQgIkxpdmUiIHsKICAgICAgICAgICAgZGVwbG95bWVudE5vZGUgIkN1c3RvbWVyJ3MgbW9iaWxlIGRldmljZSIgIiIgIkFwcGxlIGlPUyBvciBBbmRyb2lkIiB7CiAgICAgICAgICAgICAgICBsaXZlTW9iaWxlQXBwSW5zdGFuY2UgPSBjb250YWluZXJJbnN0YW5jZSBtb2JpbGVBcHAKICAgICAgICAgICAgfQogICAgICAgICAgICBkZXBsb3ltZW50Tm9kZSAiQ3VzdG9tZXIncyBjb21wdXRlciIgIiIgIk1pY3Jvc29mdCBXaW5kb3dzIG9yIEFwcGxlIG1hY09TIiB7CiAgICAgICAgICAgICAgICBkZXBsb3ltZW50Tm9kZSAiV2ViIEJyb3dzZXIiICIiICJDaHJvbWUsIEZpcmVmb3gsIFNhZmFyaSwgb3IgRWRnZSIgewogICAgICAgICAgICAgICAgICAgIGxpdmVTaW5nbGVQYWdlQXBwbGljYXRpb25JbnN0YW5jZSA9IGNvbnRhaW5lckluc3RhbmNlIHNpbmdsZVBhZ2VBcHBsaWNhdGlvbgogICAgICAgICAgICAgICAgfQogICAgICAgICAgICB9CgogICAgICAgICAgICBkZXBsb3ltZW50Tm9kZSAiQmlnIEJhbmsgcGxjIiAiIiAiQmlnIEJhbmsgcGxjIGRhdGEgY2VudGVyIiB7CiAgICAgICAgICAgICAgICBkZXBsb3ltZW50Tm9kZSAiYmlnYmFuay13ZWIqKioiICIiICJVYnVudHUgMTYuMDQgTFRTIiAiIiA0IHsKICAgICAgICAgICAgICAgICAgICBkZXBsb3ltZW50Tm9kZSAiQXBhY2hlIFRvbWNhdCIgIiIgIkFwYWNoZSBUb21jYXQgOC54IiB7CiAgICAgICAgICAgICAgICAgICAgICAgIGxpdmVXZWJBcHBsaWNhdGlvbkluc3RhbmNlID0gY29udGFpbmVySW5zdGFuY2Ugd2ViQXBwbGljYXRpb24KICAgICAgICAgICAgICAgICAgICB9CiAgICAgICAgICAgICAgICB9CiAgICAgICAgICAgICAgICBkZXBsb3ltZW50Tm9kZSAiYmlnYmFuay1hcGkqKioiICIiICJVYnVudHUgMTYuMDQgTFRTIiAiIiA4IHsKICAgICAgICAgICAgICAgICAgICBkZXBsb3ltZW50Tm9kZSAiQXBhY2hlIFRvbWNhdCIgIiIgIkFwYWNoZSBUb21jYXQgOC54IiB7CiAgICAgICAgICAgICAgICAgICAgICAgIGxpdmVBcGlBcHBsaWNhdGlvbkluc3RhbmNlID0gY29udGFpbmVySW5zdGFuY2UgYXBpQXBwbGljYXRpb24KICAgICAgICAgICAgICAgICAgICB9CiAgICAgICAgICAgICAgICB9CgogICAgICAgICAgICAgICAgZGVwbG95bWVudE5vZGUgImJpZ2JhbmstZGIwMSIgIiIgIlVidW50dSAxNi4wNCBMVFMiIHsKICAgICAgICAgICAgICAgICAgICBwcmltYXJ5RGF0YWJhc2VTZXJ2ZXIgPSBkZXBsb3ltZW50Tm9kZSAiT3JhY2xlIC0gUHJpbWFyeSIgIiIgIk9yYWNsZSAxMmMiIHsKICAgICAgICAgICAgICAgICAgICAgICAgbGl2ZVByaW1hcnlEYXRhYmFzZUluc3RhbmNlID0gY29udGFpbmVySW5zdGFuY2UgZGF0YWJhc2UKICAgICAgICAgICAgICAgICAgICB9CiAgICAgICAgICAgICAgICB9CiAgICAgICAgICAgICAgICBkZXBsb3ltZW50Tm9kZSAiYmlnYmFuay1kYjAyIiAiIiAiVWJ1bnR1IDE2LjA0IExUUyIgIkZhaWxvdmVyIiB7CiAgICAgICAgICAgICAgICAgICAgc2Vjb25kYXJ5RGF0YWJhc2VTZXJ2ZXIgPSBkZXBsb3ltZW50Tm9kZSAiT3JhY2xlIC0gU2Vjb25kYXJ5IiAiIiAiT3JhY2xlIDEyYyIgIkZhaWxvdmVyIiB7CiAgICAgICAgICAgICAgICAgICAgICAgIGxpdmVTZWNvbmRhcnlEYXRhYmFzZUluc3RhbmNlID0gY29udGFpbmVySW5zdGFuY2UgZGF0YWJhc2UgIkZhaWxvdmVyIgogICAgICAgICAgICAgICAgICAgIH0KICAgICAgICAgICAgICAgIH0KICAgICAgICAgICAgICAgIGRlcGxveW1lbnROb2RlICJiaWdiYW5rLXByb2QwMDEiICIiICIiICIiIHsKICAgICAgICAgICAgICAgICAgICBzb2Z0d2FyZVN5c3RlbUluc3RhbmNlIG1haW5mcmFtZQogICAgICAgICAgICAgICAgfQogICAgICAgICAgICB9CgogICAgICAgICAgICBwcmltYXJ5RGF0YWJhc2VTZXJ2ZXIgLT4gc2Vjb25kYXJ5RGF0YWJhc2VTZXJ2ZXIgIlJlcGxpY2F0ZXMgZGF0YSB0byIKICAgICAgICB9CiAgICB9CgogICAgdmlld3MgewogICAgICAgIHN5c3RlbWNvbnRleHQgaW50ZXJuZXRCYW5raW5nU3lzdGVtICJTeXN0ZW1Db250ZXh0IiB7CiAgICAgICAgICAgIGluY2x1ZGUgKgogICAgICAgICAgICBhbmltYXRpb24gewogICAgICAgICAgICAgICAgaW50ZXJuZXRCYW5raW5nU3lzdGVtCiAgICAgICAgICAgICAgICBjdXN0b21lcgogICAgICAgICAgICAgICAgbWFpbmZyYW1lCiAgICAgICAgICAgICAgICBlbWFpbAogICAgICAgICAgICB9CgogICAgICAgICAgICBkZXNjcmlwdGlvbiAiVGhlIHN5c3RlbSBjb250ZXh0IGRpYWdyYW0gZm9yIHRoZSBJbnRlcm5ldCBCYW5raW5nIFN5c3RlbS4iCiAgICAgICAgICAgIHByb3BlcnRpZXMgewogICAgICAgICAgICAgICAgc3RydWN0dXJpenIuZ3JvdXBzIGZhbHNlCiAgICAgICAgICAgIH0KICAgICAgICB9CgogICAgICAgIGNvbnRhaW5lciBpbnRlcm5ldEJhbmtpbmdTeXN0ZW0gIkNvbnRhaW5lcnMiIHsKICAgICAgICAgICAgaW5jbHVkZSAqCiAgICAgICAgICAgIGFuaW1hdGlvbiB7CiAgICAgICAgICAgICAgICBjdXN0b21lciBtYWluZnJhbWUgZW1haWwKICAgICAgICAgICAgICAgIHdlYkFwcGxpY2F0aW9uCiAgICAgICAgICAgICAgICBzaW5nbGVQYWdlQXBwbGljYXRpb24KICAgICAgICAgICAgICAgIG1vYmlsZUFwcAogICAgICAgICAgICAgICAgYXBpQXBwbGljYXRpb24KICAgICAgICAgICAgICAgIGRhdGFiYXNlCiAgICAgICAgICAgIH0KCiAgICAgICAgICAgIGRlc2NyaXB0aW9uICJUaGUgY29udGFpbmVyIGRpYWdyYW0gZm9yIHRoZSBJbnRlcm5ldCBCYW5raW5nIFN5c3RlbS4iCiAgICAgICAgfQoKICAgICAgICBjb21wb25lbnQgYXBpQXBwbGljYXRpb24gIkNvbXBvbmVudHMiIHsKICAgICAgICAgICAgaW5jbHVkZSAqCiAgICAgICAgICAgIGFuaW1hdGlvbiB7CiAgICAgICAgICAgICAgICBzaW5nbGVQYWdlQXBwbGljYXRpb24gbW9iaWxlQXBwIGRhdGFiYXNlIGVtYWlsIG1haW5mcmFtZQogICAgICAgICAgICAgICAgc2lnbmluQ29udHJvbGxlciBzZWN1cml0eUNvbXBvbmVudAogICAgICAgICAgICAgICAgYWNjb3VudHNTdW1tYXJ5Q29udHJvbGxlciBtYWluZnJhbWVCYW5raW5nU3lzdGVtRmFjYWRlCiAgICAgICAgICAgICAgICByZXNldFBhc3N3b3JkQ29udHJvbGxlciBlbWFpbENvbXBvbmVudAogICAgICAgICAgICB9CgogICAgICAgICAgICBkZXNjcmlwdGlvbiAiVGhlIGNvbXBvbmVudCBkaWFncmFtIGZvciB0aGUgQVBJIEFwcGxpY2F0aW9uLiIKICAgICAgICB9CgogICAgICAgIGltYWdlIG1haW5mcmFtZUJhbmtpbmdTeXN0ZW1GYWNhZGUgIk1haW5mcmFtZUJhbmtpbmdTeXN0ZW1GYWNhZGUiIHsKICAgICAgICAgICAgaW1hZ2UgbWFpbmZyYW1lLWJhbmtpbmctc3lzdGVtLWZhY2FkZS5wbmcKICAgICAgICAgICAgdGl0bGUgIkNsYXNzIGRpYWdyYW0gZm9yIHRoZSBNYWluZnJhbWUgQmFua2luZyBTeXN0ZW0gRmFjYWRlIGNvbXBvbmVudCIKICAgICAgICB9CgogICAgICAgIGR5bmFtaWMgYXBpQXBwbGljYXRpb24gIlNpZ25JbiIgewogICAgICAgICAgICBzaW5nbGVQYWdlQXBwbGljYXRpb24gLT4gc2lnbmluQ29udHJvbGxlciAiU3VibWl0cyBjcmVkZW50aWFscyB0byIKICAgICAgICAgICAgc2lnbmluQ29udHJvbGxlciAtPiBzZWN1cml0eUNvbXBvbmVudCAiVmFsaWRhdGVzIGNyZWRlbnRpYWxzIHVzaW5nIgogICAgICAgICAgICBzZWN1cml0eUNvbXBvbmVudCAtPiBkYXRhYmFzZSAic2VsZWN0ICogZnJvbSB1c2VycyB3aGVyZSB1c2VybmFtZSA9ID8iCiAgICAgICAgICAgIGRhdGFiYXNlIC0+IHNlY3VyaXR5Q29tcG9uZW50ICJSZXR1cm5zIHVzZXIgZGF0YSB0byIKICAgICAgICAgICAgc2VjdXJpdHlDb21wb25lbnQgLT4gc2lnbmluQ29udHJvbGxlciAiUmV0dXJucyB0cnVlIGlmIHRoZSBoYXNoZWQgcGFzc3dvcmQgbWF0Y2hlcyIKICAgICAgICAgICAgc2lnbmluQ29udHJvbGxlciAtPiBzaW5nbGVQYWdlQXBwbGljYXRpb24gIlNlbmRzIGJhY2sgYW4gYXV0aGVudGljYXRpb24gdG9rZW4gdG8iCgogICAgICAgICAgICBkZXNjcmlwdGlvbiAiU3VtbWFyaXNlcyBob3cgdGhlIHNpZ24gaW4gZmVhdHVyZSB3b3JrcyBpbiB0aGUgc2luZ2xlLXBhZ2UgYXBwbGljYXRpb24uIgogICAgICAgIH0KCiAgICAgICAgZGVwbG95bWVudCBpbnRlcm5ldEJhbmtpbmdTeXN0ZW0gIkRldmVsb3BtZW50IiAiRGV2ZWxvcG1lbnREZXBsb3ltZW50IiB7CiAgICAgICAgICAgIGluY2x1ZGUgKgogICAgICAgICAgICBhbmltYXRpb24gewogICAgICAgICAgICAgICAgZGV2ZWxvcGVyU2luZ2xlUGFnZUFwcGxpY2F0aW9uSW5zdGFuY2UKICAgICAgICAgICAgICAgIGRldmVsb3BlcldlYkFwcGxpY2F0aW9uSW5zdGFuY2UgZGV2ZWxvcGVyQXBpQXBwbGljYXRpb25JbnN0YW5jZQogICAgICAgICAgICAgICAgZGV2ZWxvcGVyRGF0YWJhc2VJbnN0YW5jZQogICAgICAgICAgICB9CgogICAgICAgICAgICBkZXNjcmlwdGlvbiAiQW4gZXhhbXBsZSBkZXZlbG9wbWVudCBkZXBsb3ltZW50IHNjZW5hcmlvIGZvciB0aGUgSW50ZXJuZXQgQmFua2luZyBTeXN0ZW0uIgogICAgICAgIH0KCiAgICAgICAgZGVwbG95bWVudCBpbnRlcm5ldEJhbmtpbmdTeXN0ZW0gIkxpdmUiICJMaXZlRGVwbG95bWVudCIgewogICAgICAgICAgICBpbmNsdWRlICoKICAgICAgICAgICAgYW5pbWF0aW9uIHsKICAgICAgICAgICAgICAgIGxpdmVTaW5nbGVQYWdlQXBwbGljYXRpb25JbnN0YW5jZQogICAgICAgICAgICAgICAgbGl2ZU1vYmlsZUFwcEluc3RhbmNlCiAgICAgICAgICAgICAgICBsaXZlV2ViQXBwbGljYXRpb25JbnN0YW5jZSBsaXZlQXBpQXBwbGljYXRpb25JbnN0YW5jZQogICAgICAgICAgICAgICAgbGl2ZVByaW1hcnlEYXRhYmFzZUluc3RhbmNlCiAgICAgICAgICAgICAgICBsaXZlU2Vjb25kYXJ5RGF0YWJhc2VJbnN0YW5jZQogICAgICAgICAgICB9CgogICAgICAgICAgICBkZXNjcmlwdGlvbiAiQW4gZXhhbXBsZSBsaXZlIGRlcGxveW1lbnQgc2NlbmFyaW8gZm9yIHRoZSBJbnRlcm5ldCBCYW5raW5nIFN5c3RlbS4iCiAgICAgICAgfQoKICAgICAgICBzdHlsZXMgewogICAgICAgICAgICBlbGVtZW50ICJQZXJzb24iIHsKICAgICAgICAgICAgICAgIGNvbG9yICNmZmZmZmYKICAgICAgICAgICAgICAgIGZvbnRTaXplIDIyCiAgICAgICAgICAgICAgICBzaGFwZSBQZXJzb24KICAgICAgICAgICAgfQogICAgICAgICAgICBlbGVtZW50ICJDdXN0b21lciIgewogICAgICAgICAgICAgICAgYmFja2dyb3VuZCAjMDg0MjdiCiAgICAgICAgICAgIH0KICAgICAgICAgICAgZWxlbWVudCAiQmFuayBTdGFmZiIgewogICAgICAgICAgICAgICAgYmFja2dyb3VuZCAjOTk5OTk5CiAgICAgICAgICAgIH0KICAgICAgICAgICAgZWxlbWVudCAiU29mdHdhcmUgU3lzdGVtIiB7CiAgICAgICAgICAgICAgICBiYWNrZ3JvdW5kICMxMTY4YmQKICAgICAgICAgICAgICAgIGNvbG9yICNmZmZmZmYKICAgICAgICAgICAgfQogICAgICAgICAgICBlbGVtZW50ICJFeGlzdGluZyBTeXN0ZW0iIHsKICAgICAgICAgICAgICAgIGJhY2tncm91bmQgIzk5OTk5OQogICAgICAgICAgICAgICAgY29sb3IgI2ZmZmZmZgogICAgICAgICAgICB9CiAgICAgICAgICAgIGVsZW1lbnQgIkNvbnRhaW5lciIgewogICAgICAgICAgICAgICAgYmFja2dyb3VuZCAjNDM4ZGQ1CiAgICAgICAgICAgICAgICBjb2xvciAjZmZmZmZmCiAgICAgICAgICAgIH0KICAgICAgICAgICAgZWxlbWVudCAiV2ViIEJyb3dzZXIiIHsKICAgICAgICAgICAgICAgIHNoYXBlIFdlYkJyb3dzZXIKICAgICAgICAgICAgfQogICAgICAgICAgICBlbGVtZW50ICJNb2JpbGUgQXBwIiB7CiAgICAgICAgICAgICAgICBzaGFwZSBNb2JpbGVEZXZpY2VMYW5kc2NhcGUKICAgICAgICAgICAgfQogICAgICAgICAgICBlbGVtZW50ICJEYXRhYmFzZSIgewogICAgICAgICAgICAgICAgc2hhcGUgQ3lsaW5kZXIKICAgICAgICAgICAgfQogICAgICAgICAgICBlbGVtZW50ICJDb21wb25lbnQiIHsKICAgICAgICAgICAgICAgIGJhY2tncm91bmQgIzg1YmJmMAogICAgICAgICAgICAgICAgY29sb3IgIzAwMDAwMAogICAgICAgICAgICB9CiAgICAgICAgICAgIGVsZW1lbnQgIkZhaWxvdmVyIiB7CiAgICAgICAgICAgICAgICBvcGFjaXR5IDI1CiAgICAgICAgICAgIH0KICAgICAgICB9CiAgICB9Cgp9Cg==" + }, + "model": { + "people": [ + { + "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": "16", + "tags": "Relationship", + "sourceId": "3", + "destinationId": "4", + "description": "Uses" + } + ], + "group": "Big Bank plc", + "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": "13", + "tags": "Relationship", + "sourceId": "2", + "destinationId": "4", + "description": "Uses" + } + ], + "group": "Big Bank plc", + "location": "Unspecified" + }, + { + "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": "12", + "tags": "Relationship", + "sourceId": "1", + "destinationId": "2", + "description": "Asks questions to", + "technology": "Telephone" + }, + { + "id": "30", + "tags": "Relationship", + "sourceId": "1", + "destinationId": "18", + "description": "Views account balances, and makes payments using" + }, + { + "id": "8", + "tags": "Relationship", + "sourceId": "1", + "destinationId": "7", + "description": "Views account balances, and makes payments using" + }, + { + "id": "28", + "tags": "Relationship", + "sourceId": "1", + "destinationId": "19", + "description": "Visits bigbank.com/ib using", + "technology": "HTTPS" + }, + { + "id": "29", + "tags": "Relationship", + "sourceId": "1", + "destinationId": "17", + "description": "Views account balances, and makes payments using" + }, + { + "id": "14", + "tags": "Relationship", + "sourceId": "1", + "destinationId": "6", + "description": "Withdraws cash using" + } + ], + "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": "11", + "tags": "Relationship", + "sourceId": "5", + "destinationId": "1", + "description": "Sends e-mails to" + } + ], + "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": "10", + "tags": "Relationship", + "sourceId": "7", + "destinationId": "5", + "description": "Sends e-mail using" + }, + { + "id": "9", + "tags": "Relationship", + "sourceId": "7", + "destinationId": "4", + "description": "Gets account information from, and makes payments using" + } + ], + "group": "Big Bank plc", + "location": "Unspecified", + "containers": [ + { + "id": "20", + "tags": "Element,Container", + "properties": { + "structurizr.dsl.identifier": "apiapplication" + }, + "name": "API Application", + "description": "Provides Internet banking functionality via a JSON/HTTPS API.", + "relationships": [ + { + "id": "47", + "sourceId": "20", + "destinationId": "4", + "description": "Makes API calls to", + "technology": "XML/HTTPS", + "linkedRelationshipId": "46" + }, + { + "id": "45", + "sourceId": "20", + "destinationId": "27", + "description": "Reads from and writes to", + "technology": "SQL/TCP", + "linkedRelationshipId": "44" + }, + { + "id": "49", + "sourceId": "20", + "destinationId": "5", + "description": "Sends e-mail using", + "linkedRelationshipId": "48" + } + ], + "technology": "Java and Spring MVC", + "components": [ + { + "id": "24", + "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", + "sourceId": "24", + "destinationId": "27", + "description": "Reads from and writes to", + "technology": "SQL/TCP" + } + ], + "technology": "Spring Bean", + "documentation": {} + }, + { + "id": "25", + "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", + "sourceId": "25", + "destinationId": "4", + "description": "Makes API calls to", + "technology": "XML/HTTPS" + } + ], + "technology": "Spring Bean", + "documentation": {} + }, + { + "id": "22", + "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", + "sourceId": "22", + "destinationId": "25", + "description": "Uses" + } + ], + "technology": "Spring MVC Rest Controller", + "documentation": {} + }, + { + "id": "21", + "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", + "sourceId": "21", + "destinationId": "24", + "description": "Uses" + } + ], + "technology": "Spring MVC Rest Controller", + "documentation": {} + }, + { + "id": "23", + "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", + "sourceId": "23", + "destinationId": "24", + "description": "Uses" + }, + { + "id": "43", + "tags": "Relationship", + "sourceId": "23", + "destinationId": "26", + "description": "Uses" + } + ], + "technology": "Spring MVC Rest Controller", + "documentation": {} + }, + { + "id": "26", + "tags": "Element,Component", + "properties": { + "structurizr.dsl.identifier": "emailcomponent" + }, + "name": "E-mail Component", + "description": "Sends e-mails to users.", + "relationships": [ + { + "id": "48", + "tags": "Relationship", + "sourceId": "26", + "destinationId": "5", + "description": "Sends e-mail using" + } + ], + "technology": "Spring Bean", + "documentation": {} + } + ], + "documentation": {} + }, + { + "id": "18", + "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": "37", + "sourceId": "18", + "destinationId": "20", + "description": "Makes API calls to", + "technology": "JSON/HTTPS", + "linkedRelationshipId": "36" + }, + { + "id": "39", + "tags": "Relationship", + "sourceId": "18", + "destinationId": "23", + "description": "Makes API calls to", + "technology": "JSON/HTTPS" + }, + { + "id": "36", + "tags": "Relationship", + "sourceId": "18", + "destinationId": "21", + "description": "Makes API calls to", + "technology": "JSON/HTTPS" + }, + { + "id": "38", + "tags": "Relationship", + "sourceId": "18", + "destinationId": "22", + "description": "Makes API calls to", + "technology": "JSON/HTTPS" + } + ], + "technology": "Xamarin", + "documentation": {} + }, + { + "id": "17", + "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": "33", + "sourceId": "17", + "destinationId": "20", + "description": "Makes API calls to", + "technology": "JSON/HTTPS", + "linkedRelationshipId": "32" + }, + { + "id": "34", + "tags": "Relationship", + "sourceId": "17", + "destinationId": "22", + "description": "Makes API calls to", + "technology": "JSON/HTTPS" + }, + { + "id": "32", + "tags": "Relationship", + "sourceId": "17", + "destinationId": "21", + "description": "Makes API calls to", + "technology": "JSON/HTTPS" + }, + { + "id": "35", + "tags": "Relationship", + "sourceId": "17", + "destinationId": "23", + "description": "Makes API calls to", + "technology": "JSON/HTTPS" + } + ], + "technology": "JavaScript and Angular", + "documentation": {} + }, + { + "id": "27", + "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": "19", + "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", + "sourceId": "19", + "destinationId": "17", + "description": "Delivers to the customer's web browser" + } + ], + "technology": "Java and Spring MVC", + "documentation": {} + } + ], + "documentation": {} + }, + { + "id": "6", + "tags": "Element,Software System,Existing System", + "properties": { + "structurizr.dsl.identifier": "atm" + }, + "name": "ATM", + "description": "Allows customers to withdraw cash.", + "relationships": [ + { + "id": "15", + "tags": "Relationship", + "sourceId": "6", + "destinationId": "4", + "description": "Uses" + } + ], + "group": "Big Bank plc", + "location": "Unspecified", + "documentation": {} + } + ], + "deploymentNodes": [ + { + "id": "50", + "tags": "Element,Deployment Node", + "name": "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", + "environment": "Development", + "technology": "Docker", + "instances": "1", + "children": [ + { + "id": "60", + "tags": "Element,Deployment Node", + "name": "Database Server", + "environment": "Development", + "technology": "Oracle 12c", + "instances": "1", + "containerInstances": [ + { + "id": "61", + "tags": "Container Instance", + "properties": { + "structurizr.dsl.identifier": "developerdatabaseinstance" + }, + "environment": "Development", + "deploymentGroups": [ + "Default" + ], + "instanceId": 1, + "containerId": "27" + } + ] + } + ] + }, + { + "id": "53", + "tags": "Element,Deployment Node", + "name": "Docker Container - Web Server", + "environment": "Development", + "technology": "Docker", + "instances": "1", + "children": [ + { + "id": "54", + "tags": "Element,Deployment Node", + "name": "Apache Tomcat", + "environment": "Development", + "technology": "Apache Tomcat 8.x", + "instances": "1", + "containerInstances": [ + { + "id": "55", + "tags": "Container Instance", + "properties": { + "structurizr.dsl.identifier": "developerwebapplicationinstance" + }, + "relationships": [ + { + "id": "56", + "sourceId": "55", + "destinationId": "52", + "description": "Delivers to the customer's web browser", + "linkedRelationshipId": "31" + } + ], + "environment": "Development", + "deploymentGroups": [ + "Default" + ], + "instanceId": 1, + "containerId": "19" + }, + { + "id": "57", + "tags": "Container Instance", + "properties": { + "structurizr.dsl.identifier": "developerapiapplicationinstance" + }, + "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": "20" + } + ] + } + ] + }, + { + "id": "51", + "tags": "Element,Deployment Node", + "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": "17" + } + ] + } + ] + }, + { + "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", + "properties": { + "structurizr.dsl.identifier": "livemobileappinstance" + }, + "relationships": [ + { + "id": "81", + "sourceId": "68", + "destinationId": "79", + "description": "Makes API calls to", + "technology": "JSON/HTTPS", + "linkedRelationshipId": "37" + } + ], + "environment": "Live", + "deploymentGroups": [ + "Default" + ], + "instanceId": 1, + "containerId": "18" + } + ] + }, + { + "id": "72", + "tags": "Element,Deployment Node", + "name": "Big Bank plc", + "environment": "Live", + "technology": "Big Bank plc data center", + "instances": "1", + "children": [ + { + "id": "73", + "tags": "Element,Deployment Node", + "name": "bigbank-web***", + "environment": "Live", + "technology": "Ubuntu 16.04 LTS", + "instances": "4", + "children": [ + { + "id": "74", + "tags": "Element,Deployment Node", + "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": "19" + } + ] + } + ] + }, + { + "id": "82", + "tags": "Element,Deployment Node", + "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", + "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" + }, + "environment": "Live", + "deploymentGroups": [ + "Default" + ], + "instanceId": 1, + "containerId": "27" + } + ] + } + ] + }, + { + "id": "90", + "tags": "Element,Deployment Node", + "name": "bigbank-prod001", + "environment": "Live", + "instances": "1", + "softwareSystemInstances": [ + { + "id": "91", + "tags": "Software System Instance", + "environment": "Live", + "deploymentGroups": [ + "Default" + ], + "instanceId": 1, + "softwareSystemId": "4" + } + ] + }, + { + "id": "77", + "tags": "Element,Deployment Node", + "name": "bigbank-api***", + "environment": "Live", + "technology": "Ubuntu 16.04 LTS", + "instances": "8", + "children": [ + { + "id": "78", + "tags": "Element,Deployment Node", + "name": "Apache Tomcat", + "environment": "Live", + "technology": "Apache Tomcat 8.x", + "instances": "1", + "containerInstances": [ + { + "id": "79", + "tags": "Container Instance", + "properties": { + "structurizr.dsl.identifier": "liveapiapplicationinstance" + }, + "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": "20" + } + ] + } + ] + }, + { + "id": "86", + "tags": "Element,Deployment Node,Failover", + "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" + }, + "environment": "Live", + "deploymentGroups": [ + "Default" + ], + "instanceId": 1, + "containerId": "27" + } + ] + } + ] + } + ] + }, + { + "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", + "properties": { + "structurizr.dsl.identifier": "livesinglepageapplicationinstance" + }, + "relationships": [ + { + "id": "80", + "sourceId": "71", + "destinationId": "79", + "description": "Makes API calls to", + "technology": "JSON/HTTPS", + "linkedRelationshipId": "33" + } + ], + "environment": "Live", + "deploymentGroups": [ + "Default" + ], + "instanceId": 1, + "containerId": "17" + } + ] + } + ] + }, + { + "id": "63", + "tags": "Element,Deployment Node", + "name": "Big Bank plc", + "environment": "Development", + "technology": "Big Bank plc data center", + "instances": "1", + "children": [ + { + "id": "64", + "tags": "Element,Deployment Node", + "name": "bigbank-dev001", + "environment": "Development", + "instances": "1", + "softwareSystemInstances": [ + { + "id": "65", + "tags": "Software System Instance", + "environment": "Development", + "deploymentGroups": [ + "Default" + ], + "instanceId": 1, + "softwareSystemId": "4" + } + ] + } + ] + } + ] + }, + "documentation": {}, + "views": { + "systemContextViews": [ + { + "key": "SystemContext", + "order": 1, + "description": "The system context diagram for the Internet Banking System - diagram created with Structurizr.", + "properties": { + "structurizr.groups": "false" + }, + "softwareSystemId": "7", + "paperSize": "A5_Landscape", + "dimensions": { + "width": 2480, + "height": 1748 + }, + "animations": [ + { + "order": 1, + "elements": [ + "7" + ] + }, + { + "order": 2, + "elements": [ + "1" + ], + "relationships": [ + "8" + ] + }, + { + "order": 3, + "elements": [ + "4" + ], + "relationships": [ + "9" + ] + }, + { + "order": 4, + "elements": [ + "5" + ], + "relationships": [ + "11", + "10" + ] + } + ], + "enterpriseBoundaryVisible": true, + "elements": [ + { + "id": "1", + "x": 632, + "y": 64 + }, + { + "id": "4", + "x": 607, + "y": 1154 + }, + { + "id": "5", + "x": 1422, + "y": 659 + }, + { + "id": "7", + "x": 607, + "y": 659 + } + ], + "relationships": [ + { + "id": "11" + }, + { + "id": "8" + }, + { + "id": "10" + }, + { + "id": "9" + } + ] + } + ], + "containerViews": [ + { + "key": "Containers", + "order": 2, + "description": "The container diagram for the Internet Banking System - diagram created with Structurizr.", + "softwareSystemId": "7", + "paperSize": "A5_Landscape", + "dimensions": { + "width": 2480, + "height": 1748 + }, + "animations": [ + { + "order": 1, + "elements": [ + "1", + "4", + "5" + ], + "relationships": [ + "11" + ] + }, + { + "order": 2, + "elements": [ + "19" + ], + "relationships": [ + "28" + ] + }, + { + "order": 3, + "elements": [ + "17" + ], + "relationships": [ + "29", + "31" + ] + }, + { + "order": 4, + "elements": [ + "18" + ], + "relationships": [ + "30" + ] + }, + { + "order": 5, + "elements": [ + "20" + ], + "relationships": [ + "33", + "47", + "37", + "49" + ] + }, + { + "order": 6, + "elements": [ + "27" + ], + "relationships": [ + "45" + ] + } + ], + "externalSoftwareSystemBoundariesVisible": true, + "elements": [ + { + "id": "1", + "x": 1056, + "y": 39 + }, + { + "id": "4", + "x": 2012, + "y": 1189 + }, + { + "id": "27", + "x": 37, + "y": 1189 + }, + { + "id": "5", + "x": 2012, + "y": 679 + }, + { + "id": "17", + "x": 780, + "y": 679 + }, + { + "id": "18", + "x": 1283, + "y": 679 + }, + { + "id": "19", + "x": 37, + "y": 679 + }, + { + "id": "20", + "x": 1031, + "y": 1189 + } + ], + "relationships": [ + { + "id": "29" + }, + { + "id": "28" + }, + { + "id": "37" + }, + { + "id": "11" + }, + { + "id": "33" + }, + { + "id": "45" + }, + { + "id": "31" + }, + { + "id": "30" + }, + { + "id": "47" + }, + { + "id": "49" + } + ] + } + ], + "componentViews": [ + { + "key": "Components", + "order": 3, + "description": "The component diagram for the API Application - diagram created with Structurizr.", + "paperSize": "A5_Landscape", + "dimensions": { + "width": 2480, + "height": 1748 + }, + "animations": [ + { + "order": 1, + "elements": [ + "4", + "27", + "5", + "17", + "18" + ] + }, + { + "order": 2, + "elements": [ + "24", + "21" + ], + "relationships": [ + "44", + "36", + "40", + "32" + ] + }, + { + "order": 3, + "elements": [ + "22", + "25" + ], + "relationships": [ + "34", + "46", + "38", + "41" + ] + }, + { + "order": 4, + "elements": [ + "23", + "26" + ], + "relationships": [ + "35", + "48", + "39", + "42", + "43" + ] + } + ], + "containerId": "20", + "externalContainerBoundariesVisible": true, + "elements": [ + { + "id": "22", + "x": 1925, + "y": 436 + }, + { + "id": "23", + "x": 1015, + "y": 436 + }, + { + "id": "24", + "x": 105, + "y": 812 + }, + { + "id": "25", + "x": 1925, + "y": 812 + }, + { + "id": "26", + "x": 1015, + "y": 812 + }, + { + "id": "4", + "x": 1925, + "y": 1292 + }, + { + "id": "27", + "x": 105, + "y": 1292 + }, + { + "id": "5", + "x": 1015, + "y": 1292 + }, + { + "id": "17", + "x": 560, + "y": 10 + }, + { + "id": "18", + "x": 1470, + "y": 11 + }, + { + "id": "21", + "x": 105, + "y": 436 + } + ], + "relationships": [ + { + "id": "40", + "position": 50 + }, + { + "id": "41", + "position": 50 + }, + { + "id": "42" + }, + { + "id": "43" + }, + { + "id": "32", + "position": 35 + }, + { + "id": "36", + "position": 85 + }, + { + "id": "35", + "position": 45 + }, + { + "id": "34", + "position": 85 + }, + { + "id": "44", + "position": 55 + }, + { + "id": "46" + }, + { + "id": "48", + "position": 60 + }, + { + "id": "38", + "position": 40 + }, + { + "id": "39", + "position": 40 + } + ] + } + ], + "dynamicViews": [ + { + "key": "SignIn", + "order": 5, + "description": "Summarises how the sign in feature works in the single-page application - diagram created with Structurizr.", + "paperSize": "A5_Landscape", + "elementId": "20", + "externalBoundariesVisible": true, + "relationships": [ + { + "id": "32", + "description": "Submits credentials to", + "order": "1", + "response": false, + "vertices": [ + { + "x": 1238, + "y": 236 + } + ], + "routing": "Curved", + "position": 50 + }, + { + "id": "40", + "description": "Validates credentials using", + "order": "2", + "response": false, + "vertices": [ + { + "x": 2065, + "y": 845 + } + ], + "routing": "Curved" + }, + { + "id": "44", + "description": "select * from users where username = ?", + "order": "3", + "response": false, + "vertices": [ + { + "x": 1218, + "y": 1416 + } + ], + "routing": "Curved" + }, + { + "id": "44", + "description": "Returns user data to", + "order": "4", + "response": true, + "vertices": [ + { + "x": 1240, + "y": 1220 + } + ], + "routing": "Curved" + }, + { + "id": "40", + "description": "Returns true if the hashed password matches", + "order": "5", + "response": true, + "vertices": [ + { + "x": 1828, + "y": 841 + } + ], + "routing": "Curved" + }, + { + "id": "32", + "description": "Sends back an authentication token to", + "order": "6", + "response": true, + "vertices": [ + { + "x": 1210, + "y": 450 + } + ], + "routing": "Curved" + } + ], + "elements": [ + { + "id": "24", + "x": 1720, + "y": 1182 + }, + { + "id": "27", + "x": 290, + "y": 1182 + }, + { + "id": "17", + "x": 290, + "y": 192 + }, + { + "id": "21", + "x": 1720, + "y": 192 + } + ] + } + ], + "deploymentViews": [ + { + "key": "LiveDeployment", + "order": 7, + "description": "An example live deployment scenario for the Internet Banking System - diagram created with Structurizr.", + "softwareSystemId": "7", + "paperSize": "A4_Landscape", + "dimensions": { + "width": 3508, + "height": 2480 + }, + "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" + ] + } + ], + "elements": [ + { + "id": "88", + "x": 2584, + "y": 189 + }, + { + "id": "77", + "x": 0, + "y": 0 + }, + { + "id": "67", + "x": 0, + "y": 0 + }, + { + "id": "78", + "x": 0, + "y": 0 + }, + { + "id": "68", + "x": 424, + "y": 961 + }, + { + "id": "79", + "x": 1504, + "y": 961 + }, + { + "id": "69", + "x": 0, + "y": 0 + }, + { + "id": "90", + "x": 0, + "y": 0 + }, + { + "id": "91", + "x": 2584, + "y": 1734 + }, + { + "id": "70", + "x": 0, + "y": 0 + }, + { + "id": "71", + "x": 424, + "y": 189 + }, + { + "id": "82", + "x": 0, + "y": 0 + }, + { + "id": "83", + "x": 0, + "y": 0 + }, + { + "id": "72", + "x": 0, + "y": 0 + }, + { + "id": "84", + "x": 2584, + "y": 961 + }, + { + "id": "73", + "x": 0, + "y": 0 + }, + { + "id": "74", + "x": 0, + "y": 0 + }, + { + "id": "86", + "x": 0, + "y": 0 + }, + { + "id": "75", + "x": 1504, + "y": 189 + }, + { + "id": "87", + "x": 0, + "y": 0 + } + ], + "relationships": [ + { + "id": "93" + }, + { + "id": "80" + }, + { + "id": "92" + }, + { + "id": "81" + }, + { + "id": "76" + }, + { + "id": "85" + }, + { + "id": "89" + } + ] + }, + { + "key": "DevelopmentDeployment", + "order": 6, + "description": "An example development deployment scenario for the Internet Banking System - diagram created with Structurizr.", + "softwareSystemId": "7", + "paperSize": "A5_Landscape", + "dimensions": { + "width": 2480, + "height": 1748 + }, + "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" + ] + } + ], + "elements": [ + { + "id": "55", + "x": 989, + "y": 136 + }, + { + "id": "57", + "x": 989, + "y": 476 + }, + { + "id": "59", + "x": 0, + "y": 0 + }, + { + "id": "60", + "x": 0, + "y": 0 + }, + { + "id": "61", + "x": 1827, + "y": 136 + }, + { + "id": "50", + "x": 0, + "y": 0 + }, + { + "id": "51", + "x": 0, + "y": 0 + }, + { + "id": "52", + "x": 152, + "y": 306 + }, + { + "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": 1827, + "y": 1151 + } + ], + "relationships": [ + { + "id": "62", + "position": 50 + }, + { + "id": "56" + }, + { + "id": "66" + }, + { + "id": "58" + } + ] + } + ], + "imageViews": [ + { + "key": "MainframeBankingSystemFacade", + "order": 4, + "title": "Class diagram for the Mainframe Banking System Facade component", + "elementId": "25", + "content": "data:image/png;base64,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Nothw4alL26RZAluzJgx0aFDhzjooIOirKwsfRUAADZACmwAAAAAAADA10re/vOtt96Kbt26pW7/+eGHH6av0iINDQ0xY8aM2HnnnWOHHXaIp59+On0VAAA2QApsAAAAAAAAwNeqqKiIQYMGxSabbBJ9+vSJ6urq9FVarLy8PM4555zo2LFjnHDCCa16LgAAMoMCGwAAAAAAAPC1krOm7bTTTqlZ05544on0xaslOZvb66+/HltssUXsscceMWXKlPRVAADYwCiwAQAAAAAAAM1KzpA2atSo6NChQxx88MGxaNGi9FVW28KFC+PQQw+NLl26xBVXXJG+GACADYwCGwAAAAAAANCs3Nzc+NnPfhbdu3ePW265JX3xGqmpqYl77rknOnXqFAcccEDqNQAA2HApsAEAAAAAAAArSd7u89lnn42OHTvGXnvtFQsWLEhfZY19+eWX8cMf/jB69eoV48ePT18MAMAGRIENAAAAAAAAWElFRUUcffTRqZnSzj///GhoaEhfZY2Vl5enbh+avDXp//3f/0VZWVn6KgAAbCAU2AAAAAAAAICVTJ06Nbp16xa9e/eOyZMnpy9utXfffTc1A9tuu+0WL774YvpiAAA2EApsAAAAAAAAwApqa2tjyJAhqRnSjjzyyPTFbSIvLy/69euXukXpgAEDUrcsBQBgw6PABgAAAAAAAKwgJycndt111+jRo0c8/fTT6YvbzGOPPRbdu3ePfffdNwoKCtIXAwCwAVBgAwAAAAAAAFYwbty42HjjjWO//faLxYsXpy9uM1999VUceOCBseWWW8ZLL70UDQ0N6asAAJDhFNgAAAAAAACApRYtWhT7779/bLbZZnHDDTekL25TVVVVcdNNN8Umm2wSl156aVRUVKSvAgBAhlNgAwAAAAAAAJZ68803o0uXLvHtb3875s2bl764zX3wwQex++67x09+8pOYMmVK+mIAADKcAhsAAAAAAACQUldXFyeeeGJ07NgxTj311PTFa0VRUVEMGjQoOnXqFHfeeWfU1NSkrwIAQAZTYAMAAAAAAABSZs2aFTvttFNsscUW8c4776QvXmuefPLJ6Ny5cxx99NGRlZWVvhgAgAymwAYAAAAAAABEQ0NDXH311bHpppvGr371q3U6E9rcuXNjzz33jO233z6eeuqp1ExwAABsGBTYAAAAAAAAgMjNzY0f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qlSBLfl3hQIbAAAtpcAG7ZgCGwAAsC6kF9jq6uqirKysxUnOwgYAAHDllVdGx44d48wzz1RgAwCgxRTYoB1TYAMAANYFBTYAAKAtJP+uaCqwlZaWpi8GAIBmKbBBO6bABgAArAsKbAAAQFtQYAMAYE0osEE7psAGAACsCwpsAABAW2gqsJ111lkKbAAAtJgCG7RjCmwAAMC6oMAGAAC0hcsvvzz1d8XZZ5+twAYAQIspsEE7psAGAACsCwpsAABAW7jssstSf1cMGDBAgQ0AgBZTYIN2TIENAABYFxTYAACAtjBs2DAFNgAAVpsCG7RjCmwAAMC6oMAGAAC0hWSBbeONN46BAwem/lYAAICWUGCDdkyBDQAAWBcU2AAAgLbQVGAbNGiQAhsAAC2mwAbtWFOB7fDDD1dgAwAA1ppkYW348OGpAtuZZ56Z+t8VFRUtTnV1dfpTAgAAG6BLL700VWA777zzUn8rAABASyiwQTuWLLBtueWW8dvf/jZycnJWmuVARERERESkLVJaWpqaKSFZYDvttNNS/zs3N7fFSa6f/pwiIiIiIrLhZciQIakC24ABA1J/K6QvFxERaW3Ky8ujpqYm/at1YD2nwAbt2I033hjdunWLvfbaK0aNGhVjx44VERERERFp84wZMyb+93//N1VgO+CAA+L222+Pm2++ucW59dZbV3pOERERERHZsHLHHXfEoYceGhtttFH86le/Sv2tkL6OiIhIa5L8rLn//vvj888/T/9qHVjPKbBBO3buuedG586do2vXrvGtb31LRERERERkrWWrrbaKLbbYIjULdPoyERERERGRVaV3797RvXv3VIEt+f+cv+OOO660joiISGvSq1ev+OEPfxjjx49P/2odWM8psEE71q9fv9hss81i5513jmOPPTb69u0bffr0ERERERERafMk/9444YQT/N0hIiIiIiJrlBNPPDH23HPPVIEt+c8//elP/r4QEZFWp+mz5Mgjj0yVpXv06BFXX311+lfrwHpOgQ3asaOOOio1A9shhxwSCxYsiMLCQhERERERERERERERkXaXkpKSGDRoUKrAds4558S8efNWWkdERGRNUlBQEJ988kkcccQRsemmm6Y+b4DMosAG7djxxx8fXbp0icMPPzwqKyvTFwMAAAAAALQbF110UarAduGFF0ZVVVX6YgBYIw0NDZGV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bJB/AjbIMAEbAAAAAABQDpKArVu3btGmTRsBGwAlJWCD/BOwQYYJ2AAAAAAAgHLwzjvvpAFb27ZtBWwAlJSADfJPwAYZJmADAAAAAADKweDBgwVsANQIARvkn4ANMkzABgAAAAAAlIOqgK19+/YxcuTI4t0AUG0CNsg/ARtkmIANAAAAAAAoB2+//XZ07dpVwAZAyQnYIP8EbJBhAjYAAAAAAKAcvPXWW2nA1rFjRwEbACUlYIP8E7BBhgnYAAAAAACAcrB6wPbZZ58V7waAahOwQf4J2CDDBGwAAAAAAEA5SAK2TTbZJDp16iRgA6CkBGyQfwI2yDABGwAAAAAAUA4GDhyYBmydO3cWsAFQUgI2yD8BG2SYgA0AAAAAACgHAjYAaoqADfJPwAYZJmADAAAAAADKwYABA6JLly7pJmADoJQEbJB/AjbIMAEbAAAAAABQDt58883YeOONBWwAlJyADfJPwAYZJmADAAAAAADKQb9+/dKArWvXrjFq1Kji3QBQbQI2yD8BG2SYgA0AAAAAACgHAjYAaoqADfJPwAYZJmADAAAAAADKQVXAtummmwrYACgpARvkn4ANMkzABgAAAAAAlIM33ngjOnfuLGADoOQEbJB/AjbIMAEbAAAAAABQDv7+97+vCthGjx5dvBsAqk3ABvknYIMME7ABAAAAAADlIAnYOnXqFJtvvrmADYCSErBB/gnYIMMEbAAAAAAAQDl47bXXBGwA1AgBG+SfgA0yTMAGAACUs+R7zMiRI+P999+PN954I5599tl46KGH4r777ounn346Xn/99Xj33Xdj+PDhMXv27OKbAwAAZaQqYNtyyy0FbACUlIAN8k/ABhkmYAMAAMpJZWVlzJgxI95888247bbb4txzz43jjz8+DjvssNh///1jl112ia222iqdkWHHHXeMfffdN3r16hXHHnts/OpXv4obbrghXn755Zg4cWIsW7as+O4BAIAME7ABUFMEbJB/AjbIMAEbAABQDioqKmLs2LFxzz33xOmnn57Gal26dIkGDRpEhw4dokePHmm8lvx9ErP17t07vve978Vuu+0W3/rWt2LjjTeOhg0bRtu2bWPPPfeMU045JW688cYYOnRoOriYDFICAADZ9sorr0THjh3T/2llzJgxxbsBoNoEbJB/AjbIMAEbAACQZUm4NmXKlHRJ0B//+Mfp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bER6jAE2IiIiIiIyFAywEREREREZLinAVrNmTQbYiIjoX8EAG5HhY4CNSI8xwEZERERERIaCATYiIiIiIsN1584deYDN1taWATYiInrtGGAjMnwMsBHpMQbYiIiIiIjIUDDARkRERERkuG7fvs0AGxER/WsYYCMyfAywEekxBtiIiIiIiMhQMMBGRERERGS4pABbjRo15AG269ev604mIiL6RxhgIzJ8DLAR6TEG2IiIiIiIyFAwwEZEREREZLhu3rwpD7DZ29szwEZERK8dA2xEho8BNiI9xgAbEREREREZCgbYiIiIiIgMlxRgq169OgNsRET0r2CAjcjwMcBGpMcYYCMiIiIiIkPBABsRERERkeFSBdgcHBxw48YN3clERET/CANsRIaPATYiPcYAGxERERERGQoG2IiIiIiIDJf01DUpwFasWDEG2IiI6LVjgI3I8DHARqTHGGAjIiIiIiJDwQAbEREREZHhkgJs1apVY4CNiIj+FQywERk+BtiI9BgDbEREREREZCgYYCMiIiIiMlxSgK1q1apwdHRkgI2IiF47BtiIDB8DbER6jAE2IiIiIiIyFAywEREREREZrqtXrzLARkRE/xoG2IgMHwNsRHqMATYiIiIiIjIUDLARERERERkuVYBN6u/fvHlTdzIREdE/wgAbkeFjgI1IjzHARkREREREhoIBNiIiIiIiw3XlyhUG2IiI6F/DABuR4WOAjUiPMcBGRERERESGQgqw+fr6wsHBAVu2bNGdTEREREREb7HLly+jQoUK8leIMsBGRESvGwNsRIaPATYiPcYAGxERERERGYqYmBjMmzcPX375JS5cuKA7mYiIiIiI3mJSqOCHH37AmDFjEBERoTuZiIjoH2GAjcjwMcBGpMcYYCMiIqJ3XWZmJvz8/LBixQqsXr2ahYXlLS4rV67E7NmzMX36dPz888/5prOwsLydZdWqVfKnKj548EC3GSciem3S0tJw9OhRjgtYWPS4LF++HDNnzpT396X/rTudhYVFP8umTZvkT0wn0ncMsBEZPgbYiPQYA2xERET0rnv58iW6deuGkiVLwsPDA56eniwsLG954bHMwmJYxc3NDXXr1sX27dt1m3EiotdGepLryJEj5eMCd3f3fOciFhYW/Sns77OwvD2lVKlSqFatGvbu3avb9BLpHQbYiAwfA2xEeowBNiIi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Zh2URUREKDw8nKIo6qI6efKkpk+fLn9/fwJsAADH27Jli3URyfwJAACyBxNge/DBB1WlShWtXLnSOm+1n9tSFEVldJ0+fVqvvPIKATYAcAECbID7EWADHIwAG0yAzYyoVL16dfXq1UsvvfQSRVFUmurdu7ceeugh66SNABsAwOk8Ho+ioqKsPwEAQPZgAmwtW7ZU0aJF9cQTT+jll19Oc25LURSV0dWnTx81btzYCrCZoAMBNgDIugiwAe5HgA1wMAJsMAG2woULy8/PT4GBgQoKCqIoirpkFSpUyGrGEWADAAAAADiNCbA9/PDDypkzp4oUKaJixYqlOa+lKIrK6DK/a8w1FtMz69atGwE2AMjCCLAB7keADXAwAmwwAbYCBQqobt261jRLAwcOpCiKSlODBg1S+/btrbArATYAAAAAgNMkj8BmAiU9e/a0zmPt57YURVEZXeZ3TbNmzRiBDQBcgAAb4H4E2AAHI8AGE2DLmzevOnTooMjISCtgQlEUdalasGCBNeUwATYAAAAAgNOYAFuLFi1000036YcffkhzTktRFHWt6u2337YCbF27diXABgBZGAE2wP0IsAEORoANyQG2jh07Kioqyr4YAFIsXLiQABsAIEswzcIzZ84oLi7OvggAALhUcoCtatWq2rhxo30xAFwz/fr1I8AGAC5AgA1wPwJsgIMRYAMBNgDpRYANAJBVbN682TrXWb9+vX0RAABwKQJsADILATYAcAcCbID7EWADHIwAGwiwAUgvAmwAgKzi008/tS4gjRkzxr4IAAC4FAE2AJmFABsAuAMBNsD9CLABDkaADQTYAKQXATYAQFYxa9Ys6wLSqFGj7IsAAIBLEWADkFkIsAGAOxBgA9yPABvgYATYQIANQHoRYAMAZBWzZ88mwAYAQDZDgA1AZiHABgDuQIANcD8CbICDEWADATYA6UWADQCQVSRPIWqOdQEAQPZAgA1AZiHABgDuQIANcD8CbICDEWADATYA6UWADQCQVSxYsECBgYEaP348F4MAAMgmCLAByCwE2ADAHQiwAe5HgA1wMAJsIMAGIL0IsAEAsopffvlFPXv21Jo1a7gYBABANkGADUBmIcAGAO5AgA1wPwJsgIMRYAMBNgDpRYANAJBVmAtGu3fvVmRkpH0RAABwKQJsADILATYAcAcCbID7EWADHIwAGwiwAUgv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", + "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": {}, + "metadataSymbols": "SquareBrackets", + "lastSavedView": "SystemContext" + } + } +} \ No newline at end of file diff --git a/structurizr-client/src/test/resources/theme.json b/structurizr-client/src/test/resources/theme.json new file mode 100644 index 000000000..57e64ae06 --- /dev/null +++ b/structurizr-client/src/test/resources/theme.json @@ -0,0 +1,12 @@ +{ + "name" : "Theme", + "elements" : [ { + "tag" : "Tag", + "background" : "#ff0000", + "icon" : "logo.png" + } ], + "relationships" : [ { + "tag" : "Tag", + "color" : "#00ff00" + } ] +} \ No newline at end of file diff --git a/structurizr-client/src/test/resources/workspaceValidation/ChildDeploymentNodeNamesAreNotUnique.json b/structurizr-client/src/test/resources/workspaceValidation/ChildDeploymentNodeNamesAreNotUnique.json new file mode 100644 index 000000000..ca78d57b5 --- /dev/null +++ b/structurizr-client/src/test/resources/workspaceValidation/ChildDeploymentNodeNamesAreNotUnique.json @@ -0,0 +1,39 @@ +{ + "id" : 0, + "name" : "Name", + "description" : "Description", + "configuration" : { }, + "model" : { + "deploymentNodes" : [ { + "id" : "1", + "name" : "Deployment Node", + "environment" : "Default", + "instances" : 1, + "children" : [ { + "id" : "2", + "name" : "Deployment Node", + "environment" : "Default", + "instances" : 1, + "children" : [ { + "id" : "4", + "name" : "Child", + "environment" : "Default", + "instances" : 1 + }, { + "id" : "3", + "name" : "Child", + "environment" : "Default", + "instances" : 1 + } ] + } ] + } ] + }, + "documentation" : { }, + "views" : { + "configuration" : { + "branding" : { }, + "styles" : { }, + "terminology" : { } + } + } +} \ No newline at end of file diff --git a/structurizr-client/src/test/resources/workspaceValidation/ComponentNamesAreNotUnique.json b/structurizr-client/src/test/resources/workspaceValidation/ComponentNamesAreNotUnique.json new file mode 100644 index 000000000..4ff7867c0 --- /dev/null +++ b/structurizr-client/src/test/resources/workspaceValidation/ComponentNamesAreNotUnique.json @@ -0,0 +1,38 @@ +{ + "id" : 0, + "name" : "Name", + "description" : "Description", + "configuration" : { }, + "model" : { + "softwareSystems" : [ { + "id" : "1", + "tags" : "Element,Software System", + "name" : "Software System", + "location" : "Unspecified", + "containers" : [ { + "id" : "2", + "tags" : "Element,Container", + "name" : "Container", + "components" : [ { + "id" : "3", + "tags" : "Element,Component", + "name" : "Component", + "size" : 0 + }, { + "id" : "4", + "tags" : "Element,Component", + "name" : "Component", + "size" : 0 + } ] + } ] + } ] + }, + "documentation" : { }, + "views" : { + "configuration" : { + "branding" : { }, + "styles" : { }, + "terminology" : { } + } + } +} \ No newline at end of file diff --git a/structurizr-client/src/test/resources/workspaceValidation/ContainerAssociatedWithComponentViewIsMissingFromTheModel.json b/structurizr-client/src/test/resources/workspaceValidation/ContainerAssociatedWithComponentViewIsMissingFromTheModel.json new file mode 100644 index 000000000..f7d2ebc91 --- /dev/null +++ b/structurizr-client/src/test/resources/workspaceValidation/ContainerAssociatedWithComponentViewIsMissingFromTheModel.json @@ -0,0 +1,33 @@ +{ + "id" : 0, + "name" : "Name", + "description" : "Description", + "configuration" : { }, + "model" : { + "softwareSystems" : [ { + "id" : "1", + "tags" : "Element,Software System", + "name" : "Software System", + "location" : "Unspecified", + "containers" : [ { + "id" : "2", + "tags" : "Element,Container", + "name" : "Container" + } ] + } ] + }, + "documentation" : { }, + "views" : { + "componentViews" : [ { + "softwareSystemId" : "1", + "description" : "Description", + "key" : "Components", + "containerId" : "3" + } ], + "configuration" : { + "branding" : { }, + "styles" : { }, + "terminology" : { } + } + } +} \ No newline at end of file diff --git a/structurizr-client/src/test/resources/workspaceValidation/ContainerNamesAreNotUnique.json b/structurizr-client/src/test/resources/workspaceValidation/ContainerNamesAreNotUnique.json new file mode 100644 index 000000000..ac7bb09a2 --- /dev/null +++ b/structurizr-client/src/test/resources/workspaceValidation/ContainerNamesAreNotUnique.json @@ -0,0 +1,31 @@ +{ + "id" : 0, + "name" : "Name", + "description" : "Description", + "configuration" : { }, + "model" : { + "softwareSystems" : [ { + "id" : "1", + "tags" : "Element,Software System", + "name" : "Software System", + "location" : "Unspecified", + "containers" : [ { + "id" : "2", + "tags" : "Element,Container", + "name" : "Container" + }, { + "id" : "3", + "tags" : "Element,Container", + "name" : "Container" + } ] + } ] + }, + "documentation" : { }, + "views" : { + "configuration" : { + "branding" : { }, + "styles" : { }, + "terminology" : { } + } + } +} \ No newline at end of file diff --git a/structurizr-client/src/test/resources/workspaceValidation/ElementAssociatedWithDynamicViewIsMissingFromTheModel.json b/structurizr-client/src/test/resources/workspaceValidation/ElementAssociatedWithDynamicViewIsMissingFromTheModel.json new file mode 100644 index 000000000..788c4acc0 --- /dev/null +++ b/structurizr-client/src/test/resources/workspaceValidation/ElementAssociatedWithDynamicViewIsMissingFromTheModel.json @@ -0,0 +1,27 @@ +{ + "id" : 0, + "name" : "Name", + "description" : "Description", + "configuration" : { }, + "model" : { + "softwareSystems" : [ { + "id" : "1", + "tags" : "Element,Software System", + "name" : "Software System", + "location" : "Unspecified" + } ] + }, + "documentation" : { }, + "views" : { + "dynamicViews" : [ { + "description" : "Description", + "key" : "Dynamic", + "elementId" : "2" + } ], + "configuration" : { + "branding" : { }, + "styles" : { }, + "terminology" : { } + } + } +} \ No newline at end of file diff --git a/structurizr-client/src/test/resources/workspaceValidation/ElementReferencedByViewIsMissingFromTheModel.json b/structurizr-client/src/test/resources/workspaceValidation/ElementReferencedByViewIsMissingFromTheModel.json new file mode 100644 index 000000000..573257f6d --- /dev/null +++ b/structurizr-client/src/test/resources/workspaceValidation/ElementReferencedByViewIsMissingFromTheModel.json @@ -0,0 +1,31 @@ +{ + "id" : 0, + "name" : "Name", + "description" : "Description", + "configuration" : { }, + "model" : { + "people" : [ { + "id" : "1", + "tags" : "Element,Person", + "name" : "Person", + "location" : "Unspecified" + } ] + }, + "documentation" : { }, + "views" : { + "systemLandscapeViews" : [ { + "key" : "SystemLandscape", + "enterpriseBoundaryVisible" : true, + "elements" : [ { + "id" : "2", + "x" : 0, + "y" : 0 + } ] + } ], + "configuration" : { + "branding" : { }, + "styles" : { }, + "terminology" : { } + } + } +} \ No newline at end of file diff --git a/structurizr-client/src/test/resources/workspaceValidation/PeopleAndSoftwareSystemNamesAreNotUnique.json b/structurizr-client/src/test/resources/workspaceValidation/PeopleAndSoftwareSystemNamesAreNotUnique.json new file mode 100644 index 000000000..345b1812b --- /dev/null +++ b/structurizr-client/src/test/resources/workspaceValidation/PeopleAndSoftwareSystemNamesAreNotUnique.json @@ -0,0 +1,28 @@ +{ + "id" : 0, + "name" : "Name", + "description" : "Description", + "configuration" : { }, + "model" : { + "people" : [ { + "id" : "1", + "tags" : "Element,Person", + "name" : "Name", + "location" : "Unspecified" + } ], + "softwareSystems" : [ { + "id" : "2", + "tags" : "Element,Software System", + "name" : "Name", + "location" : "Unspecified" + } ] + }, + "documentation" : { }, + "views" : { + "configuration" : { + "branding" : { }, + "styles" : { }, + "terminology" : { } + } + } +} \ No newline at end of file diff --git a/structurizr-client/src/test/resources/workspaceValidation/RelationshipDescriptionsAreNotUnique.json b/structurizr-client/src/test/resources/workspaceValidation/RelationshipDescriptionsAreNotUnique.json new file mode 100644 index 000000000..bc2e72d25 --- /dev/null +++ b/structurizr-client/src/test/resources/workspaceValidation/RelationshipDescriptionsAreNotUnique.json @@ -0,0 +1,43 @@ +{ + "id" : 0, + "name" : "Name", + "description" : "Description", + "configuration" : { }, + "model" : { + "people" : [ { + "id" : "1", + "tags" : "Element,Person", + "name" : "User", + "relationships" : [ { + "id" : "3", + "tags" : "Relationship,Synchronous", + "sourceId" : "1", + "destinationId" : "2", + "description" : "Uses", + "interactionStyle" : "Synchronous" + }, { + "id" : "4", + "tags" : "Relationship,Synchronous", + "sourceId" : "1", + "destinationId" : "2", + "description" : "Uses", + "interactionStyle" : "Synchronous" + } ], + "location" : "Unspecified" + } ], + "softwareSystems" : [ { + "id" : "2", + "tags" : "Element,Software System", + "name" : "Software System", + "location" : "Unspecified" + } ] + }, + "documentation" : { }, + "views" : { + "configuration" : { + "branding" : { }, + "styles" : { }, + "terminology" : { } + } + } +} \ No newline at end of file diff --git a/structurizr-client/src/test/resources/workspaceValidation/RelationshipReferencedByViewIsMissingFromTheModel.json b/structurizr-client/src/test/resources/workspaceValidation/RelationshipReferencedByViewIsMissingFromTheModel.json new file mode 100644 index 000000000..65cc277bf --- /dev/null +++ b/structurizr-client/src/test/resources/workspaceValidation/RelationshipReferencedByViewIsMissingFromTheModel.json @@ -0,0 +1,52 @@ +{ + "id" : 0, + "name" : "Name", + "description" : "Description", + "configuration" : { }, + "model" : { + "people" : [ { + "id" : "1", + "tags" : "Element,Person", + "name" : "Person", + "relationships" : [ { + "id" : "3", + "tags" : "Relationship,Synchronous", + "sourceId" : "1", + "destinationId" : "2", + "description" : "Uses", + "interactionStyle" : "Synchronous" + } ], + "location" : "Unspecified" + } ], + "softwareSystems" : [ { + "id" : "2", + "tags" : "Element,Software System", + "name" : "Software System", + "location" : "Unspecified" + } ] + }, + "documentation" : { }, + "views" : { + "systemLandscapeViews" : [ { + "key" : "SystemLandscape", + "enterpriseBoundaryVisible" : true, + "elements" : [ { + "id" : "1", + "x" : 0, + "y" : 0 + }, { + "id" : "2", + "x" : 0, + "y" : 0 + } ], + "relationships" : [ { + "id" : "4" + } ] + } ], + "configuration" : { + "branding" : { }, + "styles" : { }, + "terminology" : { } + } + } +} \ No newline at end of file diff --git a/structurizr-client/src/test/resources/workspaceValidation/SoftwareSystemAssociatedWithContainerViewIsMissingFromTheModel.json b/structurizr-client/src/test/resources/workspaceValidation/SoftwareSystemAssociatedWithContainerViewIsMissingFromTheModel.json new file mode 100644 index 000000000..fe0fafb75 --- /dev/null +++ b/structurizr-client/src/test/resources/workspaceValidation/SoftwareSystemAssociatedWithContainerViewIsMissingFromTheModel.json @@ -0,0 +1,27 @@ +{ + "id" : 0, + "name" : "Name", + "description" : "Description", + "configuration" : { }, + "model" : { + "softwareSystems" : [ { + "id" : "1", + "tags" : "Element,Software System", + "name" : "Software System", + "location" : "Unspecified" + } ] + }, + "documentation" : { }, + "views" : { + "containerViews" : [ { + "softwareSystemId" : "2", + "description" : "Description", + "key" : "Containers" + } ], + "configuration" : { + "branding" : { }, + "styles" : { }, + "terminology" : { } + } + } +} \ No newline at end of file diff --git a/structurizr-client/src/test/resources/workspaceValidation/SoftwareSystemAssociatedWithDeploymentViewIsMissingFromTheModel.json b/structurizr-client/src/test/resources/workspaceValidation/SoftwareSystemAssociatedWithDeploymentViewIsMissingFromTheModel.json new file mode 100644 index 000000000..2e0ae1beb --- /dev/null +++ b/structurizr-client/src/test/resources/workspaceValidation/SoftwareSystemAssociatedWithDeploymentViewIsMissingFromTheModel.json @@ -0,0 +1,27 @@ +{ + "id" : 0, + "name" : "Name", + "description" : "Description", + "configuration" : { }, + "model" : { + "softwareSystems" : [ { + "id" : "1", + "tags" : "Element,Software System", + "name" : "Software System", + "location" : "Unspecified" + } ] + }, + "documentation" : { }, + "views" : { + "deploymentViews" : [ { + "softwareSystemId" : "2", + "description" : "Description", + "key" : "Deployment" + } ], + "configuration" : { + "branding" : { }, + "styles" : { }, + "terminology" : { } + } + } +} \ No newline at end of file diff --git a/structurizr-client/src/test/resources/workspaceValidation/SoftwareSystemAssociatedWithSystemContextViewIsMissingFromTheModel.json b/structurizr-client/src/test/resources/workspaceValidation/SoftwareSystemAssociatedWithSystemContextViewIsMissingFromTheModel.json new file mode 100644 index 000000000..3d5f52048 --- /dev/null +++ b/structurizr-client/src/test/resources/workspaceValidation/SoftwareSystemAssociatedWithSystemContextViewIsMissingFromTheModel.json @@ -0,0 +1,33 @@ +{ + "id" : 0, + "name" : "Name", + "description" : "Description", + "configuration" : { }, + "model" : { + "softwareSystems" : [ { + "id" : "1", + "tags" : "Element,Software System", + "name" : "Software System", + "location" : "Unspecified" + } ] + }, + "documentation" : { }, + "views" : { + "systemContextViews" : [ { + "softwareSystemId" : "2", + "description" : "Description", + "key" : "SystemContext", + "enterpriseBoundaryVisible" : true, + "elements" : [ { + "id" : "1", + "x" : 0, + "y" : 0 + } ] + } ], + "configuration" : { + "branding" : { }, + "styles" : { }, + "terminology" : { } + } + } +} \ No newline at end of file diff --git a/structurizr-client/src/test/resources/workspaceValidation/TopLevelDeploymentNodeNamesAreNotUnique.json b/structurizr-client/src/test/resources/workspaceValidation/TopLevelDeploymentNodeNamesAreNotUnique.json new file mode 100644 index 000000000..1ed26eacd --- /dev/null +++ b/structurizr-client/src/test/resources/workspaceValidation/TopLevelDeploymentNodeNamesAreNotUnique.json @@ -0,0 +1,27 @@ +{ + "id" : 0, + "name" : "Name", + "description" : "Description", + "configuration" : { }, + "model" : { + "deploymentNodes" : [ { + "id" : "1", + "name" : "Deployment Node", + "environment" : "Default", + "instances" : 1 + }, { + "id" : "2", + "name" : "Deployment Node", + "environment" : "Default", + "instances" : 1 + } ] + }, + "documentation" : { }, + "views" : { + "configuration" : { + "branding" : { }, + "styles" : { }, + "terminology" : { } + } + } +} \ No newline at end of file diff --git a/structurizr-client/src/test/resources/workspaceValidation/TopLevelDeploymentNodeNamesAreNotUniqueButTheyExistInDifferentEnvironments.json b/structurizr-client/src/test/resources/workspaceValidation/TopLevelDeploymentNodeNamesAreNotUniqueButTheyExistInDifferentEnvironments.json new file mode 100644 index 000000000..14b569cc3 --- /dev/null +++ b/structurizr-client/src/test/resources/workspaceValidation/TopLevelDeploymentNodeNamesAreNotUniqueButTheyExistInDifferentEnvironments.json @@ -0,0 +1,27 @@ +{ + "id" : 0, + "name" : "Name", + "description" : "Description", + "configuration" : { }, + "model" : { + "deploymentNodes" : [ { + "id" : "1", + "name" : "Deployment Node", + "environment" : "Development", + "instances" : 1 + }, { + "id" : "2", + "name" : "Deployment Node", + "environment" : "Production", + "instances" : 1 + } ] + }, + "documentation" : { }, + "views" : { + "configuration" : { + "branding" : { }, + "styles" : { }, + "terminology" : { } + } + } +} \ No newline at end of file diff --git a/structurizr-client/src/test/resources/workspaceValidation/ViewAssociatedWithFilteredViewIsMissingFromTheWorkspace.json b/structurizr-client/src/test/resources/workspaceValidation/ViewAssociatedWithFilteredViewIsMissingFromTheWorkspace.json new file mode 100644 index 000000000..ad814cd11 --- /dev/null +++ b/structurizr-client/src/test/resources/workspaceValidation/ViewAssociatedWithFilteredViewIsMissingFromTheWorkspace.json @@ -0,0 +1,24 @@ +{ + "id" : 0, + "name" : "Name", + "description" : "Description", + "configuration" : { }, + "model" : { }, + "documentation" : { }, + "views" : { + "systemLandscapeViews" : [ { + "key" : "SystemLandscape", + "enterpriseBoundaryVisible" : true + } ], + "filteredViews" : [ { + "baseViewKey" : "SystemContext", + "key" : "Filtered", + "mode" : "Include" + } ], + "configuration" : { + "branding" : { }, + "styles" : { }, + "terminology" : { } + } + } +} \ No newline at end of file diff --git a/structurizr-client/src/test/resources/workspaceValidation/ViewKeysAreNotUnique.json b/structurizr-client/src/test/resources/workspaceValidation/ViewKeysAreNotUnique.json new file mode 100644 index 000000000..322d2b828 --- /dev/null +++ b/structurizr-client/src/test/resources/workspaceValidation/ViewKeysAreNotUnique.json @@ -0,0 +1,22 @@ +{ + "id" : 0, + "name" : "Name", + "description" : "Description", + "configuration" : { }, + "model" : { }, + "documentation" : { }, + "views" : { + "systemLandscapeViews" : [ { + "key" : "key", + "order": 1 + }, { + "key" : "key", + "order": 2 + } ], + "configuration" : { + "branding" : { }, + "styles" : { }, + "terminology" : { } + } + } +} \ No newline at end of file diff --git a/structurizr-component/README.md b/structurizr-component/README.md new file mode 100644 index 000000000..06bb920af --- /dev/null +++ b/structurizr-component/README.md @@ -0,0 +1,13 @@ +# 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. +It is also available via the Structurizr DSL `!components` keyword. + +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 diff --git a/structurizr-component/build.gradle b/structurizr-component/build.gradle new file mode 100644 index 000000000..79940da9f --- /dev/null +++ b/structurizr-component/build.gradle @@ -0,0 +1,11 @@ +dependencies { + + api project(':structurizr-core') + implementation 'org.apache.bcel:bcel:6.11.0' + implementation 'com.github.javaparser:javaparser-symbol-solver-core:3.27.1' + + testImplementation project(':structurizr-annotation') + +} + +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..387b66711 --- /dev/null +++ b/structurizr-component/src/main/java/com/structurizr/component/ComponentFinder.java @@ -0,0 +1,119 @@ +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.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +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 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"; + + private final TypeRepository typeRepository = new TypeRepository(); + private final Container container; + private final List componentFinderStrategies = new ArrayList<>(); + + ComponentFinder(Container container, TypeFilter typeFilter, Collection typeProviders, List componentFinderStrategies) { + this.container = container; + this.componentFinderStrategies.addAll(componentFinderStrategies); + + log.debug("Initialising component finder:"); + log.debug(" - for: " + container.getCanonicalName()); + for (TypeProvider typeProvider : typeProviders) { + 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 (Type type : typeRepository.getTypes()) { + if (type.getJavaClass() != null) { + Repository.addClass(type.getJavaClass()); + new TypeDependencyFinder().run(type, typeRepository); + } + } + } + + /** + * Find components, using all configured strategies, in the order they were added. + */ + public Set run() { + Set discoveredComponents = new LinkedHashSet<>(); + Map componentMap = new HashMap<>(); + Set componentSet = new LinkedHashSet<>(); + + for (ComponentFinderStrategy componentFinderStrategy : componentFinderStrategies) { + Set set = componentFinderStrategy.run(typeRepository); + if (set.isEmpty()) { + log.debug("No components were found by " + componentFinderStrategy); + } + discoveredComponents.addAll(set); + } + + 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()); + 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); + 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); + } + + return componentSet; + } + +} \ 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..21f1a37e1 --- /dev/null +++ b/structurizr-component/src/main/java/com/structurizr/component/ComponentFinderBuilder.java @@ -0,0 +1,105 @@ +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; +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 static final String JAR_FILE_EXTENSION = ".jar"; + + private Container container; + private final List typeProviders = new ArrayList<>(); + private TypeFilter typeFilter; + private final List componentFinderStrategies = new ArrayList<>(); + + public ComponentFinderBuilder forContainer(Container container) { + this.container = container; + + return this; + } + + public ComponentFinderBuilder fromClasses(String path) { + return fromClasses(new File(path)); + } + + public ComponentFinderBuilder fromClasses(File path) { + if (!path.exists()) { + throw new IllegalArgumentException(path.getAbsolutePath() + " does not exist"); + } + + 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 fromSource(String path) { + return fromSource(new File(path)); + } + + public ComponentFinderBuilder fromSource(File path) { + this.typeProviders.add(new SourceDirectoryTypeProvider(path)); + + return this; + } + + public ComponentFinderBuilder filteredBy(TypeFilter typeFilter) { + this.typeFilter = typeFilter; + + return this; + } + + public ComponentFinderBuilder withStrategy(ComponentFinderStrategy componentFinderStrategy) { + this.componentFinderStrategies.add(componentFinderStrategy); + + return this; + } + + 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 (typeFilter == null) { + typeFilter = new DefaultTypeFilter(); + } + + if (componentFinderStrategies.isEmpty()) { + throw new RuntimeException("One or more component finder strategies must be configured"); + } + + 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 new file mode 100644 index 000000000..6862e7abe --- /dev/null +++ b/structurizr-component/src/main/java/com/structurizr/component/ComponentFinderStrategy.java @@ -0,0 +1,118 @@ +package com.structurizr.component; + +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 org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import java.util.LinkedHashSet; +import java.util.Map; +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. + */ +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; + 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, 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; + } + + Set run(TypeRepository typeRepository) { + Set components = new LinkedHashSet<>(); + log.debug("Running " + this.toString()); + + Set types = typeRepository.getTypes(); + for (Type type : types) { + + 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); + if (supportingTypes.isEmpty()) { + log.debug(" - none"); + } else { + for (Type supportingType : supportingTypes) { + log.debug(" + supporting type: " + supportingType.getFullyQualifiedName()); + } + component.addSupportingTypes(supportingTypes); + } + } + } + + return components; + } + + void visit(Component component) { + this.componentVisitor.visit(component); + } + + @Override + public String toString() { + return "ComponentFinderStrategy{" + + "technology=" + (technology == null ? null : "'" + technology + "'") + + ", typeMatcher=" + typeMatcher + + ", typeFilter=" + typeFilter + + ", supportingTypesStrategy=" + supportingTypesStrategy + + ", namingStrategy=" + namingStrategy + + ", descriptionStrategy=" + descriptionStrategy + + ", urlStrategy=" + urlStrategy + + ", componentVisitor=" + componentVisitor + + '}'; + } + +} \ 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..6f692dc30 --- /dev/null +++ b/structurizr-component/src/main/java/com/structurizr/component/ComponentFinderStrategyBuilder.java @@ -0,0 +1,193 @@ +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; +import com.structurizr.component.naming.DefaultNamingStrategy; +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; + +/** + * Provides a way to create a {@link ComponentFinderStrategy} instance. + */ +public final class ComponentFinderStrategyBuilder { + + 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() { + } + + 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 withName(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 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 withTechnology(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 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"); + } + + if (this.componentVisitor != null) { + throw new IllegalArgumentException("A component visitor has already been configured"); + } + + this.componentVisitor = componentVisitor; + + return this; + } + + public ComponentFinderStrategy build() { + if (typeMatcher == null) { + 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 (descriptionStrategy == null) { + descriptionStrategy = new DefaultDescriptionStrategy(); + } + + if (urlStrategy == null) { + urlStrategy = new DefaultUrlStrategy(); + } + + if (componentVisitor == null) { + componentVisitor = new DefaultComponentVisitor(); + } + + return new ComponentFinderStrategy(technology, typeMatcher, typeFilter, supportingTypesStrategy, namingStrategy, descriptionStrategy, urlStrategy, componentVisitor); + } + + @Override + public String toString() { + return "ComponentFinderStrategyBuilder{" + + "technology=" + (technology == null ? null : "'" + technology + "'") + + ", typeMatcher=" + typeMatcher + + ", typeFilter=" + typeFilter + + ", supportingTypesStrategy=" + supportingTypesStrategy + + ", namingStrategy=" + namingStrategy + + ", descriptionStrategy=" + descriptionStrategy + + ", urlStrategy=" + urlStrategy + + ", componentVisitor=" + 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 new file mode 100644 index 000000000..cd77da3c1 --- /dev/null +++ b/structurizr-component/src/main/java/com/structurizr/component/DiscoveredComponent.java @@ -0,0 +1,113 @@ +package com.structurizr.component; + +import java.util.*; + +final class DiscoveredComponent { + + private final Type primaryType; + private final String name; + 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; + + 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; + } + + String getUrl() { + return url; + } + + 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); + } + + 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; + } + + ComponentFinderStrategy getComponentFinderStrategy() { + return componentFinderStrategy; + } + + 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-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..dea55129f --- /dev/null +++ b/structurizr-component/src/main/java/com/structurizr/component/Type.java @@ -0,0 +1,182 @@ +package com.structurizr.component; + +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.*; + +/** + * Represents a Java type (e.g. class or interface) - it's a wrapper around a BCEL JavaClass. + */ +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;"; + + private static final String STRUCTURIZR_PROPERTY_ANNOTATION = "Lcom/structurizr/annotation/Property;"; + private static final String STRUCTURIZR_PROPERTIES_ANNOTATION = "Lcom/structurizr/annotation/Properties;"; + + private JavaClass javaClass = null; + private final String fullyQualifiedName; + private String description; + private String source; + 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; + } + + 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; + } + + public JavaClass getJavaClass() { + return this.javaClass; + } + + void setJavaClass(JavaClass javaClass) { + this.javaClass = javaClass; + } + + public void addDependency(Type type) { + this.dependencies.add(type); + } + + 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 boolean isInterface() { + return javaClass.isInterface(); + } + + 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; + 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; + } + + 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/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..5ee4dcf3f --- /dev/null +++ b/structurizr-component/src/main/java/com/structurizr/component/TypeFinder.java @@ -0,0 +1,36 @@ +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) { + typeRepository.add(type); + } + } + } + } + +} \ 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..33683165e --- /dev/null +++ b/structurizr-component/src/main/java/com/structurizr/component/TypeRepository.java @@ -0,0 +1,35 @@ +package com.structurizr.component; + +import java.util.LinkedHashSet; +import java.util.Set; + +public final class TypeRepository { + + private final Set types = new LinkedHashSet<>(); + + public void add(Type 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() { + return new LinkedHashSet<>(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/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..cf8129d2c --- /dev/null +++ b/structurizr-component/src/main/java/com/structurizr/component/description/DescriptionStrategy.java @@ -0,0 +1,12 @@ +package com.structurizr.component.description; + +import com.structurizr.component.Type; + +/** + * Provides a way customise how component descriptions are generated. + */ +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..bdeccb146 --- /dev/null +++ b/structurizr-component/src/main/java/com/structurizr/component/description/FirstSentenceDescriptionStrategy.java @@ -0,0 +1,32 @@ +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(); + + if (StringUtils.isNullOrEmpty(description)) { + return ""; + } + + int index = description.indexOf('.'); + if (index == -1) { + return description.trim(); + } else { + return description.trim().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/filter/DefaultTypeFilter.java b/structurizr-component/src/main/java/com/structurizr/component/filter/DefaultTypeFilter.java new file mode 100644 index 000000000..64e9370d1 --- /dev/null +++ b/structurizr-component/src/main/java/com/structurizr/component/filter/DefaultTypeFilter.java @@ -0,0 +1,19 @@ +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; + } + + @Override + public String toString() { + return "DefaultTypeFilter{}"; + } + +} \ No newline at end of file diff --git a/structurizr-component/src/main/java/com/structurizr/component/filter/ExcludeAbstractClassesTypeFilter.java b/structurizr-component/src/main/java/com/structurizr/component/filter/ExcludeAbstractClassesTypeFilter.java new file mode 100644 index 000000000..2a2d979c9 --- /dev/null +++ b/structurizr-component/src/main/java/com/structurizr/component/filter/ExcludeAbstractClassesTypeFilter.java @@ -0,0 +1,19 @@ +package com.structurizr.component.filter; + +import com.structurizr.component.Type; + +/** + * A type filter that excludes abstract types. + */ +public class ExcludeAbstractClassesTypeFilter implements TypeFilter { + + public boolean accept(Type type) { + return !type.isAbstractClass(); + } + + @Override + public String toString() { + return "ExcludeAbstractClassTypeFilter{}"; + } + +} \ No newline at end of file diff --git a/structurizr-component/src/main/java/com/structurizr/component/filter/ExcludeFullyQualifiedNameRegexFilter.java b/structurizr-component/src/main/java/com/structurizr/component/filter/ExcludeFullyQualifiedNameRegexFilter.java new file mode 100644 index 000000000..c1ddaa9df --- /dev/null +++ b/structurizr-component/src/main/java/com/structurizr/component/filter/ExcludeFullyQualifiedNameRegexFilter.java @@ -0,0 +1,33 @@ +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. + */ +public class ExcludeFullyQualifiedNameRegexFilter implements TypeFilter { + + private final String regex; + + public ExcludeFullyQualifiedNameRegexFilter(String regex) { + if (StringUtils.isNullOrEmpty(regex)) { + throw new IllegalArgumentException("A regex must be supplied"); + } + + this.regex = regex; + } + + @Override + public boolean accept(Type type) { + return !type.getFullyQualifiedName().matches(regex); + } + + @Override + public String toString() { + return "ExcludeFullyQualifiedNameRegexFilter{" + + "regex='" + regex + '\'' + + '}'; + } + +} \ No newline at end of file diff --git a/structurizr-component/src/main/java/com/structurizr/component/filter/IncludeFullyQualifiedNameRegexFilter.java b/structurizr-component/src/main/java/com/structurizr/component/filter/IncludeFullyQualifiedNameRegexFilter.java new file mode 100644 index 000000000..dcf3a9597 --- /dev/null +++ b/structurizr-component/src/main/java/com/structurizr/component/filter/IncludeFullyQualifiedNameRegexFilter.java @@ -0,0 +1,33 @@ +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. + */ +public class IncludeFullyQualifiedNameRegexFilter implements TypeFilter { + + private final String regex; + + public IncludeFullyQualifiedNameRegexFilter(String regex) { + if (StringUtils.isNullOrEmpty(regex)) { + throw new IllegalArgumentException("A regex must be supplied"); + } + + this.regex = regex; + } + + @Override + public boolean accept(Type type) { + return type.getFullyQualifiedName().matches(regex); + } + + @Override + public String toString() { + return "IncludeFullyQualifiedNameRegexFilter{" + + "regex='" + regex + '\'' + + '}'; + } + +} \ 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/AnnotationTypeMatcher.java b/structurizr-component/src/main/java/com/structurizr/component/matcher/AnnotationTypeMatcher.java new file mode 100644 index 000000000..e418ce84c --- /dev/null +++ b/structurizr-component/src/main/java/com/structurizr/component/matcher/AnnotationTypeMatcher.java @@ -0,0 +1,59 @@ +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 implements TypeMatcher { + + private final String annotationType; + + public AnnotationTypeMatcher(String annotationType) { + if (StringUtils.isNullOrEmpty(annotationType)) { + throw new IllegalArgumentException("An annotation type must be supplied"); + } + + this.annotationType = "L" + annotationType.replace(".", "/") + ";"; + } + + public AnnotationTypeMatcher(Class annotation) { + if (annotation == null) { + throw new IllegalArgumentException("An annotation must be supplied"); + } + + this.annotationType = "L" + annotation.getCanonicalName().replace(".", "/") + ";"; + } + + @Override + public boolean matches(Type type) { + if (type == null) { + throw new IllegalArgumentException("A type must be specified"); + } + + if (type.getJavaClass() == null) { + return false; + } + + AnnotationEntry[] annotationEntries = type.getJavaClass().getAnnotationEntries(); + for (AnnotationEntry annotationEntry : annotationEntries) { + if (annotationType.equals(annotationEntry.getAnnotationType())) { + return true; + } + } + + 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 new file mode 100644 index 000000000..81bad21fe --- /dev/null +++ b/structurizr-component/src/main/java/com/structurizr/component/matcher/ExtendsTypeMatcher.java @@ -0,0 +1,58 @@ +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; + +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 implements TypeMatcher { + + private static final Log log = LogFactory.getLog(ExtendsTypeMatcher.class); + + private final String className; + + public ExtendsTypeMatcher(String className) { + if (StringUtils.isNullOrEmpty(className)) { + throw new IllegalArgumentException("A fully qualified class name must be supplied"); + } + + this.className = className; + } + + @Override + public boolean matches(Type type) { + if (type == null) { + throw new IllegalArgumentException("A type must be specified"); + } + + if (type.getJavaClass() == null) { + return false; + } + + 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; + } + + @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 new file mode 100644 index 000000000..4c6c24aaa --- /dev/null +++ b/structurizr-component/src/main/java/com/structurizr/component/matcher/ImplementsTypeMatcher.java @@ -0,0 +1,50 @@ +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; + +import java.util.Set; + +/** + * Matches types where the type implements the specified interface. + */ +public class ImplementsTypeMatcher implements TypeMatcher { + + private static final Log log = LogFactory.getLog(ImplementsTypeMatcher.class); + + private final String interfaceName; + + public ImplementsTypeMatcher(String interfaceName) { + if (StringUtils.isNullOrEmpty(interfaceName)) { + throw new IllegalArgumentException("A fully qualified interface name must be supplied"); + } + + this.interfaceName = interfaceName; + } + + @Override + public boolean matches(Type type) { + if (type == null) { + throw new IllegalArgumentException("A type must be specified"); + } + + if (type.getJavaClass() == null) { + return false; + } + + JavaClass javaClass = type.getJavaClass(); + Set interfaceNames = Set.of(javaClass.getInterfaceNames()); + 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 new file mode 100644 index 000000000..9f89ca07c --- /dev/null +++ b/structurizr-component/src/main/java/com/structurizr/component/matcher/NameSuffixTypeMatcher.java @@ -0,0 +1,37 @@ +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. + */ +public class NameSuffixTypeMatcher implements TypeMatcher { + + private final String suffix; + + public NameSuffixTypeMatcher(String suffix) { + if (StringUtils.isNullOrEmpty(suffix)) { + throw new IllegalArgumentException("A suffix must be supplied"); + } + + this.suffix = suffix; + } + + @Override + public boolean matches(Type type) { + if (type == null) { + throw new IllegalArgumentException("A type must be specified"); + } + + 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 new file mode 100644 index 000000000..12bc4a586 --- /dev/null +++ b/structurizr-component/src/main/java/com/structurizr/component/matcher/RegexTypeMatcher.java @@ -0,0 +1,39 @@ +package com.structurizr.component.matcher; + +import com.structurizr.component.Type; +import com.structurizr.util.StringUtils; + +import java.util.regex.Pattern; + +/** + * Matches types using a regex against the fully qualified type name. + */ +public class RegexTypeMatcher implements TypeMatcher { + + private final Pattern regex; + + public RegexTypeMatcher(String regex) { + if (StringUtils.isNullOrEmpty(regex)) { + throw new IllegalArgumentException("A regex must be supplied"); + } + + this.regex = Pattern.compile(regex); + } + + @Override + public boolean matches(Type type) { + if (type == null) { + throw new IllegalArgumentException("A type must be specified"); + } + + return Pattern.matches(regex.pattern(), type.getFullyQualifiedName()); + } + + @Override + public String toString() { + return "RegexTypeMatcher{" + + "regex='" + regex + "'" + + '}'; + } + +} 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..4293c937a --- /dev/null +++ b/structurizr-component/src/main/java/com/structurizr/component/matcher/TypeMatcher.java @@ -0,0 +1,12 @@ +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); + +} \ 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..61cc11b64 --- /dev/null +++ b/structurizr-component/src/main/java/com/structurizr/component/naming/DefaultNamingStrategy.java @@ -0,0 +1,22 @@ +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 DefaultNamingStrategy implements NamingStrategy { + + @Override + public String nameOf(Type type) { + String[] parts = org.apache.commons.lang3.StringUtils.splitByCharacterTypeCamelCase(type.getName()); + 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/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/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..1520d7313 --- /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 customise how component names are generated. + */ +public interface NamingStrategy { + + String nameOf(Type type); + +} \ No newline at end of file diff --git a/structurizr-component/src/main/java/com/structurizr/component/naming/TypeNamingStrategy.java b/structurizr-component/src/main/java/com/structurizr/component/naming/TypeNamingStrategy.java new file mode 100644 index 000000000..47ffe3b90 --- /dev/null +++ b/structurizr-component/src/main/java/com/structurizr/component/naming/TypeNamingStrategy.java @@ -0,0 +1,19 @@ +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 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/ClassDirectoryTypeProvider.java b/structurizr-component/src/main/java/com/structurizr/component/provider/ClassDirectoryTypeProvider.java new file mode 100644 index 000000000..a25606763 --- /dev/null +++ b/structurizr-component/src/main/java/com/structurizr/component/provider/ClassDirectoryTypeProvider.java @@ -0,0 +1,82 @@ +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.LinkedHashSet; +import java.util.Set; + +/** + * A type repository that uses Apache Commons BCEL to load Java classes from a local directory. + */ +public final class ClassDirectoryTypeProvider implements TypeProvider { + + private static final Log log = LogFactory.getLog(ClassDirectoryTypeProvider.class); + private static final String CLASS_FILE_EXTENSION = ".class"; + + 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 LinkedHashSet<>(); + + 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 LinkedHashSet<>(); + 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; + } + + @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 new file mode 100644 index 000000000..908f2c34b --- /dev/null +++ b/structurizr-component/src/main/java/com/structurizr/component/provider/ClassJarFileTypeProvider.java @@ -0,0 +1,68 @@ +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.LinkedHashSet; +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 ClassJarFileTypeProvider implements TypeProvider { + + private static final Log log = LogFactory.getLog(ClassJarFileTypeProvider.class); + private static final String CLASS_FILE_EXTENSION = ".class"; + + private final File jarFile; + + public ClassJarFileTypeProvider(File file) { + this.jarFile = file; + } + + public Set getTypes() { + Set types = new LinkedHashSet<>(); + 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; + } + + @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/JavadocCommentFilter.java b/structurizr-component/src/main/java/com/structurizr/component/provider/JavadocCommentFilter.java new file mode 100644 index 000000000..51b02900b --- /dev/null +++ b/structurizr-component/src/main/java/com/structurizr/component/provider/JavadocCommentFilter.java @@ -0,0 +1,21 @@ +package com.structurizr.component.provider; + +/** + * Cleans up Javadoc comments for inclusion in the software architecture model. + */ +class JavadocCommentFilter { + + String filter(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"); + + 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 new file mode 100644 index 000000000..94a0a986c --- /dev/null +++ b/structurizr-component/src/main/java/com/structurizr/component/provider/SourceDirectoryTypeProvider.java @@ -0,0 +1,138 @@ +package com.structurizr.component.provider; + +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; +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.LinkedHashSet; +import java.util.Set; + +/** + * A type provider that uses JavaParser to read Javadoc comments from source code. + */ +public final class SourceDirectoryTypeProvider implements TypeProvider { + + private static final Log log = LogFactory.getLog(SourceDirectoryTypeProvider.class); + private static final String JAVA_FILE_EXTENSION = ".java"; + + private final File directory; + private final Set types = new LinkedHashSet<>(); + + public SourceDirectoryTypeProvider(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; + StaticJavaParser.getParserConfiguration().setLanguageLevel(ParserConfiguration.LanguageLevel.JAVA_21); + } + + @Override + public Set getTypes() { + parse(directory); + + return new LinkedHashSet<>(types); + } + + private void parse(File path) { + if (path.isDirectory()) { + File[] files = path.listFiles(); + if (files != null) { + for (File file : files) { + try { + parse(file); + } 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(relativePath(path)); + + 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().filter(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(relativePath(path)); + + 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().filter(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()); + } + } + } + + 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; + } + + @Override + public String toString() { + return "SourceDirectoryTypeProvider{" + + "directory=" + directory + + '}'; + } + +} \ 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/AllReferencedTypesInPackageSupportingTypesStrategy.java b/structurizr-component/src/main/java/com/structurizr/component/supporting/AllReferencedTypesInPackageSupportingTypesStrategy.java new file mode 100644 index 000000000..2e34875e2 --- /dev/null +++ b/structurizr-component/src/main/java/com/structurizr/component/supporting/AllReferencedTypesInPackageSupportingTypesStrategy.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 AllReferencedTypesInPackageSupportingTypesStrategy implements SupportingTypesStrategy { + + @Override + public Set findSupportingTypes(Type type, TypeRepository typeRepository) { + 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..e2b4724ce --- /dev/null +++ b/structurizr-component/src/main/java/com/structurizr/component/supporting/AllReferencedTypesSupportingTypesStrategy.java @@ -0,0 +1,18 @@ +package com.structurizr.component.supporting; + +import com.structurizr.component.Type; +import com.structurizr.component.TypeRepository; + +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, TypeRepository typeRepository) { + return type.getDependencies(); + } + +} \ No newline at end of file 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 new file mode 100644 index 000000000..134d2cbae --- /dev/null +++ b/structurizr-component/src/main/java/com/structurizr/component/supporting/DefaultSupportingTypesStrategy.java @@ -0,0 +1,24 @@ +package com.structurizr.component.supporting; + +import com.structurizr.component.Type; +import com.structurizr.component.TypeRepository; + +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, 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/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/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..e3d6f21a3 --- /dev/null +++ b/structurizr-component/src/main/java/com/structurizr/component/supporting/SupportingTypesStrategy.java @@ -0,0 +1,15 @@ +package com.structurizr.component.supporting; + +import com.structurizr.component.Type; +import com.structurizr.component.TypeRepository; + +import java.util.Set; + +/** + * Provides a strategy to identify the types that support a component. + */ +public interface SupportingTypesStrategy { + + Set findSupportingTypes(Type type, TypeRepository typeRepository); + +} \ 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..2e6fa6d1e --- /dev/null +++ b/structurizr-component/src/main/java/com/structurizr/component/url/DefaultUrlStrategy.java @@ -0,0 +1,20 @@ +package com.structurizr.component.url; + +import com.structurizr.component.Type; + +/** + * Generates a null URL. + */ +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/PrefixSourceUrlStrategy.java b/structurizr-component/src/main/java/com/structurizr/component/url/PrefixSourceUrlStrategy.java new file mode 100644 index 000000000..5aa951cd5 --- /dev/null +++ b/structurizr-component/src/main/java/com/structurizr/component/url/PrefixSourceUrlStrategy.java @@ -0,0 +1,41 @@ +package com.structurizr.component.url; + +import com.structurizr.component.Type; +import com.structurizr.util.StringUtils; + +/** + * Adds a given prefix to the component source location. + */ +public class PrefixSourceUrlStrategy implements UrlStrategy { + + private final String prefix; + + public PrefixSourceUrlStrategy(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) { + if (type.getSource() != null) { + return prefix + (type.getSource().replaceAll("\\\\", "/")); + } else { + return null; + } + } + + @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..e066b3c1f --- /dev/null +++ b/structurizr-component/src/main/java/com/structurizr/component/url/UrlStrategy.java @@ -0,0 +1,12 @@ +package com.structurizr.component.url; + +import com.structurizr.component.Type; + +/** + * Provides a way customise how component URLs are generated. + */ +public interface UrlStrategy { + + String urlOf(Type type); + +} \ 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..054d7e9c9 --- /dev/null +++ b/structurizr-component/src/main/java/com/structurizr/component/visitor/DefaultComponentVisitor.java @@ -0,0 +1,19 @@ +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) { + } + + @Override + public String toString() { + return "DefaultComponentVisitor{}"; + } + +} \ No newline at end of file diff --git a/structurizr-core/test/unit/com/structurizr/AbstractWorkspaceTestBase.java b/structurizr-component/src/test/java/com/structurizr/AbstractWorkspaceTestBase.java similarity index 100% rename from structurizr-core/test/unit/com/structurizr/AbstractWorkspaceTestBase.java rename to structurizr-component/src/test/java/com/structurizr/AbstractWorkspaceTestBase.java 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..b7937f09b --- /dev/null +++ b/structurizr-component/src/test/java/com/structurizr/component/ComponentFinderStrategyBuilderTests.java @@ -0,0 +1,171 @@ +package com.structurizr.component; + +import com.structurizr.component.description.FirstSentenceDescriptionStrategy; +import com.structurizr.component.description.TruncatedDescriptionStrategy; +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; +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.*; + +public class ComponentFinderStrategyBuilderTests { + + @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 IncludeFullyQualifiedNameRegexFilter(".*")).filteredBy(new ExcludeFullyQualifiedNameRegexFilter(".*")); + 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 withName_ThrowsAnException_WhenPassedNull() { + try { + new ComponentFinderStrategyBuilder().withName(null); + fail(); + } catch (Exception e) { + assertEquals("A naming strategy must be provided", e.getMessage()); + } + } + + @Test + void withName_ThrowsAnException_WhenCalledTwice() { + try { + new ComponentFinderStrategyBuilder().withName(new TypeNamingStrategy()).withName(new FullyQualifiedNamingStrategy()); + fail(); + } catch (Exception e) { + assertEquals("A naming strategy has already been configured", e.getMessage()); + } + } + + @Test + 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 withTechnology_ThrowsAnException_WhenPassedNull() { + try { + new ComponentFinderStrategyBuilder().withTechnology(null); + fail(); + } catch (Exception e) { + assertEquals("A technology must be provided", e.getMessage()); + } + } + + @Test + void withTechnology_ThrowsAnException_WhenPassedAnEmptyString() { + try { + new ComponentFinderStrategyBuilder().withTechnology(""); + fail(); + } catch (Exception e) { + assertEquals("A technology must be provided", e.getMessage()); + } + } + + @Test + void withTechnology_ThrowsAnException_WhenCalledTwice() { + try { + new ComponentFinderStrategyBuilder().withTechnology("X").withTechnology("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() + .withTechnology("Spring MVC Controller") + .matchedBy(new NameSuffixTypeMatcher("Controller")) + .filteredBy(new IncludeFullyQualifiedNameRegexFilter("com.example.web.\\.*")) + .withName(new TypeNamingStrategy()) + .withDescription(new FirstSentenceDescriptionStrategy()) + .build(); + + 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..6b1689f04 --- /dev/null +++ b/structurizr-component/src/test/java/com/structurizr/component/ComponentFinderTests.java @@ -0,0 +1,69 @@ +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.component.url.PrefixSourceUrlStrategy; +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) + .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") + .matchedBy(new ImplementsTypeMatcher("com.structurizr.component.example.Controller")) + .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() + .withTechnology("Data Repository") + .matchedBy(new ImplementsTypeMatcher("com.structurizr.component.example.Repository")) + .filteredBy(new IncludeFullyQualifiedNameRegexFilter("com\\.structurizr\\.component\\.example\\..*")) + .withName(new TypeNamingStrategy()) + .withDescription(new FirstSentenceDescriptionStrategy()) + .withUrl(new PrefixSourceUrlStrategy("https://example.com/src/main/java")) + .build() + ) + .build(); + + componentFinder.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)); + } + +} 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..4df29513a --- /dev/null +++ b/structurizr-component/src/test/java/com/structurizr/component/SpringPetClinicTests.java @@ -0,0 +1,132 @@ +package com.structurizr.component; + +import com.structurizr.Workspace; +import com.structurizr.component.description.FirstSentenceDescriptionStrategy; +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.matcher.NameSuffixTypeMatcher; +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.4.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 ExcludeFullyQualifiedNameRegexFilter(".*.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 NameSuffixTypeMatcher("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.run(); + 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..."); + } + } + +} 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 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..1df5874d5 --- /dev/null +++ b/structurizr-component/src/test/java/com/structurizr/component/TypeTests.java @@ -0,0 +1,78 @@ +package com.structurizr.component; + +import com.structurizr.component.matcher.ImplementsTypeMatcher; +import org.apache.bcel.classfile.ClassParser; +import org.junit.jupiter.api.Test; + +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 { + + 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()); + } + + @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 new file mode 100644 index 000000000..4039cf8f8 --- /dev/null +++ b/structurizr-component/src/test/java/com/structurizr/component/description/FirstSentenceDescriptionStrategyTests.java @@ -0,0 +1,39 @@ +package com.structurizr.component.description; + +import com.structurizr.component.Type; +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"); + 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/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/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/ExcludeFullyQualifiedNameRegexFilterTests.java b/structurizr-component/src/test/java/com/structurizr/component/filter/ExcludeFullyQualifiedNameRegexFilterTests.java new file mode 100644 index 000000000..c7d156e8b --- /dev/null +++ b/structurizr-component/src/test/java/com/structurizr/component/filter/ExcludeFullyQualifiedNameRegexFilterTests.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 ExcludeFullyQualifiedNameRegexFilterTests { + + @Test + void construction_ThrowsAnException_WhenPassedANullSuffix() { + assertThrowsExactly(IllegalArgumentException.class, () -> new ExcludeFullyQualifiedNameRegexFilter(null)); + } + + @Test + void construction_ThrowsAnException_WhenPassedAnEmptySuffix() { + assertThrowsExactly(IllegalArgumentException.class, () -> new ExcludeFullyQualifiedNameRegexFilter("")); + assertThrowsExactly(IllegalArgumentException.class, () -> new ExcludeFullyQualifiedNameRegexFilter(" ")); + } + + + @Test + void filter_ReturnsTrue_WhenTheTypeDoesNotMatchRegex() { + assertTrue(new ExcludeFullyQualifiedNameRegexFilter(".*Utils").accept(new Type("com.example.CustomerComponent"))); + } + + @Test + void filter_ReturnsFalse_WhenTheTypeMatchesRegex() { + 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/IncludeFullyQualifiedNameRegexFilterTests.java b/structurizr-component/src/test/java/com/structurizr/component/filter/IncludeFullyQualifiedNameRegexFilterTests.java new file mode 100644 index 000000000..dfa1ed908 --- /dev/null +++ b/structurizr-component/src/test/java/com/structurizr/component/filter/IncludeFullyQualifiedNameRegexFilterTests.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 IncludeFullyQualifiedNameRegexFilterTests { + + @Test + void construction_ThrowsAnException_WhenPassedANullSuffix() { + assertThrowsExactly(IllegalArgumentException.class, () -> new IncludeFullyQualifiedNameRegexFilter(null)); + } + + @Test + void construction_ThrowsAnException_WhenPassedAnEmptySuffix() { + assertThrowsExactly(IllegalArgumentException.class, () -> new IncludeFullyQualifiedNameRegexFilter("")); + assertThrowsExactly(IllegalArgumentException.class, () -> new IncludeFullyQualifiedNameRegexFilter(" ")); + } + + + @Test + void filter_ReturnsFalse_WhenTheTypeDoesNotMatchRegex() { + assertFalse(new IncludeFullyQualifiedNameRegexFilter(".*Component").accept(new Type("com.example.DateUtils"))); + } + + @Test + void filter_ReturnsTrue_WhenTheTypeMatchesRegex() { + 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/matcher/AnnotationTypeMatcherTests.java b/structurizr-component/src/test/java/com/structurizr/component/matcher/AnnotationTypeMatcherTests.java new file mode 100644 index 000000000..415b00420 --- /dev/null +++ b/structurizr-component/src/test/java/com/structurizr/component/matcher/AnnotationTypeMatcherTests.java @@ -0,0 +1,61 @@ +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; + +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)); + } + + @Test + void construction_ThrowsAnException_WhenPassedAnEmptyName() { + assertThrowsExactly(IllegalArgumentException.class, () -> new AnnotationTypeMatcher("")); + assertThrowsExactly(IllegalArgumentException.class, () -> new AnnotationTypeMatcher(" ")); + } + + @Test + void construction_ThrowsAnException_WhenPassedANullClass() { + assertThrowsExactly(IllegalArgumentException.class, () -> new AnnotationTypeMatcher((Class) null)); + } + + @Test + void matches_ThrowsAnException_WhenPassedNull() { + 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").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").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(Component.class.getName()).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..66481383d --- /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)); + } + + @Test + void construction_ThrowsAnException_WhenPassedAnEmptyName() { + assertThrowsExactly(IllegalArgumentException.class, () -> new ExtendsTypeMatcher("")); + assertThrowsExactly(IllegalArgumentException.class, () -> new ExtendsTypeMatcher(" ")); + } + + @Test + void matches_ThrowsAnException_WhenPassedNull() { + 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").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").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").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..af5acd807 --- /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)); + } + + @Test + void construction_ThrowsAnException_WhenPassedAnEmptyName() { + assertThrowsExactly(IllegalArgumentException.class, () -> new ImplementsTypeMatcher("")); + assertThrowsExactly(IllegalArgumentException.class, () -> new ImplementsTypeMatcher(" ")); + } + + @Test + void matches_ThrowsAnException_WhenPassedNull() { + 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").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").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").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 new file mode 100644 index 000000000..b0055a443 --- /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)); + } + + @Test + void construction_ThrowsAnException_WhenPassedAnEmptySuffix() { + assertThrowsExactly(IllegalArgumentException.class, () -> new NameSuffixTypeMatcher("")); + assertThrowsExactly(IllegalArgumentException.class, () -> new NameSuffixTypeMatcher(" ")); + } + + @Test + void matches_ThrowsAnException_WhenPassedNull() { + assertThrowsExactly(IllegalArgumentException.class, () -> new NameSuffixTypeMatcher("Suffix").matches(null)); + } + + @Test + void matches_ReturnsFalse_WhenThereIsNoMatch() { + assertFalse(new NameSuffixTypeMatcher("Component").matches(new Type("com.example.SomeClass"))); + } + + @Test + void matches_ReturnsTrue_WhenThereIsAMatch() { + 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 new file mode 100644 index 000000000..2c757c025 --- /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)); + } + + @Test + void construction_ThrowsAnException_WhenPassedAnEmptyRegex() { + assertThrowsExactly(IllegalArgumentException.class, () -> new RegexTypeMatcher("")); + assertThrowsExactly(IllegalArgumentException.class, () -> new RegexTypeMatcher(" ")); + } + + @Test + void matches_ThrowsAnException_WhenPassedNull() { + assertThrowsExactly(IllegalArgumentException.class, () -> new RegexTypeMatcher(".*Controller").matches(null)); + } + + @Test + void matches_ReturnsFalse_WhenThereIsNoMatch() { + assertFalse(new RegexTypeMatcher(".*Controller").matches(new Type("com.example.SomeClass"))); + } + + @Test + void matches_ReturnsTrue_WhenThereIsAMatch() { + assertTrue(new RegexTypeMatcher(".*Controller").matches(new Type("com.example.SomeController"))); + } + +} \ No newline at end of file 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..2cd2b65e4 --- /dev/null +++ b/structurizr-component/src/test/java/com/structurizr/component/matcher/annotationTypeMatcher/CustomerController.java @@ -0,0 +1,7 @@ +package com.structurizr.component.matcher.annotationTypeMatcher; + +import com.structurizr.annotation.Component; + +@Component +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 { +} 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/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/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/TypeNamingStrategyTests.java b/structurizr-component/src/test/java/com/structurizr/component/naming/TypeNamingStrategyTests.java new file mode 100644 index 000000000..b86749322 --- /dev/null +++ b/structurizr-component/src/test/java/com/structurizr/component/naming/TypeNamingStrategyTests.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 TypeNamingStrategyTests { + + @Test + void nameOf() { + 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/ClassDirectoryTypeProviderTests.java b/structurizr-component/src/test/java/com/structurizr/component/provider/ClassDirectoryTypeProviderTests.java new file mode 100644 index 000000000..924ecabe9 --- /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(); + + assertFalse(types.isEmpty()); + 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/JavadocCommentFilterTests.java b/structurizr-component/src/test/java/com/structurizr/component/provider/JavadocCommentFilterTests.java new file mode 100644 index 000000000..fa1cc0b39 --- /dev/null +++ b/structurizr-component/src/test/java/com/structurizr/component/provider/JavadocCommentFilterTests.java @@ -0,0 +1,44 @@ +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_filter_ReturnsNull_WhenGivenNull() { + assertNull(new JavadocCommentFilter().filter(null)); + } + + @Test + public void test_filter_ReturnsTheOriginalText_WhenNoMaxLengthHasBeenSpecified() + { + assertEquals("Here is some text.", new JavadocCommentFilter().filter("Here is some text.")); + } + + @Test + public void test_filter_FiltersJavadocLinkTags() + { + 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_filter_FiltersJavadocLinkTagsWithLabels() + { + 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_filter_FiltersHtml() + { + assertEquals("Uses SomeClass and AnotherClass to do some work.", new JavadocCommentFilter().filter("Uses SomeClass and AnotherClass to do some work.")); + } + + @Test + public void test_filter_FiltersLineBreaks() + { + 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-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..564258a54 --- /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 sources = 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(sources, "com/example"))); + } + + @Test + void construction_ThrowsAnException_WhenPassedAFile() { + assertThrowsExactly(IllegalArgumentException.class, () -> new SourceDirectoryTypeProvider(new File(sources, "com/structurizr/component/provider/SourceDirectoryTypeProviderTests.java"))); + } + + @Test + void getTypes() { + TypeProvider typeProvider = new SourceDirectoryTypeProvider(sources); + 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 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 new file mode 100644 index 000000000..5562ed190 --- /dev/null +++ b/structurizr-component/src/test/java/com/structurizr/component/supporting/AllReferencedTypesInPackageSupportingTypesStrategyTests.java @@ -0,0 +1,27 @@ +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 AllReferencedTypesInPackageSupportingTypesStrategyTests { + + @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); + + Set supportingTypes = new AllReferencedTypesInPackageSupportingTypesStrategy().findSupportingTypes(type, null); + 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..6f3122bcc --- /dev/null +++ b/structurizr-component/src/test/java/com/structurizr/component/supporting/AllReferencedTypesSupportingTypesStrategyTests.java @@ -0,0 +1,28 @@ +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); + + Set supportingTypes = new AllReferencedTypesSupportingTypesStrategy().findSupportingTypes(type, null); + 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/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 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..ff83615a2 --- /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, null); + assertTrue(supportingTypes.isEmpty()); + } + +} \ 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-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-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 diff --git a/structurizr-core/README.md b/structurizr-core/README.md new file mode 100644 index 000000000..2a1d0b062 --- /dev/null +++ b/structurizr-core/README.md @@ -0,0 +1,8 @@ +# 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) + +This library provides the core functionality of Structurizr related to creating workspaces. + +- [Documentation](https://docs.structurizr.com/java) + diff --git a/structurizr-core/build.gradle b/structurizr-core/build.gradle index b41bf56b2..2ac80b7d1 100644 --- a/structurizr-core/build.gradle +++ b/structurizr-core/build.gradle @@ -1,37 +1,9 @@ dependencies { - compile project(':structurizr-annotations') + 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' - compile 'com.fasterxml.jackson.core:jackson-annotations:2.4.0' - compile 'com.fasterxml.jackson.core:jackson-databind:2.4.0' + testImplementation 'org.assertj:assertj-core:3.27.3' - compile 'org.reflections:reflections:0.9.10' - compile 'org.javassist:javassist:3.22.0-CR2' - - compile 'commons-logging:commons-logging:1.1.3' - - compile 'org.apache.httpcomponents:httpclient:4.3.6' - compile 'org.apache.httpcomponents:httpcore:4.3.3' - - compile 'javax.xml.bind:jaxb-api:2.3.0' - - compile files("${System.getProperty('java.home')}/../lib/tools.jar") - - testCompile 'junit:junit:4.12' - testCompile 'org.assertj:assertj-core:3.4.0' - -} - -sourceSets { - main { - java { - srcDir 'src' - } - } - test { - java { - srcDir 'test/unit' - srcDir 'test/integration' - } - } } \ No newline at end of file diff --git a/structurizr-core/src/com/structurizr/AbstractWorkspace.java b/structurizr-core/src/com/structurizr/AbstractWorkspace.java deleted file mode 100644 index 76c5dcce6..000000000 --- a/structurizr-core/src/com/structurizr/AbstractWorkspace.java +++ /dev/null @@ -1,153 +0,0 @@ -package com.structurizr; - -import java.net.MalformedURLException; -import java.net.URL; - -/** - * The superclass for regular and encrypted workspaces. - */ -public abstract class AbstractWorkspace { - - private long id; - private String name; - private String description; - private String version; - private String thumbnail; - private String source; - private String api; - - protected AbstractWorkspace() { - } - - AbstractWorkspace(String name, String description) { - this.name = name; - this.description = description; - } - - /** - * Gets the ID of this workspace. - * - * @return the ID (a positive integer) - */ - public long getId() { - return this.id; - } - - /** - * Sets the ID of this workspace. - * - * @param id the ID (a positive integer) - */ - public void setId(long id) { - this.id = id; - } - - /** - * Gets the name of this workspace. - * - * @return the name, as a String - */ - public String getName() { - return name; - } - - /** - * Sets the name of this workspace. - * - * @param name the name, as a String - */ - public void setName(String name) { - this.name = name; - } - - /** - * Gets the description of this workspace. - * - * @return the description, as a String - */ - public String getDescription() { - return description; - } - - /** - * Sets the description of this workspace. - * - * @param description the description, as a String - */ - public void setDescription(String description) { - this.description = description; - } - - /** - * Gets the version of this workspace. - * - * @return the version, as a String - */ - public String getVersion() { - return version; - } - - /** - * Sets the version of this workspace. - * - * @param version the version, as a String (e.g. 1.0.1, a git hash, etc). - */ - public void setVersion(String version) { - this.version = version; - } - - /** - * Gets the thumbnail associated with this workspace. - * - * @return a Base64 encoded PNG file as a Data URI (data:image/png;base64) - * or null if there is no thumbnail - */ - public String getThumbnail() { - return thumbnail; - } - - /** - * Sets the thumbnail associated with this workspace. - * - * @param thumbnail a Base64 encoded PNG file as a Data URI (data:image/png;base64) - */ - public void setThumbnail(String thumbnail) { - this.thumbnail = thumbnail; - } - - /** - * Gets the URL of the API where the content of this workspace can be found. - * - * @return the URL, as a String - */ - public String getApi() { - return api; - } - - /** - * Sets the URL of the API where the content of this workspace can be found. - * - * @param api a URL, as a String - * @throws IllegalArgumentException if the URL is not a valid URL - */ - public void setApi(String api) { - if (api != null && api.trim().length() > 0) { - try { - URL url = new URL(api); - this.api = api; - } catch (MalformedURLException murle) { - throw new IllegalArgumentException(api + " is not a valid URL."); - } - } - } - - /** - * Determines whether this workspace has an API set. - * - * @return true if an API URL has been specified, false otherwise - */ - public boolean hasApi() { - return this.api != null && this.api.trim().length() > 0; - } - -} diff --git a/structurizr-core/src/com/structurizr/Workspace.java b/structurizr-core/src/com/structurizr/Workspace.java deleted file mode 100644 index 0cb316b0c..000000000 --- a/structurizr-core/src/com/structurizr/Workspace.java +++ /dev/null @@ -1,177 +0,0 @@ -package com.structurizr; - -import com.fasterxml.jackson.annotation.JsonIgnore; -import com.structurizr.documentation.Documentation; -import com.structurizr.model.*; -import com.structurizr.view.ViewSet; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; - -import java.util.LinkedList; -import java.util.List; - -/** - * Represents a Structurizr workspace, which is a wrapper for a - * software architecture model, views and documentation. - */ -public final class Workspace extends AbstractWorkspace { - - private static final Log log = LogFactory.getLog(Workspace.class); - - private Model model = new Model(); - private ViewSet viewSet = new ViewSet(model); - private Documentation documentation = new Documentation(model); - - Workspace() { - } - - /** - * Creates a new workspace. - * - * @param name the name of the workspace - * @param description a short description - */ - public Workspace(String name, String description) { - super(name, description); - } - - /** - * Gets the software architecture model. - * - * @return a Model instance - */ - public Model getModel() { - return model; - } - - void setModel(Model model) { - this.model = model; - } - - /** - * Gets the set of views onto a software architecture model. - * - * @return a ViewSet instance - */ - public ViewSet getViews() { - return viewSet; - } - - void setViews(ViewSet viewSet) { - this.viewSet = viewSet; - } - - /** - * Called when deserialising JSON, to re-create the object graph - * based upon element/relationship IDs. - */ - public void hydrate() { - this.viewSet.setModel(model); - this.documentation.setModel(model); - - this.model.hydrate(); - this.viewSet.hydrate(); - this.documentation.hydrate(); - } - - /** - * Gets the documentation associated with this workspace. - * - * @return a Documentation object - */ - public Documentation getDocumentation() { - return documentation; - } - - /** - * Sets the documentation associated with this workspace. - * - * @param documentation a Documentation object - */ - public void setDocumentation(Documentation documentation) { - this.documentation = documentation; - documentation.setModel(getModel()); - } - - @JsonIgnore - 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 ContainerInstance)) - .filter(e -> e.getDescription() == null || e.getDescription().trim().length() == 0) - .forEach(e -> warnings.add("The " + typeof(e) + " \"" + e.getCanonicalName().substring(1) + "\" 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("The container \"" + c.getCanonicalName().substring(1) + "\" 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("The component \"" + c.getCanonicalName().substring(1) + "\" 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 " + typeof(relationship.getSource()) + " \"" + relationship.getSource().getCanonicalName().substring(1) + "\" and " + typeof(relationship.getDestination()) + " \"" + relationship.getDestination().getCanonicalName().substring(1) + "\" 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().getEnterpriseContextViews().stream() - .filter(v -> v.getKey() == null) - .forEach(v -> warnings.add("Enterprise Context 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(); - } - - private String typeof(Element element) { - if (element instanceof SoftwareSystem) { - return "software system"; - } else { - return element.getClass().getSimpleName().toLowerCase(); - } - } - -} diff --git a/structurizr-core/src/com/structurizr/analysis/AbstractComponentFinderStrategy.java b/structurizr-core/src/com/structurizr/analysis/AbstractComponentFinderStrategy.java deleted file mode 100644 index 5d3f00a9e..000000000 --- a/structurizr-core/src/com/structurizr/analysis/AbstractComponentFinderStrategy.java +++ /dev/null @@ -1,182 +0,0 @@ -package com.structurizr.analysis; - -import com.structurizr.model.CodeElement; -import com.structurizr.model.Component; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; - -import java.lang.annotation.Annotation; -import java.lang.reflect.Modifier; -import java.util.*; - -/** - * This is the superclass for a number of component finder strategies. - */ -public abstract class AbstractComponentFinderStrategy implements ComponentFinderStrategy { - - private static final Log log = LogFactory.getLog(AbstractComponentFinderStrategy.class); - - private TypeRepository typeRepository; - - private Set componentsFound = new HashSet<>(); - - protected ComponentFinder componentFinder; - - protected List supportingTypesStrategies = new ArrayList<>(); - - protected AbstractComponentFinderStrategy(SupportingTypesStrategy... strategies) { - Arrays.stream(strategies).forEach(this::addSupportingTypesStrategy); - } - - protected ComponentFinder getComponentFinder() { - return componentFinder; - } - - /** - * Sets a reference to the parent component finder. - * - * @param componentFinder a ComponentFinder instance - */ - public void setComponentFinder(ComponentFinder componentFinder) { - this.componentFinder = componentFinder; - } - - protected TypeRepository getTypeRepository() { - return typeRepository; - } - - @Override - public void beforeFindComponents() { - typeRepository = new DefaultTypeRepository(componentFinder.getPackageName(), componentFinder.getExclusions()); - supportingTypesStrategies.forEach(sts -> sts.setTypeRepository(typeRepository)); - } - - @Override - public Set findComponents() { - componentsFound.addAll(doFindComponents()); - - return componentsFound; - } - - /** - * A template method into which subclasses can put their component finding code. - * - * @return the Set of Components found, or an empty set if no components were found - */ - protected abstract Set doFindComponents(); - - @Override - public void afterFindComponents() { - findSupportingTypes(componentsFound); - findDependencies(); - } - - private void findSupportingTypes(Set components) { - for (Component component : components) { - for (CodeElement codeElement : component.getCode()) { - TypeVisibility visibility = TypeUtils.getVisibility(codeElement.getType()); - if (visibility != null) { - codeElement.setVisibility(visibility.getName()); - } - - TypeCategory category = TypeUtils.getCategory(codeElement.getType()); - if (category != null) { - codeElement.setCategory(category.getName()); - } - } - - for (SupportingTypesStrategy strategy : supportingTypesStrategies) { - for (String type : strategy.findSupportingTypes(component)) { - if (!isNestedClass(type) && componentFinder.getContainer().getComponentOfType(type) == null) { - CodeElement codeElement = component.addSupportingType(type); - - TypeVisibility visibility = TypeUtils.getVisibility(codeElement.getType()); - if (visibility != null) { - codeElement.setVisibility(visibility.getName()); - } - - TypeCategory category = TypeUtils.getCategory(codeElement.getType()); - if (category != null) { - codeElement.setCategory(category.getName()); - } - } - } - } - } - } - - private boolean isNestedClass(String type) { - return type != null && type.indexOf('$') > -1; - } - - private void findDependencies() { - for (Component component : componentFinder.getContainer().getComponents()) { - if (component.getType() != null) { - addEfferentDependencies(component, component.getType(), new HashSet<>()); - - // and repeat for the supporting types - for (CodeElement codeElement : component.getCode()) { - addEfferentDependencies(component, codeElement.getType(), new HashSet<>()); - } - } - } - } - - private void addEfferentDependencies(Component component, String type, Set typesVisited) { - typesVisited.add(type); - - for (Class referencedType : getTypeRepository().findReferencedTypes(type)) { - try { - String referencedTypeName = referencedType.getCanonicalName(); - Component destinationComponent = componentFinder.getContainer().getComponentOfType(referencedTypeName); - if (destinationComponent != null) { - if (component != destinationComponent) { - component.uses(destinationComponent, ""); - } - } else if (!typesVisited.contains(referencedTypeName)) { - addEfferentDependencies(component, referencedTypeName, typesVisited); - } - } catch (Throwable t) { - log.warn(t); - } - } - } - - /** - * Adds a supporting type strategy to this component finder strategy. - * - * @param supportingTypesStrategy a SupportingTypesStrategy instance - */ - public void addSupportingTypesStrategy(SupportingTypesStrategy supportingTypesStrategy) { - if (supportingTypesStrategy == null) { - throw new IllegalArgumentException("A supporting types strategy must be provided."); - } - - supportingTypesStrategies.add(supportingTypesStrategy); - } - - protected Set> findTypesAnnotatedWith(Class annotation) { - return TypeUtils.findTypesAnnotatedWith(annotation, getTypeRepository().getAllTypes()); - } - - protected Set findClassesWithAnnotation(Class type, String technology) { - return findClassesWithAnnotation(type, technology, false); - } - - protected Set findClassesWithAnnotation(Class type, String technology, boolean includePublicTypesOnly) { - Set components = new HashSet<>(); - Set> componentTypes = findTypesAnnotatedWith(type); - for (Class componentType : componentTypes) { - if (!includePublicTypesOnly || Modifier.isPublic(componentType.getModifiers())) { - components.add(getComponentFinder().getContainer().addComponent( - componentType.getSimpleName(), - componentType.getCanonicalName(), - "", - technology)); - } - } - - return components; - } - -} \ No newline at end of file diff --git a/structurizr-core/src/com/structurizr/analysis/AbstractTypeMatcher.java b/structurizr-core/src/com/structurizr/analysis/AbstractTypeMatcher.java deleted file mode 100644 index f2f800980..000000000 --- a/structurizr-core/src/com/structurizr/analysis/AbstractTypeMatcher.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.structurizr.analysis; - -/** - * A superclass used for TypeMatcher implementations. - */ -public abstract class AbstractTypeMatcher implements TypeMatcher { - - private String description; - private String technology; - - public AbstractTypeMatcher(String description, String technology) { - this.description = description; - this.technology = technology; - } - - @Override - public String getDescription() { - return description; - } - - @Override - public String getTechnology() { - return technology; - } - -} \ No newline at end of file diff --git a/structurizr-core/src/com/structurizr/analysis/AnnotationTypeMatcher.java b/structurizr-core/src/com/structurizr/analysis/AnnotationTypeMatcher.java deleted file mode 100644 index 059b5c13d..000000000 --- a/structurizr-core/src/com/structurizr/analysis/AnnotationTypeMatcher.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.structurizr.analysis; - -import java.lang.annotation.Annotation; - -/** - * Matches types based upon the presence of a type-level annotation. - */ -public class AnnotationTypeMatcher extends AbstractTypeMatcher { - - private Class annotation; - - public AnnotationTypeMatcher(Class annotation, String description, String technology) { - super(description, technology); - - if (annotation == null) { - throw new IllegalArgumentException("An annotation must be supplied"); - } - - this.annotation = annotation; - } - - @Override - public boolean matches(Class type) { - return type.getAnnotation(annotation) != null; - } - -} \ No newline at end of file diff --git a/structurizr-core/src/com/structurizr/analysis/ComponentFinder.java b/structurizr-core/src/com/structurizr/analysis/ComponentFinder.java deleted file mode 100644 index f92af3f5b..000000000 --- a/structurizr-core/src/com/structurizr/analysis/ComponentFinder.java +++ /dev/null @@ -1,129 +0,0 @@ -package com.structurizr.analysis; - -import com.structurizr.model.Component; -import com.structurizr.model.Container; - -import java.util.*; -import java.util.regex.Pattern; - -/** - * This class allows you to find components in a Java codebase, when used in conjunction - * with a number of pluggable component finder strategies. - */ -public class ComponentFinder { - - private Container container; - private String packageName; - - // this is a default of regexes representing types we're probably not interested in */ - private Set exclusions = new HashSet<>(Arrays.asList( - Pattern.compile("java\\..*"), - Pattern.compile("javax\\..*"), - Pattern.compile("sun\\..*") - )); - - // the list of strategies, which will be executed in the order they are added - private List componentFinderStrategies = new ArrayList<>(); - - /** - * Create a new component finder. - * - * @param container the Container that components will be added to - * @param packageName the Java package name to be scanned (e.g. "com.mycompany.myapp") - * @param componentFinderStrategies one or more ComponentFinderStrategy objects, describing how to find components - */ - public ComponentFinder(Container container, String packageName, ComponentFinderStrategy... componentFinderStrategies) { - if (container == null) { - throw new IllegalArgumentException("A container must be specified."); - } - - if (packageName == null || packageName.trim().length() == 0) { - throw new IllegalArgumentException("A package name must be specified."); - } - - if (componentFinderStrategies.length == 0) { - throw new IllegalArgumentException("One or more ComponentFinderStrategy objects must be specified."); - } - - this.container = container; - this.packageName = packageName; - - for (ComponentFinderStrategy componentFinderStrategy : componentFinderStrategies) { - this.componentFinderStrategies.add(componentFinderStrategy); - componentFinderStrategy.setComponentFinder(this); - } - } - - /** - * Find components, using all of the configured component finder strategies - * in the order they were added. - * - * @return the set of Components that were found - * @throws Exception if something goes wrong - */ - public Set findComponents() throws Exception { - Set componentsFound = new HashSet<>(); - - for (ComponentFinderStrategy componentFinderStrategy : componentFinderStrategies) { - componentFinderStrategy.beforeFindComponents(); - } - - for (ComponentFinderStrategy componentFinderStrategy : componentFinderStrategies) { - componentsFound.addAll(componentFinderStrategy.findComponents()); - } - - for (ComponentFinderStrategy componentFinderStrategy : componentFinderStrategies) { - componentFinderStrategy.afterFindComponents(); - } - - return componentsFound; - } - - /** - * Gets the Container that components will be added to. - * - * @return a Container instance - */ - public Container getContainer() { - return this.container; - } - - /** - * Gets the name of the package to be scanned. - * - * @return the package name, as a String - */ - public String getPackageName() { - return packageName; - } - - /** - * Gets the set of regexes that define which types should be excluded during the component finding process. - * - * @return a set of Pattern (regex) instances - */ - public Set getExclusions() { - return new HashSet<>(exclusions); - } - - /** - * Adds one or more regexes to the set of regexes that define which types should be excluded during the component finding process. - * - * @param regexes one or more regular expressions, as Strings - */ - public void exclude(String... regexes) { - if (regexes != null) { - for (String regex : regexes) { - this.exclusions.add(Pattern.compile(regex)); - } - } - } - - /** - * Clears the set of exclusions. - */ - public void clearExclusions() { - this.exclusions.clear(); - } - -} \ No newline at end of file diff --git a/structurizr-core/src/com/structurizr/analysis/ComponentFinderStrategy.java b/structurizr-core/src/com/structurizr/analysis/ComponentFinderStrategy.java deleted file mode 100644 index f0c5e25b5..000000000 --- a/structurizr-core/src/com/structurizr/analysis/ComponentFinderStrategy.java +++ /dev/null @@ -1,45 +0,0 @@ -package com.structurizr.analysis; - -import com.structurizr.model.Component; - -import java.util.Set; - -/** - * The interface that all component finder strategies must implement. - */ -public interface ComponentFinderStrategy { - - /** - * Sets a reference to the parent component finder. - * - * @param componentFinder a ComponentFinder instance - */ - void setComponentFinder(ComponentFinder componentFinder); - - /** - * Called before all component finder strategies belonging to the - * same component finder are asked to find components. - * - * @throws Exception if something goes wrong - */ - void beforeFindComponents() throws Exception; - - /** - * Finds components. - * - * @return the set of components found - * @throws Exception if something goes wrong - */ - Set findComponents() throws Exception; - - /** - * Called after all component finder strategies belonging to the - * same component finder have found components. This can be used - * to supplement the component with more information, such as - * dependencies. - * - * @throws Exception if something goes wrong - */ - void afterFindComponents() throws Exception; - -} \ No newline at end of file diff --git a/structurizr-core/src/com/structurizr/analysis/DefaultTypeRepository.java b/structurizr-core/src/com/structurizr/analysis/DefaultTypeRepository.java deleted file mode 100644 index 56cc4d094..000000000 --- a/structurizr-core/src/com/structurizr/analysis/DefaultTypeRepository.java +++ /dev/null @@ -1,160 +0,0 @@ -package com.structurizr.analysis; - -import javassist.ClassPool; -import javassist.CtClass; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; -import org.reflections.ReflectionUtils; -import org.reflections.Reflections; -import org.reflections.scanners.AbstractScanner; -import org.reflections.scanners.SubTypesScanner; -import org.reflections.util.ClasspathHelper; -import org.reflections.util.ConfigurationBuilder; -import org.reflections.util.FilterBuilder; - -import java.util.HashMap; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; -import java.util.regex.Pattern; -import java.util.stream.Collectors; - -/** - * This is an implementation of a TypeRepository that uses a combination of: - * - The Reflections library (https://github.com/ronmamo/reflections). - * - Javassist (http://jboss-javassist.github.io/javassist/) - * - Java Reflection - */ -public class DefaultTypeRepository implements TypeRepository { - - private static final Log log = LogFactory.getLog(DefaultTypeRepository.class); - - private final Set> types; - - private String packageToScan; - private Set exclusions = new HashSet<>(); - - private ClassPool classPool = ClassPool.getDefault(); - private Map>> referencedTypesCache = new HashMap<>(); - - /** - * Creates a new instance based upon a package to scan and a set of exclusions. - * - * @param packageToScan the fully qualified package name - * @param exclusions a Set of Pattern objects - */ - DefaultTypeRepository(String packageToScan, Set exclusions) { - this.packageToScan = packageToScan; - if (exclusions != null) { - this.exclusions.addAll(exclusions); - } - - AllTypesScanner allTypesScanner = new AllTypesScanner(); - Reflections reflections = new Reflections(new ConfigurationBuilder() - .setUrls(ClasspathHelper.forJavaClassPath()) - .filterInputsBy(new FilterBuilder().includePackage(packageToScan)) - .setScanners(new SubTypesScanner(false), allTypesScanner) - ); - - types = new HashSet<>(); - types.addAll(ReflectionUtils.forNames(allTypesScanner.types, reflections.getConfiguration().getClassLoaders())); - - for (Class c : types) { - System.out.println("+ " + c); - } - } - - /** - * Gets the package that this type repository is associated with scanning. - * - * @return a fully qualified package name - */ - public String getPackage() { - return packageToScan; - } - - /** - * Gets all of the types found by this type repository. - * - * @return a Set of Class objects, or an empty set of no classes were found - */ - public Set> getAllTypes() { - return new HashSet<>(types); - } - - /** - * Finds the set of types referenced by the specified type. - * - * @param typeName the starting type - * @return a Set of Class objects, or an empty set if none were found - */ - public Set> findReferencedTypes(String typeName) { - Set> referencedTypes = new HashSet<>(); - - // use the cached version if possible - if (referencedTypesCache.containsKey(typeName)) { - return referencedTypesCache.get(typeName); - } - - try { - CtClass cc = classPool.get(typeName); - for (Object referencedType : cc.getRefClasses()) { - String referencedTypeName = (String)referencedType; - - if (!isExcluded(referencedTypeName)) { - try { - referencedTypes.add(ClassLoader.getSystemClassLoader().loadClass(referencedTypeName)); - } catch (Throwable t) { - log.debug("Could not find " + referencedTypeName + " ... ignoring."); - } - } - } - - // remove the type itself - referencedTypes.remove(ClassLoader.getSystemClassLoader().loadClass(typeName)); - } catch (Exception e) { - log.debug("Error finding referenced types for " + typeName + " ... ignoring."); - - // since there was an error, we can't find the set of referenced types from it, so... - referencedTypesCache.put(typeName, new HashSet<>()); - } - - // cache for the next time - referencedTypesCache.put(typeName, referencedTypes); - - return referencedTypes; - } - - private Set> filter(Set> types) { - return types.stream().filter(c -> !isExcluded(c.getCanonicalName())).collect(Collectors.toSet()); - } - - private boolean isExcluded(String typeName) { - if (typeName == null) { - return true; - } - - for (Pattern exclude : exclusions) { - if (exclude.matcher(typeName).matches()) { - return true; - } - } - - return false; - } - - class AllTypesScanner extends AbstractScanner { - - Set types = new HashSet<>(); - - @Override - public void scan(Object cls) { - String typeName = getMetadataAdapter().getClassName(cls); - - if (!isExcluded(typeName)) { - types.add(typeName); - } - } - } - -} \ No newline at end of file diff --git a/structurizr-core/src/com/structurizr/analysis/ExtendsClassTypeMatcher.java b/structurizr-core/src/com/structurizr/analysis/ExtendsClassTypeMatcher.java deleted file mode 100644 index 129fccce2..000000000 --- a/structurizr-core/src/com/structurizr/analysis/ExtendsClassTypeMatcher.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.structurizr.analysis; - -/** - * Matches types where the type extends the specified class. - */ -public class ExtendsClassTypeMatcher extends AbstractTypeMatcher { - - private Class classType; - - public ExtendsClassTypeMatcher(Class classType, String description, String technology) { - super(description, technology); - - if (classType.isInterface() || classType.isEnum()) { - throw new IllegalArgumentException(classType + " is not a class type"); - } - - this.classType = classType; - } - - @Override - public boolean matches(Class type) { - return classType.isAssignableFrom(type); - } - -} \ No newline at end of file diff --git a/structurizr-core/src/com/structurizr/analysis/FirstImplementationOfInterfaceSupportingTypesStrategy.java b/structurizr-core/src/com/structurizr/analysis/FirstImplementationOfInterfaceSupportingTypesStrategy.java deleted file mode 100644 index d209d5073..000000000 --- a/structurizr-core/src/com/structurizr/analysis/FirstImplementationOfInterfaceSupportingTypesStrategy.java +++ /dev/null @@ -1,36 +0,0 @@ -package com.structurizr.analysis; - -import com.structurizr.model.Component; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; - -import java.util.HashSet; -import java.util.Set; - -/** - * If the component type is an interface, this strategy finds the first implementation of that interface. - */ -public class FirstImplementationOfInterfaceSupportingTypesStrategy extends SupportingTypesStrategy { - - private static final Log log = LogFactory.getLog(FirstImplementationOfInterfaceSupportingTypesStrategy.class); - - @Override - public Set findSupportingTypes(Component component) { - Set set = new HashSet<>(); - - try { - Class componentType = ClassLoader.getSystemClassLoader().loadClass(component.getType()); - if (componentType.isInterface()) { - Class type = TypeUtils.findFirstImplementationOfInterface(componentType, getTypeRepository().getAllTypes()); - if (type != null) { - set.add(type.getCanonicalName()); - } - } - } catch (ClassNotFoundException e) { - log.warn("Could not load type " + component.getType()); - } - - return set; - } - -} diff --git a/structurizr-core/src/com/structurizr/analysis/ImplementsInterfaceTypeMatcher.java b/structurizr-core/src/com/structurizr/analysis/ImplementsInterfaceTypeMatcher.java deleted file mode 100644 index a95766f42..000000000 --- a/structurizr-core/src/com/structurizr/analysis/ImplementsInterfaceTypeMatcher.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.structurizr.analysis; - -/** - * Matches types where the type implements the specified interface. - */ -public class ImplementsInterfaceTypeMatcher extends AbstractTypeMatcher { - - private Class interfaceType; - - public ImplementsInterfaceTypeMatcher(Class interfaceType, String description, String technology) { - super(description, technology); - - if (!interfaceType.isInterface()) { - throw new IllegalArgumentException(interfaceType + " is not an interface type"); - } - - this.interfaceType = interfaceType; - } - - @Override - public boolean matches(Class type) { - return interfaceType.isAssignableFrom(type); - } - -} \ No newline at end of file diff --git a/structurizr-core/src/com/structurizr/analysis/JavadocCommentFilter.java b/structurizr-core/src/com/structurizr/analysis/JavadocCommentFilter.java deleted file mode 100644 index 4d1db64f7..000000000 --- a/structurizr-core/src/com/structurizr/analysis/JavadocCommentFilter.java +++ /dev/null @@ -1,35 +0,0 @@ -package com.structurizr.analysis; - -/** - * Cleans up Javadoc comments for inclusion in the software architecture model. - */ -class JavadocCommentFilter { - - private 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-core/src/com/structurizr/analysis/NameSuffixTypeMatcher.java b/structurizr-core/src/com/structurizr/analysis/NameSuffixTypeMatcher.java deleted file mode 100644 index 6a139ed22..000000000 --- a/structurizr-core/src/com/structurizr/analysis/NameSuffixTypeMatcher.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.structurizr.analysis; - -/** - * Matches types where the name of the type ends with the specified suffix. - */ -public class NameSuffixTypeMatcher extends AbstractTypeMatcher { - - private String suffix; - - public NameSuffixTypeMatcher(String suffix, String description, String technology) { - super(description, technology); - - if (suffix == null || suffix.trim().length() == 0) { - throw new IllegalArgumentException("A suffix must be supplied"); - } - - this.suffix = suffix; - } - - @Override - public boolean matches(Class type) { - return type.getSimpleName().endsWith(suffix); - } - -} diff --git a/structurizr-core/src/com/structurizr/analysis/ReferencedTypesInSamePackageSupportingTypesStrategy.java b/structurizr-core/src/com/structurizr/analysis/ReferencedTypesInSamePackageSupportingTypesStrategy.java deleted file mode 100644 index 6d26d6a68..000000000 --- a/structurizr-core/src/com/structurizr/analysis/ReferencedTypesInSamePackageSupportingTypesStrategy.java +++ /dev/null @@ -1,35 +0,0 @@ -package com.structurizr.analysis; - -import com.structurizr.model.Component; - -import java.util.Set; -import java.util.stream.Collectors; - -/** - * This strategy finds all referenced types in the same package as the component type, - * and is useful if each component resides in its own Java package. - */ -public class ReferencedTypesInSamePackageSupportingTypesStrategy extends SupportingTypesStrategy { - - private boolean includeIndirectlyReferencedTypes; - - public ReferencedTypesInSamePackageSupportingTypesStrategy() { - this(true); - } - - public ReferencedTypesInSamePackageSupportingTypesStrategy(boolean includeIndirectlyReferencedTypes) { - this.includeIndirectlyReferencedTypes = includeIndirectlyReferencedTypes; - } - - @Override - public Set findSupportingTypes(Component component) { - ReferencedTypesSupportingTypesStrategy referencedTypesSupportingTypesStrategy = new ReferencedTypesSupportingTypesStrategy(includeIndirectlyReferencedTypes); - referencedTypesSupportingTypesStrategy.setTypeRepository(getTypeRepository()); - Set supportingTypes = referencedTypesSupportingTypesStrategy.findSupportingTypes(component); - - return supportingTypes.stream() - .filter(s -> s.startsWith(component.getPackage())) - .collect(Collectors.toSet()); - } - -} diff --git a/structurizr-core/src/com/structurizr/analysis/ReferencedTypesSupportingTypesStrategy.java b/structurizr-core/src/com/structurizr/analysis/ReferencedTypesSupportingTypesStrategy.java deleted file mode 100644 index 274de742b..000000000 --- a/structurizr-core/src/com/structurizr/analysis/ReferencedTypesSupportingTypesStrategy.java +++ /dev/null @@ -1,61 +0,0 @@ -package com.structurizr.analysis; - -import com.structurizr.model.CodeElement; -import com.structurizr.model.Component; - -import java.util.HashSet; -import java.util.Set; -import java.util.stream.Collectors; - -/** - * This strategy finds all types that are referenced by the component type - * and supporting types. - */ -public class ReferencedTypesSupportingTypesStrategy extends SupportingTypesStrategy { - - private boolean includeIndirectlyReferencedTypes; - - public ReferencedTypesSupportingTypesStrategy() { - this(true); - } - - public ReferencedTypesSupportingTypesStrategy(boolean includeIndirectlyReferencedTypes) { - this.includeIndirectlyReferencedTypes = includeIndirectlyReferencedTypes; - } - - private Set> getReferencedTypesInPackage(String type) { - return getTypeRepository().findReferencedTypes(type).stream().filter(t -> t.getCanonicalName() != null && t.getCanonicalName().startsWith(getTypeRepository().getPackage())).collect(Collectors.toSet()); - } - - @Override - public Set findSupportingTypes(Component component) { - Set> referencedTypes = new HashSet<>(); - referencedTypes.addAll(getReferencedTypesInPackage(component.getType())); - - for (CodeElement codeElement : component.getCode()) { - referencedTypes.addAll(getReferencedTypesInPackage(codeElement.getType())); - } - - if (includeIndirectlyReferencedTypes) { - int numberOfTypes = referencedTypes.size(); - boolean foundMore = true; - while (foundMore) { - Set> indirectlyReferencedTypes = new HashSet<>(); - for (Class type : referencedTypes) { - indirectlyReferencedTypes.addAll(getReferencedTypesInPackage(type.getCanonicalName())); - } - referencedTypes.addAll(indirectlyReferencedTypes); - - if (referencedTypes.size() > numberOfTypes) { - foundMore = true; - numberOfTypes = referencedTypes.size(); - } else { - foundMore = false; - } - } - } - - return referencedTypes.stream().map(Class::getName).collect(Collectors.toSet()); - } - -} diff --git a/structurizr-core/src/com/structurizr/analysis/RegexTypeMatcher.java b/structurizr-core/src/com/structurizr/analysis/RegexTypeMatcher.java deleted file mode 100644 index 7e1d5bd09..000000000 --- a/structurizr-core/src/com/structurizr/analysis/RegexTypeMatcher.java +++ /dev/null @@ -1,41 +0,0 @@ -package com.structurizr.analysis; - -import java.util.regex.Pattern; - -/** - * Matches types using a regex against the fully qualified type name. - */ -public class RegexTypeMatcher extends AbstractTypeMatcher { - - private Pattern regex; - - public RegexTypeMatcher(String regex, String description, String technology) { - super(description, technology); - - if (regex == null) { - throw new IllegalArgumentException("A regex must be supplied"); - } - - this.regex = Pattern.compile(regex); - } - - public RegexTypeMatcher(Pattern regex, String description, String technology) { - super(description, technology); - - if (regex == null) { - throw new IllegalArgumentException("A regex must be supplied"); - } - - this.regex = regex; - } - - @Override - public boolean matches(Class type) { - if (type != null && type.getCanonicalName() != null) { - return Pattern.matches(regex.pattern(), type.getCanonicalName()); - } else { - return false; - } - } - -} diff --git a/structurizr-core/src/com/structurizr/analysis/SourceCodeComponentFinderStrategy.java b/structurizr-core/src/com/structurizr/analysis/SourceCodeComponentFinderStrategy.java deleted file mode 100644 index 5d2fb6a23..000000000 --- a/structurizr-core/src/com/structurizr/analysis/SourceCodeComponentFinderStrategy.java +++ /dev/null @@ -1,130 +0,0 @@ -package com.structurizr.analysis; - -import com.structurizr.model.CodeElement; -import com.structurizr.model.CodeElementRole; -import com.structurizr.model.Component; -import com.sun.javadoc.ClassDoc; -import com.sun.javadoc.RootDoc; - -import java.io.File; -import java.nio.file.Files; -import java.nio.file.Paths; -import java.util.*; - -/** - * This component finder strategy doesn't really find components, it instead: - *
    - *
  • Extracts the top-level Javadoc comment from the code so that this can be added to existing component definitions.
  • - *
  • Calculates the size of components based upon the number of lines of source code.
  • - *
- */ -public class SourceCodeComponentFinderStrategy implements ComponentFinderStrategy { - - private ComponentFinder componentFinder; - private static RootDoc ROOTDOC; - - private File sourcePath; - private Integer maxDescriptionLength = null; - private String encoding = null; - - private Map typeToSourceFile = new HashMap<>(); - private Map typeToDescription = new HashMap<>(); - - public SourceCodeComponentFinderStrategy(File sourcePath) { - this.sourcePath = sourcePath; - } - - public SourceCodeComponentFinderStrategy(File sourcePath, int maxDescriptionLength) { - this.sourcePath = sourcePath; - this.maxDescriptionLength = maxDescriptionLength; - } - - @Override - public void setComponentFinder(ComponentFinder componentFinder) { - this.componentFinder = componentFinder; - } - - public void setEncoding(String encoding) { - this.encoding = encoding; - } - - @Override - public void beforeFindComponents() throws Exception { - } - - @Override - public Set findComponents() throws Exception { - return new HashSet<>(); // this component finder doesn't find components - } - - @Override - public void afterFindComponents() throws Exception { - runJavaDoc(); - - JavadocCommentFilter filter = new JavadocCommentFilter(maxDescriptionLength); - for (ClassDoc classDoc : ROOTDOC.classes()) { - String type = classDoc.qualifiedTypeName(); - String comment = filter.filterAndTruncate(classDoc.commentText()); - String pathToSourceFile = classDoc.position().file().getCanonicalPath(); - - typeToSourceFile.put(type, new File(pathToSourceFile)); - typeToDescription.put(type, comment); - } - - for (Component component : componentFinder.getContainer().getComponents()) { - long count = 0; - - for (CodeElement codeElement : component.getCode()) { - if (typeToDescription.containsKey(codeElement.getType())) { - codeElement.setDescription(typeToDescription.get(codeElement.getType())); - - // additionally set the description on the component, if it's not already been set - if (codeElement.getRole() == CodeElementRole.Primary) { - if (component.getDescription() == null || component.getDescription().trim().length() == 0) { - component.setDescription(typeToDescription.get(component.getType())); - } - } - } - - File sourceFile = typeToSourceFile.get(codeElement.getType()); - if (sourceFile != null) { - long numberOfLinesInFile = Files.lines(Paths.get(sourceFile.toURI())).count(); - codeElement.setUrl(sourceFile.toURI().toString()); - codeElement.setSize(numberOfLinesInFile); - count += numberOfLinesInFile; - } - } - - if (count > 0) { - component.setSize(count); - } - } - } - - private void runJavaDoc() throws Exception { - List parameters = new LinkedList<>(); - parameters.add("-sourcepath"); - parameters.add(sourcePath.getCanonicalPath()); - parameters.add("-subpackages"); - parameters.add(componentFinder.getPackageName()); - - if (encoding != null) { - parameters.add("-encoding"); - parameters.add(encoding); - } - - parameters.add("-private"); - - com.sun.tools.javadoc.Main.execute( - "StructurizrDoclet", - this.getClass().getName(), - parameters.toArray(new String[parameters.size()]) - ); - } - - public static boolean start(RootDoc rootDoc) { - ROOTDOC = rootDoc; - return true; - } - -} \ No newline at end of file diff --git a/structurizr-core/src/com/structurizr/analysis/StructurizrAnnotationsComponentFinderStrategy.java b/structurizr-core/src/com/structurizr/analysis/StructurizrAnnotationsComponentFinderStrategy.java deleted file mode 100644 index 985c0759f..000000000 --- a/structurizr-core/src/com/structurizr/analysis/StructurizrAnnotationsComponentFinderStrategy.java +++ /dev/null @@ -1,251 +0,0 @@ -package com.structurizr.analysis; - -import com.structurizr.annotation.*; -import com.structurizr.model.*; -import com.structurizr.model.Component; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; - -import java.lang.reflect.Field; -import java.util.HashSet; -import java.util.Set; - -/** - * This component finder strategy looks for the following Structurizr annotations. - * - * - Definitions: @Component - * - Efferent dependencies: @UsesSoftwareSystem, @UsesContainer, @UsesComponent - * - Afferent dependencies: @UsedByPerson, @UsedBySoftwareSystem, @UsedByContainer - */ -public class StructurizrAnnotationsComponentFinderStrategy extends AbstractComponentFinderStrategy { - - private static final Log log = LogFactory.getLog(StructurizrAnnotationsComponentFinderStrategy.class); - - public StructurizrAnnotationsComponentFinderStrategy() { - super(new FirstImplementationOfInterfaceSupportingTypesStrategy()); - } - - public StructurizrAnnotationsComponentFinderStrategy(SupportingTypesStrategy... strategies) { - super(strategies); - } - - @Override - protected Set doFindComponents() { - Set components = new HashSet<>(); - - // find all types that have been annotated @Component - Set> componentTypes = findTypesAnnotatedWith(com.structurizr.annotation.Component.class); - for (Class componentType : componentTypes) { - Component component = getComponentFinder().getContainer().addComponent( - componentType.getSimpleName(), - componentType, - componentType.getAnnotation(com.structurizr.annotation.Component.class).description(), - componentType.getAnnotation(com.structurizr.annotation.Component.class).technology() - ); - components.add(component); - } - - return components; - } - - @Override - public void afterFindComponents() { - // this will find component dependencies, but the relationship descriptions - // will be empty because we can't get that from the code - super.afterFindComponents(); - - for (Component component : getComponentFinder().getContainer().getComponents()) { - if (component.getType() != null) { - - // find the efferent dependencies - for (CodeElement codeElement : component.getCode()) { - findUsesComponentAnnotations(component, codeElement.getType()); - } - for (CodeElement codeElement : component.getCode()) { - findUsesSoftwareSystemsAnnotations(component, codeElement.getType()); - } - for (CodeElement codeElement : component.getCode()) { - findUsesContainerAnnotations(component, codeElement.getType()); - } - - // and also the afferent dependencies - for (CodeElement codeElement : component.getCode()) { - findUsedByPersonAnnotations(component, codeElement.getType()); - } - for (CodeElement codeElement : component.getCode()) { - findUsedBySoftwareSystemAnnotations(component, codeElement.getType()); - } - for (CodeElement codeElement : component.getCode()) { - findUsedByContainerAnnotations(component, codeElement.getType()); - } - } - } - } - - /** - * This will add a description to existing component dependencies, where they have - * been annotated @UsesComponent. - */ - private void findUsesComponentAnnotations(Component component, String typeName) { - try { - Class type = ClassLoader.getSystemClassLoader().loadClass(typeName); - for (Field field : type.getDeclaredFields()) { - UsesComponent annotation = field.getAnnotation(UsesComponent.class); - if (annotation != null) { - String name = field.getType().getCanonicalName(); - String description = field.getAnnotation(UsesComponent.class).description(); - String technology = annotation.technology(); - - Component destination = componentFinder.getContainer().getComponentOfType(name); - if (destination != null) { - for (Relationship relationship : component.getRelationships()) { - if (relationship.getDestination() == destination) { - relationship.setDescription(description); - relationship.setTechnology(technology); - } - } - } else { - log.warn("A component of type \"" + name + "\" could not be found."); - } - } - } - } catch (ClassNotFoundException e) { - log.warn("Could not load type " + typeName); - } - } - - /** - * Find the @UsesSoftwareSystem annotations. - */ - private void findUsesSoftwareSystemsAnnotations(Component component, String typeName) { - try { - Class type = ClassLoader.getSystemClassLoader().loadClass(typeName); - UsesSoftwareSystem[] annotations = type.getAnnotationsByType(UsesSoftwareSystem.class); - for (UsesSoftwareSystem annotation : annotations) { - String name = annotation.name(); - String description = annotation.description(); - String technology = annotation.technology(); - - SoftwareSystem softwareSystem = component.getModel().getSoftwareSystemWithName(name); - if (softwareSystem != null) { - component.uses(softwareSystem, description, technology); - } else { - log.warn("A software system named \"" + name + "\" could not be found."); - } - } - } catch (ClassNotFoundException e) { - log.warn("Could not load type " + typeName); - } - } - - /** - * Find the @UsesContainer annotations. - */ - private void findUsesContainerAnnotations(Component component, String typeName) { - try { - Class type = ClassLoader.getSystemClassLoader().loadClass(typeName); - UsesContainer[] annotations = type.getAnnotationsByType(UsesContainer.class); - for (UsesContainer annotation : annotations) { - String name = annotation.name(); - String description = annotation.description(); - String technology = annotation.technology(); - - Container container = findContainerByNameOrCanonicalName(component, name); - if (container != null) { - component.uses(container, description, technology); - } else { - log.warn("A container named \"" + name + "\" could not be found."); - } - } - } catch (ClassNotFoundException e) { - log.warn("Could not load type " + typeName); - } - } - - /** - * Finds @UsedByPerson annotations. - */ - private void findUsedByPersonAnnotations(Component component, String typeName) { - try { - Class type = ClassLoader.getSystemClassLoader().loadClass(typeName); - UsedByPerson[] annotations = type.getAnnotationsByType(UsedByPerson.class); - for (UsedByPerson annotation : annotations) { - String name = annotation.name(); - String description = annotation.description(); - String technology = annotation.technology(); - - Person person = component.getModel().getPersonWithName(name); - if (person != null) { - person.uses(component, description, technology); - } else { - log.warn("A person named \"" + name + "\" could not be found."); - } - } - } catch (ClassNotFoundException e) { - log.warn("Could not load type " + typeName); - } - } - - /** - * Finds @UsedBySoftwareSystem annotations. - */ - private void findUsedBySoftwareSystemAnnotations(Component component, String typeName) { - try { - Class type = ClassLoader.getSystemClassLoader().loadClass(typeName); - UsedBySoftwareSystem[] annotations = type.getAnnotationsByType(UsedBySoftwareSystem.class); - for (UsedBySoftwareSystem annotation : annotations) { - String name = annotation.name(); - String description = annotation.description(); - String technology = annotation.technology(); - - SoftwareSystem softwareSystem = component.getModel().getSoftwareSystemWithName(name); - if (softwareSystem != null) { - softwareSystem.uses(component, description, technology); - } else { - log.warn("A software system named \"" + name + "\" could not be found."); - } - } - } catch (ClassNotFoundException e) { - log.warn("Could not load type " + typeName); - } - } - - /** - * Finds @UsedByContainer annotations. - */ - private void findUsedByContainerAnnotations(Component component, String typeName) { - try { - Class type = ClassLoader.getSystemClassLoader().loadClass(typeName); - UsedByContainer[] annotations = type.getAnnotationsByType(UsedByContainer.class); - for (UsedByContainer annotation : annotations) { - String name = annotation.name(); - String description = annotation.description(); - String technology = annotation.technology(); - - Container container = findContainerByNameOrCanonicalName(component, name); - if (container != null) { - container.uses(component, description, technology); - } else { - log.warn("A container named \"" + name + "\" could not be found."); - } - } - } catch (ClassNotFoundException e) { - log.warn("Could not load type " + typeName); - } - } - - private Container findContainerByNameOrCanonicalName(Component component, String name) { - // assume that the container resides in the same software system - Container container = component.getContainer().getSoftwareSystem().getContainerWithName(name); - if (container == null) { - // perhaps a canonical name has been specified - Element element = component.getModel().getElementWithCanonicalName(name); - if (element != null && element instanceof Container) { - container = (Container)element; - } - } - - return container; - } - -} \ No newline at end of file diff --git a/structurizr-core/src/com/structurizr/analysis/SupportingTypesStrategy.java b/structurizr-core/src/com/structurizr/analysis/SupportingTypesStrategy.java deleted file mode 100644 index eeb9e8384..000000000 --- a/structurizr-core/src/com/structurizr/analysis/SupportingTypesStrategy.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.structurizr.analysis; - -import com.structurizr.model.Component; - -import java.util.Set; - -/** - * Superclass for strategies used to find the types that support a component. - */ -public abstract class SupportingTypesStrategy { - - private TypeRepository typeRepository; - - protected TypeRepository getTypeRepository() { - return typeRepository; - } - - void setTypeRepository(TypeRepository typeRepository) { - this.typeRepository = typeRepository; - } - - public abstract Set findSupportingTypes(Component component); - -} diff --git a/structurizr-core/src/com/structurizr/analysis/TypeCategory.java b/structurizr-core/src/com/structurizr/analysis/TypeCategory.java deleted file mode 100644 index e579c4435..000000000 --- a/structurizr-core/src/com/structurizr/analysis/TypeCategory.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.structurizr.analysis; - -public final class TypeCategory { - - public static final TypeCategory CLASS = new TypeCategory("class"); - public static final TypeCategory INTERFACE = new TypeCategory("interface"); - public static final TypeCategory ABSTRACT_CLASS = new TypeCategory("abstract"); - public static final TypeCategory ENUM = new TypeCategory("enum"); - - private String name; - - private TypeCategory(String name) { - this.name = name; - } - - public String getName() { - return name; - } - - @Override - public String toString() { - return name; - } - -} diff --git a/structurizr-core/src/com/structurizr/analysis/TypeMatcher.java b/structurizr-core/src/com/structurizr/analysis/TypeMatcher.java deleted file mode 100644 index ae012fd99..000000000 --- a/structurizr-core/src/com/structurizr/analysis/TypeMatcher.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.structurizr.analysis; - -/** - * Determines whether a given type implements the rules for being identified as a component. - */ -public interface TypeMatcher { - - boolean matches(Class type); - - String getDescription(); - - String getTechnology(); - -} diff --git a/structurizr-core/src/com/structurizr/analysis/TypeMatcherComponentFinderStrategy.java b/structurizr-core/src/com/structurizr/analysis/TypeMatcherComponentFinderStrategy.java deleted file mode 100644 index 8ae19de1c..000000000 --- a/structurizr-core/src/com/structurizr/analysis/TypeMatcherComponentFinderStrategy.java +++ /dev/null @@ -1,40 +0,0 @@ -package com.structurizr.analysis; - -import com.structurizr.model.Component; - -import java.util.*; - -/** - * A component finder strategy that uses type information to find components, based upon a number - * of pluggable {@link TypeMatcher} implementations. - */ -public class TypeMatcherComponentFinderStrategy extends AbstractComponentFinderStrategy { - - private List typeMatchers = new LinkedList<>(); - - public TypeMatcherComponentFinderStrategy(TypeMatcher... typeMatchers) { - this.typeMatchers.addAll(Arrays.asList(typeMatchers)); - } - - @Override - protected Set doFindComponents() { - Set components = new HashSet<>(); - - Set> types = getTypeRepository().getAllTypes(); - for (Class type : types) { - for (TypeMatcher typeMatcher : typeMatchers) { - if (typeMatcher.matches(type)) { - Component component = getComponentFinder().getContainer().addComponent( - type.getSimpleName(), - type.getCanonicalName(), - typeMatcher.getDescription(), - typeMatcher.getTechnology()); - components.add(component); - } - } - } - - return components; - } - -} \ No newline at end of file diff --git a/structurizr-core/src/com/structurizr/analysis/TypeRepository.java b/structurizr-core/src/com/structurizr/analysis/TypeRepository.java deleted file mode 100644 index 544d61561..000000000 --- a/structurizr-core/src/com/structurizr/analysis/TypeRepository.java +++ /dev/null @@ -1,32 +0,0 @@ -package com.structurizr.analysis; - -import java.util.Set; - -/** - * This represents an abstraction for a repository of type information. - */ -interface TypeRepository { - - /** - * Gets the package that this type repository is associated with scanning. - * - * @return a fully qualified package name - */ - String getPackage(); - - /** - * Gets all of the types found by this type repository. - * - * @return a Set of Class objects, or an empty set of no classes were found - */ - Set> getAllTypes(); - - /** - * Finds the set of types referenced by the specified type. - * - * @param typeName the starting type - * @return a Set of Class objects, or an empty set if none were found - */ - Set> findReferencedTypes(String typeName); - -} \ No newline at end of file diff --git a/structurizr-core/src/com/structurizr/analysis/TypeUtils.java b/structurizr-core/src/com/structurizr/analysis/TypeUtils.java deleted file mode 100644 index bfa609f61..000000000 --- a/structurizr-core/src/com/structurizr/analysis/TypeUtils.java +++ /dev/null @@ -1,115 +0,0 @@ -package com.structurizr.analysis; - -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; - -import java.lang.annotation.Annotation; -import java.lang.reflect.Modifier; -import java.util.Set; -import java.util.stream.Collectors; - -/** - * Some utility methods for working with types. - */ -public class TypeUtils { - - private static final Log log = LogFactory.getLog(TypeUtils.class); - - /** - * Finds the visibility of a given type. - * - * @param typeName the fully qualified type name - * @return a TypeVisibility object representing the visibility (e.g. public, package, etc) - */ - public static TypeVisibility getVisibility(String typeName) { - try { - Class type = ClassLoader.getSystemClassLoader().loadClass(typeName); - int modifiers = type.getModifiers(); - if (Modifier.isPrivate(modifiers)) { - return TypeVisibility.PRIVATE; - } else if (Modifier.isPublic(modifiers)) { - return TypeVisibility.PUBLIC; - } else if (Modifier.isProtected(modifiers)) { - return TypeVisibility.PROTECTED; - } else { - return TypeVisibility.PACKAGE; - } - } catch (ClassNotFoundException e) { - log.warn("Visibility for type " + typeName + " could not be found."); - return null; - } - } - - /** - * Finds the category of a given type. - * - * @param typeName the fully qualified type name - * @return a TypeCategory object representing the category (e.g. class, interface, enum, etc) - */ - public static TypeCategory getCategory(String typeName) { - try { - Class type = ClassLoader.getSystemClassLoader().loadClass(typeName); - if (type.isInterface()) { - return TypeCategory.INTERFACE; - } else if (type.isEnum()) { - return TypeCategory.ENUM; - } else { - if (Modifier.isAbstract(type.getModifiers())) { - return TypeCategory.ABSTRACT_CLASS; - } else{ - return TypeCategory.CLASS; - } - } - } catch (ClassNotFoundException e) { - log.warn("Category for type " + typeName + " could not be found."); - return null; - } - } - - /** - * Finds the set of types that are annotated with the specified annotation. - * - * @param annotation the Annotation to find - * @param types the set of Class objects to search through - * @return a Set of Class objects, or an empty set of none could be found - */ - public static Set> findTypesAnnotatedWith(Class annotation, Set> types) { - if (annotation == null) { - throw new IllegalArgumentException("An annotation type must be specified."); - } - - return types.stream().filter(c -> c.isAnnotationPresent(annotation)).collect(Collectors.toSet()); - } - - /** - * Finds the first implementation of the given interface. - * - * @param interfaceType a Class object representing the interface type - * @param types the set of Class objects to search through - * @return the first concrete implementation class of the given interface, - * or null if one can't be found - */ - public static Class findFirstImplementationOfInterface(Class interfaceType, Set> types) { - if (interfaceType == null) { - throw new IllegalArgumentException("An interface type must be provided."); - } else if (!interfaceType.isInterface()) { - throw new IllegalArgumentException("The interface type must represent an interface."); - } - - if (types == null) { - throw new IllegalArgumentException("The set of types to search through must be provided."); - } - - for (Class type : types) { - boolean isInterface = type.isInterface(); - boolean isAbstract = Modifier.isAbstract(type.getModifiers()); - boolean isAssignable = interfaceType.isAssignableFrom(type); - if (!isInterface && !isAbstract && isAssignable) { - return type; - } - } - - return null; - } - -} \ No newline at end of file diff --git a/structurizr-core/src/com/structurizr/analysis/TypeVisibility.java b/structurizr-core/src/com/structurizr/analysis/TypeVisibility.java deleted file mode 100644 index c79a18a7c..000000000 --- a/structurizr-core/src/com/structurizr/analysis/TypeVisibility.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.structurizr.analysis; - -public final class TypeVisibility { - - public static final TypeVisibility PUBLIC = new TypeVisibility("public"); - public static final TypeVisibility PACKAGE = new TypeVisibility("package"); - public static final TypeVisibility PROTECTED = new TypeVisibility("protected"); - public static final TypeVisibility PRIVATE = new TypeVisibility("private"); - - private String name; - - private TypeVisibility(String name) { - this.name = name; - } - - public String getName() { - return name; - } - - @Override - public String toString() { - return name; - } - -} diff --git a/structurizr-core/src/com/structurizr/api/ApiError.java b/structurizr-core/src/com/structurizr/api/ApiError.java deleted file mode 100644 index 46508d4ce..000000000 --- a/structurizr-core/src/com/structurizr/api/ApiError.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.structurizr.api; - -import com.fasterxml.jackson.databind.DeserializationFeature; -import com.fasterxml.jackson.databind.ObjectMapper; - -final class ApiError { - - private String message; - - ApiError() { - } - - String getMessage() { - return message; - } - - void setMessage(String message) { - this.message = message; - } - - static ApiError parse(String json) throws Exception { - ObjectMapper objectMapper = new ObjectMapper(); - objectMapper.enable(DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT); - return objectMapper.readValue(json, ApiError.class); - } - -} diff --git a/structurizr-core/src/com/structurizr/api/HashBasedMessageAuthenticationCode.java b/structurizr-core/src/com/structurizr/api/HashBasedMessageAuthenticationCode.java deleted file mode 100644 index 66d8defbb..000000000 --- a/structurizr-core/src/com/structurizr/api/HashBasedMessageAuthenticationCode.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.structurizr.api; - -import javax.crypto.Mac; -import javax.crypto.spec.SecretKeySpec; -import javax.xml.bind.DatatypeConverter; - -final class HashBasedMessageAuthenticationCode { - - private static final String HMAC_SHA256_ALGORITHM = "HmacSHA256"; - - private String apiSecret; - - HashBasedMessageAuthenticationCode(String apiSecret) { - this.apiSecret = apiSecret; - } - - String generate(String content) throws Exception { - SecretKeySpec signingKey = new SecretKeySpec(apiSecret.getBytes(), HMAC_SHA256_ALGORITHM); - Mac mac = Mac.getInstance(HMAC_SHA256_ALGORITHM); - mac.init(signingKey); - byte[] rawHmac = mac.doFinal(content.getBytes()); - return DatatypeConverter.printHexBinary(rawHmac).toLowerCase(); - } - -} diff --git a/structurizr-core/src/com/structurizr/api/HttpHeaders.java b/structurizr-core/src/com/structurizr/api/HttpHeaders.java deleted file mode 100644 index 3da56aee1..000000000 --- a/structurizr-core/src/com/structurizr/api/HttpHeaders.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.structurizr.api; - -final class HttpHeaders { - - static final String USER_AGENT = "User-Agent"; - static final String AUTHORIZATION = "Authorization"; - static final String CONTENT_TYPE = "Content-Type"; - static final String CONTENT_MD5 = "Content-MD5"; - static final String NONCE = "Nonce"; - -} \ No newline at end of file diff --git a/structurizr-core/src/com/structurizr/api/StructurizrClient.java b/structurizr-core/src/com/structurizr/api/StructurizrClient.java deleted file mode 100644 index b84085709..000000000 --- a/structurizr-core/src/com/structurizr/api/StructurizrClient.java +++ /dev/null @@ -1,327 +0,0 @@ -package com.structurizr.api; - -import com.structurizr.Workspace; -import com.structurizr.encryption.*; -import com.structurizr.io.json.JsonReader; -import com.structurizr.io.json.JsonWriter; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; -import org.apache.http.Header; -import org.apache.http.HttpStatus; -import org.apache.http.client.methods.CloseableHttpResponse; -import org.apache.http.client.methods.HttpGet; -import org.apache.http.client.methods.HttpPut; -import org.apache.http.client.methods.HttpRequestBase; -import org.apache.http.entity.ContentType; -import org.apache.http.entity.StringEntity; -import org.apache.http.impl.client.CloseableHttpClient; -import org.apache.http.impl.client.HttpClients; -import org.apache.http.util.EntityUtils; - -import java.io.*; -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. - */ -public final class StructurizrClient { - - private static final Log log = LogFactory.getLog(StructurizrClient.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 static final String WORKSPACE_PATH = "/workspace/"; - - private static final String VERSION = Package.getPackage("com.structurizr.api").getImplementationVersion(); - - private String url; - private String apiKey; - private String apiSecret; - - private EncryptionStrategy encryptionStrategy; - - 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 { - Properties properties = new Properties(); - InputStream in = StructurizrClient.class.getClassLoader().getResourceAsStream("structurizr.properties"); - if (in != null) { - properties.load(in); - setUrl(properties.getProperty(STRUCTURIZR_API_URL)); - this.apiKey = properties.getProperty(STRUCTURIZR_API_KEY); - this.apiSecret = properties.getProperty(STRUCTURIZR_API_SECRET); - in.close(); - } 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 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("https://api.structurizr.com", apiKey, apiSecret); - } - - /** - * Creates a new Structurizr client with the specified API URL, key and secret. - * - * @param url the URL of your structurizr web 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); - this.apiKey = apiKey; - this.apiSecret = apiSecret; - } - - /** - * Gets the URL of the Structurizr API that this client is using. - * - * @return the URL, as a String - */ - public String getUrl() { - return url; - } - - void setUrl(String url) { - if (url != null) { - if (url.endsWith("/")) { - this.url = url.substring(0, url.length() - 1); - } else { - this.url = url; - } - } - } - - /** - * 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; - } - - /** - * Gets the workspace with the given ID. - * - * @param workspaceId the ID of your workspace - * @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 { - try { - log.info("Getting workspace with ID " + workspaceId); - - CloseableHttpClient httpClient = HttpClients.createSystem(); - 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.getStatusLine().getStatusCode() == HttpStatus.SC_OK) { - archiveWorkspace(workspaceId, json); - - if (encryptionStrategy == null) { - return new JsonReader().read(new StringReader(json)); - } else { - EncryptedWorkspace encryptedWorkspace = new EncryptedJsonReader().read(new StringReader(json)); - encryptedWorkspace.getEncryptionStrategy().setPassphrase(encryptionStrategy.getPassphrase()); - return encryptedWorkspace.getWorkspace(); - } - } else { - ApiError apiError = ApiError.parse(json); - throw new StructurizrClientException(apiError.getMessage()); - } - } - } catch (Exception e) { - log.error(e); - throw new StructurizrClientException(e); - } - } - - /** - * Updates the given workspace. - * - * @param workspaceId the ID of your workspace - * @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 { - try { - if (workspace == null) { - throw new IllegalArgumentException("A workspace must be supplied"); - } else if (workspaceId <= 0) { - throw new IllegalArgumentException("The workspace ID must be set"); - } - - 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.countAndLogWarnings(); - - CloseableHttpClient httpClient = HttpClients.createSystem(); - 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.getStatusLine().getStatusCode() == HttpStatus.SC_OK) { - debugResponse(response); - log.info(json); - } else { - ApiError apiError = ApiError.parse(json); - throw new StructurizrClientException(apiError.getMessage()); - } - } - } catch (Exception e) { - log.error(e); - throw new StructurizrClientException(e); - } - } - - private void debugRequest(HttpRequestBase httpRequest, String content) { - log.debug(httpRequest.getMethod() + " " + httpRequest.getURI().getPath()); - Header[] headers = httpRequest.getAllHeaders(); - for (Header header : headers) { - log.debug(header.getName() + ": " + header.getValue()); - } - - if (content != null) { - log.debug(content); - } - } - - private void debugResponse(CloseableHttpResponse response) { - log.debug(response.getStatusLine()); - } - - private void addHeaders(HttpRequestBase httpRequest, String content, String contentType) throws Exception { - String httpMethod = httpRequest.getMethod(); - String path = httpRequest.getURI().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, "structurizr-java/" + (VERSION != null ? VERSION : "dev")); - 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(); - fileWriter.close(); - - try { - log.debug("Workspace from server archived to " + archiveFile.getCanonicalPath()); - } catch (IOException ioe) { - log.debug("Workspace from server archived to " + archiveFile.getAbsolutePath()); - } - } catch (Exception e) { - log.warn("Could not archive JSON to " + archiveFile.getAbsolutePath()); - } - } - - private String createArchiveFileName(long workspaceId) { - SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmss"); - return "structurizr-" + workspaceId + "-" + sdf.format(new Date()) + ".json"; - } - -} \ No newline at end of file diff --git a/structurizr-core/src/com/structurizr/documentation/Arc42DocumentationTemplate.java b/structurizr-core/src/com/structurizr/documentation/Arc42DocumentationTemplate.java deleted file mode 100644 index f9966db36..000000000 --- a/structurizr-core/src/com/structurizr/documentation/Arc42DocumentationTemplate.java +++ /dev/null @@ -1,334 +0,0 @@ -package com.structurizr.documentation; - -import com.structurizr.Workspace; -import com.structurizr.model.SoftwareSystem; - -import java.io.File; -import java.io.IOException; - -/** - *

- * An implementation of the arc42 documentation template, - * consisting of the following sections: - *

- * - *
    - *
  1. Introduction and Goals (1)
  2. - *
  3. Constraints (2)
  4. - *
  5. Context and Scope (2)
  6. - *
  7. Solution Strategy (3)
  8. - *
  9. Building Block View (3)
  10. - *
  11. Runtime View (3)
  12. - *
  13. Deployment View (3)
  14. - *
  15. Crosscutting Concepts (3)
  16. - *
  17. Architectural Decisions (3)
  18. - *
  19. Quality Requirements (2)
  20. - *
  21. Risks and Technical Debt (4)
  22. - *
  23. Glossary (5)
  24. - *
- * - *

- * The number in parentheses () represents the grouping, which is simply used to colour code - * section navigation buttons when rendered. - *

- */ -public class Arc42DocumentationTemplate extends DocumentationTemplate { - - public Arc42DocumentationTemplate(Workspace workspace) { - super(workspace); - } - - /** - * Adds a "Introduction and Goals" section relating to a {@link SoftwareSystem} from one or more files. - * - * @param softwareSystem the {@link SoftwareSystem} the documentation content relates to - * @param files one or more File objects that point to the documentation content - * @return a documentation {@link Section} - * @throws IOException if there is an error reading the files - */ - public Section addIntroductionAndGoalsSection(SoftwareSystem softwareSystem, File... files) throws IOException { - return addSection(softwareSystem, "Introduction and Goals", GROUP1, files); - } - - /** - * Adds a "Introduction and Goals" section relating to a {@link SoftwareSystem}. - * - * @param softwareSystem the {@link SoftwareSystem} the documentation content relates to - * @param format the {@link Format} of the documentation content - * @param content a String containing the documentation content - * @return a documentation {@link Section} - */ - public Section addIntroductionAndGoalsSection(SoftwareSystem softwareSystem, Format format, String content) { - return addSection(softwareSystem, "Introduction and Goals", GROUP1, format, content); - } - - /** - * Adds a "Constraints" section relating to a {@link SoftwareSystem} from one or more files. - * - * @param softwareSystem the {@link SoftwareSystem} the documentation content relates to - * @param files one or more File objects that point to the documentation content - * @return a documentation {@link Section} - * @throws IOException if there is an error reading the files - */ - public Section addConstraintsSection(SoftwareSystem softwareSystem, File... files) throws IOException { - return addSection(softwareSystem, "Constraints", GROUP2, files); - } - - /** - * Adds a "Constraints" section relating to a {@link SoftwareSystem}. - * - * @param softwareSystem the {@link SoftwareSystem} the documentation content relates to - * @param format the {@link Format} of the documentation content - * @param content a String containing the documentation content - * @return a documentation {@link Section} - */ - public Section addConstraintsSection(SoftwareSystem softwareSystem, Format format, String content) { - return addSection(softwareSystem, "Constraints", GROUP2, format, content); - } - - /** - * Adds a "Context and Scope" section relating to a {@link SoftwareSystem} from one or more files. - * - * @param softwareSystem the {@link SoftwareSystem} the documentation content relates to - * @param files one or more File objects that point to the documentation content - * @return a documentation {@link Section} - * @throws IOException if there is an error reading the files - */ - public Section addContextAndScopeSection(SoftwareSystem softwareSystem, File... files) throws IOException { - return addSection(softwareSystem, "Context and Scope", GROUP2, files); - } - - /** - * Adds a "Context and Scope" section relating to a {@link SoftwareSystem}. - * - * @param softwareSystem the {@link SoftwareSystem} the documentation content relates to - * @param format the {@link Format} of the documentation content - * @param content a String containing the documentation content - * @return a documentation {@link Section} - */ - public Section addContextAndScopeSection(SoftwareSystem softwareSystem, Format format, String content) { - return addSection(softwareSystem, "Context and Scope", GROUP2, format, content); - } - - /** - * Adds a "Solution Strategy" section relating to a {@link SoftwareSystem} from one or more files. - * - * @param softwareSystem the {@link SoftwareSystem} the documentation content relates to - * @param files one or more File objects that point to the documentation content - * @return a documentation {@link Section} - * @throws IOException if there is an error reading the files - */ - public Section addSolutionStrategySection(SoftwareSystem softwareSystem, File... files) throws IOException { - return addSection(softwareSystem, "Solution Strategy", GROUP3, files); - } - - /** - * Adds a "Solution Strategy" section relating to a {@link SoftwareSystem}. - * - * @param softwareSystem the {@link SoftwareSystem} the documentation content relates to - * @param format the {@link Format} of the documentation content - * @param content a String containing the documentation content - * @return a documentation {@link Section} - */ - public Section addSolutionStrategySection(SoftwareSystem softwareSystem, Format format, String content) { - return addSection(softwareSystem, "Solution Strategy", GROUP3, format, content); - } - - /** - * Adds a "Building Block View" section relating to a {@link SoftwareSystem} from one or more files. - * - * @param softwareSystem the {@link SoftwareSystem} the documentation content relates to - * @param files one or more File objects that point to the documentation content - * @return a documentation {@link Section} - * @throws IOException if there is an error reading the files - */ - public Section addBuildingBlockViewSection(SoftwareSystem softwareSystem, File... files) throws IOException { - return addSection(softwareSystem, "Building Block View", GROUP3, files); - } - - /** - * Adds a "Building Block View" section relating to a {@link SoftwareSystem}. - * - * @param softwareSystem the {@link SoftwareSystem} the documentation content relates to - * @param format the {@link Format} of the documentation content - * @param content a String containing the documentation content - * @return a documentation {@link Section} - */ - public Section addBuildingBlockViewSection(SoftwareSystem softwareSystem, Format format, String content) { - return addSection(softwareSystem, "Building Block View", GROUP3, format, content); - } - - /** - * Adds a "Runtime View" section relating to a {@link SoftwareSystem} from one or more files. - * - * @param softwareSystem the {@link SoftwareSystem} the documentation content relates to - * @param files one or more File objects that point to the documentation content - * @return a documentation {@link Section} - * @throws IOException if there is an error reading the files - */ - public Section addRuntimeViewSection(SoftwareSystem softwareSystem, File... files) throws IOException { - return addSection(softwareSystem, "Runtime View", GROUP3, files); - } - - /** - * Adds a "Runtime View" section relating to a {@link SoftwareSystem}. - * - * @param softwareSystem the {@link SoftwareSystem} the documentation content relates to - * @param format the {@link Format} of the documentation content - * @param content a String containing the documentation content - * @return a documentation {@link Section} - */ - public Section addRuntimeViewSection(SoftwareSystem softwareSystem, Format format, String content) { - return addSection(softwareSystem, "Runtime View", GROUP3, format, content); - } - - /** - * Adds a "Deployment View" section relating to a {@link SoftwareSystem} from one or more files. - * - * @param softwareSystem the {@link SoftwareSystem} the documentation content relates to - * @param files one or more File objects that point to the documentation content - * @return a documentation {@link Section} - * @throws IOException if there is an error reading the files - */ - public Section addDeploymentViewSection(SoftwareSystem softwareSystem, File... files) throws IOException { - return addSection(softwareSystem, "Deployment View", GROUP3, files); - } - - /** - * Adds a "Deployment View" section relating to a {@link SoftwareSystem}. - * - * @param softwareSystem the {@link SoftwareSystem} the documentation content relates to - * @param format the {@link Format} of the documentation content - * @param content a String containing the documentation content - * @return a documentation {@link Section} - */ - public Section addDeploymentViewSection(SoftwareSystem softwareSystem, Format format, String content) { - return addSection(softwareSystem, "Deployment View", GROUP3, format, content); - } - - /** - * Adds a "Crosscutting Concepts" section relating to a {@link SoftwareSystem} from one or more files. - * - * @param softwareSystem the {@link SoftwareSystem} the documentation content relates to - * @param files one or more File objects that point to the documentation content - * @return a documentation {@link Section} - * @throws IOException if there is an error reading the files - */ - public Section addCrosscuttingConceptsSection(SoftwareSystem softwareSystem, File... files) throws IOException { - return addSection(softwareSystem, "Crosscutting Concepts", GROUP3, files); - } - - /** - * Adds a "Crosscutting Concepts" section relating to a {@link SoftwareSystem}. - * - * @param softwareSystem the {@link SoftwareSystem} the documentation content relates to - * @param format the {@link Format} of the documentation content - * @param content a String containing the documentation content - * @return a documentation {@link Section} - */ - public Section addCrosscuttingConceptsSection(SoftwareSystem softwareSystem, Format format, String content) { - return addSection(softwareSystem, "Crosscutting Concepts", GROUP3, format, content); - } - - /** - * Adds an "Architectural Decisions" section relating to a {@link SoftwareSystem} from one or more files. - * - * @param softwareSystem the {@link SoftwareSystem} the documentation content relates to - * @param files one or more File objects that point to the documentation content - * @return a documentation {@link Section} - * @throws IOException if there is an error reading the files - */ - public Section addArchitecturalDecisionsSection(SoftwareSystem softwareSystem, File... files) throws IOException { - return addSection(softwareSystem, "Architectural Decisions", GROUP3, files); - } - - /** - * Adds an "Architectural Decisions" section relating to a {@link SoftwareSystem}. - * - * @param softwareSystem the {@link SoftwareSystem} the documentation content relates to - * @param format the {@link Format} of the documentation content - * @param content a String containing the documentation content - * @return a documentation {@link Section} - */ - public Section addArchitecturalDecisionsSection(SoftwareSystem softwareSystem, Format format, String content) { - return addSection(softwareSystem, "Architectural Decisions", GROUP3, format, content); - } - - /** - * Adds a "Quality Requirements" section relating to a {@link SoftwareSystem} from one or more files. - * - * @param softwareSystem the {@link SoftwareSystem} the documentation content relates to - * @param files one or more File objects that point to the documentation content - * @return a documentation {@link Section} - * @throws IOException if there is an error reading the files - */ - public Section addQualityRequirementsSection(SoftwareSystem softwareSystem, File... files) throws IOException { - return addSection(softwareSystem, "Quality Requirements", GROUP2, files); - } - - /** - * Adds a "Quality Requirements" section relating to a {@link SoftwareSystem}. - * - * @param softwareSystem the {@link SoftwareSystem} the documentation content relates to - * @param format the {@link Format} of the documentation content - * @param content a String containing the documentation content - * @return a documentation {@link Section} - */ - public Section addQualityRequirementsSection(SoftwareSystem softwareSystem, Format format, String content) { - return addSection(softwareSystem, "Quality Requirements", GROUP2, format, content); - } - - /** - * Adds a "Risks and Technical Debt" section relating to a {@link SoftwareSystem} from one or more files. - * - * @param softwareSystem the {@link SoftwareSystem} the documentation content relates to - * @param files one or more File objects that point to the documentation content - * @return a documentation {@link Section} - * @throws IOException if there is an error reading the files - */ - public Section addRisksAndTechnicalDebtSection(SoftwareSystem softwareSystem, File... files) throws IOException { - return addSection(softwareSystem, "Risks and Technical Debt", GROUP4, files); - } - - /** - * Adds a "Risks and Technical Debt" section relating to a {@link SoftwareSystem}. - * - * @param softwareSystem the {@link SoftwareSystem} the documentation content relates to - * @param format the {@link Format} of the documentation content - * @param content a String containing the documentation content - * @return a documentation {@link Section} - */ - public Section addRisksAndTechnicalDebtSection(SoftwareSystem softwareSystem, Format format, String content) { - return addSection(softwareSystem, "Risks and Technical Debt", GROUP4, format, content); - } - - /** - * Adds a "Glossary" section relating to a {@link SoftwareSystem} from one or more files. - * - * @param softwareSystem the {@link SoftwareSystem} the documentation content relates to - * @param files one or more File objects that point to the documentation content - * @return a documentation {@link Section} - * @throws IOException if there is an error reading the files - */ - public Section addGlossarySection(SoftwareSystem softwareSystem, File... files) throws IOException { - return addSection(softwareSystem, "Glossary", GROUP5, files); - } - - /** - * Adds a "Glossary" section relating to a {@link SoftwareSystem}. - * - * @param softwareSystem the {@link SoftwareSystem} the documentation content relates to - * @param format the {@link Format} of the documentation content - * @param content a String containing the documentation content - * @return a documentation {@link Section} - */ - public Section addGlossarySection(SoftwareSystem softwareSystem, Format format, String content) { - return addSection(softwareSystem, "Glossary", GROUP5, format, content); - } - - @Override - protected TemplateMetadata getMetadata() { - return new TemplateMetadata("arc42", "Dr. Gernot Starke and Dr. Peter Hruschka", "http://arc42.org"); - } - -} \ No newline at end of file diff --git a/structurizr-core/src/com/structurizr/documentation/AutomaticDocumentationTemplate.java b/structurizr-core/src/com/structurizr/documentation/AutomaticDocumentationTemplate.java deleted file mode 100644 index 32ef235f0..000000000 --- a/structurizr-core/src/com/structurizr/documentation/AutomaticDocumentationTemplate.java +++ /dev/null @@ -1,99 +0,0 @@ -package com.structurizr.documentation; - -import com.structurizr.Workspace; -import com.structurizr.model.Element; -import com.structurizr.model.SoftwareSystem; - -import java.io.File; -import java.io.IOException; -import java.nio.file.Files; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -/** - * This template allows you to 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. - */ -public class AutomaticDocumentationTemplate extends DocumentationTemplate { - - public AutomaticDocumentationTemplate(Workspace workspace) { - super(workspace); - } - - /** - * Adds all files in the specified directory, each in its own section. - * - * @param directory the directory to scan - * @return a List of Section objects - * @throws IOException if there is an error reading the files in the directory - */ - public List
addSections(File directory) throws IOException { - return add(null, directory); - } - - /** - * Adds all files in the specified directory, each in its own section, related to a software system. - * - * @param directory the directory to scan - * @param softwareSystem the SoftwareSystem to associate the documentation with - * @return a List of Section objects - * @throws IOException if there is an error reading the files in the directory - */ - public List
addSections(SoftwareSystem softwareSystem, File directory) throws IOException { - if (softwareSystem == null) { - throw new IllegalArgumentException("A software system must be specified."); - } - - return add(softwareSystem, directory); - } - - private List
add(SoftwareSystem softwareSystem, File directory) throws IOException { - List
sections = new ArrayList<>(); - - if (!directory.exists()) { - throw new IllegalArgumentException(directory.getCanonicalPath() + " does not exist."); - } - - if (!directory.isDirectory()) { - throw new IllegalArgumentException(directory.getCanonicalPath() + " is not a directory."); - } - - File[] filesInDirectory = directory.listFiles(); - if (filesInDirectory != null) { - Arrays.sort(filesInDirectory); - - for (File file : filesInDirectory) { - Format format = FormatFinder.findFormat(file); - String sectionDefinition = ""; - - if (format == Format.Markdown) { - sectionDefinition = "##"; - } else if (format == Format.AsciiDoc) { - sectionDefinition = "=="; - } - - String content = new String(Files.readAllBytes(file.toPath()), "UTF-8"); - String sectionName = file.getName(); - Matcher matcher = Pattern.compile("^" + sectionDefinition + " (.*?)$", Pattern.MULTILINE).matcher(content); - if (matcher.find()) { - sectionName = matcher.group(1); - } - - Section section = addSection(softwareSystem, sectionName, GROUP1, format, content); - sections.add(section); - } - } - - return sections; - } - - @Override - protected TemplateMetadata getMetadata() { - return null; - } - -} \ No newline at end of file diff --git a/structurizr-core/src/com/structurizr/documentation/Documentation.java b/structurizr-core/src/com/structurizr/documentation/Documentation.java deleted file mode 100644 index 46ad73203..000000000 --- a/structurizr-core/src/com/structurizr/documentation/Documentation.java +++ /dev/null @@ -1,128 +0,0 @@ -package com.structurizr.documentation; - -import com.fasterxml.jackson.annotation.JsonIgnore; -import com.structurizr.model.Element; -import com.structurizr.model.Model; -import com.structurizr.model.SoftwareSystem; -import com.structurizr.util.ImageUtils; - -import java.io.File; -import java.io.IOException; -import java.nio.file.Files; -import java.util.Arrays; -import java.util.HashSet; -import java.util.Set; - -/** - * Represents the documentation within a workspace - 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 Model model; - private Set
sections = new HashSet<>(); - private Set images = new HashSet<>(); - private TemplateMetadata template; - - Documentation() { - } - - public Documentation(Model model) { - if (model == null) { - throw new IllegalArgumentException("A model must be specified."); - } - - this.model = model; - } - - @JsonIgnore - Model getModel() { - return model; - } - - public void setModel(Model model) { - this.model = model; - } - - final Section addSection(Element element, String type, int group, Format format, String content) { - if (group < 1) { - group = 1; - } else if (group > 5) { - group = 5; - } - - Section section = new Section(element, type, calculateOrder(), group, format, content); - if (!sections.contains(section)) { - sections.add(section); - return section; - } else { - throw new IllegalArgumentException("A section of type " + type + - (element != null ? " for " + element.getName() : "") - + " already exists."); - } - } - - private int calculateOrder() { - return sections.size()+1; - } - - /** - * Gets the set of {@link Section}s. - * - * @return a Set of {@link Section} objects - */ - public Set
getSections() { - return new HashSet<>(sections); - } - - void setSections(Set
sections) { - this.sections = sections; - } - - void addImage(Image image) { - images.add(image); - } - - /** - * Gets the set of {@link Image}s in this workspace. - * - * @return a Set of {@link Image} objects - */ - public Set getImages() { - return new HashSet<>(images); - } - - void setImages(Set images) { - this.images = images; - } - - public void hydrate() { - for (Section section : sections) { - if (section.getElementId() != null && section.getElementId().trim().length() > 0) { - section.setElement(model.getElement(section.getElementId())); - } - } - } - - @JsonIgnore - public boolean isEmpty() { - return sections.isEmpty() && images.isEmpty(); - } - - /** - * Gets the template metadata associated with this documentation. - * - * @return a TemplateMetadata object, or null if there is none - */ - public TemplateMetadata getTemplate() { - return template; - } - - void setTemplate(TemplateMetadata template) { - this.template = template; - } - -} \ No newline at end of file diff --git a/structurizr-core/src/com/structurizr/documentation/DocumentationTemplate.java b/structurizr-core/src/com/structurizr/documentation/DocumentationTemplate.java deleted file mode 100644 index 795c87c2f..000000000 --- a/structurizr-core/src/com/structurizr/documentation/DocumentationTemplate.java +++ /dev/null @@ -1,267 +0,0 @@ -package com.structurizr.documentation; - -import com.structurizr.Workspace; -import com.structurizr.model.Component; -import com.structurizr.model.Container; -import com.structurizr.model.Element; -import com.structurizr.model.SoftwareSystem; -import com.structurizr.util.ImageUtils; - -import java.io.File; -import java.io.IOException; -import java.nio.file.Files; -import java.util.Arrays; -import java.util.Collection; -import java.util.HashSet; - -/** - * The superclass for all documentation templates. - */ -public abstract class DocumentationTemplate { - - public static final int GROUP1 = 1; - public static final int GROUP2 = 2; - public static final int GROUP3 = 3; - public static final int GROUP4 = 4; - public static final int GROUP5 = 5; - - private Documentation documentation; - - /** - * Creates a new documentation template for the given workspace. - * - * @param workspace the Workspace instance to create documentation for - */ - public DocumentationTemplate(Workspace workspace) { - if (workspace == null) { - throw new IllegalArgumentException("A workspace must be specified."); - } - - this.documentation = workspace.getDocumentation(); - documentation.setTemplate(getMetadata()); - } - - /** - * Adds a custom section from one or more files, that isn't related to any element in the model. - * - * @param name the name of the section - * @param group the group of the section (an integer between 1 and 5) - * @param files one or more File objects that point to the documentation content - * @return a documentation {@link Section} - * @throws IOException if there is an error reading the files - */ - public Section addSection(String name, int group, File... files) throws IOException { - return add(null, name, group, files); - } - - /** - * Adds a custom section, that isn't related to any element in the model. - * - * @param name the name of the section - * @param group the group of the section (an integer between 1 and 5) - * @param format the {@link Format} of the documentation content - * @param content a String containing the documentation content - * @return a documentation {@link Section} - */ - public Section addSection(String name, int group, Format format, String content) { - return add(null, name, group, format, content); - } - - /** - * Adds a section relating to a {@link SoftwareSystem} from one or more files. - * - * @param softwareSystem the {@link SoftwareSystem} the documentation content relates to - * @param name the name of the section - * @param group the group of the section (an integer between 1 and 5) - * @param files one or more File objects that point to the documentation content - * @return a documentation {@link Section} - * @throws IOException if there is an error reading the files - */ - public Section addSection(SoftwareSystem softwareSystem, String name, int group, File... files) throws IOException { - return add(softwareSystem, name, group, files); - } - - /** - * Adds a section relating to a {@link SoftwareSystem}. - * - * @param softwareSystem the {@link SoftwareSystem} the documentation content relates to - * @param name the name of the section - * @param group the group of the section (an integer between 1 and 5) - * @param format the {@link Format} of the documentation content - * @param content a String containing the documentation content - * @return a documentation {@link Section} - */ - public Section addSection(SoftwareSystem softwareSystem, String name, int group, Format format, String content) { - return add(softwareSystem, name, group, format, content); - } - - /** - * Adds a section relating to a {@link Container} from one or more files. - * - * @param container the {@link Container} the documentation content relates to - * @param name the name of the section - * @param group the group of the section (an integer between 1 and 5) - * @param files one or more File objects that point to the documentation content - * @return a documentation {@link Section} - * @throws IOException if there is an error reading the files - */ - public Section addSection(Container container, String name, int group, File... files) throws IOException { - return add(container, name, group, files); - } - - /** - * Adds a section relating to a {@link Container}. - * - * @param container the {@link Container} the documentation content relates to - * @param name the name of the section - * @param group the group of the section (an integer between 1 and 5) - * @param format the {@link Format} of the documentation content - * @param content a String containing the documentation content - * @return a documentation {@link Section} - */ - public Section addSection(Container container, String name, int group, Format format, String content) { - return add(container, name, group, format, content); - } - - /** - * Adds a section relating to a {@link Component} from one or more files. - * - * @param component the {@link Component} the documentation content relates to - * @param name the name of the section - * @param group the group of the section (an integer between 1 and 5) - * @param files one or more File objects that point to the documentation content - * @return a documentation {@link Section} - * @throws IOException if there is an error reading the files - */ - public Section addSection(Component component, String name, int group, File... files) throws IOException { - return add(component, name, group, files); - } - - /** - * Adds a section relating to a {@link Component}. - * - * @param component the {@link Component} the documentation content relates to - * @param name the name of the section - * @param group the group of the section (an integer between 1 and 5) - * @param format the {@link Format} of the documentation content - * @param content a String containing the documentation content - * @return a documentation {@link Section} - */ - public Section addSection(Component component, String name, int group, Format format, String content) { - return add(component, name, group, format, content); - } - - private Section add(Element element, String type, int group, Format format, String content) { - return documentation.addSection(element, type, group, format, content); - } - - private Section add(Element element, String type, int group, File... files) throws IOException { - FormattedContent content = readFiles(files); - return documentation.addSection(element, type, group, content.getFormat(), content.getContent()); - } - - private FormattedContent readFiles(File... files) throws IOException { - if (files == null || files.length == 0) { - throw new IllegalArgumentException("One or more files must be specified."); - } - - Format format = Format.Markdown; - StringBuilder content = new StringBuilder(); - for (File file : files) { - if (file == null) { - throw new IllegalArgumentException("One or more files must be specified."); - } - - if (!file.exists()) { - throw new IllegalArgumentException(file.getCanonicalPath() + " does not exist."); - } - - if (content.length() > 0) { - content.append(System.lineSeparator()); - } - - if (file.isFile()) { - format = FormatFinder.findFormat(file); - content.append(new String(Files.readAllBytes(file.toPath()), "UTF-8")); - } else if (file.isDirectory()) { - File[] filesInDirectory = file.listFiles(); - if (filesInDirectory != null) { - Arrays.sort(filesInDirectory); - content.append(readFiles(filesInDirectory).getContent()); - } - } - } - - return new FormattedContent(content.toString(), format); - } - - /** - * Adds png/jpg/jpeg/gif images in the given directory to the workspace. - * - * @param path a File descriptor representing a directory on disk - * @return a Collection of Image objects - * @throws IOException if the path can't be accessed - */ - public Collection addImages(File path) throws IOException { - if (path == null) { - throw new IllegalArgumentException("Directory path must not be null."); - } else if (!path.exists()) { - throw new IllegalArgumentException("The directory " + path.getCanonicalPath() + " does not exist."); - } else if (!path.isDirectory()) { - throw new IllegalArgumentException(path.getCanonicalPath() + " is not a directory."); - } - - return addImagesFromPath("", path); - } - - private Collection addImagesFromPath(String root, File path) throws IOException { - Collection images = new HashSet<>(); - - File[] files = path.listFiles(); - if (files != null) { - for (File file : files) { - String name = file.getName().toLowerCase(); - if (file.isDirectory()) { - images.addAll(addImagesFromPath(file.getName() + "/", file)); - } else { - if (name.endsWith(".png") || name.endsWith(".jpg") || name.endsWith(".jpeg") || name.endsWith(".gif")) { - Image image = addImage(file); - - if (!root.isEmpty()) { - image.setName(root + image.getName()); - } - - images.add(image); - } - } - } - } - - return images; - } - - /** - * Adds an image from the given file to the workspace. - * - * @param file a File descriptor representing an image file on disk - * @return an Image object representing the image added - * @throws IOException if there is an error reading the image - */ - public Image addImage(File file) throws IOException { - String contentType = ImageUtils.getContentType(file); - String base64Content = ImageUtils.getImageAsBase64(file); - - Image image = new Image(file.getName(), contentType, base64Content); - documentation.addImage(image); - - return image; - } - - /** - * Gets the metadata associated with this template. - * - * @return a TemplateMetadata object, or null if there is none - */ - protected abstract TemplateMetadata getMetadata(); - -} \ No newline at end of file diff --git a/structurizr-core/src/com/structurizr/documentation/FormatFinder.java b/structurizr-core/src/com/structurizr/documentation/FormatFinder.java deleted file mode 100644 index 55d5857ab..000000000 --- a/structurizr-core/src/com/structurizr/documentation/FormatFinder.java +++ /dev/null @@ -1,35 +0,0 @@ -package com.structurizr.documentation; - -import java.io.File; -import java.util.Arrays; -import java.util.HashSet; -import java.util.Set; - -class FormatFinder { - - private static Set MARKDOWN_EXTENSIONS = new HashSet<>(Arrays.asList(new String[] { - ".md", ".markdown", ".text" - })); - - private static Set ASCIIDOC_EXTENSIONS = new HashSet<>(Arrays.asList(new String[] { - ".asciidoc", ".adoc", ".asc" - })); - - 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; - } - - } - -} diff --git a/structurizr-core/src/com/structurizr/documentation/FormattedContent.java b/structurizr-core/src/com/structurizr/documentation/FormattedContent.java deleted file mode 100644 index f8d73a484..000000000 --- a/structurizr-core/src/com/structurizr/documentation/FormattedContent.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.structurizr.documentation; - -class FormattedContent { - - private String content; - private Format format; - - FormattedContent(String content, Format format) { - this.content = content; - this.format = format; - } - - String getContent() { - return content; - } - - Format getFormat() { - return format; - } - -} diff --git a/structurizr-core/src/com/structurizr/documentation/Image.java b/structurizr-core/src/com/structurizr/documentation/Image.java deleted file mode 100644 index 8346b667f..000000000 --- a/structurizr-core/src/com/structurizr/documentation/Image.java +++ /dev/null @@ -1,45 +0,0 @@ -package com.structurizr.documentation; - -/** - * Represents a base64 encoded image (png/jpg/gif). - */ -public final class Image { - - private String name; - private String content; - private String type; - - Image() { - } - - Image(String name, String type, String content) { - this.name = name; - this.type = type; - this.content = content; - } - - public String getName() { - return name; - } - - void setName(String name) { - this.name = name; - } - - public String getContent() { - return content; - } - - void setContent(String content) { - this.content = content; - } - - public String getType() { - return type; - } - - void setType(String type) { - this.type = type; - } - -} diff --git a/structurizr-core/src/com/structurizr/documentation/Section.java b/structurizr-core/src/com/structurizr/documentation/Section.java deleted file mode 100644 index a20a51f80..000000000 --- a/structurizr-core/src/com/structurizr/documentation/Section.java +++ /dev/null @@ -1,118 +0,0 @@ -package com.structurizr.documentation; - -import com.fasterxml.jackson.annotation.JsonIgnore; -import com.structurizr.model.Element; - -/** - * A documentation section. - */ -public final class Section { - - private Element element; - private String elementId; - - private String type; - private int order; - private int group; - private Format format; - private String content; - - Section() { - } - - Section(Element element, String type, int order, int group, Format format, String content) { - this.element = element; - this.type = type; - this.order = order; - this.group = group; - this.format = format; - this.content = content; - } - - @JsonIgnore - public Element getElement() { - return element; - } - - void setElement(Element element) { - this.element = element; - } - - public String getElementId() { - if (this.element != null) { - return this.element.getId(); - } else { - return elementId; - } - } - - void setElementId(String elementId) { - this.elementId = elementId; - } - - public String getType() { - return type; - } - - void setType(String type) { - this.type = type; - } - - public int getOrder() { - return order; - } - - public void setOrder(int order) { - this.order = order; - } - - public int getGroup() { - return group; - } - - void setGroup(int group) { - this.group = group; - } - - public Format getFormat() { - return format; - } - - void setFormat(Format format) { - this.format = format; - } - - public String getContent() { - return content; - } - - void setContent(String content) { - this.content = content; - } - - @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()) && getType().equals(section.getType()); - } else { - return getType().equals(section.getType()); - } - } - - @Override - public int hashCode() { - int result = getElementId() != null ? getElementId().hashCode() : 0; - result = 31 * result + getType().hashCode(); - return result; - } - -} diff --git a/structurizr-core/src/com/structurizr/documentation/StructurizrDocumentationTemplate.java b/structurizr-core/src/com/structurizr/documentation/StructurizrDocumentationTemplate.java deleted file mode 100644 index 66e1bacdb..000000000 --- a/structurizr-core/src/com/structurizr/documentation/StructurizrDocumentationTemplate.java +++ /dev/null @@ -1,412 +0,0 @@ -package com.structurizr.documentation; - -import com.structurizr.Workspace; -import com.structurizr.model.Component; -import com.structurizr.model.Container; -import com.structurizr.model.SoftwareSystem; - -import java.io.File; -import java.io.IOException; - -/** - *

- * A simple documentation template, based upon the "software guidebook" concept in Simon Brown's - * Software Architecture for Developers - * book, with the following sections: - *

- * - *
    - *
  • Context (1)
  • - *
  • Functional Overview (2)
  • - *
  • Quality Attributes (2)
  • - *
  • Constraints (2)
  • - *
  • Principles (2)
  • - *
  • Software Architecture (3)
  • - *
  • Containers (3)
  • - *
  • Components (3)
  • - *
  • Code (3)
  • - *
  • Data (3)
  • - *
  • Infrastructure Architecture (4)
  • - *
  • Deployment (4)
  • - *
  • Development Environment (4)
  • - *
  • Operation and Support (4)
  • - *
  • Decision Log (5)
  • - *
- * - *

- * The number in parentheses () represents the grouping, which is simply used to colour code - * section navigation buttons when rendered. - *

- */ -public class StructurizrDocumentationTemplate extends DocumentationTemplate { - - public StructurizrDocumentationTemplate(Workspace workspace) { - super(workspace); - } - - /** - * Adds a "Context" section relating to a {@link SoftwareSystem} from one or more files. - * - * @param softwareSystem the {@link SoftwareSystem} the documentation content relates to - * @param files one or more File objects that point to the documentation content - * @return a documentation {@link Section} - * @throws IOException if there is an error reading the files - */ - public Section addContextSection(SoftwareSystem softwareSystem, File... files) throws IOException { - return addSection(softwareSystem, "Context", GROUP1, files); - } - - /** - * Adds a "Context" section relating to a {@link SoftwareSystem}. - * - * @param softwareSystem the {@link SoftwareSystem} the documentation content relates to - * @param format the {@link Format} of the documentation content - * @param content a String containing the documentation content - * @return a documentation {@link Section} - */ - public Section addContextSection(SoftwareSystem softwareSystem, Format format, String content) { - return addSection(softwareSystem, "Context", GROUP1, format, content); - } - - /** - * Adds a "Functional Overview" section relating to a {@link SoftwareSystem} from one or more files. - * - * @param softwareSystem the {@link SoftwareSystem} the documentation content relates to - * @param files one or more File objects that point to the documentation content - * @return a documentation {@link Section} - * @throws IOException if there is an error reading the files - */ - public Section addFunctionalOverviewSection(SoftwareSystem softwareSystem, File... files) throws IOException { - return addSection(softwareSystem, "Functional Overview", GROUP2, files); - } - - /** - * Adds a "Functional Overview" section relating to a {@link SoftwareSystem}. - * - * @param softwareSystem the {@link SoftwareSystem} the documentation content relates to - * @param format the {@link Format} of the documentation content - * @param content a String containing the documentation content - * @return a documentation {@link Section} - */ - public Section addFunctionalOverviewSection(SoftwareSystem softwareSystem, Format format, String content) { - return addSection(softwareSystem, "Functional Overview", GROUP2, format, content); - } - - /** - * Adds a "Quality Attributes" section relating to a {@link SoftwareSystem} from one or more files. - * - * @param softwareSystem the {@link SoftwareSystem} the documentation content relates to - * @param files one or more File objects that point to the documentation content - * @return a documentation {@link Section} - * @throws IOException if there is an error reading the files - */ - public Section addQualityAttributesSection(SoftwareSystem softwareSystem, File... files) throws IOException { - return addSection(softwareSystem, "Quality Attributes", GROUP2, files); - } - - /** - * Adds a "Quality Attributes" section relating to a {@link SoftwareSystem}. - * - * @param softwareSystem the {@link SoftwareSystem} the documentation content relates to - * @param format the {@link Format} of the documentation content - * @param content a String containing the documentation content - * @return a documentation {@link Section} - */ - public Section addQualityAttributesSection(SoftwareSystem softwareSystem, Format format, String content) { - return addSection(softwareSystem, "Quality Attributes", GROUP2, format, content); - } - - /** - * Adds a "Constraints" section relating to a {@link SoftwareSystem} from one or more files. - * - * @param softwareSystem the {@link SoftwareSystem} the documentation content relates to - * @param files one or more File objects that point to the documentation content - * @return a documentation {@link Section} - * @throws IOException if there is an error reading the files - */ - public Section addConstraintsSection(SoftwareSystem softwareSystem, File... files) throws IOException { - return addSection(softwareSystem, "Constraints", GROUP2, files); - } - - /** - * Adds a "Constraints" section relating to a {@link SoftwareSystem}. - * - * @param softwareSystem the {@link SoftwareSystem} the documentation content relates to - * @param format the {@link Format} of the documentation content - * @param content a String containing the documentation content - * @return a documentation {@link Section} - */ - public Section addConstraintsSection(SoftwareSystem softwareSystem, Format format, String content) { - return addSection(softwareSystem, "Constraints", GROUP2, format, content); - } - - /** - * Adds a "Principles" section relating to a {@link SoftwareSystem} from one or more files. - * - * @param softwareSystem the {@link SoftwareSystem} the documentation content relates to - * @param files one or more File objects that point to the documentation content - * @return a documentation {@link Section} - * @throws IOException if there is an error reading the files - */ - public Section addPrinciplesSection(SoftwareSystem softwareSystem, File... files) throws IOException { - return addSection(softwareSystem, "Principles", GROUP2, files); - } - - /** - * Adds a "Principles" section relating to a {@link SoftwareSystem}. - * - * @param softwareSystem the {@link SoftwareSystem} the documentation content relates to - * @param format the {@link Format} of the documentation content - * @param content a String containing the documentation content - * @return a documentation {@link Section} - */ - public Section addPrinciplesSection(SoftwareSystem softwareSystem, Format format, String content) { - return addSection(softwareSystem, "Principles", GROUP2, format, content); - } - - /** - * Adds a "Software Architecture" section relating to a {@link SoftwareSystem} from one or more files. - * - * @param softwareSystem the {@link SoftwareSystem} the documentation content relates to - * @param files one or more File objects that point to the documentation content - * @return a documentation {@link Section} - * @throws IOException if there is an error reading the files - */ - public Section addSoftwareArchitectureSection(SoftwareSystem softwareSystem, File... files) throws IOException { - return addSection(softwareSystem, "Software Architecture", GROUP3, files); - } - - /** - * Adds a "Software Architecture" section relating to a {@link SoftwareSystem}. - * - * @param softwareSystem the {@link SoftwareSystem} the documentation content relates to - * @param format the {@link Format} of the documentation content - * @param content a String containing the documentation content - * @return a documentation {@link Section} - */ - public Section addSoftwareArchitectureSection(SoftwareSystem softwareSystem, Format format, String content) { - return addSection(softwareSystem, "Software Architecture", GROUP3, format, content); - } - - /** - * Adds a "Containers" section relating to a {@link SoftwareSystem} from one or more files. - * - * @param softwareSystem the {@link SoftwareSystem} the documentation content relates to - * @param files one or more File objects that point to the documentation content - * @return a documentation {@link Section} - * @throws IOException if there is an error reading the files - */ - public Section addContainersSection(SoftwareSystem softwareSystem, File... files) throws IOException { - return addSection(softwareSystem, "Containers", GROUP3, files); - } - - /** - * Adds a "Containers" section relating to a {@link SoftwareSystem}. - * - * @param softwareSystem the {@link SoftwareSystem} the documentation content relates to - * @param format the {@link Format} of the documentation content - * @param content a String containing the documentation content - * @return a documentation {@link Section} - */ - public Section addContainersSection(SoftwareSystem softwareSystem, Format format, String content) { - return addSection(softwareSystem, "Containers", GROUP3, format, content); - } - - /** - * Adds a "Components" section relating to a {@link Container} from one or more files. - * - * @param container the {@link Container} the documentation content relates to - * @param files one or more File objects that point to the documentation content - * @return a documentation {@link Section} - * @throws IOException if there is an error reading the files - */ - public Section addComponentsSection(Container container, File... files) throws IOException { - return addSection(container, "Components", GROUP3, files); - } - - /** - * Adds a "Components" section relating to a {@link Container}. - * - * @param container the {@link Container} the documentation content relates to - * @param format the {@link Format} of the documentation content - * @param content a String containing the documentation content - * @return a documentation {@link Section} - */ - public Section addComponentsSection(Container container, Format format, String content) { - return addSection(container, "Components", GROUP3, format, content); - } - - /** - * Adds a "Code" section relating to a {@link Component} from one or more files. - * - * @param component the {@link Component} the documentation content relates to - * @param files one or more File objects that point to the documentation content - * @return a documentation {@link Section} - * @throws IOException if there is an error reading the files - */ - public Section addCodeSection(Component component, File... files) throws IOException { - return addSection(component, "Code", GROUP3, files); - } - - /** - * Adds a "Code" section relating to a {@link Component}. - * - * @param component the {@link Component} the documentation content relates to - * @param format the {@link Format} of the documentation content - * @param content a String containing the documentation content - * @return a documentation {@link Section} - */ - public Section addCodeSection(Component component, Format format, String content) { - return addSection(component, "Code", GROUP3, format, content); - } - - /** - * Adds a "Data" section relating to a {@link SoftwareSystem} from one or more files. - * - * @param softwareSystem the {@link SoftwareSystem} the documentation content relates to - * @param files one or more File objects that point to the documentation content - * @return a documentation {@link Section} - * @throws IOException if there is an error reading the files - */ - public Section addDataSection(SoftwareSystem softwareSystem, File... files) throws IOException { - return addSection(softwareSystem, "Data", GROUP3, files); - } - - /** - * Adds a "Data" section relating to a {@link SoftwareSystem}. - * - * @param softwareSystem the {@link SoftwareSystem} the documentation content relates to - * @param format the {@link Format} of the documentation content - * @param content a String containing the documentation content - * @return a documentation {@link Section} - */ - public Section addDataSection(SoftwareSystem softwareSystem, Format format, String content) { - return addSection(softwareSystem, "Data", GROUP3, format, content); - } - - /** - * Adds an "Infrastructure Architecture" section relating to a {@link SoftwareSystem} from one or more files. - * - * @param softwareSystem the {@link SoftwareSystem} the documentation content relates to - * @param files one or more File objects that point to the documentation content - * @return a documentation {@link Section} - * @throws IOException if there is an error reading the files - */ - public Section addInfrastructureArchitectureSection(SoftwareSystem softwareSystem, File... files) throws IOException { - return addSection(softwareSystem, "Infrastructure Architecture", GROUP4, files); - } - - /** - * Adds a "Infrastructure Architecture" section relating to a {@link SoftwareSystem}. - * - * @param softwareSystem the {@link SoftwareSystem} the documentation content relates to - * @param format the {@link Format} of the documentation content - * @param content a String containing the documentation content - * @return a documentation {@link Section} - */ - public Section addInfrastructureArchitectureSection(SoftwareSystem softwareSystem, Format format, String content) { - return addSection(softwareSystem, "Infrastructure Architecture", GROUP4, format, content); - } - - /** - * Adds a "Deployment" section relating to a {@link SoftwareSystem} from one or more files. - * - * @param softwareSystem the {@link SoftwareSystem} the documentation content relates to - * @param files one or more File objects that point to the documentation content - * @return a documentation {@link Section} - * @throws IOException if there is an error reading the files - */ - public Section addDeploymentSection(SoftwareSystem softwareSystem, File... files) throws IOException { - return addSection(softwareSystem, "Deployment", GROUP4, files); - } - - /** - * Adds a "Deployment" section relating to a {@link SoftwareSystem}. - * - * @param softwareSystem the {@link SoftwareSystem} the documentation content relates to - * @param format the {@link Format} of the documentation content - * @param content a String containing the documentation content - * @return a documentation {@link Section} - */ - public Section addDeploymentSection(SoftwareSystem softwareSystem, Format format, String content) { - return addSection(softwareSystem, "Deployment", GROUP4, format, content); - } - - /** - * Adds a "Development Environment" section relating to a {@link SoftwareSystem} from one or more files. - * - * @param softwareSystem the {@link SoftwareSystem} the documentation content relates to - * @param files one or more File objects that point to the documentation content - * @return a documentation {@link Section} - * @throws IOException if there is an error reading the files - */ - public Section addDevelopmentEnvironmentSection(SoftwareSystem softwareSystem, File... files) throws IOException { - return addSection(softwareSystem, "Development Environment", GROUP4, files); - } - - /** - * Adds a "Development Environment" section relating to a {@link SoftwareSystem}. - * - * @param softwareSystem the {@link SoftwareSystem} the documentation content relates to - * @param format the {@link Format} of the documentation content - * @param content a String containing the documentation content - * @return a documentation {@link Section} - */ - public Section addDevelopmentEnvironmentSection(SoftwareSystem softwareSystem, Format format, String content) { - return addSection(softwareSystem, "Development Environment", GROUP4, format, content); - } - - /** - * Adds an "Operation and Support" section relating to a {@link SoftwareSystem} from one or more files. - * - * @param softwareSystem the {@link SoftwareSystem} the documentation content relates to - * @param files one or more File objects that point to the documentation content - * @return a documentation {@link Section} - * @throws IOException if there is an error reading the files - */ - public Section addOperationAndSupportSection(SoftwareSystem softwareSystem, File... files) throws IOException { - return addSection(softwareSystem, "Operation and Support", GROUP4, files); - } - - /** - * Adds a "Operation and Support" section relating to a {@link SoftwareSystem}. - * - * @param softwareSystem the {@link SoftwareSystem} the documentation content relates to - * @param format the {@link Format} of the documentation content - * @param content a String containing the documentation content - * @return a documentation {@link Section} - */ - public Section addOperationAndSupportSection(SoftwareSystem softwareSystem, Format format, String content) { - return addSection(softwareSystem, "Operation and Support", GROUP4, format, content); - } - - /** - * Adds a "Decision Log" section relating to a {@link SoftwareSystem} from one or more files. - * - * @param softwareSystem the {@link SoftwareSystem} the documentation content relates to - * @param files one or more File objects that point to the documentation content - * @return a documentation {@link Section} - * @throws IOException if there is an error reading the files - */ - public Section addDecisionLogSection(SoftwareSystem softwareSystem, File... files) throws IOException { - return addSection(softwareSystem, "Decision Log", GROUP5, files); - } - - /** - * Adds a "Decision Log" section relating to a {@link SoftwareSystem}. - * - * @param softwareSystem the {@link SoftwareSystem} the documentation content relates to - * @param format the {@link Format} of the documentation content - * @param content a String containing the documentation content - * @return a documentation {@link Section} - */ - public Section addDecisionLogSection(SoftwareSystem softwareSystem, Format format, String content) { - return addSection(softwareSystem, "Decision Log", GROUP5, format, content); - } - - @Override - protected TemplateMetadata getMetadata() { - return new TemplateMetadata("Software Guidebook", "Simon Brown", "https://leanpub.com/visualising-software-architecture"); - } - -} \ No newline at end of file diff --git a/structurizr-core/src/com/structurizr/documentation/TemplateMetadata.java b/structurizr-core/src/com/structurizr/documentation/TemplateMetadata.java deleted file mode 100644 index aab8a44fb..000000000 --- a/structurizr-core/src/com/structurizr/documentation/TemplateMetadata.java +++ /dev/null @@ -1,45 +0,0 @@ -package com.structurizr.documentation; - -/** - * Represents some basic metadata about the documentation template being used. - */ -public class TemplateMetadata { - - private String name; - private String author; - private String url; - - TemplateMetadata() { - } - - public TemplateMetadata(String name, String author, String url) { - this.name = name; - this.author = author; - this.url = url; - } - - public String getName() { - return name; - } - - void setName(String name) { - this.name = name; - } - - public String getAuthor() { - return author; - } - - void setAuthor(String author) { - this.author = author; - } - - public String getUrl() { - return url; - } - - void setUrl(String url) { - this.url = url; - } - -} \ No newline at end of file diff --git a/structurizr-core/src/com/structurizr/documentation/ViewpointsAndPerspectivesDocumentationTemplate.java b/structurizr-core/src/com/structurizr/documentation/ViewpointsAndPerspectivesDocumentationTemplate.java deleted file mode 100644 index 36c39e625..000000000 --- a/structurizr-core/src/com/structurizr/documentation/ViewpointsAndPerspectivesDocumentationTemplate.java +++ /dev/null @@ -1,209 +0,0 @@ -package com.structurizr.documentation; - -import com.structurizr.Workspace; -import com.structurizr.model.SoftwareSystem; - -import java.io.File; -import java.io.IOException; - -/** - *

- * An implementation of the "Viewpoints and Perspectives" documentation template, - * from the "Software Systems Architecture" book by Nick Rozanski and Eoin Woods, consisting of the following sections: - *

- * - *
    - *
  1. Introduction (1)
  2. - *
  3. Glossary (1)
  4. - *
  5. System Stakeholders and Requirements (2)
  6. - *
  7. Architectural Forces (2)
  8. - *
  9. Architectural Views (3)
  10. - *
  11. System Qualities (4)
  12. - *
  13. Appendices (5)
  14. - *
- * - *

- * The number in parentheses () represents the grouping, which is simply used to colour code - * section navigation buttons when rendered. - *

- */ -public class ViewpointsAndPerspectivesDocumentationTemplate extends DocumentationTemplate { - - public ViewpointsAndPerspectivesDocumentationTemplate(Workspace workspace) { - super(workspace); - } - - /** - * Adds a "Introduction" section relating to a {@link SoftwareSystem} from one or more files. - * - * @param softwareSystem the {@link SoftwareSystem} the documentation content relates to - * @param files one or more File objects that point to the documentation content - * @return a documentation {@link Section} - * @throws IOException if there is an error reading the files - */ - public Section addIntroductionSection(SoftwareSystem softwareSystem, File... files) throws IOException { - return addSection(softwareSystem, "Introduction", GROUP1, files); - } - - /** - * Adds a "Introduction" section relating to a {@link SoftwareSystem}. - * - * @param softwareSystem the {@link SoftwareSystem} the documentation content relates to - * @param format the {@link Format} of the documentation content - * @param content a String containing the documentation content - * @return a documentation {@link Section} - */ - public Section addIntroductionSection(SoftwareSystem softwareSystem, Format format, String content) { - return addSection(softwareSystem, "Introduction", GROUP1, format, content); - } - - /** - * Adds a "Glossary" section relating to a {@link SoftwareSystem} from one or more files. - * - * @param softwareSystem the {@link SoftwareSystem} the documentation content relates to - * @param files one or more File objects that point to the documentation content - * @return a documentation {@link Section} - * @throws IOException if there is an error reading the files - */ - public Section addGlossarySection(SoftwareSystem softwareSystem, File... files) throws IOException { - return addSection(softwareSystem, "Glossary", GROUP1, files); - } - - /** - * Adds a "Glossary" section relating to a {@link SoftwareSystem}. - * - * @param softwareSystem the {@link SoftwareSystem} the documentation content relates to - * @param format the {@link Format} of the documentation content - * @param content a String containing the documentation content - * @return a documentation {@link Section} - */ - public Section addGlossarySection(SoftwareSystem softwareSystem, Format format, String content) { - return addSection(softwareSystem, "Glossary", GROUP1, format, content); - } - - /** - * Adds a "System Stakeholders and Requirements" section relating to a {@link SoftwareSystem} from one or more files. - * - * @param softwareSystem the {@link SoftwareSystem} the documentation content relates to - * @param files one or more File objects that point to the documentation content - * @return a documentation {@link Section} - * @throws IOException if there is an error reading the files - */ - public Section addSystemStakeholdersAndRequirementsSection(SoftwareSystem softwareSystem, File... files) throws IOException { - return addSection(softwareSystem, "System Stakeholders and Requirements", GROUP2, files); - } - - /** - * Adds a "System Stakeholders and Requirements" section relating to a {@link SoftwareSystem}. - * - * @param softwareSystem the {@link SoftwareSystem} the documentation content relates to - * @param format the {@link Format} of the documentation content - * @param content a String containing the documentation content - * @return a documentation {@link Section} - */ - public Section addSystemStakeholdersAndRequirementsSection(SoftwareSystem softwareSystem, Format format, String content) { - return addSection(softwareSystem, "System Stakeholders and Requirements", GROUP2, format, content); - } - - /** - * Adds an "Architectural Forces" section relating to a {@link SoftwareSystem} from one or more files. - * - * @param softwareSystem the {@link SoftwareSystem} the documentation content relates to - * @param files one or more File objects that point to the documentation content - * @return a documentation {@link Section} - * @throws IOException if there is an error reading the files - */ - public Section addArchitecturalForcesSection(SoftwareSystem softwareSystem, File... files) throws IOException { - return addSection(softwareSystem, "Architectural Forces", GROUP2, files); - } - - /** - * Adds an "Architectural Forces" section relating to a {@link SoftwareSystem}. - * - * @param softwareSystem the {@link SoftwareSystem} the documentation content relates to - * @param format the {@link Format} of the documentation content - * @param content a String containing the documentation content - * @return a documentation {@link Section} - */ - public Section addArchitecturalForcesSection(SoftwareSystem softwareSystem, Format format, String content) { - return addSection(softwareSystem, "Architectural Forces", GROUP2, format, content); - } - - /** - * Adds an "Architectural Views" section relating to a {@link SoftwareSystem} from one or more files. - * - * @param softwareSystem the {@link SoftwareSystem} the documentation content relates to - * @param files one or more File objects that point to the documentation content - * @return a documentation {@link Section} - * @throws IOException if there is an error reading the files - */ - public Section addArchitecturalViewsSection(SoftwareSystem softwareSystem, File... files) throws IOException { - return addSection(softwareSystem, "Architectural Views", GROUP3, files); - } - - /** - * Adds an "Architectural Views" section relating to a {@link SoftwareSystem}. - * - * @param softwareSystem the {@link SoftwareSystem} the documentation content relates to - * @param format the {@link Format} of the documentation content - * @param content a String containing the documentation content - * @return a documentation {@link Section} - */ - public Section addArchitecturalViewsSection(SoftwareSystem softwareSystem, Format format, String content) { - return addSection(softwareSystem, "Architectural Views", GROUP3, format, content); - } - - /** - * Adds a "System Qualities" section relating to a {@link SoftwareSystem} from one or more files. - * - * @param softwareSystem the {@link SoftwareSystem} the documentation content relates to - * @param files one or more File objects that point to the documentation content - * @return a documentation {@link Section} - * @throws IOException if there is an error reading the files - */ - public Section addSystemQualitiesSection(SoftwareSystem softwareSystem, File... files) throws IOException { - return addSection(softwareSystem, "System Qualities", GROUP4, files); - } - - /** - * Adds a "System Qualities" section relating to a {@link SoftwareSystem}. - * - * @param softwareSystem the {@link SoftwareSystem} the documentation content relates to - * @param format the {@link Format} of the documentation content - * @param content a String containing the documentation content - * @return a documentation {@link Section} - */ - public Section addSystemQualitiesSection(SoftwareSystem softwareSystem, Format format, String content) { - return addSection(softwareSystem, "System Qualities", GROUP4, format, content); - } - - /** - * Adds an "Appendices" section relating to a {@link SoftwareSystem} from one or more files. - * - * @param softwareSystem the {@link SoftwareSystem} the documentation content relates to - * @param files one or more File objects that point to the documentation content - * @return a documentation {@link Section} - * @throws IOException if there is an error reading the files - */ - public Section addAppendicesSection(SoftwareSystem softwareSystem, File... files) throws IOException { - return addSection(softwareSystem, "Appendices", GROUP5, files); - } - - /** - * Adds an "Appendices" section relating to a {@link SoftwareSystem}. - * - * @param softwareSystem the {@link SoftwareSystem} the documentation content relates to - * @param format the {@link Format} of the documentation content - * @param content a String containing the documentation content - * @return a documentation {@link Section} - */ - public Section addAppendicesSection(SoftwareSystem softwareSystem, Format format, String content) { - return addSection(softwareSystem, "Appendices", GROUP5, format, content); - } - - @Override - protected TemplateMetadata getMetadata() { - return new TemplateMetadata("Viewpoints and Perspectives", "Nick Rozanski and Eoin Woods", "https://www.viewpoints-and-perspectives.info"); - } - -} \ No newline at end of file diff --git a/structurizr-core/src/com/structurizr/encryption/EncryptedJsonReader.java b/structurizr-core/src/com/structurizr/encryption/EncryptedJsonReader.java deleted file mode 100644 index a02ad6530..000000000 --- a/structurizr-core/src/com/structurizr/encryption/EncryptedJsonReader.java +++ /dev/null @@ -1,34 +0,0 @@ -package com.structurizr.encryption; - -import com.fasterxml.jackson.databind.DeserializationFeature; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.structurizr.io.WorkspaceReaderException; - -import java.io.IOException; -import java.io.Reader; - -public final class EncryptedJsonReader { - - public EncryptedJsonReader() { - } - - /** - * Reads and parses a workspace definition from a JSON document. - * - * @param reader a Reader on top of the workspace definition - * @return a Workspace object - * @throws WorkspaceReaderException if something goes wrong - */ - public EncryptedWorkspace read(Reader reader) throws WorkspaceReaderException { - try { - ObjectMapper objectMapper = new ObjectMapper(); - objectMapper.enable(DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT); - objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); - - return objectMapper.readValue(reader, EncryptedWorkspace.class); - } catch (IOException ioe) { - throw new WorkspaceReaderException("Could not read JSON", ioe); - } - } - -} \ No newline at end of file diff --git a/structurizr-core/src/com/structurizr/encryption/EncryptedJsonWriter.java b/structurizr-core/src/com/structurizr/encryption/EncryptedJsonWriter.java deleted file mode 100644 index afd238b9d..000000000 --- a/structurizr-core/src/com/structurizr/encryption/EncryptedJsonWriter.java +++ /dev/null @@ -1,47 +0,0 @@ -package com.structurizr.encryption; - -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.SerializationFeature; -import com.structurizr.io.WorkspaceWriterException; - -import java.io.Writer; - -public final class EncryptedJsonWriter { - - private boolean indentOutput = true; - - public EncryptedJsonWriter(boolean indentOutput) { - this.indentOutput = indentOutput; - } - - /** - * Writes an encrypted workspace definition as a JSON string to the specified Writer object. - * - * @param workspace the Workspace object to write - * @param writer the Writer object to write the workspace to - * @throws WorkspaceWriterException if something goes wrong - */ - public void write(EncryptedWorkspace workspace, Writer writer) throws WorkspaceWriterException { - if (workspace == null) { - throw new IllegalArgumentException("EncryptedWorkspace cannot be null."); - } - if (writer == null) { - throw new IllegalArgumentException("Writer cannot be null."); - } - - try { - ObjectMapper objectMapper = new ObjectMapper(); - if (indentOutput) { - objectMapper.enable(SerializationFeature.INDENT_OUTPUT); - } - objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); - objectMapper.setSerializationInclusion(JsonInclude.Include.NON_EMPTY); - - writer.write(objectMapper.writeValueAsString(workspace)); - } catch (Exception e) { - throw new WorkspaceWriterException("Could not write as JSON", e); - } - } - -} diff --git a/structurizr-core/src/com/structurizr/io/json/JsonReader.java b/structurizr-core/src/com/structurizr/io/json/JsonReader.java deleted file mode 100644 index c59b23282..000000000 --- a/structurizr-core/src/com/structurizr/io/json/JsonReader.java +++ /dev/null @@ -1,39 +0,0 @@ -package com.structurizr.io.json; - -import com.fasterxml.jackson.databind.DeserializationFeature; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.structurizr.Workspace; -import com.structurizr.io.WorkspaceReader; -import com.structurizr.io.WorkspaceReaderException; - -import java.io.IOException; -import java.io.Reader; - -/** - * Reads a workspace definition as JSON. - */ -public final class JsonReader implements WorkspaceReader { - - /** - * Reads and parses a workspace definition from a JSON document. - * - * @param reader a Reader on top of the workspace definition - * @return a Workspace object - * @throws WorkspaceReaderException if something goes wrong - */ - public Workspace read(Reader reader) throws WorkspaceReaderException { - try { - ObjectMapper objectMapper = new ObjectMapper(); - objectMapper.enable(DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT); - objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); - - Workspace workspace = objectMapper.readValue(reader, Workspace.class); - workspace.hydrate(); - - return workspace; - } catch (IOException ioe) { - throw new WorkspaceReaderException("Could not read JSON", ioe); - } - } - -} \ No newline at end of file diff --git a/structurizr-core/src/com/structurizr/io/json/JsonWriter.java b/structurizr-core/src/com/structurizr/io/json/JsonWriter.java deleted file mode 100644 index 45fd5b9bf..000000000 --- a/structurizr-core/src/com/structurizr/io/json/JsonWriter.java +++ /dev/null @@ -1,53 +0,0 @@ -package com.structurizr.io.json; - -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.SerializationFeature; -import com.structurizr.Workspace; -import com.structurizr.io.WorkspaceWriter; -import com.structurizr.io.WorkspaceWriterException; - -import java.io.IOException; -import java.io.Writer; - -/** - * Writes a workspace definition as a JSON string. - */ -public final class JsonWriter implements WorkspaceWriter { - - private boolean indentOutput = true; - - public JsonWriter(boolean indentOutput) { - this.indentOutput = indentOutput; - } - - /** - * Writes a workspace definition as a JSON string to the specified Writer object. - * - * @param workspace the Workspace object to write - * @param writer the Writer object to write the workspace to - * @throws WorkspaceWriterException if something goes wrong - */ - public void write(Workspace workspace, Writer writer) throws WorkspaceWriterException { - if (workspace == null) { - throw new IllegalArgumentException("Workspace cannot be null."); - } - if (writer == null) { - throw new IllegalArgumentException("Writer cannot be null."); - } - - try { - ObjectMapper objectMapper = new ObjectMapper(); - if (indentOutput) { - objectMapper.enable(SerializationFeature.INDENT_OUTPUT); - } - objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); - objectMapper.setSerializationInclusion(JsonInclude.Include.NON_EMPTY); - - writer.write(objectMapper.writeValueAsString(workspace)); - } catch (IOException ioe) { - throw new WorkspaceWriterException("Could not write as JSON", ioe); - } - } - -} diff --git a/structurizr-core/src/com/structurizr/io/plantuml/PlantUMLWriter.java b/structurizr-core/src/com/structurizr/io/plantuml/PlantUMLWriter.java deleted file mode 100644 index 55de87467..000000000 --- a/structurizr-core/src/com/structurizr/io/plantuml/PlantUMLWriter.java +++ /dev/null @@ -1,554 +0,0 @@ -package com.structurizr.io.plantuml; - -import com.structurizr.Workspace; -import com.structurizr.io.WorkspaceWriter; -import com.structurizr.io.WorkspaceWriterException; -import com.structurizr.model.*; -import com.structurizr.view.*; - -import java.io.IOException; -import java.io.StringWriter; -import java.io.Writer; -import java.util.ArrayList; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; - -import static java.lang.String.format; -import static java.util.Collections.emptyList; - -/** - * A simple PlantUML writer that outputs diagram definitions that can be copy-pasted - * into http://plantuml.com/plantuml/ ... it supports enterprise context, system context, - * container, component and dynamic diagrams. - * - * Note: This won't work if you have two elements named the same on a diagram. - */ -public final class PlantUMLWriter implements WorkspaceWriter { - - /** Maximum diagram width or height. Defaults to 2000 to match public plantuml.com installation */ - private int sizeLimit = 2000; - private boolean includeNotesForActors = true; - private final Map skinParams = new LinkedHashMap<>(); - - public PlantUMLWriter() { - // add some default skin params - addSkinParam("shadowing", "false"); - addSkinParam("arrowColor", "#707070"); - addSkinParam("actorBorderColor", "#707070"); - addSkinParam("componentBorderColor", "#707070"); - addSkinParam("rectangleBorderColor", "#707070"); - addSkinParam("noteBackgroundColor", "#ffffff"); - addSkinParam("noteBorderColor", "#707070"); - } - - public void addSkinParam(String name, String value) { - skinParams.put(name, value); - } - - public void setIncludeNotesForActors(boolean includeNotesForActors) { - this.includeNotesForActors = includeNotesForActors; - } - - public void setSizeLimit(int sizeLimit) { - this.sizeLimit = sizeLimit; - } - - @Override - public void write(Workspace workspace, Writer writer) throws WorkspaceWriterException { - if (workspace != null && writer != null) { - workspace.getViews().getEnterpriseContextViews().forEach(v -> write(v, writer)); - workspace.getViews().getSystemContextViews().forEach(v -> write(v, writer)); - workspace.getViews().getContainerViews().forEach(v -> write(v, writer)); - workspace.getViews().getComponentViews().forEach(v -> write(v, writer)); - workspace.getViews().getDynamicViews().forEach(v -> write(v, writer)); - workspace.getViews().getDeploymentViews().forEach(v -> write(v, writer)); - } - } - - /** - * Creates PlantUML diagram definitions based upon the specified workspace. - * - * @param workspace a Workspace instance - * @return an array of PlantUML diagram definitions, one per view - * @throws WorkspaceWriterException if something goes wrong - */ - public String[] toPlantUML(Workspace workspace) throws WorkspaceWriterException { - StringWriter stringWriter = new StringWriter(); - write(workspace, stringWriter); - - String diagrams = stringWriter.toString(); - if (diagrams != null && diagrams.contains("@startuml")) { - return stringWriter.toString().split("(?=@startuml)"); - } else { - return new String[0]; - } - } - - public void write(View view, Writer writer) { - if (view != null && writer != null) { - if (EnterpriseContextView.class.isAssignableFrom(view.getClass())) { - write((EnterpriseContextView) view, writer); - } else if (SystemContextView.class.isAssignableFrom(view.getClass())) { - write((SystemContextView) view, writer); - } else if (ContainerView.class.isAssignableFrom(view.getClass())) { - write((ContainerView) view, writer); - } else if (ComponentView.class.isAssignableFrom(view.getClass())) { - write((ComponentView) view, writer); - } else if (DynamicView.class.isAssignableFrom(view.getClass())) { - write((DynamicView) view, writer); - } else if (DeploymentView.class.isAssignableFrom(view.getClass())) { - write((DeploymentView) view, writer); - } - } - } - - private void write(EnterpriseContextView view, Writer writer) { - try { - writeHeader(view, writer); - - view.getElements().stream() - .map(ElementView::getElement) - .filter(e -> e instanceof Person && ((Person)e).getLocation() == Location.External) - .sorted((e1, e2) -> e1.getName().compareTo(e2.getName())) - .forEach(e -> write(view, e, writer, false)); - - view.getElements().stream() - .map(ElementView::getElement) - .filter(e -> e instanceof SoftwareSystem && ((SoftwareSystem)e).getLocation() == Location.External) - .sorted((e1, e2) -> e1.getName().compareTo(e2.getName())) - .forEach(e -> write(view, e, writer, false)); - - String name = view.getModel().getEnterprise() != null ? view.getModel().getEnterprise().getName() : "Enterprise"; - writer.write("package \"" + name + "\" {"); - writer.write(System.lineSeparator()); - - view.getElements().stream() - .map(ElementView::getElement) - .filter(e -> e instanceof Person && ((Person)e).getLocation() == Location.Internal) - .sorted((e1, e2) -> e1.getName().compareTo(e2.getName())) - .forEach(e -> write(view, e, writer, true)); - - view.getElements().stream() - .map(ElementView::getElement) - .filter(e -> e instanceof SoftwareSystem && ((SoftwareSystem)e).getLocation() == Location.Internal) - .sorted((e1, e2) -> e1.getName().compareTo(e2.getName())) - .forEach(e -> write(view, e, writer, true)); - - writer.write("}"); - writer.write(System.lineSeparator()); - - writeRelationships(view, writer); - - writeFooter(writer); - } catch (IOException e) { - e.printStackTrace(); - } - } - - private void write(SystemContextView view, Writer writer) { - try { - writeHeader(view, writer); - - view.getElements().stream() - .map(ElementView::getElement) - .sorted((e1, e2) -> e1.getName().compareTo(e2.getName())) - .forEach(e -> write(view, e, writer, false)); - writeRelationships(view, writer); - - writeFooter(writer); - } catch (IOException e) { - e.printStackTrace(); - } - } - - private void write(ContainerView view, Writer writer) { - try { - writeHeader(view, writer); - - view.getElements().stream() - .filter(ev -> !(ev.getElement() instanceof Container)) - .map(ElementView::getElement) - .sorted((e1, e2) -> e1.getName().compareTo(e2.getName())) - .forEach(e -> write(view, e, writer, false)); - - writer.write("package \"" + view.getSoftwareSystem().getName() + "\" <<" + typeOf(view.getSoftwareSystem()) + ">> {"); - writer.write(System.lineSeparator()); - - view.getElements().stream() - .filter(ev -> ev.getElement() instanceof Container) - .map(ElementView::getElement) - .sorted((e1, e2) -> e1.getName().compareTo(e2.getName())) - .forEach(e -> write(view, e, writer, true)); - - writer.write("}"); - writer.write(System.lineSeparator()); - - writeRelationships(view, writer); - - writeFooter(writer); - } catch (IOException e) { - e.printStackTrace(); - } - } - - private void write(ComponentView view, Writer writer) { - try { - writeHeader(view, writer); - - view.getElements().stream() - .filter(ev -> !(ev.getElement() instanceof Component)) - .map(ElementView::getElement) - .sorted((e1, e2) -> e1.getName().compareTo(e2.getName())) - .forEach(e -> write(view, e, writer, false)); - - writer.write("package \"" + view.getContainer().getName() + "\" <<" + typeOf(view.getContainer()) + ">> {"); - writer.write(System.lineSeparator()); - - view.getElements().stream() - .filter(ev -> ev.getElement() instanceof Component) - .map(ElementView::getElement) - .sorted((e1, e2) -> e1.getName().compareTo(e2.getName())) - .forEach(e -> write(view, e, writer, true)); - - writer.write("}"); - writer.write(System.lineSeparator()); - - writeRelationships(view, writer); - - writeFooter(writer); - } catch (IOException e) { - e.printStackTrace(); - } - } - - private void write(DynamicView view, Writer writer) { - try { - writeHeader(view, writer); - - view.getElements().stream() - .map(ElementView::getElement) - .sorted((e1, e2) -> e1.getName().compareTo(e2.getName())) - .forEach(e -> write(view, e, writer, false)); - - view.getRelationships().stream() - .sorted((rv1, rv2) -> (rv1.getOrder().compareTo(rv2.getOrder()))) - .forEach(relationship -> { - try { - writer.write( - format("%s -[%s]> %s : %s. %s", - idOf(relationship.getRelationship().getSource()), - view.getViewSet().getConfiguration().getStyles().findRelationshipStyle(relationship.getRelationship()).getColor(), - idOf(relationship.getRelationship().getDestination()), - relationship.getOrder(), - hasValue(relationship.getDescription()) ? relationship.getDescription() : hasValue(relationship.getRelationship().getDescription()) ? relationship.getRelationship().getDescription() : "" - ) - ); - writer.write(System.lineSeparator()); - } catch (IOException e) { - e.printStackTrace(); - } - }); - - writeFooter(writer); - } catch (IOException e) { - e.printStackTrace(); - } - } - - private void write(DeploymentView view, Writer writer) { - try { - writeHeader(view, writer); - - view.getElements().stream() - .filter(ev -> ev.getElement() instanceof DeploymentNode && ev.getElement().getParent() == null) - .map(ev -> (DeploymentNode)ev.getElement()) - .sorted((e1, e2) -> e1.getName().compareTo(e2.getName())) - .forEach(e -> write(view, e, writer, 0)); - - writeRelationships(view, writer); - - writeFooter(writer); - } catch (IOException e) { - e.printStackTrace(); - } - } - - private void write(View view, DeploymentNode deploymentNode, Writer writer, int indent) { - try { - writer.write( - format("%snode \"%s\" <<%s>> as %s {", - calculateIndent(indent), - deploymentNode.getName() + (deploymentNode.getInstances() > 1 ? " (x" + deploymentNode.getInstances() + ")" : ""), - typeOf(deploymentNode), - idOf(deploymentNode) - ) - ); - - writer.write(System.lineSeparator()); - - for (DeploymentNode child : deploymentNode.getChildren()) { - write(view, child, writer, indent+1); - } - - for (ContainerInstance containerInstance : deploymentNode.getContainerInstances()) { - write(view, containerInstance, writer, indent+1); - } - - writer.write( - format("%s}", calculateIndent(indent)) - ); - writer.write(System.lineSeparator()); - } catch (IOException e) { - e.printStackTrace(); - } - } - - private void write(View view, ContainerInstance containerInstance, Writer writer, int indent) { - try { - writer.write( - format("%s%s \"%s\" <<%s>> as %s %s", - calculateIndent(indent), - plantumlType(view, containerInstance.getContainer()), - containerInstance.getContainer().getName(), - typeOf(containerInstance), - idOf(containerInstance), - backgroundOf(view, containerInstance.getContainer()) - ) - ); - - - writer.write(System.lineSeparator()); - } catch (IOException e) { - e.printStackTrace(); - } - } - - private String calculateIndent(int indent) { - StringBuilder buf = new StringBuilder(); - - for (int i = 0; i < indent; i++) { - buf.append(" "); - } - - return buf.toString(); - } - - private void write(View view, Element element, Writer writer, boolean indent) { - try { - final String type = plantumlType(view, element); - final List description = lines(element.getDescription()); - - if(description.isEmpty() || "actor".equals(type)) { - writeSimpleElement(view, element, writer, indent, type); - - if (includeNotesForActors) { - writeDescriptionAsNote(element, writer, indent, description); - } - } - else { - final String prefix = indent ? " " : ""; - final String separator = System.lineSeparator(); - final String id = idOf(element); - - writer.write(format("%s%s %s <<%s>> %s [%s", - prefix, type, id, typeOf(element), backgroundOf(view, element), separator)); - writer.write(format("%s %s%s", prefix, element.getName(), separator)); - writer.write(format("%s --%s", prefix, separator)); - for (final String line : description) { - writer.write(format("%s %s%s", prefix, line, separator)); - } - writer.write(format("%s]%s", prefix, separator)); - } - - } catch (IOException e) { - e.printStackTrace(); - } - } - - private void writeSimpleElement(View view, Element element, Writer writer, boolean indent, String type) throws IOException { - writer.write(format("%s%s \"%s\" <<%s>> as %s %s%s", - indent ? " " : "", - type, - element.getName(), - typeOf(element), - idOf(element), - backgroundOf(view, element), - System.lineSeparator())); - } - - private void writeDescriptionAsNote(Element element, Writer writer, boolean indent, List description) throws IOException { - if (!description.isEmpty()) { - final String prefix = indent ? " " : ""; - final String separator = System.lineSeparator(); - final String id = idOf(element); - writer.write(format("%snote right of %s%s", prefix, id, separator)); - for (final String line : description) { - writer.write(format("%s %s%s", prefix, line, separator)); - } - writer.write(format("%send note%s", prefix, separator)); - } - } - - private List lines(final String text) { - if(text==null) { - return emptyList(); - } - final String[] words = text.trim().split("\\s+"); - final List lines = new ArrayList<>(); - 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 { - lines.add(line.toString()); - line.setLength(0); - line.append(word); - } - } - if(line.length()>0) { - lines.add(line.toString()); - } - return lines; - } - - private String backgroundOf(View view, Element element) { - return view.getViewSet().getConfiguration().getStyles().findElementStyle(element).getBackground(); - } - - private String plantumlType(View view, Element element) { - Shape shape = view.getViewSet().getConfiguration().getStyles().findElementStyle(element).getShape(); - - switch(shape) { - case Box: - return element instanceof Component ? "component" : "rectangle"; - case Person: - return "actor"; - case Cylinder: - return "database"; - case Folder: - return "folder"; - case Ellipse: - case Circle: - return "storage"; - default: - return "rectangle"; - } - } - - private void writeRelationships(View view, Writer writer) { - view.getRelationships().stream() - .map(RelationshipView::getRelationship) - .sorted((r1, r2) -> (r1.getSource().getName() + r1.getDestination().getName()).compareTo(r2.getSource().getName() + r2.getDestination().getName())) - .forEach(r -> writeRelationship(view, r, writer)); - } - - private void writeRelationship(View view, Relationship relationship, Writer writer) { - try { - String stereotypeAndDescription = - (hasValue(relationship.getTechnology()) ? "<<" + relationship.getTechnology() + ">>\\n" : "") + - (hasValue(relationship.getDescription()) ? relationship.getDescription() : ""); - - writer.write( - format("%s .[%s].> %s %s", - idOf(relationship.getSource()), - view.getViewSet().getConfiguration().getStyles().findRelationshipStyle(relationship).getColor(), - idOf(relationship.getDestination()), - hasValue(stereotypeAndDescription) ? ": " + stereotypeAndDescription : "" - ) - ); - writer.write(System.lineSeparator()); - } catch (IOException e) { - e.printStackTrace(); - } - } - - private String idOf(Element e) { - return e.getId(); - } - - private String nameOf(Element e) { - return nameOf(e.getName()); - } - - private String nameOf(String s) { - if (s != null) { - return s.replaceAll(" ", "") - .replaceAll("-", ""); - } else { - return ""; - } - } - - private String typeOf(Element e) { - if (e instanceof SoftwareSystem) { - return "Software System"; - } else if (e instanceof Component) { - Component component = (Component)e; - return hasValue(component.getTechnology()) ? component.getTechnology() : "Component"; - } else if (e instanceof DeploymentNode) { - DeploymentNode deploymentNode = (DeploymentNode)e; - return hasValue(deploymentNode.getTechnology()) ? deploymentNode.getTechnology() : "Deployment Node"; - } else if (e instanceof ContainerInstance) { - return "Container"; - } else { - return e.getClass().getSimpleName(); - } - } - - private boolean hasValue(String s) { - return s != null && s.trim().length() > 0; - } - - private void writeHeader(View view, Writer writer) throws IOException { - writer.write("@startuml"); - writer.write(System.lineSeparator()); - - PaperSize size = view.getPaperSize(); - int width; - int height; - if(size==null) { - width = height = sizeLimit; - } - else { - width = size.getWidth(); - height = size.getHeight(); - if(width>sizeLimit || height>sizeLimit) { - int max = Math.max(width, height); - width = (width * sizeLimit) / max; - height = (height * sizeLimit) / max; - } - } - writer.write("scale max "); - writer.write(Integer.toString(width)); - writer.write("x"); - writer.write(Integer.toString(height)); - writer.write(System.lineSeparator()); - - writer.write("title " + view.getName()); - writer.write(System.lineSeparator()); - - if (view.getDescription() != null && view.getDescription().trim().length() > 0) { - writer.write("caption " + view.getDescription()); - writer.write(System.lineSeparator()); - } - - writer.write(System.lineSeparator()); - - writer.write(format("skinparam {%s", System.lineSeparator())); - for (final String name : skinParams.keySet()) { - writer.write(format(" %s %s%s", name, skinParams.get(name), System.lineSeparator())); - } - writer.write(format("}%s", System.lineSeparator())); - } - - private void writeFooter(Writer writer) throws IOException { - writer.write("@enduml"); - writer.write(System.lineSeparator()); - } - -} \ No newline at end of file diff --git a/structurizr-core/src/com/structurizr/io/websequencediagrams/WebSequenceDiagramsWriter.java b/structurizr-core/src/com/structurizr/io/websequencediagrams/WebSequenceDiagramsWriter.java deleted file mode 100644 index fc49c3e20..000000000 --- a/structurizr-core/src/com/structurizr/io/websequencediagrams/WebSequenceDiagramsWriter.java +++ /dev/null @@ -1,64 +0,0 @@ -package com.structurizr.io.websequencediagrams; - -import com.structurizr.Workspace; -import com.structurizr.io.WorkspaceWriter; -import com.structurizr.io.WorkspaceWriterException; -import com.structurizr.model.InteractionStyle; -import com.structurizr.model.Relationship; -import com.structurizr.view.DynamicView; -import com.structurizr.view.RelationshipView; - -import java.io.Writer; -import java.util.Set; -import java.util.TreeSet; - -/** - * A simple writer that outputs a diagram definition 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 final class WebSequenceDiagramsWriter implements WorkspaceWriter { - - private static final String SYNCHRONOUS_INTERACTION = "->"; - private static final String ASYNCHRONOUS_INTERACTION = "->>"; - - @Override - public void write(Workspace workspace, Writer writer) throws WorkspaceWriterException { - if (workspace != null && writer != null) { - try { - for (DynamicView view : workspace.getViews().getDynamicViews()) { - write(view, writer); - } - } catch (Exception e) { - throw new WorkspaceWriterException("There was an error creating a websequencediagram", e); - } - } - } - - private void write(DynamicView view, Writer writer) throws Exception { - writer.write("title " + view.getName() + " - " + view.getKey()); - writer.write(System.lineSeparator()); - writer.write(System.lineSeparator()); - - Set relationships = new TreeSet<>((rv1, rv2) -> rv1.getOrder().compareTo(rv2.getOrder())); - relationships.addAll(view.getRelationships()); - - for (RelationshipView relationshipView : relationships) { - Relationship r = relationshipView.getRelationship(); - // Thing A->Thing B: Description - writer.write(String.format("%s%s%s: %s", - r.getSource().getName(), - r.getInteractionStyle() == InteractionStyle.Synchronous ? SYNCHRONOUS_INTERACTION : ASYNCHRONOUS_INTERACTION, - r.getDestination().getName(), - relationshipView.getDescription() - )); - writer.write(System.lineSeparator()); - } - - writer.write(System.lineSeparator()); - } - -} \ No newline at end of file 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 c05347666..000000000 --- a/structurizr-core/src/com/structurizr/model/CodeElement.java +++ /dev/null @@ -1,225 +0,0 @@ -package com.structurizr.model; - -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) { - this.name = fullyQualifiedTypeName.substring(dot+1, fullyQualifiedTypeName.length()); - this.type = fullyQualifiedTypeName; - } else { - this.name = fullyQualifiedTypeName; - this.type = 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 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 deleted file mode 100644 index 2b05726b4..000000000 --- a/structurizr-core/src/com/structurizr/model/Component.java +++ /dev/null @@ -1,168 +0,0 @@ -package com.structurizr.model; - -import com.fasterxml.jackson.annotation.JsonIgnore; - -import java.util.*; - -/** - * The word "component" is a hugely overloaded term in the software development - * industry, but in this context a component is simply a grouping of related - * functionality encapsulated behind a well-defined interface. If you're using a - * language like Java or C#, the simplest way to think of a component is that - * it's a collection of implementation classes behind an interface. Aspects such - * as how those components are packaged (e.g. one component vs many components - * per JAR file, DLL, shared library, etc) is a separate and orthogonal concern. - */ -public final class Component extends StaticStructureElement { - - private Container parent; - - private String technology; - private Set codeElements = new HashSet<>(); - private long size; - - Component() { - } - - @Override - @JsonIgnore - public Element getParent() { - return parent; - } - - @JsonIgnore - public Container getContainer() { - return parent; - } - - void setParent(Container parent) { - this.parent = parent; - } - - /** - * Gets the technology associated with this component (e.g. "Spring Bean"). - * - * @return the technology, as a String, - * or null if no technology has been specified - */ - public String getTechnology() { - return technology; - } - - /** - * Sets the technology associated with this component (e.g. "Spring Bean"). - * - * @param technology the technology, as a String - */ - 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 String getType() { - Optional optional = codeElements.stream().filter(ce -> ce.getRole() == CodeElementRole.Primary).findFirst(); - if (optional.isPresent()) { - return optional.get().getType(); - } else { - return 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 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() { - if (getType() != null) { - try { - return ClassLoader.getSystemClassLoader().loadClass(getType()).getPackage().getName(); - } catch (ClassNotFoundException e) { - e.printStackTrace(); - } - } - - return null; - } - - @Override - public String getCanonicalName() { - return getParent().getCanonicalName() + CANONICAL_NAME_SEPARATOR + formatForCanonicalName(getName()); - } - - @Override - protected Set getRequiredTags() { - return new LinkedHashSet<>(Arrays.asList(Tags.ELEMENT, Tags.COMPONENT)); - } - -} \ 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 deleted file mode 100644 index 23ddb0c21..000000000 --- a/structurizr-core/src/com/structurizr/model/Container.java +++ /dev/null @@ -1,120 +0,0 @@ -package com.structurizr.model; - -import com.fasterxml.jackson.annotation.JsonIgnore; - -import java.util.Arrays; -import java.util.LinkedHashSet; -import java.util.Optional; -import java.util.Set; - -/** - * A container represents something that hosts code or data. A container is - * something that needs to be running in order for the overall software system - * to work. In real terms, a container is something like a server-side web application, - * a client-side web application, client-side desktop application, a mobile app, - * a microservice, a database schema, a file system, etc. - * - * A container is essentially a context or boundary inside which some code is executed - * or some data is stored. And each container is a separately deployable thing. - */ -public final class Container extends StaticStructureElement { - - private SoftwareSystem parent; - private String technology; - - private Set components = new LinkedHashSet<>(); - - Container() { - } - - @Override - @JsonIgnore - public Element getParent() { - return parent; - } - - @JsonIgnore - public SoftwareSystem getSoftwareSystem() { - return parent; - } - - void setParent(SoftwareSystem parent) { - this.parent = parent; - } - - /** - * Gets the technology associated with thie container (e.g. Apache Tomcat). - * - * @return the technology, as a String, - * or null if no technology has been specified - */ - public String getTechnology() { - return technology; - } - - public void setTechnology(String technology) { - this.technology = technology; - } - - public Component addComponent(String name, String description) { - return getModel().addComponent(this, name, description); - } - - public Component addComponent(String name, String description, String technology) { - Component c = getModel().addComponent(this, name, description); - c.setTechnology(technology); - return c; - } - - public Component addComponent(String name, Class type, String description, String technology) { - return this.addComponent(name, type.getCanonicalName(), description, technology); - } - - public Component addComponent(String name, String type, String description, String technology) { - return getModel().addComponentOfType(this, name, type, description, technology); - } - - void add(Component component) { - if (getComponentWithName(component.getName()) == null) { - components.add(component); - } - } - - /** - * Gets the set of components within this software system. - * - * @return a Set of Component objects - */ - public Set getComponents() { - return components; - } - - public Component getComponentWithName(String name) { - if (name == null) { - return null; - } - - Optional component = components.stream().filter(c -> name.equals(c.getName())).findFirst(); - return component.orElse(null); - } - - public Component getComponentOfType(String type) { - if (type == null) { - return null; - } - - Optional component = components.stream().filter(c -> type.equals(c.getType())).findFirst(); - return component.orElse(null); - } - - @Override - public String getCanonicalName() { - return getParent().getCanonicalName() + CANONICAL_NAME_SEPARATOR + formatForCanonicalName(getName()); - } - - @Override - protected Set getRequiredTags() { - return new LinkedHashSet<>(Arrays.asList(Tags.ELEMENT, Tags.CONTAINER)); - } - -} diff --git a/structurizr-core/src/com/structurizr/model/ContainerInstance.java b/structurizr-core/src/com/structurizr/model/ContainerInstance.java deleted file mode 100644 index d6cf012b8..000000000 --- a/structurizr-core/src/com/structurizr/model/ContainerInstance.java +++ /dev/null @@ -1,115 +0,0 @@ -package com.structurizr.model; - -import com.fasterxml.jackson.annotation.JsonIgnore; - -import java.util.Arrays; -import java.util.LinkedHashSet; -import java.util.Set; - -/** - * Represents a deployment instance of a {@link Container}, which can be added to a {@link DeploymentNode}. - */ -public final class ContainerInstance extends Element { - - private Container container; - private String containerId; - private int instanceId; - - ContainerInstance() { - } - - ContainerInstance(Container container, int instanceId) { - this.container = container; - this.instanceId = instanceId; - } - - @JsonIgnore - public Container getContainer() { - return container; - } - - void setContainer(Container container) { - this.container = container; - } - - /** - * Gets the ID of the container that this object represents a deployment instance of. - * - * @return the container ID, as a String - */ - public String getContainerId() { - if (container != null) { - return container.getId(); - } else { - return containerId; - } - } - - void setContainerId(String containerId) { - this.containerId = containerId; - } - - /** - * Gets the instance ID of this container. - * - * @return the instance ID, an integer greater than zero - */ - public int getInstanceId() { - return instanceId; - } - - void setInstanceId(int instanceId) { - this.instanceId = instanceId; - } - - @Override - @JsonIgnore - protected Set getRequiredTags() { - return new LinkedHashSet<>(Arrays.asList(Tags.CONTAINER_INSTANCE)); - } - - @Override - public String getTags() { - return container.getTags() + "," + super.getTags(); - } - - @Override - @JsonIgnore - public String getCanonicalName() { - return container.getCanonicalName() + "[" + instanceId + "]"; - } - - @Override - @JsonIgnore - public Element getParent() { - return container.getParent(); - } - - @Override - @JsonIgnore - public String getName() { - return null; - } - - @Override - public void setName(String name) { - // no-op ... the name of a container instance is taken from the associated Container - } - - /** - * Adds a relationship between this container instance and another. - * - * @param destination the destination of the relationship (a ContainerInstance) - * @param description a description of the relationship - * @param technology the technology of the relationship - * @return a Relationship object - */ - public Relationship uses(ContainerInstance destination, String description, String technology) { - if (destination != null) { - return getModel().addRelationship(this, destination, description, technology); - } else { - throw new IllegalArgumentException("The destination of a relationship must be specified."); - } - } - -} \ No newline at end of file diff --git a/structurizr-core/src/com/structurizr/model/DeploymentNode.java b/structurizr-core/src/com/structurizr/model/DeploymentNode.java deleted file mode 100644 index f214526c6..000000000 --- a/structurizr-core/src/com/structurizr/model/DeploymentNode.java +++ /dev/null @@ -1,199 +0,0 @@ -package com.structurizr.model; - -import com.fasterxml.jackson.annotation.JsonIgnore; - -import java.util.HashSet; -import java.util.Map; -import java.util.Set; - -/** - *

- * Represents a deployment node, which 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)
  • - *
  • Database server
  • - *
  • Java EE web/application server
  • - *
  • Microsoft IIS
  • - *
  • etc
  • - *
- */ -public final class DeploymentNode extends Element { - - private DeploymentNode parent; - private String technology; - private int instances = 1; - - private Set children = new HashSet<>(); - private Set containerInstances = new HashSet<>(); - - /** - * Adds a container instance to this deployment node. - * - * @param container the Container to add an instance of - * @return a ContainerInstance object - */ - public ContainerInstance add(Container container) { - if (container == null) { - throw new IllegalArgumentException("A container must be specified."); - } - - ContainerInstance containerInstance = getModel().addContainerInstance(container); - this.containerInstances.add(containerInstance); - - return containerInstance; - } - - /** - * Adds a child deployment node. - * - * @param name the name of the deployment node - * @param description a short description - * @param technology the technology - * @return a DeploymentNode object - */ - public DeploymentNode addDeploymentNode(String name, String description, String technology) { - return addDeploymentNode(name, description, technology, 1); - } - - /** - * Adds a child deployment node. - * - * @param name the name of the deployment node - * @param description a short description - * @param technology the technology - * @param instances the number of instances - * @return a DeploymentNode object - */ - public DeploymentNode addDeploymentNode(String name, String description, String technology, int instances) { - return addDeploymentNode(name, description, technology, instances, null); - } - - /** - * Adds a child deployment node. - * - * @param name the name of the deployment node - * @param description a short description - * @param technology the technology - * @param instances the number of instances - * @param properties a Map (String,String) describing name=value properties - * @return a DeploymentNode object - */ - public DeploymentNode addDeploymentNode(String name, String description, String technology, int instances, Map properties) { - DeploymentNode deploymentNode = getModel().addDeploymentNode(this, name, description, technology, instances, properties); - if (deploymentNode != null) { - children.add(deploymentNode); - } - return deploymentNode; - } - - /** - * Gets the DeploymentNode with the specified name. - * - * @param name the name of the deployment node - * @return the DeploymentNode instance with the specified name (or null if it doesn't exist). - */ - public DeploymentNode getDeploymentNodeWithName(String name) { - if (name == null || name.trim().length() == 0) { - throw new IllegalArgumentException("A name must be specified."); - } - - for (DeploymentNode deploymentNode : getChildren()) { - if (deploymentNode.getName().equals(name)) { - return deploymentNode; - } - } - - return null; - } - - /** - * Adds a relationship between this and another deployment node. - * - * @param destination the destination DeploymentNode - * @param description a short description of the relationship - * @param technology the technology - * @return a Relationship object - */ - public Relationship uses(DeploymentNode destination, String description, String technology) { - return getModel().addRelationship(this, destination, description, technology); - } - - /** - * Gets the set of child deployment nodes. - * - * @return a Set of DeploymentNode objects - */ - public Set getChildren() { - return new HashSet<>(children); - } - - /** - * Gets the set of container instances associated with this deployment node. - * - * @return a Set of ContainerInstance objects - */ - public Set getContainerInstances() { - return new HashSet<>(containerInstances); - } - - void setChildren(Set children) { - this.children = children; - } - - /** - * Gets the parent deployment node. - * - * @return the parent DeploymentNode, or null if there is no parent - */ - @Override - @JsonIgnore - public Element getParent() { - return parent; - } - - void setParent(DeploymentNode parent) { - this.parent = parent; - } - - public String getTechnology() { - return technology; - } - - public void setTechnology(String technology) { - this.technology = technology; - } - - public int getInstances() { - return instances; - } - - public void setInstances(int instances) { - this.instances = instances; - } - - @JsonIgnore - protected Set getRequiredTags() { - // deployment nodes don't have any tags - return new HashSet<>(); - } - - @Override - public String getTags() { - // deployment nodes don't have any tags - return ""; - } - - @Override - public String getCanonicalName() { - if (getParent() != null) { - return getParent().getCanonicalName() + CANONICAL_NAME_SEPARATOR + formatForCanonicalName(getName()); - } else { - return CANONICAL_NAME_SEPARATOR + formatForCanonicalName(getName()); - } - } - -} \ 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 deleted file mode 100644 index 09daf7323..000000000 --- a/structurizr-core/src/com/structurizr/model/Element.java +++ /dev/null @@ -1,246 +0,0 @@ -package com.structurizr.model; - -import com.fasterxml.jackson.annotation.JsonIgnore; -import com.structurizr.util.Url; - -import java.util.HashMap; -import java.util.LinkedHashSet; -import java.util.Map; -import java.util.Set; - -/** - * This is the superclass for all model elements. - */ -public abstract class Element extends Taggable { - - static final String CANONICAL_NAME_SEPARATOR = "/"; - - private Model model; - - private String id = ""; - private String name; - private String description; - private String url; - private Map properties = new HashMap<>(); - - private Set relationships = new LinkedHashSet<>(); - - protected Element() { - } - - @JsonIgnore - public Model getModel() { - return this.model; - } - - protected void setModel(Model model) { - this.model = model; - } - - /** - * Gets the ID of this element in the model. - * - * @return the ID, as a String - */ - public String getId() { - return id; - } - - void setId(String id) { - this.id = id; - } - - /** - * Gets the name of this element. - * - * @return the name, as a String - */ - public String getName() { - return name; - } - - /** - * Sets the name of this element. - * - * @param name the name, as a String - */ - public void setName(String name) { - if (name == null || name.trim().length() == 0) { - throw new IllegalArgumentException("The name of an element must not be null or empty."); - } - - this.name = name; - } - - /** - * Gets the URL where more information about this element can be found. - * - * @return a URL as a String - */ - public String getUrl() { - return url; - } - - /** - * Sets the URL where more information about this 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 collection of name-value property pairs associated with this element, 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 to this element. - * - * @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 = properties; - } - } - - @JsonIgnore - public abstract String getCanonicalName(); - - String formatForCanonicalName(String name) { - return name.replace(CANONICAL_NAME_SEPARATOR, ""); - } - - /** - * Gets a description of this element. - * - * @return the description, as a String - */ - public String getDescription() { - return description; - } - - /** - * Sets the description of this element. - * - * @param description the description, as a String - */ - public void setDescription(String description) { - this.description = description; - } - - /** - * Gets the parent of this element. - * - * @return the parent Element, or null if this element doesn't have a parent (i.e. a Person or SoftwareSystem) - */ - public abstract Element getParent(); - - /** - * Gets the set of outgoing relationships. - * - * @return a Set of Relationship objects, or an empty set if none exist - */ - public Set getRelationships() { - return new LinkedHashSet<>(relationships); - } - - /** - * Determines whether this element has afferent (incoming) relationships. - * - * @return true if this element has afferent relationships, false otherwise - */ - public boolean hasAfferentRelationships() { - return getModel().getRelationships().stream().filter(r -> r.getDestination() == this).count() > 0; - } - - /** - * Determines whether this element has an efferent (outgoing) relationship with - * the specified element. - * - * @param element the element to look for - * @return true if this element has an efferent relationship with the specified element, - * false otherwise - */ - public boolean hasEfferentRelationshipWith(Element element) { - return getEfferentRelationshipWith(element) != null; - } - - /** - * Gets the efferent (outgoing) relationship with the specified element. - * - * @param element the element to look for - * @return a Relationship object if an efferent relationship exists, null otherwise - */ - public Relationship getEfferentRelationshipWith(Element element) { - if (element == null) { - return null; - } - - for (Relationship relationship : relationships) { - if (relationship.getDestination().equals(element)) { - return relationship; - } - } - - return null; - } - - boolean has(Relationship relationship) { - return relationships.contains(relationship); - } - - void addRelationship(Relationship relationship) { - relationships.add(relationship); - } - - @Override - public String toString() { - return "{" + getId() + " | " + getName() + " | " + getDescription() + "}"; - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - - if (o == null || !(o instanceof Element)) { - return false; - } - - if (!this.getClass().equals(o.getClass())) { - return false; - } - - Element element = (Element)o; - return getCanonicalName().equals(element.getCanonicalName()); - } - -} \ No newline at end of file diff --git a/structurizr-core/src/com/structurizr/model/Enterprise.java b/structurizr-core/src/com/structurizr/model/Enterprise.java deleted file mode 100644 index 9fcb12e49..000000000 --- a/structurizr-core/src/com/structurizr/model/Enterprise.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.structurizr.model; - -public final class Enterprise { - - private String name; - - Enterprise() { - } - - public Enterprise(String name) { - if (name == null || name.trim().length() == 0) { - throw new IllegalArgumentException("Name must be specified."); - } - - this.name = name; - } - - public String getName() { - return name; - } - -} diff --git a/structurizr-core/src/com/structurizr/model/IdGenerator.java b/structurizr-core/src/com/structurizr/model/IdGenerator.java deleted file mode 100644 index 35a0d3732..000000000 --- a/structurizr-core/src/com/structurizr/model/IdGenerator.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.structurizr.model; - -public interface IdGenerator { - - String generateId(Element element); - - String generateId(Relationship relationship); - - void found(String id); - -} diff --git a/structurizr-core/src/com/structurizr/model/Model.java b/structurizr-core/src/com/structurizr/model/Model.java deleted file mode 100644 index 6dadc56ce..000000000 --- a/structurizr-core/src/com/structurizr/model/Model.java +++ /dev/null @@ -1,605 +0,0 @@ -package com.structurizr.model; - -import com.fasterxml.jackson.annotation.JsonIgnore; - -import java.util.*; -import java.util.stream.Collectors; - -/** - * A software architecture model. - */ -public final class Model { - - private SequentialIntegerIdGeneratorStrategy idGenerator = new SequentialIntegerIdGeneratorStrategy(); - - private final Map elementsById = new HashMap<>(); - private final Map relationshipsById = new HashMap<>(); - - private Enterprise enterprise; - - private Set people = new LinkedHashSet<>(); - private Set softwareSystems = new LinkedHashSet<>(); - private Set deploymentNodes = new LinkedHashSet<>(); - - public Model() { - } - - public Enterprise getEnterprise() { - return enterprise; - } - - public void setEnterprise(Enterprise enterprise) { - this.enterprise = enterprise; - } - - /** - * Creates a software system (location is unspecified) and adds it to the model - * (unless one exists with the same name already). - * - * @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) - */ - public SoftwareSystem addSoftwareSystem(String name, String description) { - return addSoftwareSystem(Location.Unspecified, name, description); - } - - /** - * Creates a software system and adds it to the model - * (unless one exists with the same name already). - * - * @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) - */ - public SoftwareSystem addSoftwareSystem(Location location, String name, String description) { - if (getSoftwareSystemWithName(name) == null) { - SoftwareSystem softwareSystem = new SoftwareSystem(); - softwareSystem.setLocation(location); - softwareSystem.setName(name); - softwareSystem.setDescription(description); - - softwareSystems.add(softwareSystem); - - softwareSystem.setId(idGenerator.generateId(softwareSystem)); - addElementToInternalStructures(softwareSystem); - - return softwareSystem; - } else { - throw new IllegalArgumentException("A software system named '" + name + "' already exists."); - } - } - - /** - * Creates a person (location is unspecified) and adds it to the model - * (unless one exists with the same name already). - * - * @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) - */ - public Person addPerson(String name, String description) { - return addPerson(Location.Unspecified, name, description); - } - - /** - * Creates a person and adds it to the model - * (unless one exists with the same name already). - * - * @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) - */ - public Person addPerson(Location location, String name, String description) { - if (getPersonWithName(name) == null) { - Person person = new Person(); - person.setLocation(location); - person.setName(name); - person.setDescription(description); - - people.add(person); - - person.setId(idGenerator.generateId(person)); - addElementToInternalStructures(person); - - return person; - } else { - throw new IllegalArgumentException("A person named '" + name + "' already exists."); - } - } - - Container addContainer(SoftwareSystem parent, String name, String description, String technology) { - if (parent.getContainerWithName(name) == null) { - Container container = new Container(); - container.setName(name); - container.setDescription(description); - container.setTechnology(technology); - - container.setParent(parent); - parent.add(container); - - container.setId(idGenerator.generateId(container)); - addElementToInternalStructures(container); - - return container; - } else { - throw new IllegalArgumentException("A container named '" + name + "' already exists for this software system."); - } - } - - Component addComponentOfType(Container parent, String name, String type, String description, String technology) { - Component component = new Component(); - component.setName(name); - component.setType(type); - component.setDescription(description); - component.setTechnology(technology); - - component.setParent(parent); - parent.add(component); - - component.setId(idGenerator.generateId(component)); - addElementToInternalStructures(component); - - return component; - } - - Component addComponent(Container parent, String name, String description) { - Component component = new Component(); - component.setName(name); - component.setDescription(description); - - component.setParent(parent); - parent.add(component); - - component.setId(idGenerator.generateId(component)); - addElementToInternalStructures(component); - - return component; - } - - Relationship addRelationship(Element source, Element destination, String description) { - return addRelationship(source, destination, description, null); - } - - Relationship addRelationship(Element source, Element destination, String description, String technology) { - return addRelationship(source, destination, description, technology, InteractionStyle.Synchronous); - } - - Relationship addRelationship(Element source, Element destination, String description, String technology, InteractionStyle interactionStyle) { - if (destination == null) { - throw new IllegalArgumentException("The destination must be specified."); - - } - - Relationship relationship = new Relationship(source, destination, description, technology, interactionStyle); - if (addRelationship(relationship)) { - return relationship; - } else { - return null; - } - } - - private boolean addRelationship(Relationship relationship) { - if (!relationship.getSource().has(relationship)) { - relationship.setId(idGenerator.generateId(relationship)); - relationship.getSource().addRelationship(relationship); - - addRelationshipToInternalStructures(relationship); - return true; - } else { - return false; - } - } - - private void addElementToInternalStructures(Element element) { - elementsById.put(element.getId(), element); - element.setModel(this); - idGenerator.found(element.getId()); - } - - private void addRelationshipToInternalStructures(Relationship relationship) { - relationshipsById.put(relationship.getId(), relationship); - idGenerator.found(relationship.getId()); - } - - /** - * @return a set containing all elements in this model. - */ - @JsonIgnore - public Set getElements() { - return new HashSet<>(this.elementsById.values()); - } - - /** - * @param id the {@link Element#getId()} of the element - * @return the element in this model with the specified ID (or null if it doesn't exist). - * @see Element#getId() - */ - public Element getElement(String id) { - if (id == null || id.trim().length() == 0) { - throw new IllegalArgumentException("An ID must be specified."); - } - - - return elementsById.get(id); - } - - /** - * @return a set containing all relationships in this model. - */ - @JsonIgnore - public Set getRelationships() { - return new HashSet<>(this.relationshipsById.values()); - } - - /** - * @param id the {@link Relationship#getId()} of the relationship - * @return the relationship in this model with the specified ID (or null if it doesn't exist). - * @see Relationship#getId() - */ - public Relationship getRelationship(String id) { - return relationshipsById.get(id); - } - - /** - * @return a collection containing all of the Person instances in this model. - */ - public Collection getPeople() { - return new LinkedHashSet<>(people); - } - - /** - * @return a collection containing all of the SoftwareSystem instances in this model. - */ - public Set getSoftwareSystems() { - return new LinkedHashSet<>(softwareSystems); - } - - /** - * @return a collection containing all of the DeploymentNode instances in this model. - */ - public Set getDeploymentNodes() { - return new LinkedHashSet<>(deploymentNodes); - } - - public void hydrate() { - // add all of the elements to the model - people.forEach(this::addElementToInternalStructures); - for (SoftwareSystem softwareSystem : softwareSystems) { - addElementToInternalStructures(softwareSystem); - for (Container container : softwareSystem.getContainers()) { - softwareSystem.add(container); - addElementToInternalStructures(container); - container.setParent(softwareSystem); - for (Component component : container.getComponents()) { - container.add(component); - addElementToInternalStructures(component); - component.setParent(container); - } - } - } - - deploymentNodes.forEach(dn -> hydrateDeploymentNode(dn, null)); - - // now hydrate the relationships - people.forEach(this::hydrateRelationships); - for (SoftwareSystem softwareSystem : softwareSystems) { - hydrateRelationships(softwareSystem); - for (Container container : softwareSystem.getContainers()) { - hydrateRelationships(container); - for (Component component : container.getComponents()) { - hydrateRelationships(component); - } - } - } - - deploymentNodes.forEach(this::hydrateDeploymentNodeRelationships); - } - - private void hydrateDeploymentNode(DeploymentNode deploymentNode, DeploymentNode parent) { - deploymentNode.setParent(parent); - addElementToInternalStructures(deploymentNode); - - deploymentNode.getChildren().forEach(child -> hydrateDeploymentNode(child, deploymentNode)); - - for (ContainerInstance containerInstance : deploymentNode.getContainerInstances()) { - containerInstance.setContainer((Container)getElement(containerInstance.getContainerId())); - addElementToInternalStructures(containerInstance); - } - } - - private void hydrateDeploymentNodeRelationships(DeploymentNode deploymentNode) { - hydrateRelationships(deploymentNode); - deploymentNode.getChildren().forEach(this::hydrateDeploymentNodeRelationships); - deploymentNode.getContainerInstances().forEach(this::hydrateRelationships); - } - - private void hydrateRelationships(Element element) { - for (Relationship relationship : element.getRelationships()) { - relationship.setSource(getElement(relationship.getSourceId())); - relationship.setDestination(getElement(relationship.getDestinationId())); - addRelationshipToInternalStructures(relationship); - } - } - - /** - * Determines whether this model contains the specified element. - * - * @param element any element - * @return true, if the element is contained in this model - */ - public boolean contains(Element element) { - return elementsById.values().contains(element); - } - - /** - * @param name the name of a {@link SoftwareSystem} - * @return the SoftwareSystem instance with the specified name (or null if it doesn't exist). - */ - public SoftwareSystem getSoftwareSystemWithName(String name) { - for (SoftwareSystem softwareSystem : getSoftwareSystems()) { - if (softwareSystem.getName().equals(name)) { - return softwareSystem; - } - } - - return null; - } - - /** - * @param id the {@link SoftwareSystem#getId()} of the softwaresystem - * @return Gets the SoftwareSystem instance with the specified ID (or null if it doesn't exist). - * @see SoftwareSystem#getId() - */ - public SoftwareSystem getSoftwareSystemWithId(String id) { - for (SoftwareSystem softwareSystem : getSoftwareSystems()) { - if (softwareSystem.getId().equals(id)) { - return softwareSystem; - } - } - - return null; - } - - /** - * @param name the name of the person - * @return the Person instance with the specified name (or null if it doesn't exist). - */ - public Person getPersonWithName(String name) { - for (Person person : getPeople()) { - if (person.getName().equals(name)) { - return person; - } - } - - return null; - } - - /** - *

Propagates all relationships from children to their parents. For example, if you have two components (AAA and BBB) - * in different software systems that have a relationship, calling this method will add the following - * additional implied relationships to the model: AAA->BB AAA-->B AA->BBB AA->BB AA->B A->BBB A->BB A->B.

- * - * @return a set of all implicit relationships - */ - public Set addImplicitRelationships() { - Set implicitRelationships = new HashSet<>(); - - String descriptionKey = "D"; - String technologyKey = "T"; - Map>>> candidateRelationships = new HashMap<>(); - - for (Relationship relationship : getRelationships()) { - Element source = relationship.getSource(); - Element destination = relationship.getDestination(); - - while (source != null) { - while (destination != null) { - if (!source.hasEfferentRelationshipWith(destination)) { - if (propagatedRelationshipIsAllowed(source, destination)) { - - if (!candidateRelationships.containsKey(source)) { - candidateRelationships.put(source, new HashMap<>()); - } - - if (!candidateRelationships.get(source).containsKey(destination)) { - candidateRelationships.get(source).put(destination, new HashMap<>()); - candidateRelationships.get(source).get(destination).put(descriptionKey, new HashSet<>()); - candidateRelationships.get(source).get(destination).put(technologyKey, new HashSet<>()); - } - - if (relationship.getDescription() != null) { - candidateRelationships.get(source).get(destination).get(descriptionKey).add(relationship.getDescription()); - } - - if (relationship.getTechnology() != null) { - candidateRelationships.get(source).get(destination).get(technologyKey).add(relationship.getTechnology()); - } - } - } - - destination = destination.getParent(); - } - - destination = relationship.getDestination(); - source = source.getParent(); - } - } - - for (Element source : candidateRelationships.keySet()) { - for (Element destination : candidateRelationships.get(source).keySet()) { - Set possibleDescriptions = candidateRelationships.get(source).get(destination).get(descriptionKey); - Set possibleTechnologies = candidateRelationships.get(source).get(destination).get(technologyKey); - - String description = ""; - if (possibleDescriptions.size() == 1) { - description = possibleDescriptions.iterator().next(); - } - - String technology = ""; - if (possibleTechnologies.size() == 1) { - technology = possibleTechnologies.iterator().next(); - } - - Relationship implicitRelationship = addRelationship(source, destination, description, technology); - if (implicitRelationship != null) { - implicitRelationships.add(implicitRelationship); - } - } - } - - return implicitRelationships; - } - - private boolean propagatedRelationshipIsAllowed(Element source, Element destination) { - if (source.equals(destination)) { - return false; - } - - if (source.getParent() != null) { - if (destination.equals(source.getParent())) { - return false; - } - - if (source.getParent().getParent() != null) { - if (destination.equals(source.getParent().getParent())) { - return false; - } - } - } - - if (destination.getParent() != null) { - if (source.equals(destination.getParent())) { - return false; - } - - if (destination.getParent().getParent() != null) { - if (source.equals(destination.getParent().getParent())) { - return false; - } - } - } - - return true; - } - - @JsonIgnore - public boolean isEmpty() { - return people.isEmpty() && softwareSystems.isEmpty(); - } - - public DeploymentNode addDeploymentNode(String name, String description, String technology) { - return addDeploymentNode(name, description, technology, 1); - } - - public DeploymentNode addDeploymentNode(String name, String description, String technology, int instances) { - return addDeploymentNode(name, description, technology, instances, null); - } - - public DeploymentNode addDeploymentNode(String name, String description, String technology, int instances, Map properties) { - return addDeploymentNode(null, name, description, technology, instances, properties); - } - - DeploymentNode addDeploymentNode(DeploymentNode parent, String name, String description, String technology, int instances, Map properties) { - if (name == null || name.trim().length() == 0) { - throw new IllegalArgumentException("A name must be specified."); - } - - if ((parent == null && getDeploymentNodeWithName(name) == null) || (parent != null && parent.getDeploymentNodeWithName(name) == null)) { - DeploymentNode deploymentNode = new DeploymentNode(); - deploymentNode.setName(name); - deploymentNode.setDescription(description); - deploymentNode.setTechnology(technology); - deploymentNode.setParent(parent); - deploymentNode.setInstances(instances); - if (properties != null) { - deploymentNode.setProperties(properties); - } - - if (parent == null) { - deploymentNodes.add(deploymentNode); - } - - deploymentNode.setId(idGenerator.generateId(deploymentNode)); - addElementToInternalStructures(deploymentNode); - - return deploymentNode; - } else { - throw new IllegalArgumentException("A deployment node named '" + name + "' already exists."); - } - } - - /** - * @param name the name of the deployment node - * @return the DeploymentNode instance with the specified name (or null if it doesn't exist). - */ - public DeploymentNode getDeploymentNodeWithName(String name) { - for (DeploymentNode deploymentNode : getDeploymentNodes()) { - if (deploymentNode.getName().equals(name)) { - return deploymentNode; - } - } - - return null; - } - - ContainerInstance addContainerInstance(Container container) { - if (container == null) { - throw new IllegalArgumentException("A container must be specified."); - } - - long instanceNumber = getElements().stream().filter(e -> e instanceof ContainerInstance && ((ContainerInstance)e).getContainer().equals(container)).count(); - instanceNumber++; - ContainerInstance containerInstance = new ContainerInstance(container, (int)instanceNumber); - containerInstance.setId(idGenerator.generateId(containerInstance)); - - // find all ContainerInstance objects - Set containerInstances = getElements().stream() - .filter(e -> e instanceof ContainerInstance) - .map(e -> (ContainerInstance)e) - .collect(Collectors.toSet()); - - // and replicate the container-container relationships - for (ContainerInstance ci : containerInstances) { - Container c = ci.getContainer(); - - for (Relationship relationship : container.getRelationships()) { - if (relationship.getDestination().equals(c)) { - addRelationship(containerInstance, ci, relationship.getDescription(), relationship.getTechnology(), relationship.getInteractionStyle()); - } - } - - for (Relationship relationship : c.getRelationships()) { - if (relationship.getDestination().equals(container)) { - addRelationship(ci, containerInstance, relationship.getDescription(), relationship.getTechnology(), relationship.getInteractionStyle()); - } - } - } - - addElementToInternalStructures(containerInstance); - - return containerInstance; - } - - public Element getElementWithCanonicalName(String canonicalName) { - if (canonicalName == null || canonicalName.trim().length() == 0) { - throw new IllegalArgumentException("A canonical name must be specified."); - } - - // canonical names start with a leading slash, so add this if it's missing - if (!canonicalName.startsWith("/")) { - canonicalName = "/" + canonicalName; - } - - for (Element element : getElements()) { - if (element.getCanonicalName().equals(canonicalName)) { - return element; - } - } - - return null; - } - -} \ No newline at end of file diff --git a/structurizr-core/src/com/structurizr/model/Person.java b/structurizr-core/src/com/structurizr/model/Person.java deleted file mode 100644 index 9bef42307..000000000 --- a/structurizr-core/src/com/structurizr/model/Person.java +++ /dev/null @@ -1,83 +0,0 @@ -package com.structurizr.model; - -import com.fasterxml.jackson.annotation.JsonIgnore; - -import java.util.Arrays; -import java.util.LinkedHashSet; -import java.util.Set; - -/** - * However you think about your users (as actors, roles, personas, etc), - * people are the various human users of your software system. - * - * See Model - Person - * on the Structurizr website for more information. - */ -public final class Person extends StaticStructureElement { - - private Location location = Location.Unspecified; - - @Override - @JsonIgnore - public Element getParent() { - return null; - } - - Person() { - } - - /** - * Gets the location of this person. - * - * @return a Location - */ - public Location getLocation() { - return location; - } - - public void setLocation(Location location) { - if (location != null) { - this.location = location; - } else { - this.location = Location.Unspecified; - } - } - - @Override - public String getCanonicalName() { - return CANONICAL_NAME_SEPARATOR + formatForCanonicalName(getName()); - } - - @Override - protected Set getRequiredTags() { - return new LinkedHashSet<>(Arrays.asList(Tags.ELEMENT, Tags.PERSON)); - } - - @Override - public Relationship delivers(Person destination, String description) { - throw new UnsupportedOperationException(); - } - - @Override - public Relationship delivers(Person destination, String description, String technology) { - throw new UnsupportedOperationException(); - } - - @Override - public Relationship delivers(Person destination, String description, String technology, InteractionStyle interactionStyle) { - throw new UnsupportedOperationException(); - } - - public Relationship interactsWith(Person destination, String description) { - return getModel().addRelationship(this, destination, description); - } - - public Relationship interactsWith(Person destination, String description, String technology) { - return getModel().addRelationship(this, destination, description, technology); - } - - public Relationship interactsWith(Person destination, String description, String technology, InteractionStyle interactionStyle) { - return getModel().addRelationship(this, destination, description, technology, interactionStyle); - } - -} \ No newline at end of file diff --git a/structurizr-core/src/com/structurizr/model/Relationship.java b/structurizr-core/src/com/structurizr/model/Relationship.java deleted file mode 100644 index 640499d1c..000000000 --- a/structurizr-core/src/com/structurizr/model/Relationship.java +++ /dev/null @@ -1,178 +0,0 @@ -package com.structurizr.model; - -import com.fasterxml.jackson.annotation.JsonIgnore; - -import java.util.Arrays; -import java.util.LinkedHashSet; -import java.util.Set; - -/** - * A relationship between two elements. - */ -public final class Relationship extends Taggable { - - protected String id = ""; - - private Element source; - private String sourceId; - private Element destination; - private String destinationId; - private String description; - private String technology; - private InteractionStyle interactionStyle = InteractionStyle.Synchronous; - - Relationship() { - } - - Relationship(Element source, Element destination, String description, String technology, InteractionStyle interactionStyle) { - this(); - - this.source = source; - this.destination = destination; - this.description = description; - this.technology = technology; - setInteractionStyle(interactionStyle); - } - - @JsonIgnore - public Element getSource() { - return source; - } - - /** - * Gets the ID of the source element. - * - * @return the ID of the source element, as a String - */ - public String getSourceId() { - if (this.source != null) { - return this.source.getId(); - } else { - return this.sourceId; - } - } - - /** - * Gets the ID of this relationship in the model. - * - * @return the ID, as a String - */ - public String getId() { - return id; - } - - void setId(String id) { - this.id = id; - } - - void setSourceId(String sourceId) { - this.sourceId = sourceId; - } - - public void setSource(Element source) { - this.source = source; - } - - @JsonIgnore - public Element getDestination() { - return destination; - } - - /** - * Gets the ID of the destination element. - * - * @return the ID of the destination element, as a String - */ - public String getDestinationId() { - if (this.destination != null) { - return this.destination.getId(); - } else { - return this.destinationId; - } - } - - void setDestinationId(String destinationId) { - this.destinationId = destinationId; - } - - public void setDestination(Element destination) { - this.destination = destination; - } - - public String getDescription() { - return description != null ? description : ""; - } - - public void setDescription(String description) { - this.description = description; - } - - /** - * Gets the technology associated with this relationship (e.g. HTTPS, JDBC, etc). - * - * @return the technology as a String, - * or null if a technology is not specified - */ - public String getTechnology() { - return technology; - } - - public void setTechnology(String technology) { - this.technology = technology; - } - - /** - * Gets the interaction style (synchronous or asynchronous). - * - * @return an InteractionStyle, - * or null if an interaction style has not been specified - */ - public InteractionStyle getInteractionStyle() { - return interactionStyle; - } - - public void setInteractionStyle(InteractionStyle interactionStyle) { - this.interactionStyle = interactionStyle; - - if (interactionStyle == InteractionStyle.Synchronous) { - removeTag(Tags.ASYNCHRONOUS); - addTags(Tags.SYNCHRONOUS); - } else { - removeTag(Tags.SYNCHRONOUS); - addTags(Tags.ASYNCHRONOUS); - } - } - - @Override - protected Set getRequiredTags() { - return new LinkedHashSet(Arrays.asList(Tags.RELATIONSHIP)); - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - - Relationship that = (Relationship) o; - - if (!getDescription().equals(that.getDescription())) return false; - if (!getDestination().equals(that.getDestination())) return false; - if (!getSource().equals(that.getSource())) return false; - - return true; - } - - @Override - public int hashCode() { - int result = getSourceId().hashCode(); - result = 31 * result + getDestinationId().hashCode(); - result = 31 * result + getDescription().hashCode(); - return result; - } - - @Override - public String toString() { - return source.toString() + " ---[" + description + "]---> " + destination.toString(); - } - -} \ No newline at end of file diff --git a/structurizr-core/src/com/structurizr/model/SequentialIntegerIdGeneratorStrategy.java b/structurizr-core/src/com/structurizr/model/SequentialIntegerIdGeneratorStrategy.java deleted file mode 100644 index 64b7a388f..000000000 --- a/structurizr-core/src/com/structurizr/model/SequentialIntegerIdGeneratorStrategy.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.structurizr.model; - -class SequentialIntegerIdGeneratorStrategy implements IdGenerator { - - private int ID = 0; - - public void found(String id) { - int idAsInt = Integer.parseInt(id); - if (idAsInt > ID) { - ID = idAsInt; - } - } - - @Override - public synchronized String generateId(Element element) { - return "" + ++ID; - } - - @Override - public synchronized String generateId(Relationship relationship) { - return "" + ++ID; - } - -} diff --git a/structurizr-core/src/com/structurizr/model/SoftwareSystem.java b/structurizr-core/src/com/structurizr/model/SoftwareSystem.java deleted file mode 100644 index 0d3deefe9..000000000 --- a/structurizr-core/src/com/structurizr/model/SoftwareSystem.java +++ /dev/null @@ -1,115 +0,0 @@ -package com.structurizr.model; - -import com.fasterxml.jackson.annotation.JsonIgnore; - -import java.util.Arrays; -import java.util.HashSet; -import java.util.LinkedHashSet; -import java.util.Set; - -/** - * A software system is the highest level of abstraction and describes something - * that delivers value to its users, whether they are human or not. This includes - * the software system you are modelling, and the other software systems upon - * which your software system depends. - * - * See Model - Software System - * on the Structurizr website for more information. - */ -public final class SoftwareSystem extends StaticStructureElement { - - private Location location = Location.Unspecified; - - private Set containers = new LinkedHashSet<>(); - - @Override - @JsonIgnore - public Element getParent() { - return null; - } - - SoftwareSystem() { - } - - /** - * Gets the location of this software system. - * - * @return a Location - */ - public Location getLocation() { - return location; - } - - public void setLocation(Location location) { - if (location != null) { - this.location = location; - } else { - this.location = Location.Unspecified; - } - } - - void add(Container container) { - containers.add(container); - } - - /** - * Gets the set of containers within this software system. - * - * @return a Set of Container objects - */ - public Set getContainers() { - return new HashSet<>(containers); - } - - /** - * Adds a container with the specified name, description and technology - * (unless one exists with the same name already). - * - * @param name the name of the container (e.g. "Web Application") - * @param description a short description/list of responsibilities - * @param technology the technoogy choice (e.g. "Spring MVC", "Java EE", etc) - * @return the newly created Container instance added to the model (or null) - */ - public Container addContainer(String name, String description, String technology) { - return getModel().addContainer(this, name, description, technology); - } - - /** - * @param name the name of the {@link Container} - * @return the container with the specified name (or null if it doesn't exist). - */ - public Container getContainerWithName(String name) { - for (Container container : getContainers()) { - if (container.getName().equals(name)) { - return container; - } - } - - return null; - } - - /** - * @param id the {@link Container#getId()} of the container - * @return Gets the container with the specified ID (or null if it doesn't exist). - */ - public Container getContainerWithId(String id) { - for (Container container : getContainers()) { - if (container.getId().equals(id)) { - return container; - } - } - - return null; - } - - @Override - public String getCanonicalName() { - return CANONICAL_NAME_SEPARATOR + formatForCanonicalName(getName()); - } - - @Override - protected Set getRequiredTags() { - return new LinkedHashSet<>(Arrays.asList(Tags.ELEMENT, Tags.SOFTWARE_SYSTEM)); - } - -} diff --git a/structurizr-core/src/com/structurizr/model/StaticStructureElement.java b/structurizr-core/src/com/structurizr/model/StaticStructureElement.java deleted file mode 100644 index 82bafa143..000000000 --- a/structurizr-core/src/com/structurizr/model/StaticStructureElement.java +++ /dev/null @@ -1,156 +0,0 @@ -package com.structurizr.model; - -/** - * This is the superclass for model elements that describe the static structure - * of a software system, namely Person, SoftwareSystem, Container and Component. - */ -abstract class StaticStructureElement extends Element { - - protected StaticStructureElement() { - } - - /** - * Adds a unidirectional "uses" style relationship between this element and software system. - * - * @param destination the target of the relationship - * @param description a description of the relationship (e.g. "uses", "gets data from", "sends data to") - * @return the relationship that has just been created and added to the model - */ - public Relationship uses(SoftwareSystem destination, String description) { - return getModel().addRelationship(this, destination, description); - } - - /** - * Adds a unidirectional "uses" style relationship between this element and a software system. - * - * @param destination the target of the relationship - * @param description a description of the relationship (e.g. "uses", "gets data from", "sends data to") - * @param technology the technology details (e.g. JSON/HTTPS) - * @return the relationship that has just been created and added to the model - */ - public Relationship uses(SoftwareSystem destination, String description, String technology) { - return getModel().addRelationship(this, destination, description, technology); - } - - /** - * Adds a unidirectional "uses" style relationship between this element and a software system. - * - * @param destination the target of the relationship - * @param description a description of the relationship (e.g. "uses", "gets data from", "sends data to") - * @param technology the technology details (e.g. JSON/HTTPS) - * @param interactionStyle the interaction style (sync vs async) - * @return the relationship that has just been created and added to the model - */ - public Relationship uses(SoftwareSystem destination, String description, String technology, InteractionStyle interactionStyle) { - return getModel().addRelationship(this, destination, description, technology, interactionStyle); - } - - /** - * Adds a unidirectional "uses" style relationship between this element and container. - * - * @param destination the target of the relationship - * @param description a description of the relationship (e.g. "uses", "gets data from", "sends data to") - * @return the relationship that has just been created and added to the model - */ - public Relationship uses(Container destination, String description) { - return getModel().addRelationship(this, destination, description); - } - - /** - * Adds a unidirectional "uses" style relationship between this element and a container. - * - * @param destination the target of the relationship - * @param description a description of the relationship (e.g. "uses", "gets data from", "sends data to") - * @param technology the technology details (e.g. JSON/HTTPS) - * @return the relationship that has just been created and added to the model - */ - public Relationship uses(Container destination, String description, String technology) { - return getModel().addRelationship(this, destination, description, technology); - } - - /** - * Adds a unidirectional "uses" style relationship between this element and a container. - * - * @param destination the target of the relationship - * @param description a description of the relationship (e.g. "uses", "gets data from", "sends data to") - * @param technology the technology details (e.g. JSON/HTTPS) - * @param interactionStyle the interaction style (sync vs async) - * @return the relationship that has just been created and added to the model - */ - public Relationship uses(Container destination, String description, String technology, InteractionStyle interactionStyle) { - return getModel().addRelationship(this, destination, description, technology, interactionStyle); - } - - /** - * Adds a unidirectional "uses" style relationship between this element and component. - * - * @param destination the target of the relationship - * @param description a description of the relationship (e.g. "uses", "gets data from", "sends data to") - * @return the relationship that has just been created and added to the model - */ - public Relationship uses(Component destination, String description) { - return getModel().addRelationship(this, destination, description); - } - - /** - * Adds a unidirectional "uses" style relationship between this element and a component. - * - * @param destination the target of the relationship - * @param description a description of the relationship (e.g. "uses", "gets data from", "sends data to") - * @param technology the technology details (e.g. JSON/HTTPS) - * @return the relationship that has just been created and added to the model - */ - public Relationship uses(Component destination, String description, String technology) { - return getModel().addRelationship(this, destination, description, technology); - } - - /** - * Adds a unidirectional "uses" style relationship between this element and a component. - * - * @param destination the target of the relationship - * @param description a description of the relationship (e.g. "uses", "gets data from", "sends data to") - * @param technology the technology details (e.g. JSON/HTTPS) - * @param interactionStyle the interaction style (sync vs async) - * @return the relationship that has just been created and added to the model - */ - public Relationship uses(Component destination, String description, String technology, InteractionStyle interactionStyle) { - return getModel().addRelationship(this, destination, description, technology, interactionStyle); - } - - /** - * Adds a unidirectional relationship between this element and a person. - * - * @param destination the target of the relationship - * @param description a description of the relationship (e.g. "sends e-mail to") - * @return the relationship that has just been created and added to the model - */ - public Relationship delivers(Person destination, String description) { - return getModel().addRelationship(this, destination, description); - } - - /** - * Adds a unidirectional relationship between this element and a person. - * - * @param destination the target of the relationship - * @param description a description of the relationship (e.g. "sends e-mail to") - * @param technology the technology details (e.g. JSON/HTTPS) - * @return the relationship that has just been created and added to the model - */ - public Relationship delivers(Person destination, String description, String technology) { - return getModel().addRelationship(this, destination, description, technology); - } - - /** - * Adds a unidirectional relationship between this element and a person. - * - * @param destination the target of the relationship - * @param description a description of the relationship (e.g. "sends e-mail to") - * @param technology the technology details (e.g. JSON/HTTPS) - * @param interactionStyle the interaction style (sync vs async) - * @return the relationship that has just been created and added to the model - */ - public Relationship delivers(Person destination, String description, String technology, InteractionStyle interactionStyle) { - return getModel().addRelationship(this, destination, description, technology, interactionStyle); - } - -} diff --git a/structurizr-core/src/com/structurizr/model/Taggable.java b/structurizr-core/src/com/structurizr/model/Taggable.java deleted file mode 100644 index e4d9e151b..000000000 --- a/structurizr-core/src/com/structurizr/model/Taggable.java +++ /dev/null @@ -1,76 +0,0 @@ -package com.structurizr.model; - -import com.fasterxml.jackson.annotation.JsonIgnore; - -import java.util.Collections; -import java.util.LinkedHashSet; -import java.util.Set; - -abstract class Taggable { - - private Set tags = new LinkedHashSet<>(); - - protected abstract Set getRequiredTags(); - - /** - * Gets the comma separated list of tags. - * - * @return a comma separated list of tags, - * 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); - } - - @JsonIgnore - public Set getTagsAsSet() { - Set setOfTags = new LinkedHashSet<>(getRequiredTags()); - setOfTags.addAll(tags); - - return setOfTags; - } - - void setTags(String tags) { - if (tags == null) { - return; - } - - this.tags.clear(); - Collections.addAll(this.tags, tags.split(",")); - } - - public void addTags(String... tags) { - if (tags == null) { - return; - } - - for (String tag : tags) { - if (tag != null) { - this.tags.add(tag); - } - } - } - - public void removeTag(String tag) { - if (tag != null) { - this.tags.remove(tag); - } - } - - public boolean hasTag(String tag) { - return this.tags.contains(tag); - } -} diff --git a/structurizr-core/src/com/structurizr/model/Tags.java b/structurizr-core/src/com/structurizr/model/Tags.java deleted file mode 100644 index 88b72de0d..000000000 --- a/structurizr-core/src/com/structurizr/model/Tags.java +++ /dev/null @@ -1,29 +0,0 @@ -package com.structurizr.model; - -public class Tags { - - public static final String ELEMENT = "Element"; - public static final String RELATIONSHIP = "Relationship"; - - public static final String PERSON = "Person"; - public static final String SOFTWARE_SYSTEM = "Software System"; - public static final String CONTAINER = "Container"; - public static final String COMPONENT = "Component"; - - public static final String DEPLOYMENT_NODE = "Deployment Node"; - public static final String CONTAINER_INSTANCE = "Container Instance"; - - /** - * To be used for styling of synchronous relationships - * - * @see InteractionStyle#Synchronous - */ - public static final String SYNCHRONOUS = "Synchronous"; - /** - * To be used for styling of asynchronous relationships - * - * @see InteractionStyle#Asynchronous - */ - public static final String ASYNCHRONOUS = "Asynchronous"; - -} \ No newline at end of file diff --git a/structurizr-core/src/com/structurizr/util/ImageUtils.java b/structurizr-core/src/com/structurizr/util/ImageUtils.java deleted file mode 100644 index c9999f01d..000000000 --- a/structurizr-core/src/com/structurizr/util/ImageUtils.java +++ /dev/null @@ -1,71 +0,0 @@ -package com.structurizr.util; - -import javax.imageio.ImageIO; -import java.awt.image.BufferedImage; -import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.IOException; -import java.net.URLConnection; -import java.util.Base64; - -/** - * Some utility methods for dealing with images. - */ -public class ImageUtils { - - /** - * Gets the content type of the specified file representing an image. - * - * @param file a File 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(File file) throws IOException { - if (file == null) { - throw new IllegalArgumentException("A file must be specified."); - } else if (!file.exists()) { - throw new IllegalArgumentException(file.getCanonicalPath() + " does not exist."); - } else if (!file.isFile()) { - throw new IllegalArgumentException(file.getCanonicalPath() + " is not a file."); - } - - String contentType = URLConnection.guessContentTypeFromName(file.getName()); - if (contentType == null || !contentType.startsWith("image/")) { - throw new IllegalArgumentException(file.getCanonicalPath() + " is not a supported image file."); - } - - return contentType; - } - - /** - * Gets the content of an image as a Base64 encoded string. - * - * @param file a File pointing to an image - * @return a Base64 encoded version of that image - * @throws IOException if there is an error reading the file - */ - public static String getImageAsBase64(File file) throws IOException { - String contentType = getContentType(file); - BufferedImage bufferedImage = ImageIO.read(file); - ByteArrayOutputStream bos = new ByteArrayOutputStream(); - ImageIO.write(bufferedImage, contentType.split("/")[1], bos); - byte[] imageBytes = bos.toByteArray(); - - return Base64.getEncoder().encodeToString(imageBytes); - } - - /** - * Gets the content of an image as a data URI; e.g. "data:image/png;base64,iVBORw0KGgoAA..." - * - * @param file a File pointing to an image - * @return a data URI - * @throws IOException if there is an error reading the file - */ - public static String getImageAsDataUri(File file) throws IOException { - String contentType = ImageUtils.getContentType(file); - String base64Content = ImageUtils.getImageAsBase64(file); - - return "data:" + contentType + ";base64," + base64Content; - } - -} \ No newline at end of file diff --git a/structurizr-core/src/com/structurizr/util/MapUtils.java b/structurizr-core/src/com/structurizr/util/MapUtils.java deleted file mode 100644 index b61cb1379..000000000 --- a/structurizr-core/src/com/structurizr/util/MapUtils.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.structurizr.util; - -import java.util.HashMap; -import java.util.Map; - -public final class MapUtils { - - public static Map create(String... nameValuePairs) { - Map map = new HashMap<>(); - - if (nameValuePairs != null) { - for (String nameValuePair : nameValuePairs) { - String[] tokens = nameValuePair.split("="); - if (tokens.length == 2) { - map.put(tokens[0], tokens[1]); - } - } - } - - return map; - } - -} diff --git a/structurizr-core/src/com/structurizr/util/Url.java b/structurizr-core/src/com/structurizr/util/Url.java deleted file mode 100644 index f5e1bfbda..000000000 --- a/structurizr-core/src/com/structurizr/util/Url.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.structurizr.util; - -import java.net.MalformedURLException; -import java.net.URL; - -public class Url { - - public static boolean isUrl(String urlAsString) { - if (urlAsString != null && urlAsString.trim().length() > 0) { - try { - new URL(urlAsString); - return true; - } catch (MalformedURLException murle) { - return false; - } - } - - return false; - } - -} \ No newline at end of file diff --git a/structurizr-core/src/com/structurizr/util/WorkspaceUtils.java b/structurizr-core/src/com/structurizr/util/WorkspaceUtils.java deleted file mode 100644 index 3050dbcd1..000000000 --- a/structurizr-core/src/com/structurizr/util/WorkspaceUtils.java +++ /dev/null @@ -1,64 +0,0 @@ -package com.structurizr.util; - -import com.structurizr.Workspace; -import com.structurizr.io.WorkspaceWriterException; -import com.structurizr.io.json.JsonReader; -import com.structurizr.io.json.JsonWriter; - -import java.io.*; -import java.nio.charset.StandardCharsets; - -/** - * Some utility methods related to workspaces. - */ -public final class WorkspaceUtils { - - /** - * Loads a workspace from a JSON definition saved as a file. - * - * @param file a File representing the JSON definition - * @return a Workspace object - * @throws Exception if something goes wrong - */ - public static Workspace loadWorkspaceFromJson(File file) throws Exception { - if (file == null) { - throw new IllegalArgumentException("The path to a JSON file must be specified."); - } else if (!file.exists()) { - throw new IllegalArgumentException("The specified JSON file does not exist."); - } - - return new JsonReader().read(new FileReader(file)); - } - - /** - * Saves a workspace to a JSON definition as a file. - * - * @param workspace a Workspace object - * @param file a File representing the JSON definition - * @throws Exception if something goes wrong - */ - public static void saveWorkspaceToJson(Workspace workspace, File file) throws Exception { - if (workspace == null) { - throw new IllegalArgumentException("A workspace must be provided."); - } else if (file == null) { - throw new IllegalArgumentException("The path to a JSON file must be specified."); - } - - OutputStreamWriter writer = new OutputStreamWriter(new FileOutputStream(file), StandardCharsets.UTF_8); - new JsonWriter(true).write(workspace, writer); - writer.flush(); - writer.close(); - } - - public static void printWorkspaceAsJson(Workspace workspace) { - try { - JsonWriter jsonWriter = new JsonWriter(true); - StringWriter stringWriter = new StringWriter(); - jsonWriter.write(workspace, stringWriter); - System.out.println(stringWriter.toString()); - } catch (WorkspaceWriterException e) { - e.printStackTrace(); - } - } - -} \ No newline at end of file diff --git a/structurizr-core/src/com/structurizr/view/Border.java b/structurizr-core/src/com/structurizr/view/Border.java deleted file mode 100644 index fd92bc94c..000000000 --- a/structurizr-core/src/com/structurizr/view/Border.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.structurizr.view; - -public enum Border { - - Solid, - Dashed - -} diff --git a/structurizr-core/src/com/structurizr/view/Branding.java b/structurizr-core/src/com/structurizr/view/Branding.java deleted file mode 100644 index 05977f0a1..000000000 --- a/structurizr-core/src/com/structurizr/view/Branding.java +++ /dev/null @@ -1,95 +0,0 @@ -package com.structurizr.view; - -import com.structurizr.util.Url; - -/** - * A wrapper for the font, logo and color scheme associated with a corporate branding. - */ -public final class Branding { - - private String logo; - - private Font font; - - private ColorPair color1; - private ColorPair color2; - private ColorPair color3; - private ColorPair color4; - private ColorPair color5; - - Branding() { - } - - public String getLogo() { - return logo; - } - - /** - * Sets the URL of an image representing a logo. - * - * @param url a URL as a String - */ - public void setLogo(String url) { - if (url != null && url.trim().length() > 0) { - if (Url.isUrl(url) || url.startsWith("data:image/")) { - this.logo = url; - } else { - throw new IllegalArgumentException(url + " is not a valid URL."); - } - } - } - - public Font getFont() { - return font; - } - - /** - * Sets the font to use. - * - * @param font a Font object - */ - public void setFont(Font font) { - this.font = font; - } - - public ColorPair getColor1() { - return color1; - } - - public void setColor1(ColorPair color1) { - this.color1 = color1; - } - - public ColorPair getColor2() { - return color2; - } - - public void setColor2(ColorPair color2) { - this.color2 = color2; - } - - public ColorPair getColor3() { - return color3; - } - - public void setColor3(ColorPair color3) { - this.color3 = color3; - } - - public ColorPair getColor4() { - return color4; - } - - public void setColor4(ColorPair color4) { - this.color4 = color4; - } - - public ColorPair getColor5() { - return color5; - } - - public void setColor5(ColorPair color5) { - this.color5 = color5; - } - -} diff --git a/structurizr-core/src/com/structurizr/view/Color.java b/structurizr-core/src/com/structurizr/view/Color.java deleted file mode 100644 index c224d3f40..000000000 --- a/structurizr-core/src/com/structurizr/view/Color.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.structurizr.view; - -public class Color { - - public static boolean isHexColorCode(String colorAsString) { - return colorAsString != null && colorAsString.matches("^#[A-Fa-f0-9]{6}"); - } - -} \ No newline at end of file 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/src/com/structurizr/view/ComponentView.java b/structurizr-core/src/com/structurizr/view/ComponentView.java deleted file mode 100644 index aa1444a98..000000000 --- a/structurizr-core/src/com/structurizr/view/ComponentView.java +++ /dev/null @@ -1,215 +0,0 @@ -package com.structurizr.view; - -import com.fasterxml.jackson.annotation.JsonIgnore; -import com.structurizr.model.*; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; - -import java.util.HashSet; -import java.util.Set; - -public class ComponentView extends StaticView { - - private Container container; - private String containerId; - - private static final Log LOG = LogFactory.getLog(ComponentView.class); - - ComponentView() { - } - - ComponentView(Container container, String key, String description) { - super(container.getSoftwareSystem(), key, description); - - this.container = container; - } - - /** - * Gets the ID of the container associated with this view. - * - * @return the ID, as a String - */ - public String getContainerId() { - if (this.container != null) { - return container.getId(); - } else { - return this.containerId; - } - } - - void setContainerId(String containerId) { - this.containerId = containerId; - } - - @JsonIgnore - public Container getContainer() { - return container; - } - - public void setContainer(Container container) { - this.container = container; - } - - @Override - public void add(SoftwareSystem softwareSystem) { - if (softwareSystem != null && !softwareSystem.equals(getSoftwareSystem())) { - addElement(softwareSystem, true); - } - } - - /** - * Adds all containers in the software system to this view. - */ - public void addAllContainers() { - getSoftwareSystem().getContainers().stream() - .forEach(this::add); - } - - /** - * Adds an individual container to this view. - * - * @param container the Container to add - */ - public void add(Container container) { - if (container != null && !container.equals(getContainer())) { - if (container.getParent().equals(getSoftwareSystem())) { - addElement(container, true); - } else { - throw new IllegalArgumentException("Only containers belonging to " + getSoftwareSystem().getName() + " can be added to this view."); - } - } - } - - /** - * Adds all components in the container to this view. - */ - public void addAllComponents() { - container.getComponents().forEach(this::add); - } - - /** - * Adds an individual component to this view. - * - * @param component the Component to add - */ - public void add(Component component) { - if (component != null) { - if (!component.getContainer().equals(getContainer())) { - throw new IllegalArgumentException("Only components belonging to " + container.getName() + " can be added to this view."); - } - - addElement(component, true); - } - } - - /** - * Removes an individual container from this view. - * - * @param container the Container to remove - */ - public void remove(Container container) { - removeElement(container); - } - - /** - * Removes an individual component from this view. - * - * @param component the Component to remove - */ - public void remove(Component component) { - removeElement(component); - } - - @Override - public String getName() { - return getSoftwareSystem().getName() + " - " + getContainer().getName() + " - Components"; - } - - @Override - public void addAllElements() { - addAllSoftwareSystems(); - addAllPeople(); - addAllContainers(); - addAllComponents(); - } - - @Override - public void addNearestNeighbours(Element element) { - super.addNearestNeighbours(element, SoftwareSystem.class); - super.addNearestNeighbours(element, Person.class); - super.addNearestNeighbours(element, Container.class); - super.addNearestNeighbours(element, Component.class); - } - - /** - *

Adds all {@link Element}s external to the container (Person, SoftwareSystem or Container) - * that have {@link Relationship}s to or from {@link Component}s in this view.

- *

Not included are:

- *
    - *
  • References to and from the {@link Container} of this view (only references to and from the components are considered)
  • - *
  • {@link Relationship}s between external {@link Element}s (i.e. elements that are not part of this container)
  • - *
- *

Don't forget to add elements to your view prior to calling this method, e.g. by calling {@link #addAllComponents()} - * or be selectively choosing certain components.

- */ - public void addExternalDependencies() { - final Set components = new HashSet<>(); - getElements().stream() - .map(ElementView::getElement) - .filter(e -> e instanceof Component) - .forEach(components::add); - - // add relationships of all other elements to or from our inside components - for (Relationship relationship : getContainer().getModel().getRelationships()) { - if (components.contains(relationship.getSource())) { - addExternalDependency(relationship.getDestination(), components); - } - if (components.contains(relationship.getDestination())) { - addExternalDependency(relationship.getSource(), components); - } - } - - // remove all relationships between elements outside of this container - getRelationships().stream() - .map(RelationshipView::getRelationship) - .filter(r -> !components.contains(r.getSource()) && !components.contains(r.getDestination())) - .forEach(this::remove); - } - - private void addExternalDependency(Element element, Set components) { - if (element instanceof Component) { - if (element.getParent().equals(getContainer())) { - // the component is in the same container, so we'll ignore it since we're only interested in external dependencies - return; - } else { - // the component is in a different container, so let's try to add that instead - element = element.getParent(); - } - } - - if (element instanceof Container) { - if (element.getParent().equals(this.getContainer().getParent())) { - // the container is in the same software system - addElement(element, true); - return; - } else { - // the container is in a different software system, so add that instead - element = element.getParent(); - } - } - - if (element instanceof SoftwareSystem || element instanceof Person) { - addElement(element, true); - } - } - - private boolean hasAnyRelationship(Container container, Set components) { - for (Element component : components) { - if (component.hasEfferentRelationshipWith(container) || container.hasEfferentRelationshipWith(component)) { - return true; - } - } - return false; - } - -} \ No newline at end of file diff --git a/structurizr-core/src/com/structurizr/view/Configuration.java b/structurizr-core/src/com/structurizr/view/Configuration.java deleted file mode 100644 index fe1ebd340..000000000 --- a/structurizr-core/src/com/structurizr/view/Configuration.java +++ /dev/null @@ -1,83 +0,0 @@ -package com.structurizr.view; - -import com.fasterxml.jackson.annotation.JsonGetter; -import com.fasterxml.jackson.annotation.JsonSetter; - -/** - * Configuration associated with how information in the workspace is rendered. - */ -public final class Configuration { - - private Branding branding = new Branding(); - private Styles styles = new Styles(); - - private String defaultView; - private String lastSavedView; - - /** - * Gets the styles associated with this set of views. - * - * @return a Styles object - */ - public Styles getStyles() { - return styles; - } - - /** - * Gets the key of the view that should be shown by default. - * - * @return the key, as a String (or null if not specified) - */ - public String getDefaultView() { - return defaultView; - } - - @JsonSetter - void setDefaultView(String defaultView) { - this.defaultView = defaultView; - } - - /** - * Sets the view that should be shown by default. - * - * @param view a View object - */ - public void setDefaultView(View view) { - if (view != null) { - this.defaultView = view.getKey(); - } - } - - @JsonGetter - String getLastSavedView() { - return lastSavedView; - } - - @JsonSetter - void setLastSavedView(String lastSavedView) { - this.lastSavedView = lastSavedView; - } - - public void copyConfigurationFrom(Configuration configuration) { - setLastSavedView(configuration.getLastSavedView()); - } - - /** - * Gets the Branding object associated with this workspace. - * - * @return a Branding object - */ - public Branding getBranding() { - return branding; - } - - /** - * Sets the Branding object associated with this workspace. - * - * @param branding a Branding object - */ - void setBranding(Branding branding) { - this.branding = branding; - } - -} diff --git a/structurizr-core/src/com/structurizr/view/ContainerView.java b/structurizr-core/src/com/structurizr/view/ContainerView.java deleted file mode 100644 index 18ad74c57..000000000 --- a/structurizr-core/src/com/structurizr/view/ContainerView.java +++ /dev/null @@ -1,74 +0,0 @@ -package com.structurizr.view; - -import com.structurizr.model.Container; -import com.structurizr.model.Element; -import com.structurizr.model.Person; -import com.structurizr.model.SoftwareSystem; - -public final class ContainerView extends StaticView { - - ContainerView() { - } - - ContainerView(SoftwareSystem softwareSystem, String key, String description) { - super(softwareSystem, key, description); - } - - @Override - public void add(SoftwareSystem softwareSystem) { - if (softwareSystem != null && !softwareSystem.equals(getSoftwareSystem())) { - addElement(softwareSystem, true); - } - } - - /** - * Adds all containers in the software system to this view. - */ - public void addAllContainers() { - getSoftwareSystem().getContainers().forEach(this::add); - } - - /** - * Adds an individual container to this view. - * - * @param container the Container to add - */ - public void add(Container container) { - if (container != null) { - if (container.getParent().equals(getSoftwareSystem())) { - addElement(container, true); - } else { - throw new IllegalArgumentException("Only containers belonging to " + getSoftwareSystem().getName() + " can be added to this view."); - } - } - } - - /** - * Removes an individual container from this view. - * - * @param container the Container to remove - */ - public void remove(Container container) { - removeElement(container); - } - - @Override - public String getName() { - return getSoftwareSystem().getName() + " - Containers"; - } - - @Override - public void addAllElements() { - addAllSoftwareSystems(); - addAllPeople(); - addAllContainers(); - } - - @Override - public void addNearestNeighbours(Element element) { - super.addNearestNeighbours(element, SoftwareSystem.class); - super.addNearestNeighbours(element, Person.class); - super.addNearestNeighbours(element, Container.class); - } - -} \ 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 deleted file mode 100644 index 4337a18fd..000000000 --- a/structurizr-core/src/com/structurizr/view/DeploymentView.java +++ /dev/null @@ -1,101 +0,0 @@ -package com.structurizr.view; - -import com.fasterxml.jackson.annotation.JsonIgnore; -import com.structurizr.model.*; - -/** - * A deployment view, used to show the mapping of container instances to deployment nodes. - */ -public final class DeploymentView extends View { - - private Model model; - - DeploymentView() { - } - - DeploymentView(Model model, String key, String description) { - super(null, key, description); - - this.model = model; - } - - DeploymentView(SoftwareSystem softwareSystem, String key, String description) { - super(softwareSystem, key, description); - - this.model = softwareSystem.getModel(); - } - - @JsonIgnore - @Override - public Model getModel() { - return this.model; - } - - void setModel(Model model) { - this.model = model; - } - - /** - * Adds all of the top-level deployment nodes to this view. - */ - public void addAllDeploymentNodes() { - getModel().getDeploymentNodes().forEach(this::add); - } - - /** - * Adds a deployment node to this view. - * - * @param deploymentNode the DeploymentNode to add - */ - public void add(DeploymentNode deploymentNode) { - if (deploymentNode != null) { - if (addContainerInstancesAndDeploymentNodes(deploymentNode)) { - Element parent = deploymentNode.getParent(); - while (parent != null) { - addElement(parent, false); - parent = parent.getParent(); - } - } - } - } - - private boolean addContainerInstancesAndDeploymentNodes(DeploymentNode deploymentNode) { - boolean hasContainers = false; - for (ContainerInstance containerInstance : deploymentNode.getContainerInstances()) { - Container container = containerInstance.getContainer(); - if (getSoftwareSystem() == null || container.getParent().equals(getSoftwareSystem())) { - addElement(containerInstance, true); - hasContainers = true; - } - } - - for (DeploymentNode child : deploymentNode.getChildren()) { - hasContainers = hasContainers | addContainerInstancesAndDeploymentNodes(child); - } - - if (hasContainers) { - addElement(deploymentNode, false); - } - - return hasContainers; - } - - /** - * Removes a deployment node from this view. - * - * @param deploymentNode the DeploymentNode to remove - */ - public void remove(DeploymentNode deploymentNode) { - removeElement(deploymentNode); - } - - @Override - public String getName() { - if (getSoftwareSystem() != null) { - return getSoftwareSystem().getName() + " - Deployment"; - } else { - return "Deployment"; - } - } - -} \ 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 deleted file mode 100644 index 3b3ce01eb..000000000 --- a/structurizr-core/src/com/structurizr/view/DynamicView.java +++ /dev/null @@ -1,201 +0,0 @@ -package com.structurizr.view; - -import com.fasterxml.jackson.annotation.JsonIgnore; -import com.structurizr.model.*; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; - -/** - * A dynamic view, used to describe behaviour between static elements at runtime. - */ -public final class DynamicView extends View { - - private Model model; - - private Element element; - private String elementId; - - private SequenceNumber sequenceNumber = new SequenceNumber(); - - DynamicView() { - } - - DynamicView(Model model, String key, String description) { - super(null, key, description); - - this.model = model; - this.element = null; - } - - DynamicView(SoftwareSystem softwareSystem, String key, String description) { - super(softwareSystem, key, description); - - this.model = softwareSystem.getModel(); - this.element = softwareSystem; - } - - DynamicView(Container container, String key, String description) { - super(container.getSoftwareSystem(), key, description); - - this.model = container.getModel(); - this.element = container; - } - - @JsonIgnore - @Override - public Model getModel() { - return this.model; - } - - void setModel(Model model) { - this.model = model; - } - - @Override - @JsonIgnore - public String getSoftwareSystemId() { - return super.getSoftwareSystemId(); - } - - /** - * Gets the ID of the container associated with this view. - * - * @return the ID, as a String - */ - 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; - } - - public void setElement(Element element) { - this.element = element; - } - - public RelationshipView add(Element source, Element destination) { - return add(source, "", destination); - } - - public RelationshipView add(Element source, String description, Element destination) { - if (source != null && destination != null) { - checkElement(source); - checkElement(destination); - - // check that the relationship is in the model before adding it - Relationship relationship = source.getEfferentRelationshipWith(destination); - if (relationship != null) { - addElement(source, false); - addElement(destination, false); - RelationshipView relationshipView = addRelationship(relationship, description, sequenceNumber.getNext()); - return relationshipView; - } else { - throw new IllegalArgumentException("Relationship does not exist in model"); - } - } else { - throw new IllegalArgumentException("Source and destination must not be null"); - } - } - - /** - * This checks that only appropriate elements can be added to the view. - */ - private void checkElement(Element e) { - // people can always be added - if (e instanceof Person) { - return; - } - - // if the scope of this dynamic is a software system, we only want: - // - containers inside that software system - // - other software systems - if (element instanceof SoftwareSystem) { - if (e.equals(element)) { - throw new IllegalArgumentException(e.getName() + " is already the scope of this view and cannot be added to it."); - } - if (e instanceof Container && !e.getParent().equals(element)) { - throw new IllegalArgumentException("Only containers that reside inside " + element.getName() + " can be added to this view."); - } - if (e instanceof Component) { - throw new IllegalArgumentException("Components can't be added to a dynamic view when the scope is a software system."); - } - } - - // if the scope of this dynamic view is a container, we only want other containers inside the same software system - // and other components inside the container - if (element instanceof Container) { - if (e.equals(element) || e.equals(element.getParent())) { - throw new IllegalArgumentException(e.getName() + " is already the scope of this view and cannot be added to it."); - } - if (e instanceof Container && !e.getParent().equals(element.getParent())) { - throw new IllegalArgumentException("Only containers that reside inside " + element.getParent().getName() + " can be added to this view."); - } - - if (e instanceof Component && !e.getParent().equals(element)) { - throw new IllegalArgumentException("Only components that reside inside " + element.getName() + " can be added to this view."); - } - } - } - - @Override - public RelationshipView add(Relationship relationship) { - // when adding a relationship to a DynamicView we suppose the user really wants to also see both elements - addElement(relationship.getSource(), false); - addElement(relationship.getDestination(), false); - return super.add(relationship); - } - - @Override - protected RelationshipView findRelationshipView(RelationshipView sourceRelationshipView) { - for (RelationshipView relationshipView : getRelationships()) { - if (relationshipView.getRelationship().equals(sourceRelationshipView.getRelationship())) { - if ((relationshipView.getDescription() != null && relationshipView.getDescription().equals(sourceRelationshipView.getDescription())) && - relationshipView.getOrder().equals(sourceRelationshipView.getOrder())) { - return relationshipView; - } - } - } - - return null; - } - - @Override - public String getName() { - if (element != null) { - return element.getName() + " - Dynamic"; - } else { - return "Dynamic"; - } - } - - @Override - public String toString() { - StringBuilder buf = new StringBuilder(); - List list = new ArrayList<>(getRelationships()); - Collections.sort(list, (rv1, rv2) -> rv1.getOrder().compareTo(rv2.getOrder())); - list.forEach(rv -> buf.append(rv.toString() + "\n")); - - return buf.toString(); - } - - public void startParallelSequence() { - sequenceNumber.startParallelSequence(); - } - - public void endParallelSequence() { - sequenceNumber.endParallelSequence(); - } - -} \ 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 deleted file mode 100644 index f1efd76d5..000000000 --- a/structurizr-core/src/com/structurizr/view/ElementStyle.java +++ /dev/null @@ -1,233 +0,0 @@ -package com.structurizr.view; - -import com.fasterxml.jackson.annotation.JsonInclude; - -/** - * A definition of an element style. - */ -public final class ElementStyle { - - 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; - - @JsonInclude(value = JsonInclude.Include.NON_NULL) - private Integer height; - - @JsonInclude(value = JsonInclude.Include.NON_NULL) - private String background; - - @JsonInclude(value = JsonInclude.Include.NON_NULL) - private String color; - - @JsonInclude(value = JsonInclude.Include.NON_NULL) - private Integer fontSize; - - @JsonInclude(value = JsonInclude.Include.NON_NULL) - private Shape shape; - - @JsonInclude(value = JsonInclude.Include.NON_NULL) - private Border border; - - @JsonInclude(value = JsonInclude.Include.NON_NULL) - private Integer opacity; - - ElementStyle() { - } - - ElementStyle(String tag) { - this.tag = tag; - } - - public ElementStyle(String tag, Integer width, Integer height, String background, String color, Integer fontSize) { - this(tag, width, height, background, color, fontSize, null); - } - - public ElementStyle(String tag, Integer width, Integer height, String background, String color, Integer fontSize, Shape shape) { - this.tag = tag; - this.width = width; - this.height = height; - setBackground(background); - setColor(color); - this.fontSize = fontSize; - 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. - * - * @return the width as an Integer, or null if not specified - */ - public Integer getWidth() { - return width; - } - - public void setWidth(Integer width) { - this.width = width; - } - - public ElementStyle width(int width) { - setWidth(width); - return this; - } - - /** - * Gets the height of the element, in pixels. - * - * @return the height as an Integer, or null if not specified - */ - public Integer getHeight() { - return height; - } - - public void setHeight(Integer height) { - this.height = height; - } - - public ElementStyle height(int height) { - setHeight(height); - return this; - } - - /** - * Gets the background colour of the element, as a HTML RGB hex string (e.g. #123456). - * - * @return the background colour as a String, or null if not specified - */ - public String getBackground() { - return background; - } - - public void setBackground(String background) { - if (Color.isHexColorCode(background)) { - this.background = background; - } else { - throw new IllegalArgumentException(background + " is not a valid hex colour code."); - } - } - - public ElementStyle background(String background) { - setBackground(background); - return this; - } - - /** - * Gets the foreground (text) colour of the element, as a HTML RGB hex string (e.g. #123456). - * - * @return the foreground colour as a String, or null if not specified - */ - public String getColor() { - return color; - } - - public void setColor(String color) { - if (Color.isHexColorCode(color)) { - this.color = color; - } else { - throw new IllegalArgumentException(color + " is not a valid hex colour code."); - } - } - - public ElementStyle color(String color) { - setColor(color); - return this; - } - - /** - * Gets the standard font size used to render text, in pixels. - * - * @return the font size, in pixels, as an Integer, or null if not specified - */ - public Integer getFontSize() { - return fontSize; - } - - public void setFontSize(Integer fontSize) { - this.fontSize = fontSize; - } - - public ElementStyle fontSize(int fontSize) { - setFontSize(fontSize); - return this; - } - - /** - * Gets the shape used to render the element. - * - * @return a Shape, or null if not specified - */ - public Shape getShape() { - return shape; - } - - public void setShape(Shape shape) { - this.shape = shape; - } - - public ElementStyle shape(Shape shape) { - setShape(shape); - return this; - } - - /** - * Gets the border used when rendering the element. - * - * @return a Border, or null if not specified - */ - public Border getBorder() { - return border; - } - - public void setBorder(Border border) { - this.border = border; - } - - public ElementStyle border(Border border) { - setBorder(border); - return this; - } - - /** - * Gets the opacity used when rendering the element. - * - * @return the opacity, as an integer between 0 and 100. - */ - public Integer getOpacity() { - return opacity; - } - - public void setOpacity(Integer opacity) { - if (opacity != null) { - if (opacity < 0) { - this.opacity = 0; - } else if (opacity > 100) { - this.opacity = 100; - } else { - this.opacity = opacity; - } - } - } - - public ElementStyle opacity(int opacity) { - setOpacity(opacity); - return this; - } - -} diff --git a/structurizr-core/src/com/structurizr/view/EnterpriseContextView.java b/structurizr-core/src/com/structurizr/view/EnterpriseContextView.java deleted file mode 100644 index 0028baf3b..000000000 --- a/structurizr-core/src/com/structurizr/view/EnterpriseContextView.java +++ /dev/null @@ -1,61 +0,0 @@ -package com.structurizr.view; - -import com.fasterxml.jackson.annotation.JsonIgnore; -import com.structurizr.model.*; - -/** - * Represents an Enterprise Context view that sits above the C4 model. This is the "big picture" view, - * showing the software systems and people in an given environment. - * The permitted elements in this view are software systems and people. - */ -public final class EnterpriseContextView extends StaticView { - - private Model model; - - EnterpriseContextView() { - } - - /** - * Creates an enterprise context view. - * - * @param key the key for the view - * @param description the description for the view - */ - EnterpriseContextView(Model model, String key, String description) { - super(null, key, description); - - this.model = model; - } - - @Override - public String getName() { - Enterprise enterprise = model.getEnterprise(); - return "Enterprise Context" + (enterprise != null && enterprise.getName().trim().length() > 0 ? " for " + enterprise.getName() : ""); - } - - @JsonIgnore - @Override - public Model getModel() { - return this.model; - } - - void setModel(Model model) { - this.model = model; - } - - /** - * Adds all software systems and all people to this view. - */ - @Override - public void addAllElements() { - addAllSoftwareSystems(); - addAllPeople(); - } - - @Override - public void addNearestNeighbours(Element element) { - super.addNearestNeighbours(element, SoftwareSystem.class); - super.addNearestNeighbours(element, Person.class); - } - -} \ No newline at end of file diff --git a/structurizr-core/src/com/structurizr/view/FilteredView.java b/structurizr-core/src/com/structurizr/view/FilteredView.java deleted file mode 100644 index 2b77735c9..000000000 --- a/structurizr-core/src/com/structurizr/view/FilteredView.java +++ /dev/null @@ -1,83 +0,0 @@ -package com.structurizr.view; - -import com.fasterxml.jackson.annotation.JsonIgnore; - -import java.util.Arrays; -import java.util.HashSet; -import java.util.Set; - -/** - * Represents a view on top of a view, which can be used to include or exclude specific elements. - */ -public final class FilteredView { - - private View view; - private String baseViewKey; - - private String key; - private String description = ""; - - private FilterMode mode = FilterMode.Exclude; - private Set tags = new HashSet<>(); - - FilteredView() { - } - - FilteredView(StaticView view, String key, String description, FilterMode mode, String... tags) { - this.view = view; - this.key = key; - this.description = description; - this.mode = mode; - this.tags.addAll(Arrays.asList(tags)); - } - - @JsonIgnore - public View getView() { - return view; - } - - void setView(View view) { - this.view = view; - } - - public String getBaseViewKey() { - if (view != null) { - return view.getKey(); - } else { - return this.baseViewKey; - } - } - - void setBaseViewKey(String baseViewKey) { - this.baseViewKey = baseViewKey; - } - - public String getKey() { - return key; - } - - void setKey(String key) { - this.key = key; - } - - public String getDescription() { - return description; - } - - void setDescription(String description) { - this.description = description; - } - - public FilterMode getMode() { - return mode; - } - - void setMode(FilterMode mode) { - this.mode = mode; - } - - public Set getTags() { - return new HashSet<>(tags); - } - -} \ No newline at end of file diff --git a/structurizr-core/src/com/structurizr/view/PaperSize.java b/structurizr-core/src/com/structurizr/view/PaperSize.java deleted file mode 100644 index 5ac705eab..000000000 --- a/structurizr-core/src/com/structurizr/view/PaperSize.java +++ /dev/null @@ -1,65 +0,0 @@ -package com.structurizr.view; - -/** - * These represent paper sizes in pixels at 300dpi. - */ -public enum PaperSize { - - A6_Portrait("A6", Orientation.Portrait, 1240, 1748), - A6_Landscape("A6", Orientation.Landscape, 1748, 1240), - - A5_Portrait("A5", Orientation.Portrait, 1748, 2480), - A5_Landscape("A5", Orientation.Landscape, 2480, 1748), - - A4_Portrait("A4", Orientation.Portrait, 2480, 3508), - A4_Landscape("A4", Orientation.Landscape, 3508, 2480), - - A3_Portrait("A3", Orientation.Portrait, 3508, 4961), - A3_Landscape("A3", Orientation.Landscape, 4961, 3508), - - A2_Portrait("A2", Orientation.Portrait, 4961, 7016), - A2_Landscape("A2", Orientation.Landscape, 7016, 4961), - - Letter_Portrait("Letter", Orientation.Portrait, 2550, 3300), - Letter_Landscape("Letter", Orientation.Landscape, 3300, 2550), - - Legal_Portrait("Legal", Orientation.Portrait, 2550, 4200), - Legal_Landscape("Legal", Orientation.Landscape, 4200, 2550), - - Slide_4_3("Slide 4:3", Orientation.Landscape, 3306, 2480), - Slide_16_9("Slide 16:9", Orientation.Landscape, 3508, 1973); - - private String name; - private Orientation orientation; - private int width; - private int height; - - private PaperSize(String name, Orientation orientation, int width, int height) { - this.name = name; - this.orientation = orientation; - this.width = width; - this.height = height; - } - - public String getName() { - return name; - } - - public Orientation getOrientation() { - return orientation; - } - - public int getWidth() { - return width; - } - - public int getHeight() { - return height; - } - - enum Orientation { - Portrait, - Landscape - } - -} diff --git a/structurizr-core/src/com/structurizr/view/ParallelSequenceCounter.java b/structurizr-core/src/com/structurizr/view/ParallelSequenceCounter.java deleted file mode 100644 index 44ff56636..000000000 --- a/structurizr-core/src/com/structurizr/view/ParallelSequenceCounter.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.structurizr.view; - -class ParallelSequenceCounter extends SequenceCounter { - - private SequenceCounter root = null; - - ParallelSequenceCounter(SequenceCounter parent) { - super(parent); - this.root = (SequenceCounter)parent.clone(); - - setSequence(parent.getSequence()); - } - - @Override - void increment() { - getParent().increment(); - } - - SequenceCounter getRoot() { - return this.root; - } - - @Override - public String toString() { - return getParent().toString(); - } -} diff --git a/structurizr-core/src/com/structurizr/view/RelationshipStyle.java b/structurizr-core/src/com/structurizr/view/RelationshipStyle.java deleted file mode 100644 index 7f429eb04..000000000 --- a/structurizr-core/src/com/structurizr/view/RelationshipStyle.java +++ /dev/null @@ -1,200 +0,0 @@ -package com.structurizr.view; - -import com.fasterxml.jackson.annotation.JsonInclude; - -public final class RelationshipStyle { - - 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; - - /** the colour of the line, as a HTML hex value (e.g. #123456) */ - @JsonInclude(value = JsonInclude.Include.NON_NULL) - private String color; - - /** the font size of the annotation, in pixels */ - @JsonInclude(value = JsonInclude.Include.NON_NULL) - private Integer fontSize; - - /** the width of the annotation, in pixels */ - @JsonInclude(value = JsonInclude.Include.NON_NULL) - private Integer width; - - /** whether the line should be dashed or not */ - @JsonInclude(value = JsonInclude.Include.NON_NULL) - private Boolean dashed; - - /** the routing algorithm used when rendering lines */ - @JsonInclude(value = JsonInclude.Include.NON_NULL) - private Routing routing; - - /** the position of the annotation along the line; 0 (start) to 100 (end) */ - @JsonInclude(value = JsonInclude.Include.NON_NULL) - private Integer position; - - /** the opacity of the line/text; 0 to 100 */ - @JsonInclude(value = JsonInclude.Include.NON_NULL) - private Integer opacity; - - RelationshipStyle() { - } - - RelationshipStyle(String tag) { - this.tag = tag; - } - - public RelationshipStyle(String tag, Integer thickness, String color, Boolean dashed, Routing routing, Integer fontSize, Integer width, Integer position) { - this.tag = tag; - this.thickness = thickness; - setColor(color); - this.dashed = dashed; - this.routing = routing; - this.fontSize = fontSize; - this.width = width; - this.position = position; - } - - public String getTag() { - return tag; - } - - public void setTag(String tag) { - this.tag = tag; - } - - public Integer getThickness() { - return thickness; - } - - public void setThickness(Integer thickness) { - this.thickness = thickness; - } - - public RelationshipStyle thickness(int thickness) { - setThickness(thickness); - return this; - } - - public String getColor() { - return color; - } - - public void setColor(String color) { - if (Color.isHexColorCode(color)) { - this.color = color; - } else { - throw new IllegalArgumentException(color + " is not a valid hex colour code."); - } - } - - public RelationshipStyle color(String color) { - setColor(color); - return this; - } - - public Boolean getDashed() { - return dashed; - } - - public void setDashed(Boolean dashed) { - this.dashed = dashed; - } - - public RelationshipStyle dashed(boolean dashed) { - setDashed(dashed); - return this; - } - - public Routing getRouting() { - return routing; - } - - public void setRouting(Routing routing) { - this.routing = routing; - } - - public RelationshipStyle routing(Routing routing) { - setRouting(routing); - return this; - } - - public Integer getFontSize() { - return fontSize; - } - - public void setFontSize(Integer fontSize) { - this.fontSize = fontSize; - } - - public RelationshipStyle fontSize(int fontSize) { - setFontSize(fontSize); - return this; - } - - public Integer getWidth() { - return width; - } - - public void setWidth(Integer width) { - this.width = width; - } - - public RelationshipStyle width(int width) { - setWidth(width); - return this; - } - - public Integer getPosition() { - return position; - } - - public void setPosition(Integer position) { - if (position == null) { - this.position = null; - } else if (position < START_OF_LINE) { - this.position = START_OF_LINE; - } else if (position > END_OF_LINE) { - this.position = END_OF_LINE; - } else { - this.position = position; - } - } - - public RelationshipStyle position(int position) { - setPosition(position); - return this; - } - - /** - * Gets the opacity used when rendering the relationship. - * - * @return the opacity, as an integer between 0 and 100. - */ - public Integer getOpacity() { - return opacity; - } - - public void setOpacity(Integer opacity) { - if (opacity != null) { - if (opacity < 0) { - this.opacity = 0; - } else if (opacity > 100) { - this.opacity = 100; - } else { - this.opacity = opacity; - } - } - } - - public RelationshipStyle opacity(int opacity) { - setOpacity(opacity); - return this; - } - -} diff --git a/structurizr-core/src/com/structurizr/view/RelationshipView.java b/structurizr-core/src/com/structurizr/view/RelationshipView.java deleted file mode 100644 index 9fb197f30..000000000 --- a/structurizr-core/src/com/structurizr/view/RelationshipView.java +++ /dev/null @@ -1,121 +0,0 @@ -package com.structurizr.view; - -import com.fasterxml.jackson.annotation.JsonIgnore; -import com.structurizr.model.Relationship; - -import java.util.Collection; -import java.util.LinkedList; - -public final class RelationshipView { - - private Relationship relationship; - private String id; - private String description; - private String order; - private Collection vertices = new LinkedList<>(); - - RelationshipView() { - } - - RelationshipView(Relationship relationship) { - this.relationship = relationship; - } - - public String getId() { - if (relationship != null) { - return relationship.getId(); - } else { - return this.id; - } - } - - void setId(String id) { - this.id = id; - } - - @JsonIgnore - public Relationship getRelationship() { - return relationship; - } - - public void setRelationship(Relationship relationship) { - this.relationship = relationship; - } - - /** - * Gets the description of this relationship (used in dynamic views only). - * - * @return the description, as a String - * or an empty string if a description has not been set - */ - public String getDescription() { - return description != null ? description : ""; - } - - public void setDescription(String description) { - this.description = description; - } - - /** - * Gets the order of this relationship (used in dynamic views only; e.g. 1.0, 1.1, 2.0, etc). - * - * @return the order, as a String - */ - public String getOrder() { - return order; - } - - public void setOrder(String order) { - this.order = order; - } - - /** - * Gets the set of vertices used to render the relationship. - * - * @return a collection of Vertex objects - */ - public Collection getVertices() { - return vertices; - } - - public void setVertices(Collection vertices) { - this.vertices = vertices; - } - - void copyLayoutInformationFrom(RelationshipView source) { - if (source != null) { - setVertices(source.getVertices()); - } - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - - RelationshipView that = (RelationshipView) o; - - if (description != null ? !description.equals(that.description) : that.description != null) return false; - if (!getId().equals(that.getId())) return false; - if (order != null ? !order.equals(that.order) : that.order != null) return false; - - return true; - } - - @Override - public int hashCode() { - int result = getId().hashCode(); - result = 31 * result + (description != null ? description.hashCode() : 0); - result = 31 * result + (order != null ? order.hashCode() : 0); - return result; - } - - @Override - public String toString() { - if (relationship != null) { - return (order != null ? order + ": " : "") + (description != null ? description + " " : "") + relationship.toString(); - } - return ""; - } - -} diff --git a/structurizr-core/src/com/structurizr/view/SequenceCounter.java b/structurizr-core/src/com/structurizr/view/SequenceCounter.java deleted file mode 100644 index 49f0abe20..000000000 --- a/structurizr-core/src/com/structurizr/view/SequenceCounter.java +++ /dev/null @@ -1,51 +0,0 @@ -package com.structurizr.view; - -class SequenceCounter implements Cloneable { - - private SequenceCounter parent; - private int sequence = 0; - - SequenceCounter() { - } - - SequenceCounter(SequenceCounter parent) { - this.parent = parent; - } - - void increment() { - this.sequence++; - } - - int getSequence() { - return this.sequence; - } - - void setSequence(int sequence) { - this.sequence = sequence; - } - - SequenceCounter getParent() { - return this.parent; - } - - @Override - public String toString() { - if (getParent() == null) { - return "" + getSequence(); - } else { - return getParent().toString() + "." + getSequence(); - } - } - - @Override - protected Object clone() { - SequenceCounter counter = new SequenceCounter(); - counter.sequence = this.sequence; - if (this.parent != null) { - counter.parent = (SequenceCounter) this.parent.clone(); - } - - return counter; - } - -} diff --git a/structurizr-core/src/com/structurizr/view/SequenceNumber.java b/structurizr-core/src/com/structurizr/view/SequenceNumber.java deleted file mode 100644 index 96a4d5ded..000000000 --- a/structurizr-core/src/com/structurizr/view/SequenceNumber.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.structurizr.view; - -class SequenceNumber { - - private SequenceCounter counter = new SequenceCounter(); - - SequenceNumber() { - } - - String getNext() { - counter.increment(); - return counter.toString(); - } - - void startChildSequence() { - this.counter = new SequenceCounter(counter); - } - - void endChildSequence() { - counter = this.counter.getParent(); - } - - void startParallelSequence() { - this.counter = new ParallelSequenceCounter(this.counter); - } - - void endParallelSequence() { - this.counter = ((ParallelSequenceCounter)this.counter).getRoot(); - } - -} diff --git a/structurizr-core/src/com/structurizr/view/Shape.java b/structurizr-core/src/com/structurizr/view/Shape.java deleted file mode 100644 index fe83c3a66..000000000 --- a/structurizr-core/src/com/structurizr/view/Shape.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.structurizr.view; - -public enum Shape { - - Box, - RoundedBox, - Circle, - Ellipse, - Hexagon, - Cylinder, - Pipe, - Person, - Folder - -} diff --git a/structurizr-core/src/com/structurizr/view/StaticView.java b/structurizr-core/src/com/structurizr/view/StaticView.java deleted file mode 100644 index c51487ac8..000000000 --- a/structurizr-core/src/com/structurizr/view/StaticView.java +++ /dev/null @@ -1,146 +0,0 @@ -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 java.util.HashSet; -import java.util.Set; - -public abstract class StaticView extends View { - - StaticView() { - } - - StaticView(SoftwareSystem softwareSystem, String key, String description) { - super(softwareSystem, key, description); - } - - /** - * Adds all software systems in the model to this view. - */ - public void addAllSoftwareSystems() { - getModel().getSoftwareSystems().forEach(this::add); - } - - /** - * Adds the given software system to this view. - * - * @param softwareSystem the SoftwareSystem to add - */ - public void add(SoftwareSystem softwareSystem) { - addElement(softwareSystem, true); - } - - /** - * Removes the given software system from this view. - * - * @param softwareSystem the SoftwareSystem to remove - */ - public void remove(SoftwareSystem softwareSystem) { - removeElement(softwareSystem); - } - - /** - * Adds all people in the model to this view. - */ - public void addAllPeople() { - getModel().getPeople().forEach(this::add); - } - - /** - * Adds the given person to this view. - * - * @param person the Person to add - */ - public void add(Person person) { - addElement(person, true); - } - - /** - * Removes the given person from this view. - * - * @param person the Person to add - */ - public void remove(Person person) { - removeElement(person); - } - - public abstract void addAllElements(); - - public abstract void addNearestNeighbours(Element element); - - protected void addNearestNeighbours(Element element, Class typeOfElement) { - if (element == null) { - return; - } - - addElement(element, true); - - Set relationships = getModel().getRelationships(); - relationships.stream().filter(r -> r.getSource().equals(element) && typeOfElement.isInstance(r.getDestination())) - .map(Relationship::getDestination) - .forEach(d -> addElement(d, true)); - - relationships.stream().filter(r -> r.getDestination().equals(element) && typeOfElement.isInstance(r.getSource())) - .map(Relationship::getSource) - .forEach(s -> addElement(s, true)); - } - - /** - * Removes all elements that cannot be reached by traversing the graph of relationships - * starting with the specified element. - * - * @param element the starting element - */ - public void removeElementsThatCantBeReachedFrom(Element element) { - if (element != null) { - Set elementsToShow = new HashSet<>(); - Set elementsVisited = new HashSet<>(); - findElementsToShow(element, element, elementsToShow, elementsVisited); - - for (ElementView elementView : getElements()) { - if (!elementsToShow.contains(elementView.getElement())) { - removeElement(elementView.getElement()); - } - } - } - } - - private void findElementsToShow(Element startingElement, Element element, Set elementsToShow, Set elementsVisited) { - if (!elementsVisited.contains(element) && getElements().contains(new ElementView(element))) { - elementsVisited.add(element); - elementsToShow.add(element); - - // check that we've not gone back to the starting point of the graph - if (!element.hasEfferentRelationshipWith(startingElement)) { - element.getRelationships().forEach(r -> findElementsToShow(startingElement, r.getDestination(), elementsToShow, elementsVisited)); - } - } - } - - /** - * Removes all {@link Element}s that have the given tag from this view. - * - * @param tag a tag - */ - public final void removeElementsWithTag(String tag) { - getElements().stream() - .map(ElementView::getElement) - .filter(e -> e.hasTag(tag)) - .forEach(this::removeElement); - } - - /** - * Removes all {@link Relationship}s that have the given tag from this view. - * - * @param tag a tag - */ - public final void removeRelationshipsWithTag(String tag) { - getRelationships().stream() - .map(RelationshipView::getRelationship) - .filter(r -> r.hasTag(tag)) - .forEach(this::remove); - } -} \ No newline at end of file diff --git a/structurizr-core/src/com/structurizr/view/Styles.java b/structurizr-core/src/com/structurizr/view/Styles.java deleted file mode 100644 index a0ebf51d1..000000000 --- a/structurizr-core/src/com/structurizr/view/Styles.java +++ /dev/null @@ -1,124 +0,0 @@ -package com.structurizr.view; - -import com.structurizr.model.Element; -import com.structurizr.model.Relationship; - -import java.util.Collection; -import java.util.LinkedList; - -public final class Styles { - - private Collection elements = new LinkedList<>(); - private Collection relationships = new LinkedList<>(); - - public Collection getElements() { - return elements; - } - - public void add(ElementStyle elementStyle) { - if (elementStyle != null) { - this.elements.add(elementStyle); - } - } - - public ElementStyle addElementStyle(String tag) { - ElementStyle elementStyle = null; - - if (tag != null) { - elementStyle = new ElementStyle(); - elementStyle.setTag(tag); - add(elementStyle); - } - - return elementStyle; - } - - public Collection getRelationships() { - return relationships; - } - - public void add(RelationshipStyle relationshipStyle) { - if (relationshipStyle != null) { - this.relationships.add(relationshipStyle); - } - } - - public RelationshipStyle addRelationshipStyle(String tag) { - RelationshipStyle relationshipStyle = null; - - if (tag != null) { - relationshipStyle = new RelationshipStyle(); - relationshipStyle.setTag(tag); - add(relationshipStyle); - } - - return relationshipStyle; - } - - private ElementStyle findElementStyle(String tag) { - if (tag != null) { - for (ElementStyle elementStyle : elements) { - if (elementStyle != null && elementStyle.getTag().equals(tag)) { - return elementStyle; - } - } - } - - return null; - } - - private RelationshipStyle findRelationshipStyle(String tag) { - if (tag != null) { - for (RelationshipStyle relationshipStyle : relationships) { - if (relationshipStyle != null && relationshipStyle.getTag().equals(tag)) { - return relationshipStyle; - } - } - } - - return null; - } - - public ElementStyle findElementStyle(Element element) { - ElementStyle style = new ElementStyle("").background("#dddddd").color("#000000").shape(Shape.Box); - - if (element != null) { - for (String tag : element.getTagsAsSet()) { - ElementStyle elementStyle = findElementStyle(tag); - if (elementStyle != null) { - if (elementStyle.getBackground() != null && elementStyle.getBackground().trim().length() > 0) { - style.setBackground(elementStyle.getBackground()); - } - - if (elementStyle.getColor() != null && elementStyle.getColor().trim().length() > 0) { - style.setColor(elementStyle.getColor()); - } - - if (elementStyle.getShape() != null) { - style.setShape(elementStyle.getShape()); - } - } - } - } - - return style; - } - - public RelationshipStyle findRelationshipStyle(Relationship relationship) { - RelationshipStyle style = new RelationshipStyle("").color("#707070"); - - if (relationship != null) { - for (String tag : relationship.getTagsAsSet()) { - RelationshipStyle relationshipStyle = findRelationshipStyle(tag); - if (relationshipStyle != null) { - if (relationshipStyle.getColor() != null && relationshipStyle.getColor().trim().length() > 0) { - style.setColor(relationshipStyle.getColor()); - } - } - } - } - - return style; - } - -} diff --git a/structurizr-core/src/com/structurizr/view/SystemContextView.java b/structurizr-core/src/com/structurizr/view/SystemContextView.java deleted file mode 100644 index c73d08040..000000000 --- a/structurizr-core/src/com/structurizr/view/SystemContextView.java +++ /dev/null @@ -1,50 +0,0 @@ -package com.structurizr.view; - -import com.structurizr.model.Element; -import com.structurizr.model.Person; -import com.structurizr.model.SoftwareSystem; - -/** - * Represents the System Context view from the C4 model. This is the "big picture" view, - * showing how a software system fits into its environment, in terms of key types of - * users and system dependencies. The permitted elements in this view are - * software systems and people. - */ -public final class SystemContextView extends StaticView { - - SystemContextView() { - } - - /** - * Creates a system context view for the given software system. - * - * @param softwareSystem the SoftwareSystem to create a view for - * @param description the (optional) description for the view - */ - SystemContextView(SoftwareSystem softwareSystem, String key, String description) { - super(softwareSystem, key, description); - - addElement(softwareSystem, true); - } - - @Override - public String getName() { - return getSoftwareSystem().getName() + " - System Context"; - } - - /** - * Adds all software systems and all people to this view. - */ - @Override - public void addAllElements() { - addAllSoftwareSystems(); - addAllPeople(); - } - - @Override - public void addNearestNeighbours(Element element) { - super.addNearestNeighbours(element, SoftwareSystem.class); - super.addNearestNeighbours(element, Person.class); - } - -} \ No newline at end of file diff --git a/structurizr-core/src/com/structurizr/view/Vertex.java b/structurizr-core/src/com/structurizr/view/Vertex.java deleted file mode 100644 index 8d85f2566..000000000 --- a/structurizr-core/src/com/structurizr/view/Vertex.java +++ /dev/null @@ -1,45 +0,0 @@ -package com.structurizr.view; - -/** - * The X, Y coordinate of a bend in a line. - */ -public final class Vertex { - - private int x; - private int y; - - Vertex() { - } - - public Vertex(int x, int y) { - this.x = x; - this.y = y; - } - - /** - * Gets the horizontal position of the vertex when rendered. - * - * @return the X coordinate, as an int - */ - public int getX() { - return x; - } - - public void setX(int x) { - this.x = x; - } - - /** - * Gets the vertical position of the vertex when rendered. - * - * @return the Y coordinate, as an int - */ - public int getY() { - return y; - } - - public void setY(int y) { - this.y = y; - } - -} diff --git a/structurizr-core/src/com/structurizr/view/View.java b/structurizr-core/src/com/structurizr/view/View.java deleted file mode 100644 index aaa734cff..000000000 --- a/structurizr-core/src/com/structurizr/view/View.java +++ /dev/null @@ -1,321 +0,0 @@ -package com.structurizr.view; - -import com.fasterxml.jackson.annotation.JsonIgnore; -import com.structurizr.model.Element; -import com.structurizr.model.Model; -import com.structurizr.model.Relationship; -import com.structurizr.model.SoftwareSystem; - -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. - */ -public abstract class View { - - private SoftwareSystem softwareSystem; - private String softwareSystemId; - private String description = ""; - private String key; - private PaperSize paperSize = null; - - private Set elementViews = new LinkedHashSet<>(); - - private Set relationshipViews = new LinkedHashSet<>(); - private ViewSet viewSet; - - View() { - } - - View(SoftwareSystem softwareSystem, String key, String description) { - this.softwareSystem = softwareSystem; - if (key != null && key.trim().length() > 0) { - setKey(key); - } else { - throw new IllegalArgumentException("A key must be specified."); - } - setDescription(description); - } - - @JsonIgnore - public Model getModel() { - return softwareSystem.getModel(); - } - - @JsonIgnore - public SoftwareSystem getSoftwareSystem() { - return softwareSystem; - } - - public 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 - */ - public String getSoftwareSystemId() { - if (this.softwareSystem != null) { - return this.softwareSystem.getId(); - } else { - return this.softwareSystemId; - } - } - - void setSoftwareSystemId(String softwareSystemId) { - this.softwareSystemId = softwareSystemId; - } - - public String getDescription() { - return description; - } - - public void setDescription(String description) { - if (description == null) { - this.description = ""; - } else { - this.description = description; - } - } - - /** - * Gets the identifer for this view. - * - * @return the identifier, as a String, - * or null if no key has been specified - */ - public String getKey() { - return key; - } - - void setKey(String key) { - this.key = key; - } - - /** - * Gets the paper size that should be used to render this view. - * - * @return a PaperSize (A4_Portrait by default) - */ - public PaperSize getPaperSize() { - return paperSize; - } - - public void setPaperSize(PaperSize paperSize) { - this.paperSize = paperSize; - } - - /** - * Gets the name of this view. - * - * @return the name, as a String - */ - @JsonIgnore - public abstract String getName(); - - protected final void addElement(Element element, boolean addRelationships) { - if (element != null) { - if (getModel().contains(element)) { - elementViews.add(new ElementView(element)); - - if (addRelationships) { - addRelationships(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) { - 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()); - } - } - } - } - - public RelationshipView add(Relationship relationship) { - if (relationship != null) { - if (isElementInView(relationship.getSource()) && isElementInView(relationship.getDestination())) { - RelationshipView relationshipView = new RelationshipView(relationship); - relationshipViews.add(relationshipView); - - return relationshipView; - } - } - - return null; - } - - private boolean isElementInView(Element element) { - return this.elementViews.stream().filter(ev -> ev.getElement().equals(element)).count() > 0; - } - - protected RelationshipView addRelationship(Relationship relationship, String description, String order) { - RelationshipView relationshipView = add(relationship); - if (relationshipView != null) { - relationshipView.setDescription(description); - relationshipView.setOrder(order); - } - - return relationshipView; - } - - 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) { - this.elementViews = elementViews; - } - - /** - * Gets the set of relationships in this view. - * - * @return a Set of RelationshipView objects - */ - public Set getRelationships() { - return new HashSet<>(this.relationshipViews); - } - - public void setRelationships(Set relationships) { - this.relationshipViews = relationships; - } - - /** - * 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()); - } - } - } - - public void copyLayoutInformationFrom(View source) { - if (this.getPaperSize() == null) { - this.setPaperSize(source.getPaperSize()); - } - - for (ElementView sourceElementView : source.getElements()) { - ElementView destinationElementView = findElementView(sourceElementView); - if (destinationElementView != null) { - destinationElementView.copyLayoutInformationFrom(sourceElementView); - } - } - - for (RelationshipView sourceRelationshipView : source.getRelationships()) { - RelationshipView destinationRelationshipView = findRelationshipView(sourceRelationshipView); - if (destinationRelationshipView != null) { - destinationRelationshipView.copyLayoutInformationFrom(sourceRelationshipView); - } - } - } - - private ElementView findElementView(ElementView sourceElementView) { - for (ElementView elementView : getElements()) { - if (elementView.getElement().equals(sourceElementView.getElement())) { - return elementView; - } - } - - return null; - } - - public ElementView getElementView(Element element) { - Optional elementView = this.elementViews.stream().filter(ev -> ev.getElement().equals(element)).findFirst(); - return elementView.isPresent() ? elementView.get() : null; - } - - protected RelationshipView findRelationshipView(RelationshipView sourceRelationshipView) { - for (RelationshipView relationshipView : getRelationships()) { - if (relationshipView.getRelationship().equals(sourceRelationshipView.getRelationship())) { - return relationshipView; - } - } - - return null; - } - - public RelationshipView getRelationshipView(Relationship relationship) { - Optional relationshipView = this.relationshipViews.stream().filter(rv -> rv.getRelationship().equals(relationship)).findFirst(); - return relationshipView.isPresent() ? relationshipView.get() : null; - } - - void setViewSet(ViewSet viewSet) { - this.viewSet = viewSet; - } - - @JsonIgnore - public ViewSet getViewSet() { - return viewSet; - } - -} \ 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 deleted file mode 100644 index b75c49ea2..000000000 --- a/structurizr-core/src/com/structurizr/view/ViewSet.java +++ /dev/null @@ -1,501 +0,0 @@ -package com.structurizr.view; - -import com.fasterxml.jackson.annotation.JsonIgnore; -import com.structurizr.model.Container; -import com.structurizr.model.Model; -import com.structurizr.model.SoftwareSystem; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; - -import java.util.Collection; -import java.util.HashSet; -import java.util.Set; - -/** - * A set of views onto a software architecture model. - */ -public final class ViewSet { - - private static final Log log = LogFactory.getLog(ViewSet.class); - - private Model model; - - private Collection enterpriseContextViews = 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 filteredViews = new HashSet<>(); - - private Configuration configuration = new Configuration(); - - ViewSet() { - } - - public ViewSet(Model model) { - this.model = model; - } - - @JsonIgnore - public Model getModel() { - return model; - } - - public void setModel(Model model) { - this.model = model; - } - - /** - * Creates an enterprise context view. - * - * @param key the key for the view (must be unique) - * @param description a description of the view - * @return an EnterpriseContextView object - * @throws IllegalArgumentException if the key is not unique - */ - public EnterpriseContextView createEnterpriseContextView(String key, String description) { - assertThatTheViewKeyIsUnique(key); - - EnterpriseContextView view = new EnterpriseContextView(model, key, description); - view.setViewSet(this); - enterpriseContextViews.add(view); - 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) - * @param description a description of the view - * @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, String description) { - assertThatTheSoftwareSystemIsNotNull(softwareSystem); - assertThatTheViewKeyIsUnique(key); - - SystemContextView view = new SystemContextView(softwareSystem, key, description); - view.setViewSet(this); - systemContextViews.add(view); - 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) - * @param description a description of the view - * @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, String description) { - assertThatTheSoftwareSystemIsNotNull(softwareSystem); - assertThatTheViewKeyIsUnique(key); - - ContainerView view = new ContainerView(softwareSystem, key, description); - view.setViewSet(this); - containerViews.add(view); - 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) - * @param description a description of the view - * @return a ContainerView object - * @throws IllegalArgumentException if the container is null or the key is not unique - */ - public ComponentView createComponentView(Container container, String key, String description) { - assertThatTheContainerIsNotNull(container); - assertThatTheViewKeyIsUnique(key); - - ComponentView view = new ComponentView(container, key, description); - view.setViewSet(this); - componentViews.add(view); - return view; - } - - /** - * Creates a dynamic view. - * - * @param key the key for the view (must be unique) - * @param description a description of the view - * @return a DynamicView object - * @throws IllegalArgumentException if the key is not unique - */ - public DynamicView createDynamicView(String key, String description) { - assertThatTheViewKeyIsUnique(key); - - DynamicView view = new DynamicView(getModel(), key, description); - view.setViewSet(this); - dynamicViews.add(view); - 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) - * @param description a description of the view - * @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, String description) { - assertThatTheSoftwareSystemIsNotNull(softwareSystem); - assertThatTheViewKeyIsUnique(key); - - DynamicView view = new DynamicView(softwareSystem, key, description); - view.setViewSet(this); - dynamicViews.add(view); - 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) - * @param description a description of the view - * @return a DynamicView object - * @throws IllegalArgumentException if the container is null or the key is not unique - */ - public DynamicView createDynamicView(Container container, String key, String description) { - assertThatTheContainerIsNotNull(container); - assertThatTheViewKeyIsUnique(key); - - DynamicView view = new DynamicView(container, key, description); - view.setViewSet(this); - dynamicViews.add(view); - return view; - } - - /** - * Creates a deployment view. - * - * @param key the key for the deployment view (must be unique) - * @param description a description of the view - * @return a DeploymentView object - * @throws IllegalArgumentException if the key is not unique - */ - public DeploymentView createDeploymentView(String key, String description) { - assertThatTheViewKeyIsUnique(key); - - DeploymentView view = new DeploymentView(getModel(), key, description); - view.setViewSet(this); - deploymentViews.add(view); - 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) - * @param description a description of the view - * @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, String description) { - assertThatTheSoftwareSystemIsNotNull(softwareSystem); - assertThatTheViewKeyIsUnique(key); - - DeploymentView view = new DeploymentView(softwareSystem, key, description); - view.setViewSet(this); - deploymentViews.add(view); - 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 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(StaticView view, String key, String description, FilterMode mode, String... tags) { - assertThatTheViewKeyIsUnique(key); - - FilteredView filteredView = new FilteredView(view, key, description, mode, tags); - filteredViews.add(filteredView); - return filteredView; - } - - private void assertThatTheViewKeyIsUnique(String key) { - if (getViewWithKey(key) != null || getFilteredViewWithKey(key) != null) { - throw new IllegalArgumentException("A view with the key " + key + " already exists."); - } - } - - private void assertThatTheSoftwareSystemIsNotNull(SoftwareSystem softwareSystem) { - if (softwareSystem == null) { - throw new IllegalArgumentException("Software system must not be null."); - } - } - - private void assertThatTheContainerIsNotNull(Container container) { - if (container == null) { - throw new IllegalArgumentException("Container must not be null."); - } - } - - /** - * Finds the view with the specified key, or null if the view does not exist. - * - * @param key the key - * @return a View object, or null if a view with the specified key could not be found - */ - public View getViewWithKey(String key) { - if (key == null) { - throw new IllegalArgumentException("A key must be specified."); - } - - Set views = new HashSet<>(); - 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); - } - - /** - * Finds the filtered view with the specified key, or null if the view does not exist. - * - * @param key the key - * @return a FilteredView object, or null if a view with the specified key could not be found - */ - public FilteredView getFilteredViewWithKey(String key) { - if (key == null) { - throw new IllegalArgumentException("A key must be specified."); - } - - return filteredViews.stream().filter(v -> key.equals(v.getKey())).findFirst().orElse(null); - } - - /** - * Gets the set of enterprise context views. - * - * @return a Collection of EnterpriseContextView objects - */ - public Collection getEnterpriseContextViews() { - return new HashSet<>(enterpriseContextViews); - } - - /** - * Gets the set of system context views. - * - * @return a Collection of SystemContextView objects - */ - public Collection getSystemContextViews() { - return new HashSet<>(systemContextViews); - } - - /** - * Gets the set of container views. - * - * @return a Collection of ContainerView objects - */ - public Collection getContainerViews() { - return new HashSet<>(containerViews); - } - - /** - * Gets the set of component views. - * - * @return a Collection of ComponentView objects - */ - public Collection getComponentViews() { - return new HashSet<>(componentViews); - } - - /** - * Gets the set of dynamic views. - * - * @return a Collection of DynamicView objects - */ - public Collection getDynamicViews() { - return new HashSet<>(dynamicViews); - } - - public Collection getFilteredViews() { - return new HashSet<>(filteredViews); - } - - /** - * Gets the set of dynamic views. - * - * @return a Collection of DynamicView objects - */ - public Collection getDeploymentViews() { - return new HashSet<>(deploymentViews); - } - - public void hydrate() { - for (EnterpriseContextView view : enterpriseContextViews) { - view.setModel(model); - hydrateView(view); - } - - for (SystemContextView view : systemContextViews) { - view.setSoftwareSystem(model.getSoftwareSystemWithId(view.getSoftwareSystemId())); - hydrateView(view); - } - - for (ContainerView view : containerViews) { - view.setSoftwareSystem(model.getSoftwareSystemWithId(view.getSoftwareSystemId())); - hydrateView(view); - } - - for (ComponentView view : componentViews) { - view.setSoftwareSystem(model.getSoftwareSystemWithId(view.getSoftwareSystemId())); - view.setContainer(view.getSoftwareSystem().getContainerWithId(view.getContainerId())); - hydrateView(view); - } - - for (DynamicView view : dynamicViews) { - view.setModel(model); - hydrateView(view); - } - - for (DeploymentView view : deploymentViews) { - view.setSoftwareSystem(model.getSoftwareSystemWithId(view.getSoftwareSystemId())); - view.setModel(model); - hydrateView(view); - } - - for (FilteredView filteredView : filteredViews) { - filteredView.setView(getViewWithKey(filteredView.getBaseViewKey())); - } - } - - private void hydrateView(View view) { - view.setViewSet(this); - - for (ElementView elementView : view.getElements()) { - elementView.setElement(model.getElement(elementView.getId())); - } - - for (RelationshipView relationshipView : view.getRelationships()) { - relationshipView.setRelationship(model.getRelationship(relationshipView.getId())); - } - } - - /** - * Gets the configuration object associated with this set of views. - * - * @return a Configuration object - */ - public Configuration getConfiguration() { - return configuration; - } - - public void copyLayoutInformationFrom(ViewSet source) { - for (EnterpriseContextView view : enterpriseContextViews) { - EnterpriseContextView sourceView = findView(source.getEnterpriseContextViews(), view); - if (sourceView != null) { - view.copyLayoutInformationFrom(sourceView); - } else { - log.warn("Could not find a matching view for \"" + view.getName() + "\" ... diagram layout information may be lost."); - } - } - - for (SystemContextView view : systemContextViews) { - SystemContextView sourceView = findView(source.getSystemContextViews(), view); - if (sourceView != null) { - view.copyLayoutInformationFrom(sourceView); - } else { - log.warn("Could not find a matching view for \"" + view.getName() + "\" ... diagram layout information may be lost."); - } - } - - for (ContainerView view : containerViews) { - ContainerView sourceView = findView(source.getContainerViews(), view); - if (sourceView != null) { - view.copyLayoutInformationFrom(sourceView); - } else { - log.warn("Could not find a matching view for \"" + view.getName() + "\" ... diagram layout information may be lost."); - } - } - - for (ComponentView view : componentViews) { - ComponentView sourceView = findView(source.getComponentViews(), view); - if (sourceView != null) { - view.copyLayoutInformationFrom(sourceView); - } else { - log.warn("Could not find a matching view for \"" + view.getName() + "\" ... diagram layout information may be lost."); - } - } - - for (DynamicView view : dynamicViews) { - DynamicView sourceView = findView(source.getDynamicViews(), view); - if (sourceView != null) { - view.copyLayoutInformationFrom(sourceView); - } else { - log.warn("Could not find a matching view for \"" + view.getName() + "\" ... diagram layout information may be lost."); - } - } - - for (DeploymentView view : deploymentViews) { - DeploymentView sourceView = findView(source.getDeploymentViews(), view); - if (sourceView != null) { - view.copyLayoutInformationFrom(sourceView); - } else { - log.warn("Could not find a matching view for \"" + view.getName() + "\" ... diagram layout information may be lost."); - } - } - } - - private T findView(Collection views, T sourceView) { - for (T view : views) { - if (view.getKey() != null && view.getKey().equals(sourceView.getKey())) { - return view; - } - } - - for (T view : views) { - if (view.getName().equals(sourceView.getName())) { - if (view.getDescription() != null) { - if (view.getDescription().equals(sourceView.getDescription())) { - return view; - } - } else { - return view; - } - } - } - - return null; - } - - @JsonIgnore - public boolean isEmpty() { - return enterpriseContextViews.isEmpty() && systemContextViews.isEmpty() && containerViews.isEmpty() && componentViews.isEmpty() && filteredViews.isEmpty(); - } - -} diff --git a/structurizr-core/src/main/java/com/structurizr/AbstractWorkspace.java b/structurizr-core/src/main/java/com/structurizr/AbstractWorkspace.java new file mode 100644 index 000000000..9f067fb6f --- /dev/null +++ b/structurizr-core/src/main/java/com/structurizr/AbstractWorkspace.java @@ -0,0 +1,263 @@ +package com.structurizr; + +import com.structurizr.configuration.WorkspaceConfiguration; +import com.structurizr.util.StringUtils; + +import java.lang.reflect.Constructor; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; + +/** + * The superclass for regular and encrypted workspaces. + */ +public abstract class AbstractWorkspace implements PropertyHolder { + + private long id; + private String name; + private String description; + private String version; + private Date lastModifiedDate; + private String lastModifiedUser; + private String lastModifiedAgent; + private String thumbnail; + + private Map properties = new HashMap<>(); + + private WorkspaceConfiguration configuration; + + protected AbstractWorkspace() { + configuration = createWorkspaceConfiguration(); + } + + AbstractWorkspace(String name, String description) { + this(); + + this.name = name; + this.description = description; + } + + /** + * Gets the ID of this workspace. + * + * @return the ID (a positive integer) + */ + public long getId() { + return this.id; + } + + /** + * Sets the ID of this workspace. + * + * @param id the ID (a positive integer) + */ + public void setId(long id) { + this.id = id; + } + + /** + * Gets the name of this workspace. + * + * @return the name, as a String + */ + public String getName() { + return name; + } + + /** + * Sets the name of this workspace. + * + * @param name the name, as a String + */ + public void setName(String name) { + this.name = name; + } + + /** + * Gets the description of this workspace. + * + * @return the description, as a String + */ + public String getDescription() { + return description; + } + + /** + * Sets the description of this workspace. + * + * @param description the description, as a String + */ + public void setDescription(String description) { + this.description = description; + } + + /** + * Gets the version of this workspace. + * + * @return the version, as a String + */ + public String getVersion() { + return version; + } + + /** + * Sets the version of this workspace. + * + * @param version the version, as a String (e.g. 1.0.1, a git hash, etc). + */ + public void setVersion(String version) { + this.version = version; + } + + + /** + * Gets the last modified date of this workspace. + * + * @return a Date object + */ + public Date getLastModifiedDate() { + return lastModifiedDate; + } + + /** + * Sets the last modified date of this workspace. + * + * @param lastModifiedDate a Date object + */ + public void setLastModifiedDate(Date lastModifiedDate) { + this.lastModifiedDate = lastModifiedDate; + } + + /** + * Gets the name of the user who last modified this workspace (e.g. a username). + * + * @return the last modified user, as a String + */ + public String getLastModifiedUser() { + return lastModifiedUser; + } + + /** + * Sets the name of the user who last modified tihs workspace (e.g. a username). + * + * @param lastModifiedUser the last modified user, as a String + */ + public void setLastModifiedUser(String lastModifiedUser) { + this.lastModifiedUser = lastModifiedUser; + } + + /** + * Gets the name of the agent that was used to last modify this workspace (e.g. "Structurizr for Java"). + * + * @return the last modified agent, as a String + */ + public String getLastModifiedAgent() { + return lastModifiedAgent; + } + + /** + * Sets the name of the agent that was used to last modify this workspace (e.g. "Structurizr for Java"). + * + * @param lastModifiedAgent the last modified user, as a String + */ + public void setLastModifiedAgent(String lastModifiedAgent) { + this.lastModifiedAgent = lastModifiedAgent; + } + + /** + * Gets the thumbnail associated with this workspace. + * + * @return a Base64 encoded PNG file as a Data URI (data:image/png;base64) + * or null if there is no thumbnail + */ + public String getThumbnail() { + return thumbnail; + } + + /** + * Sets the thumbnail associated with this workspace. + * + * @param thumbnail a Base64 encoded PNG file as a Data URI (data:image/png;base64) + */ + public void setThumbnail(String thumbnail) { + this.thumbnail = thumbnail; + } + + /** + * Gets the configuration associated with this workspace. + * + * @return a Configuration object + */ + public WorkspaceConfiguration getConfiguration() { + return configuration; + } + + protected void setConfiguration(WorkspaceConfiguration configuration) { + this.configuration = configuration; + } + + private WorkspaceConfiguration createWorkspaceConfiguration() { + try { + Constructor constructor = WorkspaceConfiguration.class.getDeclaredConstructor(); + constructor.setAccessible(true); + return (WorkspaceConfiguration)constructor.newInstance(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + /** + * Clears the configuration associated with this workspace. + */ + public void clearConfiguration() { + this.configuration = createWorkspaceConfiguration(); + } + + /** + * Gets the collection of name-value property pairs associated with this workspace, 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 workspace. + * + * @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); + } + + /** + * 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); + } + } + +} \ No newline at end of file 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 new file mode 100644 index 000000000..b90616a91 --- /dev/null +++ b/structurizr-core/src/main/java/com/structurizr/PropertyHolder.java @@ -0,0 +1,22 @@ +package com.structurizr; + +import java.util.Map; + +public interface PropertyHolder { + + /** + * 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 object. + * + * @param name the name of the property + * @param value the value of the property + */ + public void addProperty(String name, String value); + +} diff --git a/structurizr-core/src/main/java/com/structurizr/Workspace.java b/structurizr-core/src/main/java/com/structurizr/Workspace.java new file mode 100644 index 000000000..0b785e708 --- /dev/null +++ b/structurizr-core/src/main/java/com/structurizr/Workspace.java @@ -0,0 +1,391 @@ +package com.structurizr; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.structurizr.documentation.Documentable; +import com.structurizr.documentation.Documentation; +import com.structurizr.model.*; +import com.structurizr.view.*; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import javax.annotation.Nonnull; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * Represents a Structurizr workspace, which is a wrapper for a + * software architecture model, views and documentation. + */ +public final class Workspace extends AbstractWorkspace implements Documentable { + + private static final Log log = LogFactory.getLog(Workspace.class); + + private Model model = createModel(); + private ViewSet viewSet; + private Documentation documentation; + + Workspace() { + } + + /** + * Creates a new workspace. + * + * @param name the name of the workspace + */ + public Workspace(String name) { + this(name, ""); + } + + /** + * Creates a new workspace. + * + * @param name the name of the workspace + * @param description a short description + */ + public Workspace(String name, String description) { + super(name, description); + + model = createModel(); + viewSet = createViewSet(); + documentation = new Documentation(); + } + + /** + * Gets the software architecture model. + * + * @return a Model instance + */ + public Model getModel() { + return model; + } + + void setModel(Model model) { + this.model = model; + } + + /** + * Gets the set of views onto a software architecture model. + * + * @return a ViewSet instance + */ + public ViewSet getViews() { + return viewSet; + } + + void setViews(ViewSet viewSet) { + this.viewSet = viewSet; + } + + private Model createModel() { + try { + Constructor constructor = Model.class.getDeclaredConstructor(); + constructor.setAccessible(true); + return (Model)constructor.newInstance(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private ViewSet createViewSet() { + try { + Constructor constructor = ViewSet.class.getDeclaredConstructor(Model.class); + constructor.setAccessible(true); + return (ViewSet)constructor.newInstance(model); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + /** + * Called when deserialising JSON, to re-create the object graph + * based upon element/relationship IDs. + */ + public void hydrate() { + if (viewSet == null) { + viewSet = createViewSet(); + } + + hydrateModel(); + hydrateViewSet(); + } + + private void hydrateModel() { + try { + Method hydrateMethod = Model.class.getDeclaredMethod("hydrate"); + hydrateMethod.setAccessible(true); + hydrateMethod.invoke(model); + } catch (InvocationTargetException ite) { + if (ite.getCause() != null && ite.getCause() instanceof WorkspaceValidationException) { + throw (WorkspaceValidationException)ite.getCause(); + } else { + throw new RuntimeException(ite.getCause()); + } + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private void hydrateViewSet() { + try { + Method hydrateMethod = ViewSet.class.getDeclaredMethod("hydrate", Model.class); + hydrateMethod.setAccessible(true); + hydrateMethod.invoke(viewSet, model); + } catch (InvocationTargetException ite) { + if (ite.getCause() != null && ite.getCause() instanceof WorkspaceValidationException) { + throw (WorkspaceValidationException)ite.getCause(); + } else { + throw new RuntimeException(ite.getCause()); + } + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + /** + * Gets the documentation associated with this workspace. + * + * @return a Documentation object + */ + public Documentation getDocumentation() { + return documentation; + } + + /** + * Sets the documentation associated with this workspace. + * + * @param documentation a Documentation object + */ + void setDocumentation(@Nonnull Documentation documentation) { + this.documentation = documentation; + } + + /** + * Determines whether this model is empty. + * + * @return true if the model has no elements, views or documentation; false otherwise + */ + @JsonIgnore + public boolean isEmpty() { + return model.isEmpty() && viewSet.isEmpty() && documentation.isEmpty(); + } + + /** + * 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); + } + } + } + + /** + * 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; + + // 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/main/java/com/structurizr/WorkspaceValidationException.java b/structurizr-core/src/main/java/com/structurizr/WorkspaceValidationException.java new file mode 100644 index 000000000..7608dbb84 --- /dev/null +++ b/structurizr-core/src/main/java/com/structurizr/WorkspaceValidationException.java @@ -0,0 +1,9 @@ +package com.structurizr; + +public class WorkspaceValidationException extends RuntimeException { + + public WorkspaceValidationException(String message) { + super(message); + } + +} \ No newline at end of file diff --git a/structurizr-core/src/main/java/com/structurizr/configuration/Role.java b/structurizr-core/src/main/java/com/structurizr/configuration/Role.java new file mode 100644 index 000000000..7c2cfa56d --- /dev/null +++ b/structurizr-core/src/main/java/com/structurizr/configuration/Role.java @@ -0,0 +1,11 @@ +package com.structurizr.configuration; + +/** + * Represents the access that a user has to a workspace. + */ +public enum Role { + + ReadWrite, + ReadOnly + +} \ No newline at end of file diff --git a/structurizr-core/src/main/java/com/structurizr/configuration/User.java b/structurizr-core/src/main/java/com/structurizr/configuration/User.java new file mode 100644 index 000000000..c903452da --- /dev/null +++ b/structurizr-core/src/main/java/com/structurizr/configuration/User.java @@ -0,0 +1,83 @@ +package com.structurizr.configuration; + +import com.structurizr.util.StringUtils; + +/** + * Represents a user, and the role-based access they have to a workspace. + */ +public final class User implements Comparable { + + private String username; + private Role role; + + User() { + } + + 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); + } + + /** + * Gets the username (e.g. e-mail address). + * + * @return the username, as a String + */ + public String getUsername() { + return username; + } + + void setUsername(String username) { + this.username = username; + } + + /** + * Gets the role. + * + * @return a Role enum + */ + public Role getRole() { + return role; + } + + void setRole(Role role) { + this.role = role; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + User user = (User) o; + + return username.equals(user.username); + } + + @Override + public int hashCode() { + return username.hashCode(); + } + + @Override + public String toString() { + return "User {" + + "username='" + username + '\'' + + ", role=" + role + + '}'; + } + + @Override + public int compareTo(User user) { + return getUsername().compareTo(user.getUsername()); + } + +} \ No newline at end of file diff --git a/structurizr-core/src/main/java/com/structurizr/configuration/Visibility.java b/structurizr-core/src/main/java/com/structurizr/configuration/Visibility.java new file mode 100644 index 000000000..a61fff520 --- /dev/null +++ b/structurizr-core/src/main/java/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/main/java/com/structurizr/configuration/WorkspaceConfiguration.java b/structurizr-core/src/main/java/com/structurizr/configuration/WorkspaceConfiguration.java new file mode 100644 index 000000000..a38edf28c --- /dev/null +++ b/structurizr-core/src/main/java/com/structurizr/configuration/WorkspaceConfiguration.java @@ -0,0 +1,95 @@ +package com.structurizr.configuration; + +import java.util.Set; +import java.util.TreeSet; + +/** + * A wrapper for configuration options related to the workspace. + */ +public final class WorkspaceConfiguration { + + private WorkspaceScope scope = null; + private Visibility visibility = null; + private Set users = new TreeSet<>(); + + 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). + * + * @return a Visibility enum + */ + public Visibility getVisibility() { + return visibility; + } + + /** + * Sets the visibility of this workspace (private or public). + * + * @param visibility a Visibility enum, or null to indicate that no changes should be made + */ + public void setVisibility(Visibility visibility) { + this.visibility = visibility; + } + + /** + * Gets the set of users should have read-write or read-only access to the workspace. + * + * @return a Set of User objects (could be empty) + */ + public Set getUsers() { + return new TreeSet<>(users); + } + + void setUsers(Set users) { + if (users != null) { + this.users = new TreeSet<>(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. + * + * @param username the username (e.g. an e-mail address) + * @param role the user's role + */ + public void addUser(String username, Role role) { + users.add(new User(username, role)); + } + + /** + * Clears all configured users. + */ + public void clearUsers() { + this.users = new TreeSet<>(); + } + +} \ No newline at end of file diff --git a/structurizr-core/src/main/java/com/structurizr/configuration/WorkspaceScope.java b/structurizr-core/src/main/java/com/structurizr/configuration/WorkspaceScope.java new file mode 100644 index 000000000..8d271c504 --- /dev/null +++ b/structurizr-core/src/main/java/com/structurizr/configuration/WorkspaceScope.java @@ -0,0 +1,8 @@ +package com.structurizr.configuration; + +public enum WorkspaceScope { + + Landscape, + SoftwareSystem + +} \ No newline at end of file diff --git a/structurizr-core/src/main/java/com/structurizr/documentation/Decision.java b/structurizr-core/src/main/java/com/structurizr/documentation/Decision.java new file mode 100644 index 000000000..dac0969bd --- /dev/null +++ b/structurizr-core/src/main/java/com/structurizr/documentation/Decision.java @@ -0,0 +1,215 @@ +package com.structurizr.documentation; + +import com.structurizr.util.StringUtils; + +import java.util.Date; +import java.util.Set; +import java.util.TreeSet; + +/** + * Represents a single (architecture) decision, as described at http://thinkrelevance.com/blog/2011/11/15/documenting-architecture-decisions + */ +public final class Decision extends DocumentationContent implements Comparable { + + private String id; + private String title; + private Date date; + private String status; + + private Set links = new TreeSet<>(); + + Decision() { + } + + public Decision(String id) { + this.id = id; + } + + /** + * Gets the ID of this decision. + * + * @return the ID, as a String + */ + public String getId() { + return id; + } + + 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. + * + * @return a Date object + */ + public Date getDate() { + return date; + } + + public void setDate(Date date) { + this.date = date; + } + + /** + * Gets the status of this decision. + * + * @return the status, as a String + */ + public String getStatus() { + return status; + } + + public void setStatus(String status) { + this.status = status; + } + + /** + * Gets the set of links from this decision. + * + * @return a Set of Link objects + */ + public Set getLinks() { + return new TreeSet<>(links); + } + + void setLinks(Set links) { + this.links = links; + } + + /** + * Adds a link between this decision and another. + * + * @param decision the Decision to link to + * @param type the "type" of the link (e.g. "superseded by") + */ + public void addLink(Decision decision, String type) { + if (!decision.getId().equals(this.getId())) { + links.add(new Link(decision.getId(), type)); + } + } + + /** + * Determines whether a decision already has a link to another decision + * + * @param decision the Decision to check against + * @return true if a link exists, false otherwise + */ + public boolean hasLinkTo(Decision decision) { + return links.stream().anyMatch(l -> l.getId().equals(decision.getId())); + } + + @Override + public boolean equals(Object object) { + if (this == object) { + return true; + } + + if (object == null || getClass() != object.getClass()) { + return false; + } + + Decision decision = (Decision)object; + if (getElementId() != null) { + return getElementId().equals(decision.getElementId()) && getId().equals(decision.getId()); + } else { + return getId().equals(decision.getId()); + } + } + + @Override + public int hashCode() { + int result = getElementId() != null ? getElementId().hashCode() : 0; + result = 31 * result + getId().hashCode(); + return result; + } + + /** + * Represents a link between two decisions. + */ + public static final class Link implements Comparable { + + private String id; + private String description = ""; + + Link() { + } + + Link(String id, String description) { + if (StringUtils.isNullOrEmpty(id)) { + throw new IllegalArgumentException("Link ID must be specified"); + } + + setId(id); + setDescription(description); + } + + public String getId() { + return id; + } + + void setId(String id) { + this.id = id; + } + + /** + * Gets the description of this link. + * + * @return a String description + */ + public String getDescription() { + return description; + } + + void setDescription(String description) { + if (!StringUtils.isNullOrEmpty(description)) { + this.description = description; + } else { + this.description = ""; + } + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + Link link = (Link) o; + + if (!description.equals(link.description)) return false; + return id.equals(link.id); + } + + @Override + public int hashCode() { + int result = description.hashCode(); + result = 31 * result + id.hashCode(); + return result; + } + + @Override + public int compareTo(Link link) { + return getId().compareTo(link.getId()); + } + + } + + @Override + public int compareTo(Decision decision) { + return getId().compareTo(decision.getId()); + } + +} \ No newline at end of file diff --git a/structurizr-core/src/main/java/com/structurizr/documentation/Documentable.java b/structurizr-core/src/main/java/com/structurizr/documentation/Documentable.java new file mode 100644 index 000000000..495310feb --- /dev/null +++ b/structurizr-core/src/main/java/com/structurizr/documentation/Documentable.java @@ -0,0 +1,10 @@ +package com.structurizr.documentation; + +/** + * Marker interface for items that can have documentation attached (i.e. workspaces and software systems). + */ +public interface Documentable { + + Documentation getDocumentation(); + +} \ No newline at end of file diff --git a/structurizr-core/src/main/java/com/structurizr/documentation/Documentation.java b/structurizr-core/src/main/java/com/structurizr/documentation/Documentation.java new file mode 100644 index 000000000..1b3548066 --- /dev/null +++ b/structurizr-core/src/main/java/com/structurizr/documentation/Documentation.java @@ -0,0 +1,162 @@ +package com.structurizr.documentation; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.structurizr.util.StringUtils; + +import java.util.*; + +/** + * 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 + * and Decisions for more details. + */ +public final class Documentation { + + private List
sections = new ArrayList<>(); + private Set decisions = new TreeSet<>(); + private Set images = new TreeSet<>(); + + public Documentation() { + } + + /** + * Adds a section to this documentation. + * + * @param section a Section object + */ + public void addSection(Section section) { + checkFormatIsSpecified(section.getFormat()); + + section.setOrder(calculateOrder()); + sections.add(section); + } + + private void checkTitleIsSpecified(String title) { + if (StringUtils.isNullOrEmpty(title)) { + throw new IllegalArgumentException("A title must be specified."); + } + } + + private void checkContentIsSpecified(String content) { + if (StringUtils.isNullOrEmpty(content)) { + throw new IllegalArgumentException("Content must be specified."); + } + } + + private void checkFormatIsSpecified(Format format) { + if (format == null) { + throw new IllegalArgumentException("A format must be specified."); + } + } + + private int calculateOrder() { + return sections.size() + 1; + } + + /** + * Gets the set of {@link Section}s. + * + * @return a Set of {@link Section} objects + */ + public Collection
getSections() { + return new ArrayList<>(sections); + } + + void setSections(Collection
sections) { + if (sections != null) { + this.sections = new ArrayList<>(sections); + } + } + + /** + * Gets the set of decisions associated with this workspace. + * + * @return a Set of Decision objects + */ + public Set getDecisions() { + return new TreeSet<>(decisions); + } + + void setDecisions(Set decisions) { + if (decisions != null) { + this.decisions = new TreeSet<>(decisions); + } + } + + /** + * Adds a new decision to this documentation. + * + * @param decision the Decision object + */ + public void addDecision(Decision decision) { + checkIdIsSpecified(decision.getId()); + checkTitleIsSpecified(decision.getTitle()); + checkContentIsSpecified(decision.getContent()); + checkDecisionStatusIsSpecified(decision.getStatus()); + checkFormatIsSpecified(decision.getFormat()); + checkDecisionIsUnique(decision.getId()); + + this.decisions.add(decision); + } + + private void checkIdIsSpecified(String id) { + if (StringUtils.isNullOrEmpty(id)) { + throw new IllegalArgumentException("An ID must be specified."); + } + } + + private void checkDecisionStatusIsSpecified(String status) { + if (status == null) { + throw new IllegalArgumentException("A status must be specified."); + } + } + + private void checkDecisionIsUnique(String id) { + for (Decision decision : decisions) { + if (id.equals(decision.getId())) { + throw new IllegalArgumentException("A decision with an ID of " + id + " already exists in this scope."); + } + } + } + + /** + * Adds an image to the documentation. + * + * @param image an Image object + */ + public void addImage(Image image) { + images.add(image); + } + + /** + * Gets the set of {@link Image}s in this workspace. + * + * @return a Set of {@link Image} objects + */ + public Set getImages() { + return new TreeSet<>(images); + } + + void setImages(Set images) { + if (images != null) { + this.images = new TreeSet<>(images); + } + } + + @JsonIgnore + public boolean isEmpty() { + return sections.isEmpty() && images.isEmpty() && decisions.isEmpty(); + } + + /** + * Removes all documentation, decisions, and images. + */ + public void clear() { + sections = new ArrayList<>(); + decisions = new TreeSet<>(); + images = new TreeSet<>(); + } + +} \ No newline at end of file diff --git a/structurizr-core/src/main/java/com/structurizr/documentation/DocumentationContent.java b/structurizr-core/src/main/java/com/structurizr/documentation/DocumentationContent.java new file mode 100644 index 000000000..1759f81df --- /dev/null +++ b/structurizr-core/src/main/java/com/structurizr/documentation/DocumentationContent.java @@ -0,0 +1,57 @@ +package com.structurizr.documentation; + +/** + * Represents a piece of documentation content ... a section or a decision. + */ +public abstract class DocumentationContent { + + // elementId is here for backwards compatibility + private String elementId; + + private String content; + private Format format; + + DocumentationContent() { + } + + /** + * Gets the ID of the element that this documentation content is associated with. + * Please note this is unused, and only here for backwards compatibility. + * + * @return the element ID, as a String + */ + public String getElementId() { + return elementId; + } + + void setElementId(String elementId) { + this.elementId = elementId; + } + + /** + * Gets the content. + * + * @return the content, as a String + */ + public String getContent() { + return content; + } + + public void setContent(String content) { + this.content = content; + } + + /** + * Gets the format of this content. + * + * @return Markdown or AsciiDoc + */ + public Format getFormat() { + return format; + } + + public void setFormat(Format format) { + this.format = format; + } + +} \ No newline at end of file diff --git a/structurizr-core/src/com/structurizr/documentation/Format.java b/structurizr-core/src/main/java/com/structurizr/documentation/Format.java similarity index 97% rename from structurizr-core/src/com/structurizr/documentation/Format.java rename to structurizr-core/src/main/java/com/structurizr/documentation/Format.java index 628eb37e0..0eb970408 100644 --- a/structurizr-core/src/com/structurizr/documentation/Format.java +++ b/structurizr-core/src/main/java/com/structurizr/documentation/Format.java @@ -5,4 +5,4 @@ public enum Format { Markdown, AsciiDoc -} +} \ 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 new file mode 100644 index 000000000..b28404215 --- /dev/null +++ b/structurizr-core/src/main/java/com/structurizr/documentation/Image.java @@ -0,0 +1,50 @@ +package com.structurizr.documentation; + +/** + * Represents a base64 encoded image (png/jpg/gif). + */ +public final class Image implements Comparable { + + private String name; + private String content; + private String type; + + Image() { + } + + public Image(String name, String type, String content) { + this.name = name; + this.type = type; + this.content = content; + } + + public String getName() { + return name; + } + + void setName(String name) { + this.name = name; + } + + public String getContent() { + return content; + } + + void setContent(String content) { + this.content = content; + } + + public String getType() { + return type; + } + + 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/documentation/Section.java b/structurizr-core/src/main/java/com/structurizr/documentation/Section.java new file mode 100644 index 000000000..007e53fa3 --- /dev/null +++ b/structurizr-core/src/main/java/com/structurizr/documentation/Section.java @@ -0,0 +1,57 @@ +package com.structurizr.documentation; + +import com.fasterxml.jackson.annotation.JsonGetter; +import com.fasterxml.jackson.annotation.JsonInclude; + +/** + * A documentation section. + */ +public final class Section extends DocumentationContent { + + private String filename; + private int order; + + public Section() { + } + + public Section(Format format, String content) { + setFormat(format); + setContent(content); + } + + /** + * This method is retained for backwards compatibility. + */ + @JsonGetter + @JsonInclude(JsonInclude.Include.ALWAYS) + public String getTitle() { + return ""; + } + + /** + * 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; + } + + void setOrder(int order) { + this.order = order; + } + +} \ No newline at end of file diff --git a/structurizr-core/src/main/java/com/structurizr/model/AbstractImpliedRelationshipsStrategy.java b/structurizr-core/src/main/java/com/structurizr/model/AbstractImpliedRelationshipsStrategy.java new file mode 100644 index 000000000..ba051fbe7 --- /dev/null +++ b/structurizr-core/src/main/java/com/structurizr/model/AbstractImpliedRelationshipsStrategy.java @@ -0,0 +1,51 @@ +package com.structurizr.model; + +/** + * Abstract base class for supplied ImpliedRelationshipsStrategy implementations. + */ +public abstract class AbstractImpliedRelationshipsStrategy implements ImpliedRelationshipsStrategy { + + protected boolean impliedRelationshipIsAllowed(Element source, Element destination) { + if (source.equals(destination)) { + return false; + } + + return !(isChildOf(source, destination) || isChildOf(destination, source)); + } + + private boolean isChildOf(Element e1, Element e2) { + if (e1 instanceof Person || e2 instanceof Person) { + return false; + } + + Element parent = e2.getParent(); + while (parent != null) { + if (parent.getId().equals(e1.getId())) { + return true; + } + + parent = parent.getParent(); + } + + return false; + } + + /** + * Creates an implied relationship based upon the specified relationship, between the specified source and destination elements. + * + * @param relationship the Relationship on which the implied relationship is based + * @param source the implied relationship source + * @param destination the implied relationship destination + * @return a Relationship object representing the implied relationship, or null if one wasn't created + */ + protected Relationship createImpliedRelationship(Relationship relationship, Element source, Element destination) { + Model model = relationship.getModel(); + Relationship impliedRelationship = model.addRelationship(source, destination, relationship.getDescription(), relationship.getTechnology(), false); + if (impliedRelationship != null) { + impliedRelationship.setLinkedRelationshipId(relationship.getId()); + } + + return impliedRelationship; + } + +} \ No newline at end of file diff --git a/structurizr-core/src/main/java/com/structurizr/model/CanonicalNameGenerator.java b/structurizr-core/src/main/java/com/structurizr/model/CanonicalNameGenerator.java new file mode 100644 index 000000000..b998e7d9d --- /dev/null +++ b/structurizr-core/src/main/java/com/structurizr/model/CanonicalNameGenerator.java @@ -0,0 +1,100 @@ +package com.structurizr.model; + +import com.structurizr.util.StringUtils; + +final class CanonicalNameGenerator { + + private static final String CUSTOM_ELEMENT_TYPE = "Custom://"; + private static final String PERSON_TYPE = "Person://"; + private static final String SOFTWARE_SYSTEM_TYPE = "SoftwareSystem://"; + private static final String CONTAINER_TYPE = "Container://"; + private static final String COMPONENT_TYPE = "Component://"; + + private static final String DEPLOYMENT_NODE_TYPE = "DeploymentNode://"; + private static final String INFRASTRUCTURE_NODE_TYPE = "InfrastructureNode://"; + private static final String CONTAINER_INSTANCE_TYPE = "ContainerInstance://"; + private static final String SOFTWARE_SYSTEM_INSTANCE_TYPE = "SoftwareSystemInstance://"; + + private static final String STATIC_CANONICAL_NAME_SEPARATOR = "."; + private static final String DEPLOYMENT_CANONICAL_NAME_SEPARATOR = "/"; + + private static final String RELATIONSHIP_TYPE = "Relationship://"; + + private String formatName(Element element) { + return formatName(element.getName()); + } + + private String formatName(String name) { + return name + .replace(STATIC_CANONICAL_NAME_SEPARATOR, "") + .replace(DEPLOYMENT_CANONICAL_NAME_SEPARATOR, ""); + } + + String generate(CustomElement customElement) { + return CUSTOM_ELEMENT_TYPE + formatName(customElement); + } + + String generate(Person person) { + return PERSON_TYPE + formatName(person); + } + + String generate(SoftwareSystem softwareSystem) { + return SOFTWARE_SYSTEM_TYPE + formatName(softwareSystem); + } + + String generate(Container container) { + return CONTAINER_TYPE + formatName(container.getSoftwareSystem()) + STATIC_CANONICAL_NAME_SEPARATOR + formatName(container); + } + + String generate(Component component) { + return COMPONENT_TYPE + formatName(component.getContainer().getSoftwareSystem()) + STATIC_CANONICAL_NAME_SEPARATOR + formatName(component.getContainer()) + STATIC_CANONICAL_NAME_SEPARATOR + formatName(component); + } + + String generate(DeploymentNode deploymentNode) { + StringBuilder buf = new StringBuilder(); + buf.append(DEPLOYMENT_NODE_TYPE); + + buf.append(formatName(deploymentNode.getEnvironment())); + buf.append(DEPLOYMENT_CANONICAL_NAME_SEPARATOR); + + String parents = ""; + DeploymentNode parent = (DeploymentNode)deploymentNode.getParent(); + while (parent != null) { + parents = formatName(parent) + DEPLOYMENT_CANONICAL_NAME_SEPARATOR + parents; + parent = (DeploymentNode)parent.getParent(); + } + + buf.append(parents); + buf.append(formatName(deploymentNode)); + + return buf.toString(); + } + + String generate(InfrastructureNode infrastructureNode) { + String deploymentNodeCanonicalName = generate((DeploymentNode)infrastructureNode.getParent()).substring(DEPLOYMENT_NODE_TYPE.length()); + + return INFRASTRUCTURE_NODE_TYPE + deploymentNodeCanonicalName + DEPLOYMENT_CANONICAL_NAME_SEPARATOR + formatName(infrastructureNode); + } + + String generate(SoftwareSystemInstance softwareSystemInstance) { + String deploymentNodeCanonicalName = generate((DeploymentNode)softwareSystemInstance.getParent()).substring(DEPLOYMENT_NODE_TYPE.length()); + + return SOFTWARE_SYSTEM_INSTANCE_TYPE + deploymentNodeCanonicalName + DEPLOYMENT_CANONICAL_NAME_SEPARATOR + formatName(softwareSystemInstance.getSoftwareSystem()) + "[" + softwareSystemInstance.getInstanceId() + "]"; + } + + String generate(ContainerInstance containerInstance) { + String deploymentNodeCanonicalName = generate((DeploymentNode)containerInstance.getParent()).substring(DEPLOYMENT_NODE_TYPE.length()); + + return CONTAINER_INSTANCE_TYPE + deploymentNodeCanonicalName + DEPLOYMENT_CANONICAL_NAME_SEPARATOR + generate(containerInstance.getContainer()).substring(CONTAINER_TYPE.length()) + "[" + containerInstance.getInstanceId() + "]"; + } + + String generate(Relationship relationship) { + if (StringUtils.isNullOrEmpty(relationship.getDescription())) { + return RELATIONSHIP_TYPE + relationship.getSource().getCanonicalName() + " -> " + relationship.getDestination().getCanonicalName(); + } else { + return RELATIONSHIP_TYPE + relationship.getSource().getCanonicalName() + " -> " + relationship.getDestination().getCanonicalName() + " (" + relationship.getDescription() + ")"; + } + } + + +} \ No newline at end of file diff --git a/structurizr-core/src/main/java/com/structurizr/model/Component.java b/structurizr-core/src/main/java/com/structurizr/model/Component.java new file mode 100644 index 000000000..a1b63db61 --- /dev/null +++ b/structurizr-core/src/main/java/com/structurizr/model/Component.java @@ -0,0 +1,92 @@ +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; +import java.util.Set; + +/** + * Represents a "component" in the C4 model. + */ +public final class Component extends StaticStructureElement implements Documentable { + + private Container parent; + + private String technology; + + private Documentation documentation = new Documentation(); + + Component() { + } + + @Override + @JsonIgnore + public Element getParent() { + return parent; + } + + @JsonIgnore + public Container getContainer() { + return parent; + } + + void setParent(Container parent) { + this.parent = parent; + } + + /** + * Gets the technology associated with this component (e.g. "Spring Bean"). + * + * @return the technology, as a String, + * or null if no technology has been specified + */ + public String getTechnology() { + return technology; + } + + /** + * Sets the technology associated with this component (e.g. "Spring Bean"). + * + * @param technology the technology, as a String + */ + public void setTechnology(String technology) { + this.technology = technology; + } + + /** + * Gets the canonical name of this component, in the form "/Software System/Container/Component". + * + * @return the canonical name, as a String + */ + @Override + public String getCanonicalName() { + return new CanonicalNameGenerator().generate(this); + } + + @Override + 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 diff --git a/structurizr-core/src/main/java/com/structurizr/model/Constants.java b/structurizr-core/src/main/java/com/structurizr/model/Constants.java new file mode 100644 index 000000000..54a2aa8b2 --- /dev/null +++ b/structurizr-core/src/main/java/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 diff --git a/structurizr-core/src/main/java/com/structurizr/model/Container.java b/structurizr-core/src/main/java/com/structurizr/model/Container.java new file mode 100644 index 000000000..5e7f799f0 --- /dev/null +++ b/structurizr-core/src/main/java/com/structurizr/model/Container.java @@ -0,0 +1,189 @@ +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 implements Documentable { + + private SoftwareSystem parent; + private String technology; + + private Set components = new TreeSet<>(); + + private Documentation documentation = new Documentation(); + + Container() { + } + + /** + * Gets the parent software system. + * + * @return the parent SoftwareSystem instance + */ + @Override + @JsonIgnore + public Element getParent() { + return parent; + } + + /** + * Gets the parent software system. + * + * @return the parent SoftwareSystem instance + */ + @JsonIgnore + public SoftwareSystem getSoftwareSystem() { + return parent; + } + + void setParent(SoftwareSystem parent) { + this.parent = parent; + } + + /** + * Gets the technology associated with this container (e.g. "Spring MVC application"). + * + * @return the technology, as a String, + * or null if no technology has been specified + */ + public String getTechnology() { + return technology; + } + + /** + * Sets the technology associated with this container. + * + * @param technology the technology, as a String + */ + public void setTechnology(String technology) { + this.technology = technology; + } + + /** + * Adds a component to this container. + * + * @param name the name 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) { + return this.addComponent(name, ""); + } + + /** + * Adds a component to this container. + * + * @param name the name of the component + * @param description a description 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 description) { + return this.addComponent(name, description, null); + } + + /** + * Adds a component to this container. + * + * @param name the name 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 description, String technology) { + return getModel().addComponent(this, name, description, technology); + } + + void add(Component component) { + if (getComponentWithName(component.getName()) == null) { + components.add(component); + } + } + + void remove(Component component) { + components.remove(component); + } + + /** + * Gets the set of components within this software system. + * + * @return a Set of Component objects + */ + public Set getComponents() { + return new TreeSet<>(components); + } + + void setComponents(Set components) { + if (components != null) { + this.components = new TreeSet<>(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. + * + * @param name the name of the component + * @return the Component instance, or null if a component with the specified name does not exist + * @throws IllegalArgumentException if the name is null or empty + */ + public Component getComponentWithName(String name) { + if (name == null || name.trim().length() == 0) { + throw new IllegalArgumentException("A component name must be provided."); + } + + Optional component = components.stream().filter(c -> name.equals(c.getName())).findFirst(); + return component.orElse(null); + } + + /** + * Gets the canonical name of this container, in the form "/Software System/Container". + * + * @return the canonical name, as a String + */ + @Override + public String getCanonicalName() { + return new CanonicalNameGenerator().generate(this); + } + + @Override + 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 diff --git a/structurizr-core/src/main/java/com/structurizr/model/ContainerInstance.java b/structurizr-core/src/main/java/com/structurizr/model/ContainerInstance.java new file mode 100644 index 000000000..e9cb7d4bb --- /dev/null +++ b/structurizr-core/src/main/java/com/structurizr/model/ContainerInstance.java @@ -0,0 +1,60 @@ +package com.structurizr.model; + +import com.fasterxml.jackson.annotation.JsonIgnore; + +/** + * Represents a deployment instance of a {@link Container}, which can be added to a {@link DeploymentNode}. + */ +public final class ContainerInstance extends StaticStructureElementInstance { + + private Container container; + private String containerId; + + ContainerInstance() { + } + + ContainerInstance(Container container, int instanceId, String environment, String... deploymentGroups) { + super(instanceId, environment, deploymentGroups); + + setContainer(container); + addTags(Tags.CONTAINER_INSTANCE); + } + + @JsonIgnore + public Container getContainer() { + return container; + } + + void setContainer(Container container) { + this.container = container; + } + + @Override + public StaticStructureElement getElement() { + return getContainer(); + } + + /** + * Gets the ID of the container that this object represents a deployment instance of. + * + * @return the container ID, as a String + */ + public String getContainerId() { + if (container != null) { + return container.getId(); + } else { + return containerId; + } + } + + void setContainerId(String containerId) { + this.containerId = containerId; + } + + @Override + @JsonIgnore + public String getCanonicalName() { + return new CanonicalNameGenerator().generate(this); + } + +} \ No newline at end of file diff --git a/structurizr-core/src/main/java/com/structurizr/model/CreateImpliedRelationshipsUnlessAnyRelationshipExistsStrategy.java b/structurizr-core/src/main/java/com/structurizr/model/CreateImpliedRelationshipsUnlessAnyRelationshipExistsStrategy.java new file mode 100644 index 000000000..1c75952c4 --- /dev/null +++ b/structurizr-core/src/main/java/com/structurizr/model/CreateImpliedRelationshipsUnlessAnyRelationshipExistsStrategy.java @@ -0,0 +1,34 @@ +package com.structurizr.model; + +/** + * This strategy creates implied relationships between all valid combinations of the parent elements, + * unless any relationship already exists between them. + */ +public class CreateImpliedRelationshipsUnlessAnyRelationshipExistsStrategy extends AbstractImpliedRelationshipsStrategy { + + @Override + public void createImpliedRelationships(Relationship relationship) { + Element source = relationship.getSource(); + Element destination = relationship.getDestination(); + + Model model = source.getModel(); + + while (source != null) { + while (destination != null) { + if (impliedRelationshipIsAllowed(source, destination)) { + boolean createRelationship = !source.hasEfferentRelationshipWith(destination); + + if (createRelationship) { + createImpliedRelationship(relationship, source, destination); + } + } + + destination = destination.getParent(); + } + + destination = relationship.getDestination(); + source = source.getParent(); + } + } + +} \ No newline at end of file diff --git a/structurizr-core/src/main/java/com/structurizr/model/CreateImpliedRelationshipsUnlessSameRelationshipExistsStrategy.java b/structurizr-core/src/main/java/com/structurizr/model/CreateImpliedRelationshipsUnlessSameRelationshipExistsStrategy.java new file mode 100644 index 000000000..e849294db --- /dev/null +++ b/structurizr-core/src/main/java/com/structurizr/model/CreateImpliedRelationshipsUnlessSameRelationshipExistsStrategy.java @@ -0,0 +1,34 @@ +package com.structurizr.model; + +/** + * This strategy creates implied relationships between all valid combinations of the parent elements, + * unless the same relationship already exists between them. + */ +public class CreateImpliedRelationshipsUnlessSameRelationshipExistsStrategy extends AbstractImpliedRelationshipsStrategy { + + @Override + public void createImpliedRelationships(Relationship relationship) { + Element source = relationship.getSource(); + Element destination = relationship.getDestination(); + + Model model = source.getModel(); + + while (source != null) { + while (destination != null) { + if (impliedRelationshipIsAllowed(source, destination)) { + boolean createRelationship = !source.hasEfferentRelationshipWith(destination, relationship.getDescription()); + + if (createRelationship) { + createImpliedRelationship(relationship, source, destination); + } + } + + destination = destination.getParent(); + } + + destination = relationship.getDestination(); + source = source.getParent(); + } + } + +} \ No newline at end of file diff --git a/structurizr-core/src/main/java/com/structurizr/model/CustomElement.java b/structurizr-core/src/main/java/com/structurizr/model/CustomElement.java new file mode 100644 index 000000000..53105607a --- /dev/null +++ b/structurizr-core/src/main/java/com/structurizr/model/CustomElement.java @@ -0,0 +1,96 @@ +package com.structurizr.model; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.Set; + +/** + * Represents a custom element. + */ +public final class CustomElement extends GroupableElement { + + private String metadata; + + protected CustomElement() { + } + + @Override + public Element getParent() { + return null; + } + + @Override + public Set getDefaultTags() { + return new LinkedHashSet<>(Collections.singletonList(Tags.ELEMENT)); + } + + @Override + public String getCanonicalName() { + return new CanonicalNameGenerator().generate(this); + } + + public String getMetadata() { + return metadata; + } + + public void setMetadata(String metadata) { + this.metadata = metadata; + } + + /** + * Adds a unidirectional "uses" style relationship between this custom element and the specified element. + * + * @param destination the target of the relationship + * @param description a description of the relationship (e.g. "uses", "gets data from", "sends data to") + * @return the relationship that has just been created and added to the model + */ + @Nullable + public Relationship uses(@Nonnull Element destination, String description) { + return uses(destination, description, null); + } + + /** + * Adds a unidirectional "uses" style relationship between this custom element and the specified element. + * + * @param destination the target of the relationship + * @param description a description of the relationship (e.g. "uses", "gets data from", "sends data to") + * @param technology the technology details (e.g. JSON/HTTPS) + * @return the relationship that has just been created and added to the model + */ + @Nullable + public Relationship uses(@Nonnull Element destination, String description, String technology) { + return uses(destination, description, technology, null); + } + + /** + * Adds a unidirectional "uses" style relationship between this custom element and the specified element. + * + * @param destination the target of the relationship + * @param description a description of the relationship (e.g. "uses", "gets data from", "sends data to") + * @param technology the technology details (e.g. JSON/HTTPS) + * @param interactionStyle the interaction style (sync vs async) + * @return the relationship that has just been created and added to the model + */ + @Nullable + public Relationship uses(@Nonnull Element destination, String description, String technology, InteractionStyle interactionStyle) { + return uses(destination, description, technology, interactionStyle, null); + } + + /** + * Adds a unidirectional "uses" style relationship between this custom element and the specified element. + * + * @param destination the target of the relationship + * @param description a description of the relationship (e.g. "uses", "gets data from", "sends data to") + * @param technology the technology details (e.g. JSON/HTTPS) + * @param interactionStyle the interaction style (sync vs async) + * @param tags an array of tags + * @return the relationship that has just been created and added to the model + */ + @Nullable + public Relationship uses(@Nonnull Element destination, String description, String technology, InteractionStyle interactionStyle, String[] tags) { + return getModel().addRelationship(this, destination, description, technology, interactionStyle, tags); + } + +} \ No newline at end of file diff --git a/structurizr-core/src/main/java/com/structurizr/model/DefaultImpliedRelationshipsStrategy.java b/structurizr-core/src/main/java/com/structurizr/model/DefaultImpliedRelationshipsStrategy.java new file mode 100644 index 000000000..82e0d06b5 --- /dev/null +++ b/structurizr-core/src/main/java/com/structurizr/model/DefaultImpliedRelationshipsStrategy.java @@ -0,0 +1,13 @@ +package com.structurizr.model; + +/** + * The default strategy is to NOT create implied relationships. + */ +public class DefaultImpliedRelationshipsStrategy extends AbstractImpliedRelationshipsStrategy { + + @Override + public void createImpliedRelationships(Relationship relationship) { + // do nothing + } + +} \ No newline at end of file diff --git a/structurizr-core/src/main/java/com/structurizr/model/DeploymentElement.java b/structurizr-core/src/main/java/com/structurizr/model/DeploymentElement.java new file mode 100644 index 000000000..91d55cd21 --- /dev/null +++ b/structurizr-core/src/main/java/com/structurizr/model/DeploymentElement.java @@ -0,0 +1,42 @@ +package com.structurizr.model; + +import com.fasterxml.jackson.annotation.JsonIgnore; + +/** + * This is the superclass for model elements that describe deployment nodes, infrastructure nodes, and container instances. + */ +public abstract class DeploymentElement extends GroupableElement { + + public static final String DEFAULT_DEPLOYMENT_ENVIRONMENT = "Default"; + public static final String DEFAULT_DEPLOYMENT_GROUP = "Default"; + + private DeploymentNode parent; + private String environment = DEFAULT_DEPLOYMENT_ENVIRONMENT; + + DeploymentElement() { + } + + /** + * Gets the parent deployment node. + * + * @return the parent DeploymentNode, or null if there is no parent + */ + @Override + @JsonIgnore + public final Element getParent() { + return parent; + } + + void setParent(DeploymentNode parent) { + this.parent = parent; + } + + public String getEnvironment() { + return environment; + } + + void setEnvironment(String environment) { + this.environment = environment; + } + +} \ No newline at end of file diff --git a/structurizr-core/src/main/java/com/structurizr/model/DeploymentNode.java b/structurizr-core/src/main/java/com/structurizr/model/DeploymentNode.java new file mode 100644 index 000000000..41f872174 --- /dev/null +++ b/structurizr-core/src/main/java/com/structurizr/model/DeploymentNode.java @@ -0,0 +1,454 @@ +package com.structurizr.model; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonSetter; + +import java.util.*; + +/** + *

+ * Represents a deployment node, which 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)
  • + *
  • Database server
  • + *
  • Java EE web/application server
  • + *
  • Microsoft IIS
  • + *
  • etc
  • + *
+ */ +public final class DeploymentNode extends DeploymentElement { + + private String technology; + private String instances = "1"; + + 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. + * + * @param softwareSystem the SoftwareSystem to add an instance of + * @return a SoftwareSystemInstance object + */ + public SoftwareSystemInstance add(SoftwareSystem softwareSystem) { + return add(softwareSystem, DEFAULT_DEPLOYMENT_GROUP); + } + + /** + * Adds a software system instance to this deployment node, replicating relationships. + * + * @param softwareSystem the SoftwareSystem to add an instance of + * @param deploymentGroups the deployment group(s) + * @return a SoftwareSystemInstance object + */ + public SoftwareSystemInstance add(SoftwareSystem softwareSystem, String... deploymentGroups) { + SoftwareSystemInstance softwareSystemInstance = getModel().addSoftwareSystemInstance(this, softwareSystem, deploymentGroups); + this.softwareSystemInstances.add(softwareSystemInstance); + + 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. + * + * @param container the Container to add an instance of + * @return a ContainerInstance object + */ + public ContainerInstance add(Container container) { + return add(container, DEFAULT_DEPLOYMENT_GROUP); + } + + /** + * Adds a container instance to this deployment node, optionally replicating relationships. + * + * @param container the Container to add an instance of + * @param deploymentGroups the deployment group(s) + * @return a ContainerInstance object + */ + public ContainerInstance add(Container container, String... deploymentGroups) { + ContainerInstance containerInstance = getModel().addContainerInstance(this, container, deploymentGroups); + this.containerInstances.add(containerInstance); + + return containerInstance; + } + + /** + * Adds a child deployment node. + * + * @param name the name of the deployment node + * @return a DeploymentNode object + */ + public DeploymentNode addDeploymentNode(String name) { + return addDeploymentNode(name, null, null); + } + + /** + * Adds a child deployment node. + * + * @param name the name of the deployment node + * @param description a short description + * @param technology the technology + * @return a DeploymentNode object + */ + public DeploymentNode addDeploymentNode(String name, String description, String technology) { + return addDeploymentNode(name, description, technology, 1); + } + + /** + * Adds a child deployment node. + * + * @param name the name of the deployment node + * @param description a short description + * @param technology the technology + * @param instances the number of instances + * @return a DeploymentNode object + */ + public DeploymentNode addDeploymentNode(String name, String description, String technology, int instances) { + return addDeploymentNode(name, description, technology, instances, null); + } + + /** + * Adds a child deployment node. + * + * @param name the name of the deployment node + * @param description a short description + * @param technology the technology + * @param instances the number of instances + * @param properties a Map (String,String) describing name=value properties + * @return a DeploymentNode object + */ + public DeploymentNode addDeploymentNode(String name, String description, String technology, int instances, Map properties) { + DeploymentNode deploymentNode = getModel().addDeploymentNode(this, this.getEnvironment(), name, description, technology, instances, properties); + if (deploymentNode != null) { + children.add(deploymentNode); + } + return deploymentNode; + } + + void remove(DeploymentNode deploymentNode) { + children.remove(deploymentNode); + } + + /** + * Gets the DeploymentNode with the specified name. + * + * @param name the name of the deployment node + * @return the DeploymentNode instance with the specified name (or null if it doesn't exist). + */ + public DeploymentNode getDeploymentNodeWithName(String name) { + if (name == null || name.trim().length() == 0) { + throw new IllegalArgumentException("A name must be specified."); + } + + for (DeploymentNode deploymentNode : getChildren()) { + if (deploymentNode.getName().equals(name)) { + return deploymentNode; + } + } + + return null; + } + + /** + * Gets the infrastructure node with the specified name. + * + * @param name the name of the infrastructure node + * @return the InfrastructureNode instance with the specified name (or null if it doesn't exist). + */ + public InfrastructureNode getInfrastructureNodeWithName(String name) { + if (name == null || name.trim().length() == 0) { + throw new IllegalArgumentException("A name must be specified."); + } + + for (InfrastructureNode infrastructureNode : getInfrastructureNodes()) { + if (infrastructureNode.getName().equals(name)) { + return infrastructureNode; + } + } + + return null; + } + + /** + * Adds a child infrastructure node. + * + * @param name the name of the infrastructure node + * @return an InfrastructureNode object + */ + public InfrastructureNode addInfrastructureNode(String name) { + return addInfrastructureNode(name, null, null); + } + + /** + * Adds a child infrastructure node. + * + * @param name the name of the infrastructure node + * @param description a short description + * @param technology the technology + * @return an InfrastructureNode object + */ + public InfrastructureNode addInfrastructureNode(String name, String description, String technology) { + return addInfrastructureNode(name, description, technology, null); + } + + /** + * Adds a child infrastructure node. + * + * @param name the name of the infrastructure node + * @param description a short description + * @param technology the technology + * @param properties a Map (String,String) describing name=value properties + * @return an InfrastructureNode object + */ + public InfrastructureNode addInfrastructureNode(String name, String description, String technology, Map properties) { + InfrastructureNode infrastructureNode = getModel().addInfrastructureNode(this, name, description, technology, properties); + if (infrastructureNode != null) { + infrastructureNodes.add(infrastructureNode); + } + return infrastructureNode; + } + + /** + * Adds a relationship between this and another deployment node. + * + * @param destination the destination DeploymentNode + * @param description a short description of the relationship + * @param technology the technology + * @return a Relationship object + */ + public Relationship uses(DeploymentNode destination, String description, String technology) { + return uses(destination, description, technology, null); + } + + /** + * Adds a relationship between this and another deployment node. + * + * @param destination the destination DeploymentNode + * @param description a short description of the relationship + * @param technology the technology + * @param interactionStyle the interaction style (Synchronous vs Asynchronous) + * @return a Relationship object + */ + public Relationship uses(DeploymentNode destination, String description, String technology, InteractionStyle interactionStyle) { + return uses(destination, description, technology, interactionStyle, new String[0]); + } + + /** + * Adds a relationship between this and another deployment node. + * + * @param destination the destination DeploymentNode + * @param description a short description of the relationship + * @param technology the technology + * @param interactionStyle the interaction style (Synchronous vs Asynchronous) + * @param tags an array of tags + * @return a Relationship object + */ + public Relationship uses(DeploymentNode destination, String description, String technology, InteractionStyle interactionStyle, String[] tags) { + return getModel().addRelationship(this, destination, description, technology, interactionStyle, tags); + } + + /** + * Adds a relationship between this deployment node and an infrastructure node. + * + * @param destination the destination InfrastructureNode + * @param description a short description of the relationship + * @param technology the technology + * @return a Relationship object + */ + public Relationship uses(InfrastructureNode destination, String description, String technology) { + return uses(destination, description, technology, null); + } + + /** + * Adds a relationship between this deployment node and an infrastructure node. + * + * @param destination the destination InfrastructureNode + * @param description a short description of the relationship + * @param technology the technology + * @param interactionStyle the interaction style (Synchronous vs Asynchronous) + * @return a Relationship object + */ + public Relationship uses(InfrastructureNode destination, String description, String technology, InteractionStyle interactionStyle) { + return uses(destination, description, technology, interactionStyle, new String[0]); + } + + /** + * Adds a relationship between this deployment node and an infrastructure node. + * + * @param destination the destination InfrastructureNode + * @param description a short description of the relationship + * @param technology the technology + * @param interactionStyle the interaction style (Synchronous vs Asynchronous) + * @param tags an array of tags + * @return a Relationship object + */ + public Relationship uses(InfrastructureNode destination, String description, String technology, InteractionStyle interactionStyle, String[] tags) { + return getModel().addRelationship(this, destination, description, technology, interactionStyle, tags); + } + + /** + * Gets the set of child deployment nodes. + * + * @return a Set of DeploymentNode objects + */ + public Set getChildren() { + return new TreeSet<>(children); + } + + void setChildren(Set children) { + if (children != null) { + this.children = new TreeSet<>(children); + } + } + + /** + * Gets the set of child infrastructure nodes. + * + * @return a Set of InfrastructureNode objects + */ + public Set getInfrastructureNodes() { + return new TreeSet<>(infrastructureNodes); + } + + void setInfrastructureNodes(Set infrastructureNodes) { + if (infrastructureNodes != null) { + this.infrastructureNodes = new TreeSet<>(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. + * + * @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. + * + * @return a Set of SoftwareSystemInstance objects + */ + public Set getSoftwareSystemInstances() { + return new TreeSet<>(softwareSystemInstances); + } + + void setSoftwareSystemInstances(Set softwareSystemInstances) { + if (softwareSystemInstances != null) { + this.softwareSystemInstances = new TreeSet<>(softwareSystemInstances); + } + } + + /** + * Gets the set of container instances associated with this deployment node. + * + * @return a Set of ContainerInstance objects + */ + public Set getContainerInstances() { + return new TreeSet<>(containerInstances); + } + + void setContainerInstances(Set containerInstances) { + if (containerInstances != null) { + this.containerInstances = new TreeSet<>(containerInstances); + } + } + + public String getTechnology() { + return technology; + } + + public void setTechnology(String technology) { + this.technology = technology; + } + + public String getInstances() { + return instances; + } + + public void setInstances(int instances) { + 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; + } + + @JsonIgnore + public Set getDefaultTags() { + return new LinkedHashSet<>(Arrays.asList(Tags.ELEMENT, Tags.DEPLOYMENT_NODE)); + } + + @Override + public String getCanonicalName() { + return new CanonicalNameGenerator().generate(this); + } + +} \ No newline at end of file diff --git a/structurizr-core/src/main/java/com/structurizr/model/Element.java b/structurizr-core/src/main/java/com/structurizr/model/Element.java new file mode 100644 index 000000000..7294b30bd --- /dev/null +++ b/structurizr-core/src/main/java/com/structurizr/model/Element.java @@ -0,0 +1,252 @@ +package com.structurizr.model; + +import com.fasterxml.jackson.annotation.JsonIgnore; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.Set; +import java.util.TreeSet; + +/** + * This is the superclass for all model elements. + */ +public abstract class Element extends ModelItem { + + private Model model; + + private String name; + private String description; + + private Set relationships = new TreeSet<>(); + + protected Element() { + } + + @JsonIgnore + public Model getModel() { + return this.model; + } + + protected void setModel(Model model) { + this.model = model; + } + + /** + * Gets the name of this element. + * + * @return the name, as a String + */ + public String getName() { + return name; + } + + /** + * Sets the name of this element. + * + * @param name the name, as a String + */ + void setName(String name) { + if (name == null || name.trim().length() == 0) { + throw new IllegalArgumentException("The name of an element must not be null or empty."); + } + + this.name = name; + } + + /** + * Gets a description of this element. + * + * @return the description, as a String + */ + public String getDescription() { + return description; + } + + /** + * Sets the description of this element. + * + * @param description the description, as a String + */ + public void setDescription(String description) { + this.description = description; + } + + /** + * Gets the parent of this element. + * + * @return the parent Element, or null if this element doesn't have a parent (i.e. a Person or SoftwareSystem) + */ + public abstract Element getParent(); + + /** + * Gets the set of outgoing relationships. + * + * @return a Set of Relationship objects, or an empty set if none exist + */ + public Set getRelationships() { + return new TreeSet<>(relationships); + } + + void setRelationships(Set relationships) { + if (relationships != null) { + this.relationships = new TreeSet<>(relationships); + } + } + + /** + * Determines whether this element has afferent (incoming) relationships. + * + * @return true if this element has afferent relationships, false otherwise + */ + public boolean hasAfferentRelationships() { + return getModel().getRelationships().stream().filter(r -> r.getDestination() == this).count() > 0; + } + + /** + * Determines whether this element has an efferent (outgoing) relationship with + * the specified element. + * + * @param element the element to look for + * @return true if this element has an efferent relationship with the specified element, + * false otherwise + */ + public boolean hasEfferentRelationshipWith(Element element) { + return getEfferentRelationshipWith(element) != null; + } + + /** + * Determines whether this element has an efferent (outgoing) relationship with + * the specified element and description. + * + * @param element the element to look for + * @param description the relationship description + * @return true if this element has an efferent relationship with the specified element and description, + * false otherwise + */ + public boolean hasEfferentRelationshipWith(Element element, String description) { + return getEfferentRelationshipWith(element, description) != null; + } + + /** + * Gets the efferent (outgoing) relationship with the specified element. + * + * @param element the element to look for + * @return a Relationship object if an efferent relationship exists, null otherwise + */ + public Relationship getEfferentRelationshipWith(Element element) { + if (element == null) { + return null; + } + + for (Relationship relationship : relationships) { + if (relationship.getDestination().equals(element)) { + return relationship; + } + } + + return null; + } + + /** + * Gets the efferent (outgoing) relationship with the specified element. + * + * @param element the element to look for + * @return a Set of Relationship objects; empty if no relationships exist + */ + public Set getEfferentRelationshipsWith(Element element) { + Set set = new TreeSet<>(); + + if (element != null) { + for (Relationship relationship : relationships) { + if (relationship.getDestination().equals(element)) { + set.add(relationship); + } + } + } + + return set; + } + + /** + * Gets the efferent (outgoing) relationship with the specified element and description. + * + * @param element the element to look for + * @param description the relationship description + * @return a Relationship object, or null if the specified relationship doesn't exist + */ + public Relationship getEfferentRelationshipWith(Element element, String description) { + if (element == null) { + return null; + } + + if (description == null) { + description = ""; + } + + for (Relationship relationship : relationships) { + if (relationship.getDestination().equals(element) && description.equals(relationship.getDescription())) { + return relationship; + } + } + + return null; + } + + boolean has(Relationship relationship) { + return relationships.stream().anyMatch(r -> r.getDestination().equals(relationship.getDestination()) && r.getDescription().equals(relationship.getDescription())); + } + + 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. + * + * @param destination the target of the relationship + * @param description a description of the relationship (e.g. "uses", "gets data from", "sends data to") + * @return the relationship that has just been created and added to the model + */ + @Nullable + public Relationship uses(@Nonnull CustomElement destination, String description) { + return uses(destination, description, null); + } + + /** + * Adds a unidirectional "uses" style relationship between this element and the specified custom element. + * + * @param destination the target of the relationship + * @param description a description of the relationship (e.g. "uses", "gets data from", "sends data to") + * @param technology the technology details (e.g. JSON/HTTPS) + * @return the relationship that has just been created and added to the model + */ + @Nullable + public Relationship uses(@Nonnull CustomElement destination, String description, String technology) { + return getModel().addRelationship(this, destination, description, technology, null); + } + + /** + * Adds a unidirectional "uses" style relationship between this element and the specified custom element. + * + * @param destination the target of the relationship + * @param description a description of the relationship (e.g. "uses", "gets data from", "sends data to") + * @param technology the technology details (e.g. JSON/HTTPS) + * @param interactionStyle the interaction style (sync vs async) + * @param tags an array of tags + * @return the relationship that has just been created and added to the model + */ + @Nullable + public Relationship uses(@Nonnull CustomElement destination, String description, String technology, InteractionStyle interactionStyle, String[] tags) { + return getModel().addRelationship(this, destination, description, technology, interactionStyle, tags); + } + + @Override + public String toString() { + return "{" + getId() + " | " + getName() + " | " + getDescription() + "}"; + } + +} \ No newline at end of file diff --git a/structurizr-core/src/main/java/com/structurizr/model/Enterprise.java b/structurizr-core/src/main/java/com/structurizr/model/Enterprise.java new file mode 100644 index 000000000..ff841e0fb --- /dev/null +++ b/structurizr-core/src/main/java/com/structurizr/model/Enterprise.java @@ -0,0 +1,37 @@ +package com.structurizr.model; + +/** + * Represents an "enterprise" (e.g. an organisation, a department, etc). + */ +public final class Enterprise { + + private String name; + + Enterprise() { + } + + /** + * Creates a new enterprise with the specified name. + * + * @param name the name, as a String + * @throws IllegalArgumentException if the name is not specified + */ + @Deprecated + Enterprise(String name) { + if (name == null || name.trim().length() == 0) { + throw new IllegalArgumentException("Name must be specified."); + } + + this.name = name; + } + + /** + * Gets the name of this enterprise. + * + * @return the name, as a String + */ + public String getName() { + return name; + } + +} \ No newline at end of file diff --git a/structurizr-core/src/main/java/com/structurizr/model/GroupableElement.java b/structurizr-core/src/main/java/com/structurizr/model/GroupableElement.java new file mode 100644 index 000000000..cae1cb4b8 --- /dev/null +++ b/structurizr-core/src/main/java/com/structurizr/model/GroupableElement.java @@ -0,0 +1,41 @@ +package com.structurizr.model; + +import com.structurizr.util.StringUtils; + +/** + * Represents an element that can be included in a group. + */ +public abstract class GroupableElement extends Element { + + private String group; + + GroupableElement() { + } + + /** + * Gets the name of the group in which this element should be included in. + * + * @return the group name, or null if not set + */ + public String getGroup() { + return group; + } + + /** + * Sets the name of the group in which this element should be included in. + * + * @param group the group name + */ + public void setGroup(String group) { + if (group == null) { + this.group = null; + } else { + this.group = group.trim(); + + if (StringUtils.isNullOrEmpty(this.group)) { + this.group = null; + } + } + } + +} \ No newline at end of file diff --git a/structurizr-core/src/main/java/com/structurizr/model/HttpHealthCheck.java b/structurizr-core/src/main/java/com/structurizr/model/HttpHealthCheck.java new file mode 100644 index 000000000..4ff1620f0 --- /dev/null +++ b/structurizr-core/src/main/java/com/structurizr/model/HttpHealthCheck.java @@ -0,0 +1,145 @@ +package com.structurizr.model; + +import java.util.HashMap; +import java.util.Map; +import java.util.TreeMap; + +/** + * Describes a HTTP based health check. + */ +public final class HttpHealthCheck implements Comparable { + + /** a name for the health check */ + private String name; + + /** the health check URL/endpoint */ + private String url; + + /** the headers that should be sent in the HTTP request */ + private final Map headers = new TreeMap<>(); + + /** the polling interval, in seconds */ + private int interval; + + /** the timeout after which a health check is deemed as failed, in milliseconds */ + private long timeout; + + HttpHealthCheck() { + } + + HttpHealthCheck(String name, String url, int interval, long timeout) { + setName(name); + setUrl(url); + setInterval(interval); + setTimeout(timeout); + } + + /** + * Gets the name of this health check. + * + * @return the name, as a String + */ + public String getName() { + return name; + } + + void setName(String name) { + this.name = name; + } + + /** + * Gets the URL for this health check. + * + * @return the URL, as a String + */ + public String getUrl() { + return url; + } + + void setUrl(String url) { + this.url = url; + } + + /** + * Adds a HTTP header, which will be sent with the HTTP request to the health check URL. + * + * @param name the name of the header + * @param value the value of the header + */ + public void addHeader(String name, String value) { + if (name == null || name.trim().length() == 0) { + throw new IllegalArgumentException("The header name must not be null or empty."); + } + + if (value == null) { + throw new IllegalArgumentException("The header value must not be null."); + } + + this.headers.put(name, value); + } + + /** + * Gets a the HTTP headers associated with this health check. + * + * @return a Map (name=value) + */ + public Map getHeaders() { + return new HashMap<>(headers); + } + + /** + * Gets the polling interval of this health check. + * + * @return the polling interval (in seconds), as an integer + */ + public int getInterval() { + return interval; + } + + void setInterval(int interval) { + this.interval = interval; + } + + /** + * Gets the timeout associated with this health check. + * + * @return the timeout (in milliseconds) + */ + public long getTimeout() { + return timeout; + } + + void setTimeout(long timeout) { + this.timeout = timeout; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + HttpHealthCheck that = (HttpHealthCheck) o; + + if (!name.equals(that.name)) return false; + return url.equals(that.url); + } + + @Override + public int hashCode() { + int result = name.hashCode(); + result = 31 * result + url.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/IdGenerator.java b/structurizr-core/src/main/java/com/structurizr/model/IdGenerator.java new file mode 100644 index 000000000..002d84150 --- /dev/null +++ b/structurizr-core/src/main/java/com/structurizr/model/IdGenerator.java @@ -0,0 +1,32 @@ +package com.structurizr.model; + +/** + * The interface that ID generators, used when creating IDs for model elements/relationships, must implement. + */ +public interface IdGenerator { + + /** + * Generates an ID for the specified model element. + * + * @param element an Element instance + * @return the ID, as a String + */ + String generateId(Element element); + + /** + * Generates an ID for the specified model relationship. + * + * @param relationship a Relationship instance + * @return the ID, as a String + */ + String generateId(Relationship relationship); + + /** + * Called when loading/deserializing a model, to indicate that the specified ID has been found + * (and shouldn't be reused when generating new IDs). + * + * @param id the ID that has been found + */ + void found(String id); + +} \ No newline at end of file diff --git a/structurizr-core/src/main/java/com/structurizr/model/ImpliedRelationshipsStrategy.java b/structurizr-core/src/main/java/com/structurizr/model/ImpliedRelationshipsStrategy.java new file mode 100644 index 000000000..f192d2132 --- /dev/null +++ b/structurizr-core/src/main/java/com/structurizr/model/ImpliedRelationshipsStrategy.java @@ -0,0 +1,17 @@ +package com.structurizr.model; + +/** + * Defines the interface for strategies to create implied relationships in the model, + * after a relationship has been created. + */ +public interface ImpliedRelationshipsStrategy { + + /** + * Called after a relationship has been created in the model, + * providing an opportunity to create any resulting implied relationships. + * + * @param relationship the newly created Relationship + */ + void createImpliedRelationships(Relationship relationship); + +} \ 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 new file mode 100644 index 000000000..c9c293c18 --- /dev/null +++ b/structurizr-core/src/main/java/com/structurizr/model/InfrastructureNode.java @@ -0,0 +1,83 @@ +package com.structurizr.model; + +import com.fasterxml.jackson.annotation.JsonIgnore; + +import java.util.Arrays; +import java.util.LinkedHashSet; +import java.util.Set; + +/** + *

+ * Represents an infrastructure node, which is something like: + *

+ * + *
    + *
  • Load balancer
  • + *
  • Firewall
  • + *
  • DNS service
  • + *
  • etc
  • + *
+ */ +public final class InfrastructureNode extends DeploymentElement { + + private DeploymentNode parent; + private String technology; + + /** + * Adds a relationship between this and another deployment element (deployment node, infrastructure node, or container instance). + * + * @param destination the destination DeploymentElement + * @param description a short description of the relationship + * @param technology the technology + * @return a Relationship object + */ + public Relationship uses(DeploymentElement destination, String description, String technology) { + return uses(destination, description, technology, InteractionStyle.Synchronous); + } + + /** + * Adds a relationship between this and another deployment element (deployment node, infrastructure node, or container instance). + * + * @param destination the destination DeploymentElement + * @param description a short description of the relationship + * @param technology the technology + * @param interactionStyle the interaction style (Synchronous vs Asynchronous) + * @return a Relationship object + */ + public Relationship uses(DeploymentElement destination, String description, String technology, InteractionStyle interactionStyle) { + return uses(destination, description, technology, interactionStyle, new String[0]); + } + + /** + * Adds a relationship between this and another deployment element (deployment node, infrastructure node, or container instance). + * + * @param destination the destination DeploymentElement + * @param description a short description of the relationship + * @param technology the technology + * @param interactionStyle the interaction style (Synchronous vs Asynchronous) + * @param tags an array of tags + * @return a Relationship object + */ + public Relationship uses(DeploymentElement destination, String description, String technology, InteractionStyle interactionStyle, String[] tags) { + return getModel().addRelationship(this, destination, description, technology, interactionStyle, tags); + } + + public String getTechnology() { + return technology; + } + + public void setTechnology(String technology) { + this.technology = technology; + } + + @JsonIgnore + public Set getDefaultTags() { + return new LinkedHashSet<>(Arrays.asList(Tags.ELEMENT, Tags.INFRASTRUCTURE_NODE)); + } + + @Override + public String getCanonicalName() { + return new CanonicalNameGenerator().generate(this); + } + +} \ No newline at end of file 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 98% rename from structurizr-core/src/com/structurizr/model/Location.java rename to structurizr-core/src/main/java/com/structurizr/model/Location.java index d020abd0e..29b0193bc 100644 --- a/structurizr-core/src/com/structurizr/model/Location.java +++ b/structurizr-core/src/main/java/com/structurizr/model/Location.java @@ -6,4 +6,4 @@ public enum Location { External, Unspecified -} +} \ 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 new file mode 100644 index 000000000..da7058f49 --- /dev/null +++ b/structurizr-core/src/main/java/com/structurizr/model/Model.java @@ -0,0 +1,1180 @@ +package com.structurizr.model; + +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; +import java.util.*; +import java.util.stream.Collectors; + +/** + * Represents a software architecture model, into which all model elements are added. + */ +public final class Model implements PropertyHolder { + + private IdGenerator idGenerator = new SequentialIntegerIdGeneratorStrategy(); + + private final Set elements = new TreeSet<>(); + private final Map elementsById = new HashMap<>(); + + private final Set relationships = new TreeSet<>(); + private final Map relationshipsById = new HashMap<>(); + + 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(); + + private Map properties = new HashMap<>(); + + Model() { + } + + /** + * Creates a software system (with an unspecified location) and adds it to the model. + * + * @param name the name 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 + */ + public SoftwareSystem addSoftwareSystem(@Nonnull String name) { + return addSoftwareSystem(name, ""); + } + + /** + * 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 + * @return the SoftwareSystem instance created and added to the model (or null) + * @throws IllegalArgumentException if a software system with the same name already exists + */ + public SoftwareSystem addSoftwareSystem(@Nonnull String name, @Nullable String description) { + if (getSoftwareSystemWithName(name) == null) { + SoftwareSystem softwareSystem = new SoftwareSystem(); + softwareSystem.setName(name); + softwareSystem.setDescription(description); + softwareSystem.setId(idGenerator.generateId(softwareSystem)); + + softwareSystems.add(softwareSystem); + addElementToInternalStructures(softwareSystem); + + return softwareSystem; + } else { + throw new IllegalArgumentException("A top-level element named '" + name + "' already exists."); + } + } + + /** + * Creates a person (with an unspecified location) and adds it to the model. + * + * @param name the name of the person (e.g. "Admin User" or "Bob the Business User") + * @return the Person instance created and added to the model (or null) + * @throws IllegalArgumentException if a person with the same name already exists + */ + @Nonnull + public Person addPerson(@Nonnull String name) { + return addPerson(name, ""); + } + + /** + * 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 + * @return the Person instance created and added to the model (or null) + * @throws IllegalArgumentException if a person with the same name already exists + */ + @Nonnull + public Person addPerson(@Nonnull String name, @Nullable String description) { + if (getPersonWithName(name) == null) { + Person person = new Person(); + person.setName(name); + person.setDescription(description); + person.setId(idGenerator.generateId(person)); + + people.add(person); + addElementToInternalStructures(person); + + return person; + } else { + throw new IllegalArgumentException("A top-level element named '" + name + "' already exists."); + } + } + + /** + * Creates a custom element and adds it to the model. + * + * @param name the name of the custom element + * @return the CustomElement instance created and added to the model (or null) + * @throws IllegalArgumentException if a custom element/person/software system with the same name already exists + */ + @Nonnull + public CustomElement addCustomElement(@Nonnull String name) { + return addCustomElement(name, "", ""); + } + + /** + * Creates a custom element and adds it to the model. + * + * @param name the name of the custom element + * @param description a short description of the custom element + * @param metadata the metadata of the custom element + * @return the CustomElement instance created and added to the model (or null) + * @throws IllegalArgumentException if a custom element/person/software system with the same name already exists + */ + @Nonnull + public CustomElement addCustomElement(@Nonnull String name, @Nullable String metadata, @Nullable String description) { + if (getCustomElementWithName(name) == null) { + CustomElement customElement = new CustomElement(); + customElement.setName(name); + customElement.setMetadata(metadata); + customElement.setDescription(description); + + customElements.add(customElement); + + customElement.setId(idGenerator.generateId(customElement)); + addElementToInternalStructures(customElement); + + return customElement; + } else { + throw new IllegalArgumentException("A top-level element named '" + name + "' already exists."); + } + } + + @Nonnull + Container addContainer(SoftwareSystem parent, @Nonnull String name, @Nullable String description, @Nullable String technology) { + if (parent.getContainerWithName(name) == null) { + Container container = new Container(); + container.setName(name); + container.setDescription(description); + container.setTechnology(technology); + container.setId(idGenerator.generateId(container)); + + container.setParent(parent); + parent.add(container); + + addElementToInternalStructures(container); + + return container; + } else { + throw new IllegalArgumentException("A container named '" + name + "' already exists for this software system."); + } + } + + 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); + component.setId(idGenerator.generateId(component)); + + component.setParent(parent); + parent.add(component); + + addElementToInternalStructures(component); + + return component; + } else { + throw new IllegalArgumentException("A component named '" + name + "' already exists for this container."); + } + } + + @Nullable + Relationship addRelationship(Element source, @Nonnull Element destination, String description, String technology, boolean createImpliedRelationships) { + return addRelationship(source, destination, description, technology, null, new String[0], createImpliedRelationships); + } + + @Nullable + Relationship addRelationship(Element source, @Nonnull Element destination, String description, String technology, InteractionStyle interactionStyle) { + return addRelationship(source, destination, description, technology, interactionStyle, new String[0], true); + } + + @Nullable + Relationship addRelationship(Element source, @Nonnull Element destination, String description, String technology, InteractionStyle interactionStyle, String[] tags) { + return addRelationship(source, destination, description, technology, interactionStyle, tags, true); + } + + @Nullable + Relationship addRelationship(Element source, @Nonnull Element destination, String description, String technology, InteractionStyle interactionStyle, String[] tags, boolean createImpliedRelationships) { + if (destination == null) { + throw new IllegalArgumentException("The destination must be specified."); + } + + if (isChildOf(source, destination) || isChildOf(destination, source)) { + throw new IllegalArgumentException("Relationships cannot be added between parents and children."); + } + + Relationship relationship = new Relationship(source, destination, description, technology, interactionStyle, tags); + + if (addRelationship(relationship)) { + + if (createImpliedRelationships) { + if + ( + (source instanceof CustomElement || source instanceof Person || source instanceof SoftwareSystem || source instanceof Container || source instanceof Component) && + (destination instanceof CustomElement || destination instanceof Person || destination instanceof SoftwareSystem || destination instanceof Container || destination instanceof Component) + ) { + impliedRelationshipsStrategy.createImpliedRelationships(relationship); + } + } + + return relationship; + } + + return null; + } + + private boolean isChildOf(Element e1, Element e2) { + if (e1 instanceof Person || e2 instanceof Person) { + return false; + } + + Element parent = e2.getParent(); + while (parent != null) { + if (parent.getId().equals(e1.getId())) { + return true; + } + + parent = parent.getParent(); + } + + return false; + } + + private boolean addRelationship(Relationship relationship) { + if (!relationship.getSource().has(relationship)) { + relationship.setId(idGenerator.generateId(relationship)); + relationship.getSource().add(relationship); + + addRelationshipToInternalStructures(relationship); + return true; + } else { + return false; + } + } + + private void addElementToInternalStructures(Element element) { + // check that the ID is unique + if (getElement(element.getId()) != null || getRelationship(element.getId()) != null) { + throw new WorkspaceValidationException("The element " + element.getCanonicalName() + " has a non-unique ID of " + element.getId() + "."); + } + + elementsById.put(element.getId(), element); + elements.add(element); + element.setModel(this); + idGenerator.found(element.getId()); + } + + private void addRelationshipToInternalStructures(Relationship relationship) { + // check that the ID is unique + if (getElement(relationship.getId()) != null || getRelationship(relationship.getId()) != null) { + throw new WorkspaceValidationException("The relationship " + relationship.toString() + " has a non-unique ID of " + relationship.getId() + "."); + } + + relationshipsById.put(relationship.getId(), relationship); + relationships.add(relationship); + relationship.setModel(this); + idGenerator.found(relationship.getId()); + } + + private void removeRelationshipFromInternalStructures(Relationship relationship) { + relationshipsById.remove(relationship.getId()); + relationships.remove(relationship); + } + + /** + * Gets the set of all elements in this model. + * + * @return a Set of Element instances + */ + @JsonIgnore + @Nonnull + public Set getElements() { + return new TreeSet<>(elements); + } + + /** + * Gets the element with the specified ID. + * + * @param id the {@link Element#getId()} of the element + * @return the element in this model with the specified ID (or null if it doesn't exist) + * @see Element#getId() + */ + @Nullable + public Element getElement(@Nonnull String id) { + if (id == null || id.trim().length() == 0) { + throw new IllegalArgumentException("An element ID must be specified."); + } + + return elementsById.get(id); + } + + /** + * Gets the set of all relationships in this model. + * + * @return a Set of Relationship objects + */ + @JsonIgnore + @Nonnull + public Set getRelationships() { + return new TreeSet<>(this.relationships); + } + + /** + * Gets the relationship with the specified ID. + * + * @param id the {@link Relationship#getId()} of the relationship + * @return the relationship in this model with the specified ID (or null if it doesn't exist). + * @see Relationship#getId() + */ + @Nullable + public Relationship getRelationship(@Nonnull String id) { + if (id == null || id.trim().length() == 0) { + throw new IllegalArgumentException("A relationship ID must be specified."); + } + + return relationshipsById.get(id); + } + + /** + * Gets the set of all custom elements in this model. + * + * @return a Set of CustomElement instances + */ + @Nonnull + public Set getCustomElements() { + return new TreeSet<>(customElements); + } + + void setCustomElements(Set customElements) { + if (customElements != null) { + this.customElements = new TreeSet<>(customElements); + } + } + + /** + * Gets the set of all people in this model. + * + * @return a Set of Person instances + */ + @Nonnull + public Set getPeople() { + return new TreeSet<>(people); + } + + void setPeople(Set people) { + if (people != null) { + this.people = new TreeSet<>(people); + } + } + + /** + * Gets the set of all software systems in this model. + * + * @return a Set of SoftwareSystem instances + */ + @Nonnull + public Set getSoftwareSystems() { + return new TreeSet<>(softwareSystems); + } + + void setSoftwareSystems(Set softwareSystems) { + if (softwareSystems != null) { + this.softwareSystems = new TreeSet<>(softwareSystems); + } + } + + /** + * Gets the set of all top-level deployment nodes in this model. + * + * @return a Set of DeploymentNode instances + */ + @Nonnull + public Set getDeploymentNodes() { + return new TreeSet<>(deploymentNodes); + } + + void setDeploymentNodes(Set deploymentNodes) { + if (deploymentNodes != null) { + this.deploymentNodes = new TreeSet<>(deploymentNodes); + } + } + + void hydrate() { + // add all of the elements to the model + customElements.forEach(this::addElementToInternalStructures); + people.forEach(this::addElementToInternalStructures); + + for (SoftwareSystem softwareSystem : softwareSystems) { + addElementToInternalStructures(softwareSystem); + for (Container container : softwareSystem.getContainers()) { + addElementToInternalStructures(container); + container.setParent(softwareSystem); + for (Component component : container.getComponents()) { + addElementToInternalStructures(component); + component.setParent(container); + } + } + } + + deploymentNodes.forEach(dn -> hydrateDeploymentNode(dn, null)); + + // now hydrate the relationships + getElements().forEach(this::hydrateRelationships); + + // now check all of the element names are unique + Collection peopleAndSoftwareSystems = new ArrayList<>(); + peopleAndSoftwareSystems.addAll(people); + peopleAndSoftwareSystems.addAll(softwareSystems); + for (Element element : peopleAndSoftwareSystems) { + checkNameIsUnique(peopleAndSoftwareSystems, element.getName(), "A person or software system named \"%s\" already exists."); + } + + for (SoftwareSystem softwareSystem : softwareSystems) { + for (Container container : softwareSystem.getContainers()) { + checkNameIsUnique(softwareSystem.getContainers(), container.getName(), "A container named \"%s\" already exists within \"" + softwareSystem.getName() + "\"."); + + for (Component component : container.getComponents()) { + checkNameIsUnique(container.getComponents(), component.getName(), "A component named \"%s\" already exists within \"" + container.getName() + "\"."); + } + } + } + + for (DeploymentNode deploymentNode : deploymentNodes) { + checkNameIsUnique(deploymentNodes, deploymentNode.getName(), deploymentNode.getEnvironment(), "A top-level deployment node named \"%s\" already exists for the environment named \"" + deploymentNode.getEnvironment() + "\"."); + + if (deploymentNode.hasChildren()) { + checkChildNamesAreUnique(deploymentNode); + } + } + + // and check that all relationships are unique + for (Element element : getElements()) { + for (Relationship relationship : element.getRelationships()) { + checkDescriptionIsUnique(element.getRelationships(), relationship); + } + } + } + + private void hydrateDeploymentNode(DeploymentNode deploymentNode, DeploymentNode parent) { + deploymentNode.setParent(parent); + addElementToInternalStructures(deploymentNode); + + deploymentNode.getChildren().forEach(child -> hydrateDeploymentNode(child, deploymentNode)); + + for (SoftwareSystemInstance softwareSystemInstance : deploymentNode.getSoftwareSystemInstances()) { + Element softwareSystem = getElement(softwareSystemInstance.getSoftwareSystemId()); + if (!(softwareSystem instanceof SoftwareSystem)) { + throw new WorkspaceValidationException( + String.format("A software system instance is associated with a software system (id=%s) that does not exist in the model.", softwareSystemInstance.getSoftwareSystemId())); + } + + softwareSystemInstance.setSoftwareSystem((SoftwareSystem)softwareSystem); + softwareSystemInstance.setParent(deploymentNode); + addElementToInternalStructures(softwareSystemInstance); + } + + for (ContainerInstance containerInstance : deploymentNode.getContainerInstances()) { + Element container = getElement(containerInstance.getContainerId()); + if (!(container instanceof Container)) { + throw new WorkspaceValidationException( + String.format("A container instance is associated with a container (id=%s) that does not exist in the model.", containerInstance.getContainerId())); + } + + containerInstance.setContainer((Container)container); + containerInstance.setParent(deploymentNode); + addElementToInternalStructures(containerInstance); + } + + for (InfrastructureNode infrastructureNode : deploymentNode.getInfrastructureNodes()) { + infrastructureNode.setParent(deploymentNode); + addElementToInternalStructures(infrastructureNode); + } + } + + private void checkNameIsUnique(Collection elements, String name, String errorMessage) { + 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().equals(name) && dn.getEnvironment().equals(environment)).count() != 1) { + throw new WorkspaceValidationException( + String.format(errorMessage, name)); + } + } + + private void checkChildNamesAreUnique(DeploymentNode deploymentNode) { + for (DeploymentNode child : deploymentNode.getChildren()) { + checkNameIsUnique(deploymentNode.getChildren(), child.getName(), deploymentNode.getEnvironment(), "A deployment node named \"%s\" already exists within \"" + deploymentNode.getName() + "\"."); + + if (child.hasChildren()) { + checkChildNamesAreUnique(child); + } + } + } + + private void checkDescriptionIsUnique(Collection relationships, Relationship relationship) { + 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\".", + relationship.getDescription(), relationship.getSource().getName(), relationship.getDestination().getName())); + } + } + + private void hydrateRelationships(Element element) { + for (Relationship relationship : element.getRelationships()) { + relationship.setSource(getElement(relationship.getSourceId())); + relationship.setDestination(getElement(relationship.getDestinationId())); + addRelationshipToInternalStructures(relationship); + } + } + + /** + * Determines whether this model contains the specified element. + * + * @param element an element + * @return true, if the element is contained in this model + */ + public boolean contains(Element element) { + return elements.contains(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 relationships.contains(relationship); + } + + /** + * Gets the software system with the specified name. + * + * @param name the name of a {@link SoftwareSystem} + * @return the SoftwareSystem instance with the specified name (or null if it doesn't exist) + * @throws IllegalArgumentException if the name is null or empty + */ + @Nullable + public SoftwareSystem getSoftwareSystemWithName(@Nonnull String name) { + if (name == null || name.trim().length() == 0) { + throw new IllegalArgumentException("A software system name must be specified."); + } + + for (SoftwareSystem softwareSystem : getSoftwareSystems()) { + if (softwareSystem.getName().equals(name)) { + return softwareSystem; + } + } + + return null; + } + + /** + * Gets the software system with the specified ID. + * + * @param id the {@link SoftwareSystem#getId()} of the software system + * @return the SoftwareSystem instance with the specified ID (or null if it doesn't exist). + * @throws IllegalArgumentException if the id is null or empty + * @see SoftwareSystem#getId() + */ + @Nullable + public SoftwareSystem getSoftwareSystemWithId(@Nonnull String id) { + if (id == null || id.trim().length() == 0) { + throw new IllegalArgumentException("A software system ID must be specified."); + } + + for (SoftwareSystem softwareSystem : getSoftwareSystems()) { + if (softwareSystem.getId().equals(id)) { + return softwareSystem; + } + } + + return null; + } + + /** + * Gets the person with the specified name. + * + * @param name the name of the person + * @return the Person instance with the specified name (or null if it doesn't exist) + * @throws IllegalArgumentException if the name is null or empty + */ + @Nullable + public Person getPersonWithName(@Nonnull String name) { + if (name == null || name.trim().length() == 0) { + throw new IllegalArgumentException("A person name must be specified."); + } + + for (Person person : getPeople()) { + if (person.getName().equals(name)) { + return person; + } + } + + return null; + } + + /** + * Gets the custom element with the specified name. + * + * @param name the name of the custom element + * @return the CustomElement instance with the specified name (or null if it doesn't exist) + * @throws IllegalArgumentException if the name is null or empty + */ + @Nullable + public CustomElement getCustomElementWithName(@Nonnull String name) { + if (name == null || name.trim().length() == 0) { + throw new IllegalArgumentException("A custom element name must be specified."); + } + + for (CustomElement customElement : getCustomElements()) { + if (customElement.getName().equals(name)) { + return customElement; + } + } + + return null; + } + + /** + * Determines whether this model is empty. + * + * @return true if the model contains no people, software systems or deployment nodes; false otherwise + */ + @JsonIgnore + public boolean isEmpty() { + return people.isEmpty() && softwareSystems.isEmpty() && deploymentNodes.isEmpty(); + } + + /** + * Adds a top-level deployment node to this model. + * + * @param name the name of the deployment node + * @return a DeploymentNode instance + * @throws IllegalArgumentException if the name is not specified, or a top-level deployment node with the same name already exists in the model + */ + @Nonnull + public DeploymentNode addDeploymentNode(@Nonnull String name) { + return addDeploymentNode(DeploymentNode.DEFAULT_DEPLOYMENT_ENVIRONMENT, name, null, null); + } + + /** + * Adds a top-level deployment node to this model. + * + * @param name the name of the deployment node + * @param description the description of the deployment node + * @param technology the technology associated with the deployment node + * @return a DeploymentNode instance + * @throws IllegalArgumentException if the name is not specified, or a top-level deployment node with the same name already exists in the model + */ + @Nonnull + public DeploymentNode addDeploymentNode(@Nonnull String name, @Nullable String description, @Nullable String technology) { + return addDeploymentNode(DeploymentElement.DEFAULT_DEPLOYMENT_ENVIRONMENT, name, description, technology); + } + + /** + * Adds a top-level deployment node to this model. + * + * @param environment the name of the deployment environment + * @param name the name of the deployment node + * @param description the description of the deployment node + * @param technology the technology associated with the deployment node + * @return a DeploymentNode instance + * @throws IllegalArgumentException if the name is not specified, or a top-level deployment node with the same name already exists in the model + */ + @Nonnull + public DeploymentNode addDeploymentNode(@Nullable String environment, @Nonnull String name, @Nullable String description, @Nullable String technology) { + return addDeploymentNode(environment, name, description, technology, 1); + } + + /** + * Adds a top-level deployment node to this model. + * + * @param name the name of the deployment node + * @param description the description of the deployment node + * @param technology the technology associated with the deployment node + * @param instances the number of instances of the deployment node + * @return a DeploymentNode instance + * @throws IllegalArgumentException if the name is not specified, or a top-level deployment node with the same name already exists in the model + */ + @Nonnull + public DeploymentNode addDeploymentNode(@Nonnull String name, @Nullable String description, @Nullable String technology, int instances) { + return addDeploymentNode(DeploymentElement.DEFAULT_DEPLOYMENT_ENVIRONMENT, name, description, technology, instances); + } + + /** + * Adds a top-level deployment node to this model. + * + * @param environment the name of the deployment environment + * @param name the name of the deployment node + * @param description the description of the deployment node + * @param technology the technology associated with the deployment node + * @param instances the number of instances of the deployment node + * @return a DeploymentNode instance + * @throws IllegalArgumentException if the name is not specified, or a top-level deployment node with the same name already exists in the model + */ + @Nonnull + public DeploymentNode addDeploymentNode(@Nullable String environment, @Nonnull String name, @Nullable String description, @Nullable String technology, int instances) { + return addDeploymentNode(environment, name, description, technology, instances, null); + } + + /** + * Adds a top-level deployment node to this model. + * + * @param name the name of the deployment node + * @param description the description of the deployment node + * @param technology the technology associated with the deployment node + * @param instances the number of instances of the deployment node + * @param properties a map of name/value properties associated with the deployment node + * @return a DeploymentNode instance + * @throws IllegalArgumentException if the name is not specified, or a top-level deployment node with the same name already exists in the model + */ + @Nonnull + public DeploymentNode addDeploymentNode(@Nonnull String name, String description, String technology, int instances, Map properties) { + return addDeploymentNode(DeploymentElement.DEFAULT_DEPLOYMENT_ENVIRONMENT, name, description, technology, instances, properties); + } + + /** + * Adds a top-level deployment node to this model. + * + * @param environment the name of the deployment environment + * @param name the name of the deployment node + * @param description the description of the deployment node + * @param technology the technology associated with the deployment node + * @param instances the number of instances of the deployment node + * @param properties a map of name/value properties associated with the deployment node + * @return a DeploymentNode instance + * @throws IllegalArgumentException if the name is not specified, or a top-level deployment node with the same name already exists in the model + */ + @Nonnull + public DeploymentNode addDeploymentNode(@Nullable String environment, @Nonnull String name, String description, String technology, int instances, Map properties) { + return addDeploymentNode(null, environment, name, description, technology, instances, properties); + } + + @Nonnull + DeploymentNode addDeploymentNode(DeploymentNode parent, @Nullable String environment, @Nonnull String name, String description, String technology, int instances, Map properties) { + if (name == null || name.trim().length() == 0) { + throw new IllegalArgumentException("A name must be specified."); + } + + if ((parent == null && getDeploymentNodeWithName(name, environment) == null) || (parent != null && parent.getDeploymentNodeWithName(name) == null && parent.getInfrastructureNodeWithName(name) == null)) { + DeploymentNode deploymentNode = new DeploymentNode(); + deploymentNode.setName(name); + deploymentNode.setDescription(description); + deploymentNode.setTechnology(technology); + deploymentNode.setParent(parent); + deploymentNode.setInstances(instances); + deploymentNode.setEnvironment(environment); + deploymentNode.setId(idGenerator.generateId(deploymentNode)); + + if (properties != null) { + deploymentNode.setProperties(properties); + } + + if (parent == null) { + deploymentNodes.add(deploymentNode); + } + + addElementToInternalStructures(deploymentNode); + + return deploymentNode; + } else { + throw new IllegalArgumentException("A deployment/infrastructure node named '" + name + "' already exists."); + } + } + + @Nonnull + InfrastructureNode addInfrastructureNode(DeploymentNode parent, @Nonnull String name, String description, String technology, Map properties) { + if (name == null || name.trim().length() == 0) { + throw new IllegalArgumentException("A name must be specified."); + } + + if (parent.getDeploymentNodeWithName(name) == null && parent.getInfrastructureNodeWithName(name) == null) { + InfrastructureNode infrastructureNode = new InfrastructureNode(); + infrastructureNode.setName(name); + infrastructureNode.setDescription(description); + infrastructureNode.setTechnology(technology); + infrastructureNode.setParent(parent); + infrastructureNode.setEnvironment(parent.getEnvironment()); + infrastructureNode.setId(idGenerator.generateId(infrastructureNode)); + + if (properties != null) { + infrastructureNode.setProperties(properties); + } + + addElementToInternalStructures(infrastructureNode); + + return infrastructureNode; + } else { + throw new IllegalArgumentException("A deployment/infrastructure node named '" + name + "' already exists."); + } + } + + /** + * Gets the deployment node with the specified name and default environment. + * + * @param name the name of the deployment node + * @return the DeploymentNode instance with the specified name (or null if it doesn't exist). + */ + public DeploymentNode getDeploymentNodeWithName(String name) { + return getDeploymentNodeWithName(name, DeploymentElement.DEFAULT_DEPLOYMENT_ENVIRONMENT); + } + + /** + * Gets the deployment node with the specified name and environment. + * + * @param name the name of the deployment node + * @param environment the name of the deployment environment + * @return the DeploymentNode instance with the specified name (or null if it doesn't exist). + */ + public DeploymentNode getDeploymentNodeWithName(String name, String environment) { + for (DeploymentNode deploymentNode : getDeploymentNodes()) { + if (deploymentNode.getEnvironment().equals(environment) && deploymentNode.getName().equals(name)) { + return deploymentNode; + } + } + + return null; + } + + SoftwareSystemInstance addSoftwareSystemInstance(DeploymentNode deploymentNode, SoftwareSystem softwareSystem, String... deploymentGroups) { + if (softwareSystem == null) { + throw new IllegalArgumentException("A software system must be specified."); + } + + long instanceNumber = deploymentNode.getSoftwareSystemInstances().stream().filter(ssi -> ssi.getSoftwareSystem().equals(softwareSystem)).count(); + instanceNumber++; + SoftwareSystemInstance softwareSystemInstance = new SoftwareSystemInstance(softwareSystem, (int)instanceNumber, deploymentNode.getEnvironment(), deploymentGroups); + softwareSystemInstance.setParent(deploymentNode); + softwareSystemInstance.setId(idGenerator.generateId(softwareSystemInstance)); + + replicateElementRelationships(softwareSystemInstance); + + addElementToInternalStructures(softwareSystemInstance); + + return softwareSystemInstance; + } + + ContainerInstance addContainerInstance(DeploymentNode deploymentNode, Container container, String... deploymentGroups) { + if (container == null) { + throw new IllegalArgumentException("A container must be specified."); + } + + long instanceNumber = deploymentNode.getContainerInstances().stream().filter(ci -> ci.getContainer().equals(container)).count(); + instanceNumber++; + ContainerInstance containerInstance = new ContainerInstance(container, (int)instanceNumber, deploymentNode.getEnvironment(), deploymentGroups); + containerInstance.setParent(deploymentNode); + containerInstance.setId(idGenerator.generateId(containerInstance)); + + replicateElementRelationships(containerInstance); + + addElementToInternalStructures(containerInstance); + + return containerInstance; + } + + private void replicateElementRelationships(StaticStructureElementInstance elementInstance) { + StaticStructureElement element = elementInstance.getElement(); + + // find all StaticStructureElementInstance objects in the same deployment environment and deployment group + TreeSet elementInstances = getElements().stream() + .filter(e -> e instanceof StaticStructureElementInstance) + .map(e -> (StaticStructureElementInstance) e) + .filter(ssei -> ssei.getEnvironment().equals(elementInstance.getEnvironment())) + .filter(ssei -> ssei.inSameDeploymentGroup(elementInstance)).collect(Collectors.toCollection(TreeSet::new)); + + // and replicate the relationships to/from the element instance + for (StaticStructureElementInstance ssei : elementInstances) { + StaticStructureElement sse = ssei.getElement(); + + for (Relationship relationship : element.getRelationships()) { + if (relationship.getDestination().equals(sse)) { + Relationship newRelationship = addRelationship(elementInstance, ssei, relationship.getDescription(), relationship.getTechnology(), relationship.getInteractionStyle()); + if (newRelationship != null) { + newRelationship.setTags(null); + newRelationship.setLinkedRelationshipId(relationship.getId()); + } + } + } + + for (Relationship relationship : sse.getRelationships()) { + if (relationship.getDestination().equals(element)) { + Relationship newRelationship = addRelationship(ssei, elementInstance, relationship.getDescription(), relationship.getTechnology(), relationship.getInteractionStyle()); + if (newRelationship != null) { + newRelationship.setTags(null); + newRelationship.setLinkedRelationshipId(relationship.getId()); + } + } + } + } + } + + /** + * Gets the element with the specified canonical name. + * + * @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 (StringUtils.isNullOrEmpty(canonicalName)) { + throw new IllegalArgumentException("A canonical name must be specified."); + } + + for (Element element : getElements()) { + if (element.getCanonicalName().equals(canonicalName)) { + return element; + } + } + + 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. + * + * @param idGenerator an IdGenerate instance + * @throws IllegalArgumentException if the ID generator is null + */ + public void setIdGenerator(IdGenerator idGenerator) { + if (idGenerator == null) { + throw new IllegalArgumentException("An ID generator must be provided."); + } + + this.idGenerator = idGenerator; + } + + /** + * Provides a way for the description and technology to be modified on an existing relationship. + * + * @param relationship a Relationship instance + * @param description the new description + * @param technology the new technology + */ + public void modifyRelationship(Relationship relationship, String description, String technology) { + if (relationship == null) { + throw new IllegalArgumentException("A relationship must be specified."); + } + + if (!relationship.getSource().hasEfferentRelationshipWith(relationship.getDestination(), description)) { + relationship.setDescription(description); + relationship.setTechnology(technology); + } else { + throw new IllegalArgumentException( + String.format("A relationship named \"%s\" between \"%s\" and \"%s\" already exists.", + description, + relationship.getSource().getName(), + relationship.getDestination().getName())); + } + } + + /** + * Gets the strategy in use for creating implied relationships. + * + * @return an ImpliedRelationshipStrategy implementation + */ + @JsonIgnore + public ImpliedRelationshipsStrategy getImpliedRelationshipsStrategy() { + return impliedRelationshipsStrategy; + } + + /** + * Sets the strategy is use for creating implied relationships. + * + * @param impliedRelationshipStrategy an ImpliedRelationshipStrategy implementation + */ + public void setImpliedRelationshipsStrategy(ImpliedRelationshipsStrategy impliedRelationshipStrategy) { + if (impliedRelationshipStrategy != null) { + this.impliedRelationshipsStrategy = impliedRelationshipStrategy; + } else { + this.impliedRelationshipsStrategy = new DefaultImpliedRelationshipsStrategy(); + } + } + + /** + * 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 Collections.unmodifiableMap(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); + } + } + + /** + * 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 || relationship.getDestination() == element) { + remove(relationship); + } + } + + elementsById.remove(element.getId()); + 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/model/ModelItem.java b/structurizr-core/src/main/java/com/structurizr/model/ModelItem.java new file mode 100644 index 000000000..9cba7e71a --- /dev/null +++ b/structurizr-core/src/main/java/com/structurizr/model/ModelItem.java @@ -0,0 +1,276 @@ +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; +import com.structurizr.util.Url; + +import java.util.*; + +/** + * The base class for elements and relationships. + */ +public abstract class ModelItem implements PropertyHolder, PerspectivesHolder, Comparable { + + private String id = ""; + private final Set tags = new LinkedHashSet<>(); + + private String url; + private Map properties = new HashMap<>(); + private final Set perspectives = new TreeSet<>(); + + @JsonIgnore + public abstract String getCanonicalName(); + + @JsonIgnore + public abstract Set getDefaultTags(); + + /** + * Gets the ID of this item in the model. + * + * @return the ID, as a String + */ + public String getId() { + return id; + } + + protected void setId(String id) { + this.id = id; + } + + /** + * Gets the comma separated list of tags. + * + * @return a comma separated list of tags, + * or an empty string if there are no tags + */ + public String getTags() { + return TagUtils.toString(getTagsAsSet()); + } + + @JsonIgnore + public Set getTagsAsSet() { + Set setOfTags = new LinkedHashSet<>(getDefaultTags()); + setOfTags.addAll(tags); + + return setOfTags; + } + + void setTags(String tags) { + this.tags.clear(); + + if (tags == null) { + return; + } + + Collections.addAll(this.tags, tags.split(",")); + } + + public void addTags(String... tags) { + if (tags == null) { + return; + } + + for (String tag : tags) { + if (tag != null) { + this.tags.add(tag.trim()); + } + } + } + + /** + * Removes the given tag. + * + * @param tag the tag to remove + * @return true if the tag was removed; will return false if a non-existent tag is passed, or if an attempt is + * made to remove required tags, which cannot be removed. + */ + public boolean removeTag(String tag) { + if (tag != null) { + return this.tags.remove(tag.trim()); + } + return false; + } + + /** + * Determines whether this model item has the given tag. + * + * @param tag the tag to check for + * @return true if tag is present as a tag on this item, or if it is one of the + * required tags defined by the model in getRequiredTags(), false otherwise + */ + 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. + * + * @return a URL as a String + */ + public String getUrl() { + return url; + } + + /** + * Sets the URL where more information about this item 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.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; + } else { + throw new IllegalArgumentException(url + " is not a valid URL."); + } + } + } + + /** + * 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 (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); + } + + /** + * 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); + } + } + + /** + * Gets the set of perspectives associated with this model item. + * + * @return a Set of Perspective objects (empty if there are none) + */ + public Set getPerspectives() { + return new TreeSet<>(perspectives); + } + + void setPerspectives(Set perspectives) { + this.perspectives.clear(); + + if (perspectives == null) { + return; + } + + this.perspectives.addAll(perspectives); + } + + /** + * Adds a perspective to this model item. + * + * @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 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."); + } + + 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; + } + + /** + * 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 { + 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/Person.java b/structurizr-core/src/main/java/com/structurizr/model/Person.java new file mode 100644 index 000000000..84d445b1c --- /dev/null +++ b/structurizr-core/src/main/java/com/structurizr/model/Person.java @@ -0,0 +1,85 @@ +package com.structurizr.model; + +import com.fasterxml.jackson.annotation.JsonIgnore; + +import javax.annotation.Nonnull; +import java.util.Arrays; +import java.util.LinkedHashSet; +import java.util.Set; + +/** + * Represents a "person" in the C4 model. + */ +public final class Person extends StaticStructureElement { + + @Override + @JsonIgnore + public Element getParent() { + return null; + } + + Person() { + } + + @Override + public String getCanonicalName() { + return new CanonicalNameGenerator().generate(this); + } + + @Override + public Set getDefaultTags() { + return new LinkedHashSet<>(Arrays.asList(Tags.ELEMENT, Tags.PERSON)); + } + + @Override + public Relationship delivers(@Nonnull Person destination, String description) { + throw new UnsupportedOperationException(); + } + + @Override + public Relationship delivers(Person destination, String description, String technology) { + throw new UnsupportedOperationException(); + } + + @Override + public Relationship delivers(Person destination, String description, String technology, InteractionStyle interactionStyle) { + throw new UnsupportedOperationException(); + } + + /** + * Adds an interaction between this person and another. + * + * @param destination the Person being interacted with + * @param description a description of the interaction + * @return the resulting Relationship + */ + public Relationship interactsWith(Person destination, String description) { + return interactsWith(destination, description, null); + } + + /** + * Adds an interaction between this person and another. + * + * @param destination the Person being interacted with + * @param description a description of the interaction + * @param technology the technology of the interaction (e.g. Telephone) + * @return the resulting Relationship + */ + public Relationship interactsWith(Person destination, String description, String technology) { + return interactsWith(destination, description, technology, null); + } + + /** + * Adds an interaction between this person and another. + * + * @param destination the Person being interacted with + * @param description a description of the interaction + * @param technology the technology of the interaction (e.g. Telephone) + * @param interactionStyle the interaction style (e.g. Synchronous or Asynchronous) + * @return the resulting Relationship + */ + public Relationship interactsWith(Person destination, String description, String technology, InteractionStyle interactionStyle) { + return getModel().addRelationship(this, destination, description, technology, interactionStyle); + } + +} \ 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 new file mode 100644 index 000000000..fdb9e95f5 --- /dev/null +++ b/structurizr-core/src/main/java/com/structurizr/model/Perspective.java @@ -0,0 +1,81 @@ +package com.structurizr.model; + +/** + * 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 implements Comparable { + + private String name; + private String description; + private String value; + + Perspective() { + } + + public Perspective(String name, String description, String value) { + this.name = name; + this.description = description; + this.value = value; + } + + /** + * Gets the name of this perspective (e.g. "Security"). + * + * @return the name of this perspective, as a String + */ + public String getName() { + return name; + } + + void setName(String name) { + this.name = name; + } + + /** + * Gets the description of this perspective. + * + * @return the description of this perspective, as a String + */ + public String getDescription() { + return description; + } + + 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; + if (o == null || getClass() != o.getClass()) return false; + + Perspective that = (Perspective) o; + + return getName().equals(that.getName()); + } + + @Override + 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/Relationship.java b/structurizr-core/src/main/java/com/structurizr/model/Relationship.java new file mode 100644 index 000000000..e8e0fee0f --- /dev/null +++ b/structurizr-core/src/main/java/com/structurizr/model/Relationship.java @@ -0,0 +1,174 @@ +package com.structurizr.model; + +import com.fasterxml.jackson.annotation.JsonIgnore; + +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.Set; + +/** + * A relationship between two elements. + */ +public final class Relationship extends ModelItem { + + private Model model; + + private Element source; + private String sourceId; + private Element destination; + private String destinationId; + private String description; + private String technology; + private InteractionStyle interactionStyle; + + private String linkedRelationshipId; + + Relationship() { + } + + Relationship(Element source, Element destination, String description, String technology, InteractionStyle interactionStyle, String[] tags) { + this(); + + setSource(source); + setDestination(destination); + setDescription(description); + setTechnology(technology); + setInteractionStyle(interactionStyle); + + addTags(tags); + } + + @JsonIgnore + public Model getModel() { + return this.model; + } + + protected void setModel(Model model) { + this.model = model; + } + + @Override + public String getCanonicalName() { + return new CanonicalNameGenerator().generate(this); + } + + @JsonIgnore + public Element getSource() { + return source; + } + + /** + * Gets the ID of the source element. + * + * @return the ID of the source element, as a String + */ + public String getSourceId() { + if (this.source != null) { + return this.source.getId(); + } else { + return this.sourceId; + } + } + + void setSourceId(String sourceId) { + this.sourceId = sourceId; + } + + void setSource(Element source) { + this.source = source; + } + + @JsonIgnore + public Element getDestination() { + return destination; + } + + /** + * Gets the ID of the destination element. + * + * @return the ID of the destination element, as a String + */ + public String getDestinationId() { + if (this.destination != null) { + return this.destination.getId(); + } else { + return this.destinationId; + } + } + + void setDestinationId(String destinationId) { + this.destinationId = destinationId; + } + + void setDestination(Element destination) { + this.destination = destination; + } + + public String getDescription() { + return description != null ? description : ""; + } + + void setDescription(String description) { + this.description = description; + } + + /** + * 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 + */ + public String getTechnology() { + return technology; + } + + void setTechnology(String technology) { + this.technology = technology; + } + + /** + * Gets the interaction style (synchronous or asynchronous). + * + * @return an InteractionStyle, + * or null if an interaction style has not been specified + */ + public InteractionStyle getInteractionStyle() { + return interactionStyle; + } + + void setInteractionStyle(InteractionStyle interactionStyle) { + this.interactionStyle = interactionStyle; + } + + public String getLinkedRelationshipId() { + return linkedRelationshipId; + } + + void setLinkedRelationshipId(String baseRelationshipId) { + this.linkedRelationshipId = baseRelationshipId; + } + + @Override + public Set getDefaultTags() { + if (linkedRelationshipId == null) { + Set tags = new LinkedHashSet<>(); + tags.add(Tags.RELATIONSHIP); + + if (interactionStyle == InteractionStyle.Synchronous) { + tags.add(Tags.SYNCHRONOUS); + } else if (interactionStyle == InteractionStyle.Asynchronous) { + tags.add(Tags.ASYNCHRONOUS); + } + + return tags; + } else { + return Collections.emptySet(); + } + } + + @Override + public String toString() { + return source.toString() + " ---[" + description + "]---> " + destination.toString(); + } + +} \ No newline at end of file diff --git a/structurizr-core/src/main/java/com/structurizr/model/SequentialIntegerIdGeneratorStrategy.java b/structurizr-core/src/main/java/com/structurizr/model/SequentialIntegerIdGeneratorStrategy.java new file mode 100644 index 000000000..e8f57950c --- /dev/null +++ b/structurizr-core/src/main/java/com/structurizr/model/SequentialIntegerIdGeneratorStrategy.java @@ -0,0 +1,33 @@ +package com.structurizr.model; + +/** + * An ID generator that simply uses a sequential number when generating IDs for model elements and relationships. + * This is the default ID generator; any non-numeric IDs are ignored. + */ +public class SequentialIntegerIdGeneratorStrategy implements IdGenerator { + + private int ID = 0; + + @Override + public synchronized String generateId(Element element) { + return "" + ++ID; + } + + @Override + public synchronized String generateId(Relationship relationship) { + return "" + ++ID; + } + + @Override + public void found(String id) { + try { + int idAsInt = Integer.parseInt(id); + if (idAsInt > ID) { + ID = idAsInt; + } + } catch (NumberFormatException e) { + // ignore non-numeric IDs + } + } + +} \ 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 new file mode 100644 index 000000000..13d7a450c --- /dev/null +++ b/structurizr-core/src/main/java/com/structurizr/model/SoftwareSystem.java @@ -0,0 +1,187 @@ +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 javax.annotation.Nullable; +import java.util.Arrays; +import java.util.LinkedHashSet; +import java.util.Set; +import java.util.TreeSet; + +/** + * Represents a "software system" in the C4 model. + */ +public final class SoftwareSystem extends StaticStructureElement implements Documentable { + + private Set containers = new TreeSet<>(); + + private Documentation documentation = new Documentation(); + + /** + * Gets the parent of this software system. + * + * @return null, as software systems don't have a parent element + */ + @Override + @JsonIgnore + public Element getParent() { + return null; + } + + SoftwareSystem() { + } + + void add(Container container) { + containers.add(container); + } + + /** + * Gets the set of containers within this software system. + * + * @return a Set of Container objects + */ + @Nonnull + public Set getContainers() { + return new TreeSet<>(containers); + } + + void setContainers(Set containers) { + if (containers != null) { + this.containers = new TreeSet<>(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. + * + * @param name the name of the container (e.g. "Web Application") + * @return the newly created Container instance added to the model (or null) + * @throws IllegalArgumentException if a container with the same name exists already + */ + @Nonnull + public Container addContainer(@Nonnull String name) { + return addContainer(name, ""); + } + + /** + * Adds a container with the specified name and description. + * + * @param name the name of the container (e.g. "Web Application") + * @param description a short description/list of responsibilities + * @return the newly created Container instance added to the model (or null) + * @throws IllegalArgumentException if a container with the same name exists already + */ + @Nonnull + public Container addContainer(@Nonnull String name, String description) { + return addContainer(name, description, ""); + } + + /** + * Adds a container with the specified name, description and technology. + * + * @param name the name of the container (e.g. "Web Application") + * @param description a short description/list of responsibilities + * @param technology the technology choice (e.g. "Spring MVC", "Java EE", etc) + * @return the newly created Container instance added to the model (or null) + * @throws IllegalArgumentException if a container with the same name exists already + */ + @Nonnull + public Container addContainer(@Nonnull String name, String description, String technology) { + return getModel().addContainer(this, name, description, technology); + } + + void remove(Container container) { + containers.remove(container); + } + + /** + * Gets the container with the specified name. + * + * @param name the name of the {@link Container} + * @return the Container instance with the specified name, or null if it doesn't exist + * @throws IllegalArgumentException if the name is null or empty + */ + @Nullable + public Container getContainerWithName(@Nonnull String name) { + if (name == null || name.trim().length() == 0) { + throw new IllegalArgumentException("A container name must be provided."); + } + + for (Container container : getContainers()) { + if (container.getName().equals(name)) { + return container; + } + } + + return null; + } + + /** + * Gets the container with the specified ID. + * + * @param id the {@link Container#getId()} of the container + * @return the Container instance with the specified ID, or null if it doesn't exist + * @throws IllegalArgumentException if the ID is null or empty + */ + @Nullable + public Container getContainerWithId(@Nonnull String id) { + if (id == null || id.trim().length() == 0) { + throw new IllegalArgumentException("A container ID must be provided."); + } + + for (Container container : getContainers()) { + if (container.getId().equals(id)) { + return container; + } + } + + return null; + } + + /** + * Gets the canonical name of this software system, in the form "/Software System". + * + * @return the canonical name, as a String + */ + @Override + public String getCanonicalName() { + return new CanonicalNameGenerator().generate(this); + } + + @Override + public Set getDefaultTags() { + return new LinkedHashSet<>(Arrays.asList(Tags.ELEMENT, Tags.SOFTWARE_SYSTEM)); + } + + /** + * Gets the documentation associated with this software system. + * + * @return a Documentation object + */ + public Documentation getDocumentation() { + return documentation; + } + + /** + * Sets the documentation associated with this software system. + * + * @param documentation a Documentation object + */ + void setDocumentation(@Nonnull Documentation documentation) { + this.documentation = documentation; + } + +} \ No newline at end of file diff --git a/structurizr-core/src/main/java/com/structurizr/model/SoftwareSystemInstance.java b/structurizr-core/src/main/java/com/structurizr/model/SoftwareSystemInstance.java new file mode 100644 index 000000000..1c60f085d --- /dev/null +++ b/structurizr-core/src/main/java/com/structurizr/model/SoftwareSystemInstance.java @@ -0,0 +1,60 @@ +package com.structurizr.model; + +import com.fasterxml.jackson.annotation.JsonIgnore; + +/** + * Represents a deployment instance of a {@link SoftwareSystem}, which can be added to a {@link DeploymentNode}. + */ +public final class SoftwareSystemInstance extends StaticStructureElementInstance { + + private SoftwareSystem softwareSystem; + private String softwareSystemId; + + SoftwareSystemInstance() { + } + + SoftwareSystemInstance(SoftwareSystem softwareSystem, int instanceId, String environment, String... deploymentGroups) { + super(instanceId, environment, deploymentGroups); + + setSoftwareSystem(softwareSystem); + addTags(Tags.SOFTWARE_SYSTEM_INSTANCE); + } + + @JsonIgnore + public SoftwareSystem getSoftwareSystem() { + return softwareSystem; + } + + void setSoftwareSystem(SoftwareSystem softwareSystem) { + this.softwareSystem = softwareSystem; + } + + @Override + public StaticStructureElement getElement() { + return getSoftwareSystem(); + } + + /** + * Gets the ID of the software system that this object represents a deployment instance of. + * + * @return the software system ID, as a String + */ + public String getSoftwareSystemId() { + if (softwareSystem != null) { + return softwareSystem.getId(); + } else { + return softwareSystemId; + } + } + + void setSoftwareSystemId(String softwareSystemId) { + this.softwareSystemId = softwareSystemId; + } + + @Override + @JsonIgnore + public String getCanonicalName() { + return new CanonicalNameGenerator().generate(this); + } + +} \ No newline at end of file diff --git a/structurizr-core/src/main/java/com/structurizr/model/StaticStructureElement.java b/structurizr-core/src/main/java/com/structurizr/model/StaticStructureElement.java new file mode 100644 index 000000000..ff6566249 --- /dev/null +++ b/structurizr-core/src/main/java/com/structurizr/model/StaticStructureElement.java @@ -0,0 +1,260 @@ +package com.structurizr.model; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +/** + * This is the superclass for model elements that describe the static structure + * of a software system, namely Person, SoftwareSystem, Container and Component. + */ +public abstract class StaticStructureElement extends GroupableElement { + + StaticStructureElement() { + } + + /** + * Adds a unidirectional "uses" style relationship between this element and software system. + * + * @param destination the target of the relationship + * @param description a description of the relationship (e.g. "uses", "gets data from", "sends data to") + * @return the relationship that has just been created and added to the model + */ + @Nullable + public Relationship uses(@Nonnull SoftwareSystem destination, String description) { + return uses(destination, description, null); + } + + /** + * Adds a unidirectional "uses" style relationship between this element and a software system. + * + * @param destination the target of the relationship + * @param description a description of the relationship (e.g. "uses", "gets data from", "sends data to") + * @param technology the technology details (e.g. JSON/HTTPS) + * @return the relationship that has just been created and added to the model + */ + @Nullable + public Relationship uses(@Nonnull SoftwareSystem destination, String description, String technology) { + return uses(destination, description, technology, null); + } + + /** + * Adds a unidirectional "uses" style relationship between this element and a software system. + * + * @param destination the target of the relationship + * @param description a description of the relationship (e.g. "uses", "gets data from", "sends data to") + * @param technology the technology details (e.g. JSON/HTTPS) + * @param interactionStyle the interaction style (sync vs async) + * @return the relationship that has just been created and added to the model + */ + @Nullable + public Relationship uses(@Nonnull SoftwareSystem destination, String description, String technology, InteractionStyle interactionStyle) { + return uses(destination, description, technology, interactionStyle, new String[0]); + } + + /** + * Adds a unidirectional "uses" style relationship between this element and a software system. + * + * @param destination the target of the relationship + * @param description a description of the relationship (e.g. "uses", "gets data from", "sends data to") + * @param technology the technology details (e.g. JSON/HTTPS) + * @param interactionStyle the interaction style (sync vs async) + * @param tags an array of tags + * @return the relationship that has just been created and added to the model + */ + @Nullable + public Relationship uses(@Nonnull SoftwareSystem destination, String description, String technology, InteractionStyle interactionStyle, String[] tags) { + return uses((StaticStructureElement)destination, description, technology, interactionStyle, tags); + } + + /** + * Adds a unidirectional "uses" style relationship between this element and container. + * + * @param destination the target of the relationship + * @param description a description of the relationship (e.g. "uses", "gets data from", "sends data to") + * @return the relationship that has just been created and added to the model + */ + @Nullable + public Relationship uses(@Nonnull Container destination, String description) { + return uses(destination, description, null); + } + + /** + * Adds a unidirectional "uses" style relationship between this element and a container. + * + * @param destination the target of the relationship + * @param description a description of the relationship (e.g. "uses", "gets data from", "sends data to") + * @param technology the technology details (e.g. JSON/HTTPS) + * @return the relationship that has just been created and added to the model + */ + @Nullable + public Relationship uses(@Nonnull Container destination, String description, String technology) { + return uses(destination, description, technology, null); + } + + /** + * Adds a unidirectional "uses" style relationship between this element and a container. + * + * @param destination the target of the relationship + * @param description a description of the relationship (e.g. "uses", "gets data from", "sends data to") + * @param technology the technology details (e.g. JSON/HTTPS) + * @param interactionStyle the interaction style (sync vs async) + * @return the relationship that has just been created and added to the model + */ + @Nullable + public Relationship uses(@Nonnull Container destination, String description, String technology, InteractionStyle interactionStyle) { + return uses(destination, description, technology, interactionStyle, new String[0]); + } + + /** + * Adds a unidirectional "uses" style relationship between this element and a container. + * + * @param destination the target of the relationship + * @param description a description of the relationship (e.g. "uses", "gets data from", "sends data to") + * @param technology the technology details (e.g. JSON/HTTPS) + * @param interactionStyle the interaction style (sync vs async) + * @param tags an array of tags + * @return the relationship that has just been created and added to the model + */ + @Nullable + public Relationship uses(@Nonnull Container destination, String description, String technology, InteractionStyle interactionStyle, String[] tags) { + return uses((StaticStructureElement)destination, description, technology, interactionStyle, tags); + } + + /** + * Adds a unidirectional "uses" style relationship between this element and component. + * + * @param destination the target of the relationship + * @param description a description of the relationship (e.g. "uses", "gets data from", "sends data to") + * @return the relationship that has just been created and added to the model + */ + @Nullable + public Relationship uses(@Nonnull Component destination, String description) { + return uses(destination, description, null); + } + + /** + * Adds a unidirectional "uses" style relationship between this element and a component. + * + * @param destination the target of the relationship + * @param description a description of the relationship (e.g. "uses", "gets data from", "sends data to") + * @param technology the technology details (e.g. JSON/HTTPS) + * @return the relationship that has just been created and added to the model + */ + @Nullable + public Relationship uses(@Nonnull Component destination, String description, String technology) { + return uses(destination, description, technology, null); + } + + /** + * Adds a unidirectional "uses" style relationship between this element and a component. + * + * @param destination the target of the relationship + * @param description a description of the relationship (e.g. "uses", "gets data from", "sends data to") + * @param technology the technology details (e.g. JSON/HTTPS) + * @param interactionStyle the interaction style (sync vs async) + * @return the relationship that has just been created and added to the model + */ + @Nullable + public Relationship uses(@Nonnull Component destination, String description, String technology, InteractionStyle interactionStyle) { + return uses(destination, description, technology, interactionStyle, new String[0]); + } + + /** + * Adds a unidirectional "uses" style relationship between this element and a component. + * + * @param destination the target of the relationship + * @param description a description of the relationship (e.g. "uses", "gets data from", "sends data to") + * @param technology the technology details (e.g. JSON/HTTPS) + * @param interactionStyle the interaction style (sync vs async) + * @param tags an array of tags + * @return the relationship that has just been created and added to the model + */ + @Nullable + public Relationship uses(@Nonnull Component destination, String description, String technology, InteractionStyle interactionStyle, String[] tags) { + return uses((StaticStructureElement)destination, description, technology, interactionStyle, tags); + } + + /** + * Adds a unidirectional relationship between this element and a person. + * + * @param destination the target of the relationship + * @param description a description of the relationship (e.g. "sends e-mail to") + * @return the relationship that has just been created and added to the model + */ + @Nullable + public Relationship delivers(@Nonnull Person destination, String description) { + return delivers(destination, description, null); + } + + /** + * Adds a unidirectional relationship between this element and a person. + * + * @param destination the target of the relationship + * @param description a description of the relationship (e.g. "sends e-mail to") + * @param technology the technology details (e.g. JSON/HTTPS) + * @return the relationship that has just been created and added to the model + */ + @Nullable + public Relationship delivers(@Nonnull Person destination, String description, String technology) { + return delivers(destination, description, technology, null); + } + + /** + * Adds a unidirectional relationship between this element and a person. + * + * @param destination the target of the relationship + * @param description a description of the relationship (e.g. "sends e-mail to") + * @param technology the technology details (e.g. JSON/HTTPS) + * @param interactionStyle the interaction style (sync vs async) + * @return the relationship that has just been created and added to the model + */ + @Nullable + public Relationship delivers(@Nonnull Person destination, String description, String technology, InteractionStyle interactionStyle) { + return delivers(destination, description, technology, interactionStyle, new String[0]); + } + + /** + * Adds a unidirectional relationship between this element and a person. + * + * @param destination the target of the relationship + * @param description a description of the relationship (e.g. "sends e-mail to") + * @param technology the technology details (e.g. JSON/HTTPS) + * @param interactionStyle the interaction style (sync vs async) + * @param tags an array of tags + * @return the relationship that has just been created and added to the model + */ + @Nullable + public Relationship delivers(@Nonnull Person destination, String description, String technology, InteractionStyle interactionStyle, String[] tags) { + return getModel().addRelationship(this, destination, description, technology, interactionStyle, tags); + } + + /** + * Adds a unidirectional "uses" style relationship between this element and the specified element. + * + * @param destination the target of the relationship + * @param description a description of the relationship (e.g. "uses", "gets data from", "sends data to") + * @param technology the technology details (e.g. JSON/HTTPS) + * @param interactionStyle the interaction style (sync vs async) + * @return the relationship that has just been created and added to the model + */ + @Nullable + public Relationship uses(@Nonnull StaticStructureElement destination, String description, String technology, InteractionStyle interactionStyle) { + return uses(destination, description, technology, interactionStyle, new String[0]); + } + + /** + * Adds a unidirectional "uses" style relationship between this element and the specified element. + * + * @param destination the target of the relationship + * @param description a description of the relationship (e.g. "uses", "gets data from", "sends data to") + * @param technology the technology details (e.g. JSON/HTTPS) + * @param interactionStyle the interaction style (sync vs async) + * @param tags an array of tags + * @return the relationship that has just been created and added to the model + */ + @Nullable + public Relationship uses(@Nonnull StaticStructureElement destination, String description, String technology, InteractionStyle interactionStyle, String[] tags) { + return getModel().addRelationship(this, destination, description, technology, interactionStyle, tags); + } + +} \ No newline at end of file diff --git a/structurizr-core/src/main/java/com/structurizr/model/StaticStructureElementInstance.java b/structurizr-core/src/main/java/com/structurizr/model/StaticStructureElementInstance.java new file mode 100644 index 000000000..d7c5d3ae7 --- /dev/null +++ b/structurizr-core/src/main/java/com/structurizr/model/StaticStructureElementInstance.java @@ -0,0 +1,223 @@ +package com.structurizr.model; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.structurizr.util.StringUtils; +import com.structurizr.util.Url; + +import javax.annotation.Nonnull; +import java.util.Collections; +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}. + */ +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 TreeSet<>(); + private int instanceId; + private Set healthChecks = new TreeSet<>(); + + StaticStructureElementInstance() { + } + + StaticStructureElementInstance(int instanceId, String environment, String... deploymentGroups) { + setInstanceId(instanceId); + setEnvironment(environment); + + if (deploymentGroups != null) { + for (String deploymentGroup : deploymentGroups) { + if (!StringUtils.isNullOrEmpty(deploymentGroup)) { + this.deploymentGroups.add(deploymentGroup.trim()); + } + } + } + + if (this.deploymentGroups.isEmpty()) { + this.deploymentGroups.add(DEFAULT_DEPLOYMENT_GROUP); + } + } + + @JsonIgnore + public abstract StaticStructureElement getElement(); + + /** + * Gets the deployment group of this element instance. + * + * @return a deployment group name + */ + public Set getDeploymentGroups() { + if (deploymentGroups.isEmpty()) { + return Collections.singleton(DEFAULT_DEPLOYMENT_GROUP); + } else { + return new TreeSet<>(deploymentGroups); + } + } + + void setDeploymentGroups(Set deploymentGroups) { + if (deploymentGroups != null) { + this.deploymentGroups = new TreeSet<>(deploymentGroups); + } else { + this.deploymentGroups = new TreeSet<>(); + } + } + + // provided for backwards compatibility + void setDeploymentGroup(String deploymentGroup) { + this.deploymentGroups = Collections.singleton(deploymentGroup); + } + + boolean inSameDeploymentGroup(StaticStructureElementInstance ssei) { + for (String deploymentGroup : getDeploymentGroups()) { + if (ssei.getDeploymentGroups().contains(deploymentGroup)) { + return true; + } + } + + return false; + } + + /** + * Gets the instance ID of this element instance. + * + * @return the instance ID, an integer greater than zero + */ + public int getInstanceId() { + return instanceId; + } + + void setInstanceId(int instanceId) { + this.instanceId = instanceId; + } + + @Override + @JsonIgnore + public Set getDefaultTags() { + return Collections.emptySet(); + } + + @Override + public boolean removeTag(String tag) { + // do nothing ... tags cannot be removed from element instances (they should reflect the element they are based upon) + return false; + } + + @Override + @JsonIgnore + public String getName() { + return getElement().getName(); + } + + @Override + public void setName(String name) { + // no-op ... the name of an element instance is taken from the associated element + } + + /** + * Gets the set of health checks associated with this element instance. + * + * @return a Set of HttpHealthCheck instances + */ + @Nonnull + public Set getHealthChecks() { + return new TreeSet<>(healthChecks); + } + + void setHealthChecks(Set healthChecks) { + this.healthChecks = healthChecks; + } + + /** + * Adds a new health check, with the default interval (60 seconds) and timeout (0 milliseconds). + * + * @param name the name of the health check + * @param url the URL of the health check + * @return a HttpHealthCheck instance representing the health check that has been added + * @throws IllegalArgumentException if the name is empty, or the URL is not a well-formed URL + */ + @Nonnull + public HttpHealthCheck addHealthCheck(String name, String url) { + return addHealthCheck(name, url, DEFAULT_HEALTH_CHECK_INTERVAL_IN_SECONDS, DEFAULT_HEALTH_CHECK_TIMEOUT_IN_MILLISECONDS); + } + + /** + * Adds a new health check. + * + * @param name the name of the health check + * @param url the URL of the health check + * @param interval the polling interval, in seconds + * @param timeout the timeout, in milliseconds + * @return a HttpHealthCheck instance representing the health check that has been added + * @throws IllegalArgumentException if the name is empty, the URL is not a well-formed URL, or the interval/timeout is not zero/a positive integer + */ + @Nonnull + public HttpHealthCheck addHealthCheck(String name, String url, int interval, long timeout) { + if (name == null || name.trim().length() == 0) { + throw new IllegalArgumentException("The name must not be null or empty."); + } + + if (url == null || url.trim().length() == 0) { + throw new IllegalArgumentException("The URL must not be null or empty."); + } + + if (!Url.isUrl(url)) { + throw new IllegalArgumentException(url + " is not a valid URL."); + } + + if (interval < 0) { + throw new IllegalArgumentException("The polling interval must be zero or a positive integer."); + } + + if (timeout < 0) { + throw new IllegalArgumentException("The timeout must be zero or a positive integer."); + } + + HttpHealthCheck healthCheck = new HttpHealthCheck(name, url, interval, timeout); + healthChecks.add(healthCheck); + + return healthCheck; + } + + /** + * Adds a relationship between this element instance and an infrastructure node. + * + * @param destination the destination InfrastructureNode + * @param description a short description of the relationship + * @param technology the technology + * @return a Relationship object + */ + public Relationship uses(InfrastructureNode destination, String description, String technology) { + return uses(destination, description, technology, null); + } + + /** + * Adds a relationship between this element instance and an infrastructure node. + * + * @param destination the destination InfrastructureNode + * @param description a short description of the relationship + * @param technology the technology + * @param interactionStyle the interaction style (Synchronous vs Asynchronous) + * @return a Relationship object + */ + public Relationship uses(InfrastructureNode destination, String description, String technology, InteractionStyle interactionStyle) { + return uses(destination, description, technology, interactionStyle, new String[0]); + } + + /** + * Adds a relationship between this element instance and an infrastructure node. + * + * @param destination the destination InfrastructureNode + * @param description a short description of the relationship + * @param technology the technology + * @param interactionStyle the interaction style (Synchronous vs Asynchronous) + * @param tags an array of tags + * @return a Relationship object + */ + public Relationship uses(InfrastructureNode destination, String description, String technology, InteractionStyle interactionStyle, String[] tags) { + return getModel().addRelationship(this, destination, description, technology, interactionStyle, tags); + } + +} \ No newline at end of file diff --git a/structurizr-core/src/main/java/com/structurizr/model/Tags.java b/structurizr-core/src/main/java/com/structurizr/model/Tags.java new file mode 100644 index 000000000..46cbdc487 --- /dev/null +++ b/structurizr-core/src/main/java/com/structurizr/model/Tags.java @@ -0,0 +1,31 @@ +package com.structurizr.model; + +public class Tags { + + public static final String ELEMENT = "Element"; + public static final String RELATIONSHIP = "Relationship"; + + public static final String PERSON = "Person"; + public static final String SOFTWARE_SYSTEM = "Software System"; + public static final String CONTAINER = "Container"; + public static final String COMPONENT = "Component"; + + public static final String DEPLOYMENT_NODE = "Deployment Node"; + public static final String INFRASTRUCTURE_NODE = "Infrastructure Node"; + public static final String SOFTWARE_SYSTEM_INSTANCE = "Software System Instance"; + public static final String CONTAINER_INSTANCE = "Container Instance"; + + /** + * To be used for styling of synchronous relationships + * + * @see InteractionStyle#Synchronous + */ + public static final String SYNCHRONOUS = "Synchronous"; + /** + * To be used for styling of asynchronous relationships + * + * @see InteractionStyle#Asynchronous + */ + public static final String ASYNCHRONOUS = "Asynchronous"; + +} \ No newline at end of file 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/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-core/src/main/java/com/structurizr/util/ImageUtils.java b/structurizr-core/src/main/java/com/structurizr/util/ImageUtils.java new file mode 100644 index 000000000..65a9f8260 --- /dev/null +++ b/structurizr-core/src/main/java/com/structurizr/util/ImageUtils.java @@ -0,0 +1,165 @@ +package com.structurizr.util; + +import javax.annotation.Nonnull; +import javax.imageio.ImageIO; +import java.awt.image.BufferedImage; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.net.URLConnection; +import java.nio.file.Files; +import java.util.Base64; + +/** + * Some utility methods for dealing with images as files and data URIs. + */ +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. + * + * @param file a File 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(@Nonnull File file) throws IOException { + if (file == null) { + throw new IllegalArgumentException("A file must be specified."); + } else if (!file.exists()) { + throw new IllegalArgumentException(file.getCanonicalPath() + " does not exist."); + } else if (!file.isFile()) { + throw new IllegalArgumentException(file.getCanonicalPath() + " is not a file."); + } + + String contentType = URLConnection.guessContentTypeFromName(file.getName()); + if (contentType == null || !contentType.startsWith("image/")) { + throw new IllegalArgumentException(file.getCanonicalPath() + " is not a supported image file."); + } + + return contentType; + } + + /** + * 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. + * + * @param file a File pointing to an image + * @return a Base64 encoded version of that image + * @throws IOException if there is an error reading the file + */ + public static String getImageAsBase64(@Nonnull File file) throws IOException { + String contentType = getContentType(file); + + if (ImageUtils.CONTENT_TYPE_IMAGE_SVG.equalsIgnoreCase(contentType)) { + return Base64.getEncoder().encodeToString(Files.readAllBytes(file.toPath())); + } + + BufferedImage bufferedImage = ImageIO.read(file); + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + ImageIO.write(bufferedImage, contentType.split("/")[1], bos); + byte[] imageBytes = bos.toByteArray(); + + return Base64.getEncoder().encodeToString(imageBytes); + } + + /** + * Gets the content of an image as a data URI; e.g. "data:image/png;base64,iVBORw0KGgoAA..." + * + * @param file a File pointing to an image + * @return a data URI + * @throws IOException if there is an error reading the file + */ + public static String getImageAsDataUri(File file) throws IOException { + String contentType = getContentType(file); + String base64Content = getImageAsBase64(file); + + return DATA_URI_PREFIX + contentType + ";base64," + base64Content; + } + + /** + * 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()); + } + + /** + * 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) { + if (StringUtils.isNullOrEmpty(imageDescriptor)) { + return; + } + + imageDescriptor = imageDescriptor.trim(); + + if (Url.isUrl(imageDescriptor)) { + // all good + return; + } + + 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/SVG data URI + return; + } else { + // it's a data URI, but not supported + throw new IllegalArgumentException("Only PNG, JPG, and SVG data URIs are supported: " + imageDescriptor); + } + } + + throw new IllegalArgumentException("Expected a URL or data URI"); + } + + public static boolean isSupportedDataUri(String uri) { + 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/src/main/java/com/structurizr/util/MapUtils.java b/structurizr-core/src/main/java/com/structurizr/util/MapUtils.java new file mode 100644 index 000000000..fdcf17c10 --- /dev/null +++ b/structurizr-core/src/main/java/com/structurizr/util/MapUtils.java @@ -0,0 +1,30 @@ +package com.structurizr.util; + +import java.util.HashMap; +import java.util.Map; + +public final class MapUtils { + + /** + * A helper method to create a Map from an array of Strings ("name=value"). + * + * @param nameValuePairs one or more "name=value" pairs + * + * @return a Map + */ + public static Map create(String... nameValuePairs) { + Map map = new HashMap<>(); + + if (nameValuePairs != null) { + for (String nameValuePair : nameValuePairs) { + String[] tokens = nameValuePair.split("="); + if (tokens.length == 2) { + map.put(tokens[0], tokens[1]); + } + } + } + + return map; + } + +} \ No newline at end of file diff --git a/structurizr-core/src/main/java/com/structurizr/util/StringUtils.java b/structurizr-core/src/main/java/com/structurizr/util/StringUtils.java new file mode 100644 index 000000000..26a9bce31 --- /dev/null +++ b/structurizr-core/src/main/java/com/structurizr/util/StringUtils.java @@ -0,0 +1,9 @@ +package com.structurizr.util; + +public final class StringUtils { + + public static boolean isNullOrEmpty(String s) { + return s == null || s.trim().length() == 0; + } + +} \ No newline at end of file diff --git a/structurizr-core/src/main/java/com/structurizr/util/TagUtils.java b/structurizr-core/src/main/java/com/structurizr/util/TagUtils.java new file mode 100644 index 000000000..c41056584 --- /dev/null +++ b/structurizr-core/src/main/java/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/main/java/com/structurizr/util/Url.java b/structurizr-core/src/main/java/com/structurizr/util/Url.java new file mode 100644 index 000000000..ee26a38bb --- /dev/null +++ b/structurizr-core/src/main/java/com/structurizr/util/Url.java @@ -0,0 +1,56 @@ +package com.structurizr.util; + +import java.net.MalformedURLException; +import java.net.URL; + +/** + * Utilities for dealing with URLs. + */ +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+\\}.*"; + + /** + * Determines whether the supplied string is a valid URL. + * + * @param urlAsString the URL, as a String + * @return true if the URL is valid, false otherwise + */ + public static boolean isUrl(String urlAsString) { + if (!StringUtils.isNullOrEmpty(urlAsString)) { + try { + new URL(urlAsString); + return true; + } catch (MalformedURLException murle) { + return false; + } + } + + 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/main/java/com/structurizr/validation/LandscapeWorkspaceScopeValidator.java b/structurizr-core/src/main/java/com/structurizr/validation/LandscapeWorkspaceScopeValidator.java new file mode 100644 index 000000000..feb6b4abd --- /dev/null +++ b/structurizr-core/src/main/java/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/main/java/com/structurizr/validation/SoftwareSystemWorkspaceScopeValidator.java b/structurizr-core/src/main/java/com/structurizr/validation/SoftwareSystemWorkspaceScopeValidator.java new file mode 100644 index 000000000..d2f7ce32b --- /dev/null +++ b/structurizr-core/src/main/java/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/main/java/com/structurizr/validation/UndefinedWorkspaceScopeValidator.java b/structurizr-core/src/main/java/com/structurizr/validation/UndefinedWorkspaceScopeValidator.java new file mode 100644 index 000000000..ffd4d3865 --- /dev/null +++ b/structurizr-core/src/main/java/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/main/java/com/structurizr/validation/WorkspaceScopeValidationException.java b/structurizr-core/src/main/java/com/structurizr/validation/WorkspaceScopeValidationException.java new file mode 100644 index 000000000..5183f3d3b --- /dev/null +++ b/structurizr-core/src/main/java/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/main/java/com/structurizr/validation/WorkspaceScopeValidator.java b/structurizr-core/src/main/java/com/structurizr/validation/WorkspaceScopeValidator.java new file mode 100644 index 000000000..c54361736 --- /dev/null +++ b/structurizr-core/src/main/java/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/main/java/com/structurizr/validation/WorkspaceScopeValidatorFactory.java b/structurizr-core/src/main/java/com/structurizr/validation/WorkspaceScopeValidatorFactory.java new file mode 100644 index 000000000..ce011e601 --- /dev/null +++ b/structurizr-core/src/main/java/com/structurizr/validation/WorkspaceScopeValidatorFactory.java @@ -0,0 +1,18 @@ +package com.structurizr.validation; + +import com.structurizr.Workspace; +import com.structurizr.configuration.WorkspaceScope; + +public final class WorkspaceScopeValidatorFactory { + + public static WorkspaceScopeValidator getValidator(Workspace workspace) { + if (workspace.getConfiguration().getScope() == WorkspaceScope.Landscape) { + return new LandscapeWorkspaceScopeValidator(); + } else if (workspace.getConfiguration().getScope() == WorkspaceScope.SoftwareSystem) { + return new SoftwareSystemWorkspaceScopeValidator(); + } else { + return new UndefinedWorkspaceScopeValidator(); + } + } + +} \ 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 new file mode 100644 index 000000000..168519a8e --- /dev/null +++ b/structurizr-core/src/main/java/com/structurizr/view/AbstractStyle.java @@ -0,0 +1,114 @@ +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, 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. + * + * @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. + * + * @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 workspace. + * + * @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); + } + + void setProperties(Map properties) { + if (properties != null) { + this.properties = new HashMap<>(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/AnimatedView.java b/structurizr-core/src/main/java/com/structurizr/view/AnimatedView.java new file mode 100644 index 000000000..6d0fa9e2c --- /dev/null +++ b/structurizr-core/src/main/java/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/main/java/com/structurizr/view/Animation.java b/structurizr-core/src/main/java/com/structurizr/view/Animation.java new file mode 100644 index 000000000..0e08bb070 --- /dev/null +++ b/structurizr-core/src/main/java/com/structurizr/view/Animation.java @@ -0,0 +1,61 @@ +package com.structurizr.view; + +import com.structurizr.model.Element; +import com.structurizr.model.Relationship; + +import java.util.Set; +import java.util.TreeSet; + +/** + * A wrapper for a collection of animation steps. + */ +public final class Animation { + + private int order; + private Set elements = new TreeSet<>(); + private Set relationships = new TreeSet<>(); + + Animation() { + } + + Animation(int order, Set elements, Set relationships) { + this.order = order; + + for (Element element : elements) { + this.elements.add(element.getId()); + } + + for (Relationship relationship : relationships) { + this.relationships.add(relationship.getId()); + } + } + + public int getOrder() { + return order; + } + + void setOrder(int order) { + this.order = order; + } + + public Set getElements() { + return new TreeSet<>(elements); + } + + void setElements(Set elements) { + if (elements != null) { + this.elements = new TreeSet<>(elements); + } + } + + public Set getRelationships() { + return new TreeSet<>(relationships); + } + + void setRelationships(Set relationships) { + if (relationships != null) { + this.relationships = new TreeSet<>(relationships); + } + } + +} \ No newline at end of file diff --git a/structurizr-core/src/main/java/com/structurizr/view/AutomaticLayout.java b/structurizr-core/src/main/java/com/structurizr/view/AutomaticLayout.java new file mode 100644 index 000000000..5b373d038 --- /dev/null +++ b/structurizr-core/src/main/java/com/structurizr/view/AutomaticLayout.java @@ -0,0 +1,153 @@ +package com.structurizr.view; + +/** + * A wrapper for automatic layout configuration. + */ +public final class AutomaticLayout { + + private Implementation implementation; + private RankDirection rankDirection; + private int rankSeparation; + private int nodeSeparation; + private int edgeSeparation; + private boolean vertices; + private boolean applied; + + AutomaticLayout() { + } + + AutomaticLayout(Implementation implementation, RankDirection rankDirection, int rankSeparation, int nodeSeparation, int edgeSeparation, boolean vertices) { + setImplementation(implementation); + setRankDirection(rankDirection); + setRankSeparation(rankSeparation); + setNodeSeparation(nodeSeparation); + setEdgeSeparation(edgeSeparation); + setVertices(vertices); + setApplied(false); + } + + /** + * Gets the name of the implementation to use. + * + * @return an enum representing Graphviz or Dagre + */ + public Implementation getImplementation() { + return implementation; + } + + void setImplementation(Implementation implementation) { + this.implementation = implementation; + } + + /** + * Gets the rank direction. + * + * @return a RankDirection enum + */ + public RankDirection getRankDirection() { + return rankDirection; + } + + void setRankDirection(RankDirection rankDirection) { + if (rankDirection == null) { + throw new IllegalArgumentException("A rank direction must be specified."); + } + + this.rankDirection = rankDirection; + } + + /** + * Gets the rank separation (in pixels). + * + * @return a positive integer + */ + public int getRankSeparation() { + return rankSeparation; + } + + void setRankSeparation(int rankSeparation) { + if (rankSeparation < 0) { + throw new IllegalArgumentException("The rank separation must be a positive integer."); + } + + this.rankSeparation = rankSeparation; + } + + /** + * Gets the node separation (in pixels). + * + * @return a positive integer + */ + public int getNodeSeparation() { + return nodeSeparation; + } + + void setNodeSeparation(int nodeSeparation) { + if (nodeSeparation < 0) { + throw new IllegalArgumentException("The node separation must be a positive integer."); + } + + this.nodeSeparation = nodeSeparation; + } + + /** + * Gets the edge separation (in pixels). + * + * @return a positive integer + */ + public int getEdgeSeparation() { + return edgeSeparation; + } + + void setEdgeSeparation(int edgeSeparation) { + if (edgeSeparation < 0) { + throw new IllegalArgumentException("The edge separation must be a positive integer."); + } + + this.edgeSeparation = edgeSeparation; + } + + /** + * Gets whether the automatic layout algorithm should create vertices. + * + * @return a boolean + */ + public boolean isVertices() { + return vertices; + } + + 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 + } + + public enum RankDirection { + TopBottom, + BottomTop, + LeftRight, + RightLeft + } + +} \ No newline at end of file diff --git a/structurizr-core/src/main/java/com/structurizr/view/Border.java b/structurizr-core/src/main/java/com/structurizr/view/Border.java new file mode 100644 index 000000000..b834be030 --- /dev/null +++ b/structurizr-core/src/main/java/com/structurizr/view/Border.java @@ -0,0 +1,9 @@ +package com.structurizr.view; + +public enum Border { + + Solid, + Dashed, + Dotted + +} diff --git a/structurizr-core/src/main/java/com/structurizr/view/Branding.java b/structurizr-core/src/main/java/com/structurizr/view/Branding.java new file mode 100644 index 000000000..f0de5a11b --- /dev/null +++ b/structurizr-core/src/main/java/com/structurizr/view/Branding.java @@ -0,0 +1,49 @@ +package com.structurizr.view; + +import com.structurizr.util.ImageUtils; +import com.structurizr.util.StringUtils; + +/** + * A wrapper for the font, logo and color scheme associated with a corporate branding. + */ +public final class Branding { + + private String logo; + + private Font font; + + Branding() { + } + + 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 diff --git a/structurizr-core/src/main/java/com/structurizr/view/Color.java b/structurizr-core/src/main/java/com/structurizr/view/Color.java new file mode 100644 index 000000000..c876ca294 --- /dev/null +++ b/structurizr-core/src/main/java/com/structurizr/view/Color.java @@ -0,0 +1,173 @@ +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) { + if (name != null) { + return NAMED_COLORS.getOrDefault(name.toLowerCase(), null); + } else { + return 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/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/ComponentView.java b/structurizr-core/src/main/java/com/structurizr/view/ComponentView.java new file mode 100644 index 000000000..c9168cee6 --- /dev/null +++ b/structurizr-core/src/main/java/com/structurizr/view/ComponentView.java @@ -0,0 +1,320 @@ +package com.structurizr.view; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.structurizr.model.*; + +import javax.annotation.Nonnull; +import java.util.HashSet; +import java.util.Set; + +/** + * Represents a Component view from the C4 model, showing the components within a given container. + */ +public final class ComponentView extends StaticView { + + private Container container; + private String containerId; + + private boolean externalContainerBoundariesVisible = false; + + ComponentView() { + } + + ComponentView(Container container, String key, String description) { + super(container.getSoftwareSystem(), key, description); + + this.container = container; + } + + @JsonIgnore + @Override + public String getSoftwareSystemId() { + return super.getSoftwareSystemId(); + } + + /** + * Gets the ID of the container associated with this view. + * + * @return the ID, as a String + */ + public String getContainerId() { + if (this.container != null) { + return container.getId(); + } else { + return this.containerId; + } + } + + void setContainerId(String containerId) { + this.containerId = containerId; + } + + /** + * Gets the container associated with this view. + * + * @return a Container object + */ + @JsonIgnore + public Container getContainer() { + return container; + } + + void setContainer(Container container) { + this.container = container; + } + + /** + * Adds all other containers in the software system to this view. + */ + public void addAllContainers() { + getSoftwareSystem().getContainers().forEach(c -> { + try { + add(c); + } catch (ElementNotPermittedInViewException e) { + // ignore + } + }); + } + + /** + * Adds an individual container to this view, including relationships to/from that container. + * + * @param container the Container to add + */ + public void add(Container container) { + add(container, true); + } + + /** + * Adds an individual container to this view. + * + * @param container the Container to add + * @param addRelationships whether to add relationships to/from the container + */ + public void add(Container container, boolean addRelationships) { + addElement(container, addRelationships); + } + + /** + * Adds all components in the container to this view. + */ + public void addAllComponents() { + container.getComponents().forEach(this::add); + } + + /** + * Adds an individual component to this view, including relationships to/from that component. + * + * @param component the Component to add + */ + public void add(Component component) { + add(component, true); + } + + /** + * Adds an individual component to this view. + * + * @param component the Component to add + * @param addRelationships whether to add relationships to/from the component + */ + public void add(Component component, boolean addRelationships) { + if (component != null) { + addElement(component, addRelationships); + } + } + + /** + * Removes an individual container from this view. + * + * @param container the Container to remove + */ + public void remove(Container container) { + removeElement(container); + } + + /** + * Removes an individual component from this view. + * + * @param component the Component to remove + */ + public void remove(Component component) { + removeElement(component); + } + + /** + * Gets the (computed) name of this view. + * + * @return the name, as a String + */ + @Override + public String getName() { + return "Component View: " + getSoftwareSystem().getName() + " - " + getContainer().getName(); + } + + /** + * Adds the default set of elements to this view. + */ + @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); + + for (Container container : getSoftwareSystem().getContainers()) { + if (container.hasEfferentRelationshipWith(component) || component.hasEfferentRelationshipWith(container)) { + add(container); + } + }; + + addNearestNeighbours(component, CustomElement.class); + addNearestNeighbours(component, Person.class); + addNearestNeighbours(component, SoftwareSystem.class); + } + + if (!greedy) { + removeRelationshipsNotConnectedToElements(getContainer().getComponents()); + } + } + + /** + * Adds all people, software systems, sibling containers and components belonging to the container in scope. + */ + @Override + public void addAllElements() { + addAllSoftwareSystems(); + addAllPeople(); + addAllContainers(); + addAllComponents(); + } + + /** + * Adds all people, software systems, sibling containers and components that are directly connected to the specified element. + * + * @param element an Element + */ + @Override + public void addNearestNeighbours(@Nonnull Element element) { + super.addNearestNeighbours(element, SoftwareSystem.class); + super.addNearestNeighbours(element, Person.class); + super.addNearestNeighbours(element, Container.class); + super.addNearestNeighbours(element, Component.class); + } + + /** + *

Adds all {@link Element}s external to the container (Person, SoftwareSystem or Container) + * that have {@link Relationship}s to or from {@link Component}s in this view.

+ *

Not included are:

+ *
    + *
  • References to and from the {@link Container} of this view (only references to and from the components are considered)
  • + *
  • {@link Relationship}s between external {@link Element}s (i.e. elements that are not part of this container)
  • + *
+ *

Don't forget to add elements to your view prior to calling this method, e.g. by calling {@link #addAllComponents()} + * or be selectively choosing certain components.

+ */ + public void addExternalDependencies() { + final Set components = new HashSet<>(); + getElements().stream() + .map(ElementView::getElement) + .filter(e -> e instanceof Component) + .forEach(components::add); + + // add relationships of all other elements to or from our inside components + for (Relationship relationship : getContainer().getModel().getRelationships()) { + if (components.contains(relationship.getSource())) { + addExternalDependency(relationship.getDestination(), components); + } + if (components.contains(relationship.getDestination())) { + addExternalDependency(relationship.getSource(), components); + } + } + + // remove all relationships between elements outside of this container + getRelationships().stream() + .map(RelationshipView::getRelationship) + .filter(r -> !components.contains(r.getSource()) && !components.contains(r.getDestination())) + .forEach(this::remove); + } + + private void addExternalDependency(Element element, Set components) { + if (element instanceof Component) { + if (element.getParent().equals(getContainer())) { + // the component is in the same container, so we'll ignore it since we're only interested in external dependencies + return; + } else { + // the component is in a different container, so let's try to add that instead + element = element.getParent(); + } + } + + if (element instanceof Container) { + if (element.getParent().equals(this.getContainer().getParent())) { + // the container is in the same software system + addElement(element, true); + return; + } else { + // the container is in a different software system, so add that instead + element = element.getParent(); + } + } + + if (element instanceof SoftwareSystem || element instanceof Person) { + addElement(element, true); + } + } + + @Override + protected void checkElementCanBeAdded(Element element) { + if (element instanceof CustomElement || element instanceof Person) { + return; + } + + if (element instanceof SoftwareSystem) { + if (element.equals(getContainer().getParent())) { + throw new ElementNotPermittedInViewException("The software system in scope cannot be added to a component view."); + } else { + checkParentAndChildrenHaveNotAlreadyBeenAdded((SoftwareSystem)element); + return; + } + } + + if (element instanceof Container) { + if (element.equals(getContainer())) { + throw new ElementNotPermittedInViewException("The container in scope cannot be added to a component view."); + } else { + checkParentAndChildrenHaveNotAlreadyBeenAdded((Container)element); + return; + } + } + + if (element instanceof Component) { + checkParentAndChildrenHaveNotAlreadyBeenAdded((Component)element); + return; + } + + throw new ElementNotPermittedInViewException("Only people, software systems, containers, and components can be added to a component view."); + } + + @Override + protected boolean canBeRemoved(Element element) { + return true; + } + + @Deprecated + public boolean getExternalContainerBoundariesVisible() { + return externalContainerBoundariesVisible; + } + + @Deprecated + void setExternalSoftwareSystemBoundariesVisible(boolean externalContainerBoundariesVisible) { + this.externalContainerBoundariesVisible = externalContainerBoundariesVisible; + } + +} \ No newline at end of file diff --git a/structurizr-core/src/main/java/com/structurizr/view/Configuration.java b/structurizr-core/src/main/java/com/structurizr/view/Configuration.java new file mode 100644 index 000000000..267c79ba3 --- /dev/null +++ b/structurizr-core/src/main/java/com/structurizr/view/Configuration.java @@ -0,0 +1,230 @@ +package com.structurizr.view; + +import com.fasterxml.jackson.annotation.JsonGetter; +import com.fasterxml.jackson.annotation.JsonSetter; +import com.structurizr.PropertyHolder; +import com.structurizr.util.Url; + +import java.util.*; + +/** + * Configuration associated with how information in the workspace is rendered. + */ +public final class Configuration implements PropertyHolder { + + private Branding branding = new Branding(); + private Styles styles = new Styles(); + private List themes = new ArrayList<>(); + private Terminology terminology = new Terminology(); + + private MetadataSymbols metadataSymbols; + + private String defaultView; + private String lastSavedView; + private ViewSortOrder viewSortOrder; + + private Map properties = new HashMap<>(); + + /** + * Gets the styles associated with this set of views. + * + * @return a Styles object + */ + public Styles getStyles() { + return styles; + } + + /** + * Sets the theme used to render views. + * + * @param url the URL of theme + */ + @JsonSetter + void setTheme(String url) { + setThemes(url); + } + + /** + * Gets the URLs of the themes used to render views. + * + * @return an array of URLs + */ + public String[] getThemes() { + return themes.toArray(new String[0]); + } + + /** + * Sets the themes used to render views. + * + * @param themes an array of URLs + */ + public void setThemes(String... themes) { + if (themes != null) { + for (String url : themes) { + addTheme(url); + } + } + } + + /** + * Adds a theme. + * + * @param url the URL of the theme to be added + */ + public void addTheme(String url) { + if (url != null && url.trim().length() > 0) { + if (Url.isUrl(url)) { + if (!themes.contains(url)) { + themes.add(url.trim()); + } + } else { + throw new IllegalArgumentException(url + " is not a valid URL."); + } + } + } + + /** + * Gets the key of the view that should be shown by default. + * + * @return the key, as a String (or null if not specified) + */ + public String getDefaultView() { + return defaultView; + } + + @JsonSetter + void setDefaultView(String defaultView) { + this.defaultView = defaultView; + } + + /** + * Sets the view that should be shown by default. + * + * @param view a View object + */ + public void setDefaultView(View view) { + if (view != null) { + this.defaultView = view.getKey(); + } + } + + @JsonGetter + String getLastSavedView() { + return lastSavedView; + } + + @JsonSetter + void setLastSavedView(String lastSavedView) { + this.lastSavedView = lastSavedView; + } + + public void copyConfigurationFrom(Configuration configuration) { + setLastSavedView(configuration.getLastSavedView()); + } + + /** + * Gets the Branding object associated with this workspace. + * + * @return a Branding object + */ + public Branding getBranding() { + return branding; + } + + /** + * Sets the Branding object associated with this workspace. + * + * @param branding a Branding object + */ + void setBranding(Branding branding) { + this.branding = branding; + } + + /** + * Gets the Terminology object associated with this workspace. + * + * @return a Terminology object + */ + public Terminology getTerminology() { + return terminology; + } + + /** + * Sets the Terminology object associated with this workspace. + * + * @param terminology a Terminology object + */ + void setTerminology(Terminology terminology) { + this.terminology = terminology; + } + + /** + * Gets the type of symbols to use when rendering metadata. + * + * @return a MetadataSymbols enum value + */ + public MetadataSymbols getMetadataSymbols() { + return metadataSymbols; + } + + /** + * Sets the type of symbols to use when rendering metadata. + * + * @param metadataSymbols a MetadataSymbols enum value + */ + public void setMetadataSymbols(MetadataSymbols metadataSymbols) { + this.metadataSymbols = metadataSymbols; + } + + /** + * Gets the sort order used when displaying the list of views. + * + * @return a ViewSortOrder enum + */ + public ViewSortOrder getViewSortOrder() { + return viewSortOrder; + } + + /** + * Sets the sort order used when displaying the list of views. + * + * @param viewSortOrder a ViewSortOrder enum + */ + public void setViewSortOrder(ViewSortOrder viewSortOrder) { + this.viewSortOrder = viewSortOrder; + } + + /** + * 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 Collections.unmodifiableMap(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 diff --git a/structurizr-core/src/main/java/com/structurizr/view/ContainerView.java b/structurizr-core/src/main/java/com/structurizr/view/ContainerView.java new file mode 100644 index 000000000..a5b3e4bf0 --- /dev/null +++ b/structurizr-core/src/main/java/com/structurizr/view/ContainerView.java @@ -0,0 +1,226 @@ +package com.structurizr.view; + +import com.structurizr.model.*; + +import javax.annotation.Nonnull; + +/** + * Represents a Container view from the C4 model, showing the containers within a given software system. + */ +public final class ContainerView extends StaticView { + + private boolean externalSoftwareSystemBoundariesVisible = false; + + ContainerView() { + } + + ContainerView(SoftwareSystem softwareSystem, String key, String description) { + super(softwareSystem, key, description); + } + + /** + * Adds a software system to this view, including relationships to/from that software system. + * Please note that you cannot add the software system that is the scope of this view. + * + * @param softwareSystem the SoftwareSystem to add + */ + @Override + public void add(@Nonnull SoftwareSystem softwareSystem) { + add(softwareSystem, true); + } + + /** + * Adds all containers within the software system in scope to this view. + */ + public void addAllContainers() { + getSoftwareSystem().getContainers().forEach(c -> { + try { + add(c); + } catch (ElementNotPermittedInViewException e) { + // ignore + } + }); + } + + /** + * Adds an individual container (belonging to any software system) to this view, including relationships to/from that container. + * + * @param container the Container to add + */ + public void add(Container container) { + add(container, true); + } + + /** + * Adds an individual container (belonging to any software system) to this view. + * + * @param container the Container to add + * @param addRelationships whether to add relationships to/from the container + */ + public void add(Container container, boolean addRelationships) { + addElement(container, addRelationships); + } + + /** + * Removes an individual container from this view. + * + * @param container the Container to remove + */ + public void remove(Container container) { + removeElement(container); + } + + /** + * Gets the (computed) name of this view. + * + * @return the name, as a String + */ + @Override + public String getName() { + return "Container View: " + getSoftwareSystem().getName(); + } + + /** + * Adds the default set of elements to this view. + */ + @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()); + } + } + + /** + * Adds all people, software systems and containers that belong to the software system in scope. + */ + @Override + public void addAllElements() { + addAllSoftwareSystems(); + addAllPeople(); + addAllContainers(); + } + + /** + * Adds all people, software systems and containers that are directly connected to the specified element. + * + * @param element an Element + */ + @Override + public void addNearestNeighbours(@Nonnull Element element) { + super.addNearestNeighbours(element, Person.class); + super.addNearestNeighbours(element, SoftwareSystem.class); + super.addNearestNeighbours(element, Container.class); + } + + /** + *

Adds all {@link com.structurizr.model.Container}s of the given {@link ContainerView} as well as all external influencers, that is all + * persons and all other software systems with incoming or outgoing dependencies.

+ *

Additionally, all relationships of external dependencies are omitted to keep the diagram clean

+ */ + public final void addAllInfluencers() { + + // add all software systems with incoming or outgoing dependencies + getModel().getSoftwareSystems() + .stream() + .filter(softwareSystem -> softwareSystem.hasEfferentRelationshipWith(getSoftwareSystem()) || getSoftwareSystem().hasEfferentRelationshipWith(softwareSystem)) + .forEach(this::add); + + // then add all people with incoming or outgoing dependencies + getModel().getPeople() + .stream() + .filter(person -> person.hasEfferentRelationshipWith(getSoftwareSystem()) || getSoftwareSystem().hasEfferentRelationshipWith(person)) + .forEach(this::add); + + // then remove all relationships of external elements to keep the container view clean + getRelationships() + .stream() + .map(view -> view.getRelationship()) + .filter(relationship -> !isPartOf(relationship.getDestination(), getSoftwareSystem()) && !isPartOf(relationship.getSource(), getSoftwareSystem())) + .forEach(this::remove); + } + + /** + *

Adds all {@link com.structurizr.model.Container}s of the given {@link ContainerView} as well as all external influencers, that is all + * persons and all other software systems with incoming or outgoing dependencies.

+ *

Additionally, all relationships of external dependencies are omitted to keep the diagram clean

+ */ + public final void addAllContainersAndInfluencers() { + // first add all containers of the underlying software system + this.addAllContainers(); + addAllInfluencers(); + } + + private static boolean isPartOf(Element element, Element other) { + if (element.getId().equals(other.getId())) { + return true; + } else if (element.getParent() != null) { + return isPartOf(element.getParent(), other); + } + return false; + } + + /* + * Adds all {@link SoftwareSystem}s that have efferent {@link com.structurizr.model.Relationship}s with the + * {@link SoftwareSystem} of this {@link ContainerView}. + */ + public final void addDependentSoftwareSystems() { + getModel().getSoftwareSystems().stream() + .filter(softwareSystem -> softwareSystem.hasEfferentRelationshipWith(this.getSoftwareSystem())) + .forEach(this::add); + } + + @Override + protected void checkElementCanBeAdded(Element element) { + if (element instanceof CustomElement || element instanceof Person) { + return; + } + + if (element instanceof SoftwareSystem) { + if (element.equals(getSoftwareSystem())) { + throw new ElementNotPermittedInViewException("The software system in scope cannot be added to a container view."); + } else { + checkParentAndChildrenHaveNotAlreadyBeenAdded((SoftwareSystem)element); + return; + } + } + + if (element instanceof Container) { + checkParentAndChildrenHaveNotAlreadyBeenAdded((Container)element); + return; + } + + throw new ElementNotPermittedInViewException("Only people, software systems, and containers can be added to a container view."); + } + + @Override + protected boolean canBeRemoved(Element element) { + return true; + } + + @Deprecated + public boolean getExternalSoftwareSystemBoundariesVisible() { + return externalSoftwareSystemBoundariesVisible; + } + + @Deprecated + void setExternalSoftwareSystemBoundariesVisible(boolean externalSoftwareSystemBoundariesVisible) { + this.externalSoftwareSystemBoundariesVisible = externalSoftwareSystemBoundariesVisible; + } + +} \ No newline at end of file diff --git a/structurizr-core/src/main/java/com/structurizr/view/CustomView.java b/structurizr-core/src/main/java/com/structurizr/view/CustomView.java new file mode 100644 index 000000000..35469259a --- /dev/null +++ b/structurizr-core/src/main/java/com/structurizr/view/CustomView.java @@ -0,0 +1,191 @@ +package com.structurizr.view; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.structurizr.model.CustomElement; +import com.structurizr.model.Element; +import com.structurizr.model.Model; +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; +import java.util.Set; + +/** + * Represents a custom view, containing custom elements. + */ +public final class CustomView extends ModelView implements AnimatedView { + + @Nonnull + private List animations = new ArrayList<>(); + + private Model model; + + CustomView() { + } + + CustomView(Model model, String key, String title, String description) { + super(null, key, description); + + setTitle(title); + this.model = model; + } + + /** + * Gets the (computed) name of this view. + * + * @return the name, as a String + */ + @Override + public String getName() { + return "Custom View: " + getTitle(); + } + + /** + * Gets the model that this view belongs to. + * + * @return a Model object + */ + @JsonIgnore + @Override + public Model getModel() { + return this.model; + } + + void setModel(Model model) { + this.model = model; + } + + @Override + protected void checkElementCanBeAdded(Element element) { + if (element instanceof CustomElement) { + // all good + } else { + throw new ElementNotPermittedInViewException("Only custom elements can be added to a custom view."); + } + } + + @Override + protected boolean canBeRemoved(Element element) { + return true; + } + + /** + * Adds a specific relationship to this view. + * + * @param relationship the Relationship to be added + * @return a RelationshipView object representing the relationship added + */ + public RelationshipView add(@Nonnull Relationship relationship) { + return addRelationship(relationship); + } + + /** + * Adds an animation step, with the specified elements. + * + * @param elements the elements that should be shown in the animation step + */ + public void addAnimation(CustomElement... elements) { + if (elements == null || elements.length == 0) { + throw new IllegalArgumentException("One or more elements must be specified."); + } + + Set elementIdsInPreviousAnimationSteps = new HashSet<>(); + + for (Animation animationStep : animations) { + elementIdsInPreviousAnimationSteps.addAll(animationStep.getElements()); + } + + Set elementsInThisAnimationStep = new HashSet<>(); + Set relationshipsInThisAnimationStep = new HashSet<>(); + + for (CustomElement element : elements) { + if (isElementInView(element)) { + if (!elementIdsInPreviousAnimationSteps.contains(element.getId())) { + elementIdsInPreviousAnimationSteps.add(element.getId()); + elementsInThisAnimationStep.add(element); + } + } + } + + 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())) + ) { + relationshipsInThisAnimationStep.add(relationshipView.getRelationship()); + } + } + + animations.add(new Animation(animations.size() + 1, elementsInThisAnimationStep, relationshipsInThisAnimationStep)); + } + + @Nonnull + @Override + public List getAnimations() { + return new ArrayList<>(animations); + } + + void setAnimations(@Nullable List animations) { + if (animations != null) { + this.animations = new ArrayList<>(animations); + } else { + this.animations = new ArrayList<>(); + } + } + + /** + * 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. + */ + public void addDefaultElements() { + addAllCustomElements(); + } + + /** + * Adds all custom elements to this view. + */ + public void addAllCustomElements() { + getModel().getCustomElements().forEach(ce -> { + try { + add(ce); + } catch (ElementNotPermittedInViewException e) { + // ignore + } + }); + } + +} diff --git a/structurizr-core/src/main/java/com/structurizr/view/DefaultLayoutMergeStrategy.java b/structurizr-core/src/main/java/com/structurizr/view/DefaultLayoutMergeStrategy.java new file mode 100644 index 000000000..e4faeb92c --- /dev/null +++ b/structurizr-core/src/main/java/com/structurizr/view/DefaultLayoutMergeStrategy.java @@ -0,0 +1,179 @@ +package com.structurizr.view; + +import com.structurizr.model.Element; +import com.structurizr.model.Relationship; +import com.structurizr.util.StringUtils; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import javax.annotation.Nonnull; +import java.util.HashMap; +import java.util.Map; + +/** + * A default implementation of a LayoutMergeStrategy that: + * + * - Sets the paper size (if not set). + * - Copies element x,y positions. + * - Copies relationship vertices. + * + * Elements are matched using the following properties, in order: + * - the element's full canonical name + * - the element's name + * - the element's description + */ +public class DefaultLayoutMergeStrategy implements LayoutMergeStrategy { + + private static final Log log = LogFactory.getLog(View.class); + + /** + * Attempts to copy the visual layout information (e.g. x,y coordinates) of elements and relationships + * from the specified source view into the specified destination view. + * + * @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 ModelView viewWithLayoutInformation, @Nonnull ModelView viewWithoutLayoutInformation) { + setPaperSizeIfNotSpecified(viewWithLayoutInformation, viewWithoutLayoutInformation); + setDimensionsIfNotSpecified(viewWithLayoutInformation, viewWithoutLayoutInformation); + + Map elementViewMap = new HashMap<>(); + Map elementMap = new HashMap<>(); + + for (ElementView elementViewWithoutLayoutInformation : viewWithoutLayoutInformation.getElements()) { + ElementView elementViewWithLayoutInformation = findElementView(viewWithLayoutInformation, elementViewWithoutLayoutInformation.getElement()); + if (elementViewWithLayoutInformation != null) { + elementViewMap.put(elementViewWithoutLayoutInformation, elementViewWithLayoutInformation); + elementMap.put(elementViewWithoutLayoutInformation.getElement(), elementViewWithLayoutInformation.getElement()); + } else { + log.warn("There is no layout information for the element named " + elementViewWithoutLayoutInformation.getElement().getName() + " on view " + viewWithLayoutInformation.getKey()); + } + } + + for (ElementView elementViewWithoutLayoutInformation : elementViewMap.keySet()) { + ElementView elementViewWithLayoutInformation = elementViewMap.get(elementViewWithoutLayoutInformation); + elementViewWithoutLayoutInformation.copyLayoutInformationFrom(elementViewWithLayoutInformation); + } + + for (RelationshipView relationshipViewWithoutLayoutInformation : viewWithoutLayoutInformation.getRelationships()) { + RelationshipView relationshipViewWithLayoutInformation; + if (viewWithoutLayoutInformation instanceof DynamicView) { + relationshipViewWithLayoutInformation = findRelationshipView(viewWithLayoutInformation, relationshipViewWithoutLayoutInformation, elementMap); + } else { + relationshipViewWithLayoutInformation = findRelationshipView(viewWithLayoutInformation, relationshipViewWithoutLayoutInformation.getRelationship(), elementMap); + } + + if (relationshipViewWithLayoutInformation != null) { + relationshipViewWithoutLayoutInformation.copyLayoutInformationFrom(relationshipViewWithLayoutInformation); + } + } + } + + private void setPaperSizeIfNotSpecified(@Nonnull ModelView remoteView, @Nonnull ModelView localView) { + if (localView.getPaperSize() == null) { + localView.setPaperSize(remoteView.getPaperSize()); + } + } + + private void setDimensionsIfNotSpecified(@Nonnull ModelView remoteView, @Nonnull ModelView localView) { + if (localView.getDimensions() == null) { + localView.setDimensions(remoteView.getDimensions()); + } + } + + /** + * Finds an element. Override this to change the behaviour. + * + * @param viewWithLayoutInformation the view to search + * @param elementWithoutLayoutInformation the Element to find + * @return an ElementView + */ + 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); + + if (elementView == null) { + // no element was found, so try finding an element of the same type with the same name (in this situation, the parent element may have been renamed) + elementView = viewWithLayoutInformation.getElements().stream().filter(ev -> ev.getElement().getName().equals(elementWithoutLayoutInformation.getName()) && ev.getElement().getClass().equals(elementWithoutLayoutInformation.getClass())).findFirst().orElse(null); + } + + if (elementView == null) { + // no element was found, so try finding an element of the same type with the same description if set (in this situation, the element itself may have been renamed) + if (!StringUtils.isNullOrEmpty(elementWithoutLayoutInformation.getDescription())) { + elementView = viewWithLayoutInformation.getElements().stream().filter(ev -> elementWithoutLayoutInformation.getDescription().equals(ev.getElement().getDescription()) && ev.getElement().getClass().equals(elementWithoutLayoutInformation.getClass())).findFirst().orElse(null); + } + } + + if (elementView == null) { + // no element was found, so try finding an element of the same type with the same ID (in this situation, the name and description may have changed) + elementView = viewWithLayoutInformation.getElements().stream().filter(ev -> ev.getElement().getId().equals(elementWithoutLayoutInformation.getId()) && ev.getElement().getClass().equals(elementWithoutLayoutInformation.getClass())).findFirst().orElse(null); + } + + return elementView; + } + + private RelationshipView findRelationshipView(ModelView viewWithLayoutInformation, Relationship relationshipWithoutLayoutInformation, Map elementMap) { + if (!elementMap.containsKey(relationshipWithoutLayoutInformation.getSource()) || !elementMap.containsKey(relationshipWithoutLayoutInformation.getDestination())) { + return null; + } + + Element sourceElementWithLayoutInformation = elementMap.get(relationshipWithoutLayoutInformation.getSource()); + Element destinationElementWithLayoutInformation = elementMap.get(relationshipWithoutLayoutInformation.getDestination()); + + for (RelationshipView rv : viewWithLayoutInformation.getRelationships()) { + if ( + rv.getRelationship().getSource().equals(sourceElementWithLayoutInformation) && + rv.getRelationship().getDestination().equals(destinationElementWithLayoutInformation) && + rv.getRelationship().getDescription().equals(relationshipWithoutLayoutInformation.getDescription()) + ) { + return rv; + } + } + + // 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; + } + + private RelationshipView findRelationshipView(ModelView view, RelationshipView relationshipWithoutLayoutInformation, Map elementMap) { + if (!elementMap.containsKey(relationshipWithoutLayoutInformation.getRelationship().getSource()) || !elementMap.containsKey(relationshipWithoutLayoutInformation.getRelationship().getDestination())) { + return null; + } + + Element sourceElementWithLayoutInformation = elementMap.get(relationshipWithoutLayoutInformation.getRelationship().getSource()); + Element destinationElementWithLayoutInformation = elementMap.get(relationshipWithoutLayoutInformation.getRelationship().getDestination()); + + for (RelationshipView rv : view.getRelationships()) { + if ( + rv.getRelationship().getSource().equals(sourceElementWithLayoutInformation) && + rv.getRelationship().getDestination().equals(destinationElementWithLayoutInformation) && + rv.getDescription().equals(relationshipWithoutLayoutInformation.getDescription()) && + rv.getOrder().equals(relationshipWithoutLayoutInformation.getOrder())) { + return rv; + } + } + + // 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; + } + +} \ No newline at end of file diff --git a/structurizr-core/src/main/java/com/structurizr/view/DeploymentView.java b/structurizr-core/src/main/java/com/structurizr/view/DeploymentView.java new file mode 100644 index 000000000..444bb2250 --- /dev/null +++ b/structurizr-core/src/main/java/com/structurizr/view/DeploymentView.java @@ -0,0 +1,482 @@ +package com.structurizr.view; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.structurizr.model.*; +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 implements AnimatedView { + + private Model model; + private String environment = DeploymentElement.DEFAULT_DEPLOYMENT_ENVIRONMENT; + + @Nonnull + private List animations = new ArrayList<>(); + + DeploymentView() { + } + + DeploymentView(Model model, String key, String description) { + super(null, key, description); + + this.model = model; + } + + DeploymentView(SoftwareSystem softwareSystem, String key, String description) { + super(softwareSystem, key, description); + + this.model = softwareSystem.getModel(); + } + + @JsonIgnore + @Override + public Model getModel() { + return this.model; + } + + void setModel(Model model) { + this.model = model; + } + + /** + * Adds the default set of elements to this view. + */ + public void addDefaultElements() { + addAllDeploymentNodes(); + + getElements().stream().map(ElementView::getElement).forEach(e -> addNearestNeighbours(e, CustomElement.class)); + } + + /** + * Adds all of the top-level deployment nodes to this view, for the same deployment environment (if set). + */ + public void addAllDeploymentNodes() { + for (DeploymentNode deploymentNode : getModel().getDeploymentNodes()) { + if (deploymentNode.getParent() == null) { + if (this.getEnvironment() == null || this.getEnvironment().equals(deploymentNode.getEnvironment())) { + add(deploymentNode); + } + } + } + } + + /** + * Adds a deployment node to this view, including relationships to/from that deployment node (and children). + * + * @param deploymentNode the DeploymentNode to add + */ + public void add(@Nonnull DeploymentNode deploymentNode) { + add(deploymentNode, true); + } + + /** + * Adds a deployment node to this view. + * + * @param deploymentNode the DeploymentNode to add + * @param addRelationships whether to add relationships to/from the person + */ + public void add(@Nonnull DeploymentNode deploymentNode, boolean addRelationships) { + if (deploymentNode == null) { + throw new IllegalArgumentException("A deployment node must be specified."); + } + + if (addElementInstancesAndDeploymentNodesAndInfrastructureNodes(deploymentNode, addRelationships)) { + Element parent = deploymentNode.getParent(); + while (parent != null) { + addElement(parent, addRelationships); + parent = parent.getParent(); + } + } + } + + /** + * Adds an infrastructure node (and its parent deployment nodes) to this view. + * + * @param infrastructureNode the InfrastructureNode to add + */ + public void add(@Nonnull InfrastructureNode infrastructureNode) { + addElement(infrastructureNode, true); + DeploymentNode parent = (DeploymentNode)infrastructureNode.getParent(); + while (parent != null) { + addElement(parent, true); + parent = (DeploymentNode)parent.getParent(); + } + } + + /** + * Adds a software system instance (and its parent deployment nodes) to this view. + * + * @param softwareSystemInstance the SoftwareSystemInstance to add + */ + public void add(@Nonnull SoftwareSystemInstance softwareSystemInstance) { + addElement(softwareSystemInstance, true); + DeploymentNode parent = (DeploymentNode)softwareSystemInstance.getParent(); + while (parent != null) { + addElement(parent, true); + parent = (DeploymentNode)parent.getParent(); + } + } + + /** + * Adds a container instance (and its parent deployment nodes) to this view. + * + * @param containerInstance the ContainerInstance to add + */ + public void add(@Nonnull ContainerInstance containerInstance) { + addElement(containerInstance, true); + DeploymentNode parent = (DeploymentNode)containerInstance.getParent(); + while (parent != null) { + addElement(parent, true); + parent = (DeploymentNode)parent.getParent(); + } + } + + /** + * Removes the given deployment node from this view. + * + * @param deploymentNode the DeploymentNode to be removed + */ + public void remove(@Nonnull DeploymentNode deploymentNode) { + for (SoftwareSystemInstance softwareSystemInstance : deploymentNode.getSoftwareSystemInstances()) { + remove(softwareSystemInstance); + } + + for (ContainerInstance containerInstance : deploymentNode.getContainerInstances()) { + remove(containerInstance); + } + + for (InfrastructureNode infrastructureNode : deploymentNode.getInfrastructureNodes()) { + remove(infrastructureNode); + } + + for (DeploymentNode child : deploymentNode.getChildren()) { + remove(child); + } + + removeElement(deploymentNode); + } + + /** + * Removes an infrastructure node from this view. + * + * @param infrastructureNode the InfrastructureNode to be removed + */ + public void remove(@Nonnull InfrastructureNode infrastructureNode) { + removeElement(infrastructureNode); + } + + /** + * Removes a software system instance from this view. + * + * @param softwareSystemInstance the SoftwareSystemInstance to be removed + */ + public void remove(@Nonnull SoftwareSystemInstance softwareSystemInstance) { + removeElement(softwareSystemInstance); + } + + /** + * Removes a container instance from this view. + * + * @param containerInstance the ContainerInstance to be removed + */ + public void remove(@Nonnull ContainerInstance containerInstance) { + removeElement(containerInstance); + } + + private boolean addElementInstancesAndDeploymentNodesAndInfrastructureNodes(DeploymentNode deploymentNode, boolean addRelationships) { + boolean hasElementsOrInfrastructureNodes = false; + for (SoftwareSystemInstance softwareSystemInstance : deploymentNode.getSoftwareSystemInstances()) { + try { + addElement(softwareSystemInstance, addRelationships); + hasElementsOrInfrastructureNodes = true; + } catch (ElementNotPermittedInViewException e) { + // the element can't be added, so ignore it + } + } + + for (ContainerInstance containerInstance : deploymentNode.getContainerInstances()) { + Container container = containerInstance.getContainer(); + if (getSoftwareSystem() == null || container.getParent().equals(getSoftwareSystem())) { + try { + addElement(containerInstance, addRelationships); + hasElementsOrInfrastructureNodes = true; + } catch (ElementNotPermittedInViewException e) { + // the element can't be added, so ignore it + } + } + } + + for (InfrastructureNode infrastructureNode : deploymentNode.getInfrastructureNodes()) { + addElement(infrastructureNode, addRelationships); + hasElementsOrInfrastructureNodes = true; + } + + for (DeploymentNode child : deploymentNode.getChildren()) { + hasElementsOrInfrastructureNodes = hasElementsOrInfrastructureNodes | addElementInstancesAndDeploymentNodesAndInfrastructureNodes(child, addRelationships); + } + + if (hasElementsOrInfrastructureNodes) { + addElement(deploymentNode, addRelationships); + } + + return hasElementsOrInfrastructureNodes; + } + + /** + * Adds a Relationship to this view. + * + * @param relationship the Relationship to be added + * @return a RelationshipView object representing the relationship added + */ + public RelationshipView add(@Nonnull Relationship relationship) { + return addRelationship(relationship); + } + + /** + * Gets the (computed) name of this view. + * + * @return the name, as a String + */ + @Override + public String getName() { + String name; + if (getSoftwareSystem() != null) { + name = "Deployment View: " + getSoftwareSystem().getName() + " - " + getEnvironment(); + } else { + name = "Deployment View: " + getEnvironment(); + } + + return name; + } + + /** + * Gets the name of the environment that this deployment view is for (e.g. "Development", "Live", etc). + * + * @return the environment name, as a String + */ + public String getEnvironment() { + return environment; + } + + /** + * Sets the name of the environment that this deployment view is for (e.g. "Development", "Live", etc). + * + * @param environment the environment name, as a String + */ + public void setEnvironment(String environment) { + this.environment = environment; + } + + @Override + protected void checkElementCanBeAdded(Element elementToBeAdded) { + if (elementToBeAdded instanceof CustomElement) { + return; + } + + if (!(elementToBeAdded instanceof DeploymentElement)) { + throw new ElementNotPermittedInViewException("Only deployment nodes, infrastructure nodes, software system instances, and container instances can be added to deployment views."); + } + + DeploymentElement deploymentElementToBeAdded = (DeploymentElement) elementToBeAdded; + if (!deploymentElementToBeAdded.getEnvironment().equals(this.getEnvironment())) { + throw new ElementNotPermittedInViewException("Only elements in the " + this.getEnvironment() + " deployment environment can be added to this view."); + } + + if (this.getSoftwareSystem() != null && elementToBeAdded instanceof SoftwareSystemInstance) { + SoftwareSystemInstance ssi = (SoftwareSystemInstance) elementToBeAdded; + + if (ssi.getSoftwareSystem().equals(this.getSoftwareSystem())) { + // adding an instance of the scoped software system isn't permitted + throw new ElementNotPermittedInViewException("The software system in scope cannot be added to a deployment view."); + } + } + + if (elementToBeAdded instanceof SoftwareSystemInstance) { + // check that a child container instance hasn't been added already + SoftwareSystemInstance softwareSystemInstanceToBeAdded = (SoftwareSystemInstance) elementToBeAdded; + Set softwareSystemIds = getElements().stream().map(ElementView::getElement).filter(e -> e instanceof ContainerInstance).map(e -> (ContainerInstance)e).map(ci -> ci.getContainer().getSoftwareSystem().getId()).collect(Collectors.toSet()); + + if (softwareSystemIds.contains(softwareSystemInstanceToBeAdded.getSoftwareSystemId())) { + throw new ElementNotPermittedInViewException("A child of " + elementToBeAdded.getName() + " is already in this view."); + } + } + + if (elementToBeAdded instanceof ContainerInstance) { + // check that the parent software system instance hasn't been added already + ContainerInstance containerInstanceToBeAdded = (ContainerInstance)elementToBeAdded; + Set softwareSystemIds = getElements().stream().map(ElementView::getElement).filter(e -> e instanceof SoftwareSystemInstance).map(e -> (SoftwareSystemInstance)e).map(SoftwareSystemInstance::getSoftwareSystemId).collect(Collectors.toSet()); + + if (softwareSystemIds.contains(containerInstanceToBeAdded.getContainer().getSoftwareSystem().getId())) { + throw new ElementNotPermittedInViewException("A parent of " + elementToBeAdded.getName() + " is already in this view."); + } + } + } + + @Override + protected boolean canBeRemoved(Element element) { + return true; + } + + /** + * Adds an animation step, with the specified infrastructure nodes. + * + * @param infrastructureNodes the infrastructure nodes that should be shown in the animation step + */ + public void addAnimation(InfrastructureNode... infrastructureNodes) { + if (infrastructureNodes == null || infrastructureNodes.length == 0) { + throw new IllegalArgumentException("One or more infrastructure nodes must be specified."); + } + + addAnimation(new ContainerInstance[0], infrastructureNodes); + } + + /** + * Adds an animation step, with the specified element instances. + * + * @param elementInstances the element instances that should be shown in the animation step + */ + public void addAnimation(StaticStructureElementInstance... elementInstances) { + if (elementInstances == null || elementInstances.length == 0) { + throw new IllegalArgumentException("One or more software system/container instances must be specified."); + } + + addAnimation(elementInstances, new InfrastructureNode[0]); + } + + /** + * Adds an animation step, with the specified container instances and infrastructure nodes. + * + * @param elementInstances the element instances that should be shown in the animation step + * @param infrastructureNodes the container infrastructure nodes that should be shown in the animation step + */ + public void addAnimation(StaticStructureElementInstance[] elementInstances, InfrastructureNode[] infrastructureNodes) { + if ((elementInstances == null || elementInstances.length == 0) && (infrastructureNodes == null || infrastructureNodes.length == 0)) { + throw new IllegalArgumentException("One or more software system/container instances and/or infrastructure nodes must be specified."); + } + + List elements = new ArrayList<>(); + if (elementInstances != null) { + Collections.addAll(elements, elementInstances); + } + if (infrastructureNodes != null) { + Collections.addAll(elements, infrastructureNodes); + } + + addAnimationStep(elements.toArray(new Element[0])); + } + + private void addAnimationStep(Element... elements) { + + Set elementIdsInPreviousAnimationSteps = new HashSet<>(); + for (Animation animationStep : animations) { + elementIdsInPreviousAnimationSteps.addAll(animationStep.getElements()); + } + + Set elementsInThisAnimationStep = new HashSet<>(); + Set relationshipsInThisAnimationStep = new HashSet<>(); + + for (Element element : elements) { + if (isElementInView(element) && !elementIdsInPreviousAnimationSteps.contains(element.getId())) { + elementIdsInPreviousAnimationSteps.add(element.getId()); + elementsInThisAnimationStep.add(element); + + Element deploymentNode = findDeploymentNode(element); + while (deploymentNode != null) { + if (!elementIdsInPreviousAnimationSteps.contains(deploymentNode.getId())) { + elementIdsInPreviousAnimationSteps.add(deploymentNode.getId()); + elementsInThisAnimationStep.add(deploymentNode); + } + + deploymentNode = deploymentNode.getParent(); + } + } + } + + 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())) + ) { + relationshipsInThisAnimationStep.add(relationshipView.getRelationship()); + } + } + + animations.add(new Animation(animations.size() + 1, elementsInThisAnimationStep, relationshipsInThisAnimationStep)); + } + + private DeploymentNode findDeploymentNode(Element e) { + for (Element element : getModel().getElements()) { + if (element instanceof DeploymentNode) { + DeploymentNode deploymentNode = (DeploymentNode) element; + + if (e instanceof ContainerInstance) { + if (deploymentNode.getContainerInstances().contains(e)) { + return deploymentNode; + } + } + + if (e instanceof InfrastructureNode) { + if (deploymentNode.getInfrastructureNodes().contains(e)) { + return deploymentNode; + } + } + } + } + + return null; + } + + @Nonnull + @Override + public List getAnimations() { + return new ArrayList<>(animations); + } + + void setAnimations(@Nullable List animations) { + if (animations != null) { + this.animations = new ArrayList<>(animations); + } else { + this.animations = new ArrayList<>(); + } + } + + /** + * 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); + } + +} diff --git a/structurizr-core/src/main/java/com/structurizr/view/Dimensions.java b/structurizr-core/src/main/java/com/structurizr/view/Dimensions.java new file mode 100644 index 000000000..e5d9ba408 --- /dev/null +++ b/structurizr-core/src/main/java/com/structurizr/view/Dimensions.java @@ -0,0 +1,40 @@ +package com.structurizr.view; + +public final class Dimensions { + + private int width; + private int height; + + Dimensions() { + } + + public Dimensions(int width, int height) { + setWidth(width); + setHeight(height); + } + + public int getWidth() { + return width; + } + + public void setWidth(int width) { + if (width < 0) { + throw new IllegalArgumentException("The width must be a positive integer."); + } + + this.width = width; + } + + public int getHeight() { + return height; + } + + public void setHeight(int height) { + if (height < 0) { + throw new IllegalArgumentException("The height must be a positive integer."); + } + + this.height = height; + } + +} \ No newline at end of file diff --git a/structurizr-core/src/main/java/com/structurizr/view/DynamicView.java b/structurizr-core/src/main/java/com/structurizr/view/DynamicView.java new file mode 100644 index 000000000..5f206caca --- /dev/null +++ b/structurizr-core/src/main/java/com/structurizr/view/DynamicView.java @@ -0,0 +1,390 @@ +package com.structurizr.view; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.structurizr.model.*; +import com.structurizr.util.StringUtils; + +import javax.annotation.Nonnull; +import java.util.*; + +/** + * A dynamic view, used to describe behaviour between static elements at runtime. + */ +public final class DynamicView extends ModelView { + + private Model model; + + private Element element; + private String elementId; + + private boolean externalBoundariesVisible = false; + + private SequenceNumber sequenceNumber = new SequenceNumber(); + + DynamicView() { + } + + DynamicView(Model model, String key, String description) { + super(null, key, description); + + setModel(model); + setElement(null); + } + + DynamicView(SoftwareSystem softwareSystem, String key, String description) { + super(softwareSystem, key, description); + + setModel(softwareSystem.getModel()); + setElement(softwareSystem); + } + + DynamicView(Container container, String key, String description) { + super(container.getSoftwareSystem(), key, description); + + setModel(container.getModel()); + setElement(container); + } + + @JsonIgnore + @Override + public Model getModel() { + return this.model; + } + + void setModel(Model model) { + this.model = model; + } + + @Override + @JsonIgnore + public String getSoftwareSystemId() { + return super.getSoftwareSystemId(); + } + + /** + * Gets the ID of the software system or container 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; + + if (element instanceof SoftwareSystem) { + setSoftwareSystem((SoftwareSystem)element); + } + } + + public RelationshipView add(@Nonnull StaticStructureElement source, @Nonnull StaticStructureElement destination) { + return add(source, "", destination); + } + + public RelationshipView add(@Nonnull StaticStructureElement source, String description, @Nonnull StaticStructureElement destination) { + return add(source, description, "", destination); + } + + 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."); + } + + if (destination == null) { + throw new IllegalArgumentException("A destination element must be specified."); + } + + checkElementCanBeAdded(source); + checkElementCanBeAdded(destination); + + // check that the relationship is in the model before adding it + Relationship relationship = null; + + if (StringUtils.isNullOrEmpty(technology)) { + // no technology is specified, so just pick the first relationship we find + relationship = source.getEfferentRelationshipWith(destination, description); + if (relationship == null) { + relationship = source.getEfferentRelationshipWith(destination); + } + } else { + Set relationships = source.getEfferentRelationshipsWith(destination); + for (Relationship rel : relationships) { + if (technology.equals(rel.getTechnology())) { + relationship = rel; + } + } + } + + if (relationship != null) { + addElement(source, false); + addElement(destination, false); + + return addRelationship(relationship, description, sequenceNumber.getNext(), false); + } else { + // perhaps model this as a return/reply/response message instead, if the reverse relationship exists + + if (StringUtils.isNullOrEmpty(technology)) { + // no technology is specified, so just pick the first relationship we find + relationship = destination.getEfferentRelationshipWith(source); + } else { + Set relationships = destination.getEfferentRelationshipsWith(source); + for (Relationship rel : relationships) { + if (technology.equals(rel.getTechnology())) { + relationship = rel; + } + } + } + + if (relationship != null) { + addElement(source, false); + addElement(destination, false); + + return addRelationship(relationship, description, sequenceNumber.getNext(), true); + } else { + if (StringUtils.isNullOrEmpty(technology)) { + throw new IllegalArgumentException("A relationship between " + source.getName() + " and " + destination.getName() + " does not exist in model."); + } else { + throw new IllegalArgumentException("A relationship between " + source.getName() + " and " + destination.getName() + " with technology " + technology + " does not exist in model."); + } + } + } + } + + /** + * 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); + } + + private RelationshipView addRelationship(Relationship relationship, String description, String order, boolean response) { + RelationshipView relationshipView = addRelationship(relationship); + if (relationshipView != null) { + relationshipView.setDescription(description); + relationshipView.setOrder(order); + relationshipView.setResponse(response); + } + + return relationshipView; + } + + /** + * Gets the (computed) name of this view. + * + * @return the name, as a String + */ + @Override + public String getName() { + if (element != null) { + 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 View"; + } + } + + @Override + public String toString() { + StringBuilder buf = new StringBuilder(); + List list = new ArrayList<>(getRelationships()); + Collections.sort(list, (rv1, rv2) -> rv1.getOrder().compareTo(rv2.getOrder())); + list.forEach(rv -> buf.append(rv.toString() + "\n")); + + return buf.toString(); + } + + public void startParallelSequence() { + sequenceNumber.startParallelSequence(); + } + + public void endParallelSequence() { + endParallelSequence(false); + } + + public void endParallelSequence(boolean endAllParallelSequencesAndContinueNumbering) { + sequenceNumber.endParallelSequence(endAllParallelSequencesAndContinueNumbering); + } + + @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."); + } + + StaticStructureElement staticStructureElementToBeAdded = (StaticStructureElement)elementToBeAdded; + + // people can always be added + if (staticStructureElementToBeAdded instanceof Person) { + return; + } + + // if the scope of this dynamic view is a software system, we only want: + // - containers + // - other software systems + if (element instanceof SoftwareSystem) { + if (staticStructureElementToBeAdded.equals(element)) { + throw new ElementNotPermittedInViewException(staticStructureElementToBeAdded.getName() + " is already the scope of this view and cannot be added to it."); + } + + if (staticStructureElementToBeAdded instanceof SoftwareSystem || staticStructureElementToBeAdded instanceof Container) { + checkParentAndChildrenHaveNotAlreadyBeenAdded(staticStructureElementToBeAdded); + } else if (staticStructureElementToBeAdded instanceof Component) { + throw new ElementNotPermittedInViewException("Components can't be added to a dynamic view when the scope is a software system."); + } + } + + // dynamic view with container scope: + // - other containers + // - components + if (element instanceof Container) { + if (staticStructureElementToBeAdded.equals(element) || staticStructureElementToBeAdded.equals(element.getParent())) { + throw new ElementNotPermittedInViewException(staticStructureElementToBeAdded.getName() + " is already the scope of this view and cannot be added to it."); + } + + checkParentAndChildrenHaveNotAlreadyBeenAdded(staticStructureElementToBeAdded); + } + + // dynamic view with no scope + // - software systems + if (element == null) { + if (!(staticStructureElementToBeAdded instanceof SoftwareSystem)) { + throw new ElementNotPermittedInViewException("Only people and software systems can be added to this dynamic view."); + } + } + } + + @Override + protected boolean canBeRemoved(Element element) { + return true; + } + + /** + * Gets the set of RelationshipView objects for this view, ordered by the order property. + * + * @return an ordered set of RelationshipView objects + */ + @Override + public Set getRelationships() { + List list = new LinkedList<>(super.getRelationships()); + boolean ordersAreNumeric = true; + + for (RelationshipView relationshipView : list) { + ordersAreNumeric = ordersAreNumeric && isNumeric(relationshipView.getOrder()); + } + + if (ordersAreNumeric) { + list.sort(Comparator.comparingDouble(rv -> Double.parseDouble(rv.getOrder()))); + } else { + list.sort(Comparator.comparing(RelationshipView::getOrder)); + } + + return new LinkedHashSet<>(list); + } + + private boolean isNumeric(String str) { + try { + Double.parseDouble(str); + return true; + } catch(NumberFormatException e){ + return false; + } + } + + @Deprecated + public boolean getExternalBoundariesVisible() { + return externalBoundariesVisible; + } + + @Deprecated + void setExternalBoundariesVisible(boolean externalBoundariesVisible) { + this.externalBoundariesVisible = externalBoundariesVisible; + } + +} \ No newline at end of file diff --git a/structurizr-core/src/main/java/com/structurizr/view/ElementNotPermittedInViewException.java b/structurizr-core/src/main/java/com/structurizr/view/ElementNotPermittedInViewException.java new file mode 100644 index 000000000..aae65d5a7 --- /dev/null +++ b/structurizr-core/src/main/java/com/structurizr/view/ElementNotPermittedInViewException.java @@ -0,0 +1,9 @@ +package com.structurizr.view; + +public class ElementNotPermittedInViewException extends RuntimeException { + + ElementNotPermittedInViewException(String message) { + super(message); + } + +} \ 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 new file mode 100644 index 000000000..2787334b1 --- /dev/null +++ b/structurizr-core/src/main/java/com/structurizr/view/ElementStyle.java @@ -0,0 +1,453 @@ +package com.structurizr.view; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.structurizr.util.ImageUtils; +import com.structurizr.util.StringUtils; + +/** + * A definition of an element style. + */ +public final class ElementStyle extends AbstractStyle { + + @JsonInclude(value = JsonInclude.Include.NON_NULL) + private Integer width; + + @JsonInclude(value = JsonInclude.Include.NON_NULL) + private Integer height; + + @JsonInclude(value = JsonInclude.Include.NON_NULL) + private String background; + + @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; + + @JsonInclude(value = JsonInclude.Include.NON_NULL) + private Integer fontSize; + + @JsonInclude(value = JsonInclude.Include.NON_NULL) + private Shape shape; + + @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; + + @JsonInclude(value = JsonInclude.Include.NON_NULL) + private Integer opacity; + + @JsonInclude(value = JsonInclude.Include.NON_NULL) + private Boolean metadata; + + @JsonInclude(value = JsonInclude.Include.NON_NULL) + private Boolean description; + + ElementStyle() { + } + + public ElementStyle(String tag) { + super(tag); + } + + ElementStyle(String tag, ColorScheme colorScheme) { + super(tag, colorScheme); + } + + public ElementStyle(String tag, Integer width, Integer height, String background, String color, Integer fontSize) { + this(tag, width, height, background, color, fontSize, null); + } + + public ElementStyle(String tag, Integer width, Integer height, String background, String color, Integer fontSize, Shape shape) { + super(tag); + + this.width = width; + this.height = height; + setBackground(background); + setColor(color); + this.fontSize = fontSize; + this.shape = shape; + } + + /** + * Gets the width of the element, in pixels. + * + * @return the width as an Integer, or null if not specified + */ + public Integer getWidth() { + return width; + } + + public void setWidth(Integer width) { + this.width = width; + } + + public ElementStyle width(int width) { + setWidth(width); + return this; + } + + /** + * Gets the height of the element, in pixels. + * + * @return the height as an Integer, or null if not specified + */ + public Integer getHeight() { + return height; + } + + public void setHeight(Integer height) { + this.height = height; + } + + public ElementStyle height(int height) { + setHeight(height); + return this; + } + + /** + * Gets the background colour of the element, as a HTML RGB hex string (e.g. #123456). + * + * @return the background colour as a String, or null if not specified + */ + public String getBackground() { + return background; + } + + public void setBackground(String color) { + if (Color.isHexColorCode(color)) { + this.background = color.toLowerCase(); + } else { + 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."); + } + } + } + + public ElementStyle background(String background) { + setBackground(background); + return this; + } + + /** + * Gets the stroke colour of the element, as a HTML RGB hex string (e.g. #123456). + * + * @return the stroke colour as a String, or null if not specified + */ + public String getStroke() { + return stroke; + } + + public void setStroke(String color) { + if (Color.isHexColorCode(color)) { + this.stroke = color.toLowerCase(); + } else { + 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."); + } + } + } + + public ElementStyle stroke(String color) { + setStroke(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). + * + * @return the foreground colour as a String, or null if not specified + */ + public String getColor() { + return color; + } + + public void setColor(String color) { + if (Color.isHexColorCode(color)) { + this.color = color.toLowerCase(); + } else { + 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."); + } + } + } + + public ElementStyle color(String color) { + setColor(color); + return this; + } + + /** + * Gets the standard font size used to render text, in pixels. + * + * @return the font size, in pixels, as an Integer, or null if not specified + */ + public Integer getFontSize() { + return fontSize; + } + + public void setFontSize(Integer fontSize) { + this.fontSize = fontSize; + } + + public ElementStyle fontSize(int fontSize) { + setFontSize(fontSize); + return this; + } + + /** + * Gets the shape used to render the element. + * + * @return a Shape, or null if not specified + */ + public Shape getShape() { + return shape; + } + + public void setShape(Shape shape) { + this.shape = shape; + } + + public ElementStyle shape(Shape shape) { + setShape(shape); + return this; + } + + /** + * Gets the icon of the element (a URL, or a data URI representing a Base64 encoded PNG/JPG/GIF file). + * + * @return the icon, or null if not specified + */ + public String getIcon() { + return icon; + } + + public void setIcon(String icon) { + if (StringUtils.isNullOrEmpty(icon)) { + this.icon = null; + } else { + ImageUtils.validateImage(icon); + this.icon = icon.trim(); + } + } + + public ElementStyle icon(String icon) { + setIcon(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. + * + * @return a Border, or null if not specified + */ + public Border getBorder() { + return border; + } + + public void setBorder(Border border) { + this.border = border; + } + + public ElementStyle border(Border border) { + setBorder(border); + return this; + } + + /** + * Gets the opacity used when rendering the element. + * + * @return the opacity, as an integer between 0 and 100. + */ + public Integer getOpacity() { + return opacity; + } + + public void setOpacity(Integer opacity) { + if (opacity != null) { + if (opacity < 0) { + this.opacity = 0; + } else if (opacity > 100) { + this.opacity = 100; + } else { + this.opacity = opacity; + } + } + } + + public ElementStyle opacity(int opacity) { + setOpacity(opacity); + return this; + } + + /** + * Determines whether the element metadata should be shown or not. + * + * @return true (shown), false (hidden) or null (not set) + */ + public Boolean getMetadata() { + return metadata; + } + + /** + * Sets whether the element metadata should be shown or not. + * + * @param metadata true (shown), false (hidden) or null (not set) + */ + public void setMetadata(Boolean metadata) { + this.metadata = metadata; + } + + public ElementStyle metadata(boolean metadata) { + setMetadata(metadata); + return this; + } + + /** + * Determines whether the element description should be shown or not. + * + * @return true (shown), false (hidden) or null (not set) + */ + public Boolean getDescription() { + return description; + } + + /** + * Sets whether the element description should be shown or not. + * + * @param description true (shown), false (hidden) or null (not set) + */ + public void setDescription(Boolean description) { + this.description = description; + } + + public ElementStyle description(boolean description) { + setDescription(description); + return this; + } + + void copyFrom(ElementStyle elementStyle) { + if (elementStyle.getWidth() != null) { + this.setWidth(elementStyle.getWidth()); + } + + if (elementStyle.getHeight() != null) { + this.setHeight(elementStyle.getHeight()); + } + + if (!StringUtils.isNullOrEmpty(elementStyle.getBackground())) { + this.setBackground(elementStyle.getBackground()); + } + + if (!StringUtils.isNullOrEmpty(elementStyle.getStroke())) { + this.setStroke(elementStyle.getStroke()); + } + + if (elementStyle.getStrokeWidth() != null) { + this.setStrokeWidth(elementStyle.getStrokeWidth()); + } + + if (!StringUtils.isNullOrEmpty(elementStyle.getColor())) { + this.setColor(elementStyle.getColor()); + } + + if (elementStyle.getFontSize() != null) { + this.setFontSize(elementStyle.getFontSize()); + } + + if (elementStyle.getShape() != null) { + this.setShape(elementStyle.getShape()); + } + + if (!StringUtils.isNullOrEmpty(elementStyle.getIcon())) { + this.setIcon(elementStyle.getIcon()); + } + + if (elementStyle.getIconPosition() != null) { + this.setIconPosition(elementStyle.getIconPosition()); + } + + if (elementStyle.getBorder() != null) { + this.setBorder(elementStyle.getBorder()); + } + + if (elementStyle.getOpacity() != null) { + this.setOpacity(elementStyle.getOpacity()); + } + + if (elementStyle.getMetadata() != null) { + this.setMetadata(elementStyle.getMetadata()); + } + + 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/ElementView.java b/structurizr-core/src/main/java/com/structurizr/view/ElementView.java similarity index 80% rename from structurizr-core/src/com/structurizr/view/ElementView.java rename to structurizr-core/src/main/java/com/structurizr/view/ElementView.java index 29c9b08ec..09518ebc6 100644 --- a/structurizr-core/src/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; @@ -25,7 +25,7 @@ public Element getElement() { return element; } - public void setElement(Element element) { + void setElement(Element element) { this.element = element; } @@ -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/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/main/java/com/structurizr/view/FilteredView.java b/structurizr-core/src/main/java/com/structurizr/view/FilteredView.java new file mode 100644 index 000000000..256bc3763 --- /dev/null +++ b/structurizr-core/src/main/java/com/structurizr/view/FilteredView.java @@ -0,0 +1,69 @@ +package com.structurizr.view; + +import com.fasterxml.jackson.annotation.JsonIgnore; + +import java.util.Arrays; +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. + */ +public final class FilteredView extends View { + + private ModelView view; + private String baseViewKey; + + private FilterMode mode = FilterMode.Exclude; + private final Set tags = new TreeSet<>(); + + FilteredView() { + } + + FilteredView(ModelView view, String key, String description, FilterMode mode, String... tags) { + this.view = view; + setKey(key); + setDescription(description); + this.mode = mode; + this.tags.addAll(Arrays.asList(tags)); + } + + @JsonIgnore + public View getView() { + return view; + } + + void setView(ModelView view) { + this.view = view; + } + + public String getBaseViewKey() { + if (view != null) { + return view.getKey(); + } else { + return this.baseViewKey; + } + } + + void setBaseViewKey(String baseViewKey) { + this.baseViewKey = baseViewKey; + } + + public FilterMode getMode() { + return mode; + } + + void setMode(FilterMode mode) { + this.mode = mode; + } + + public Set getTags() { + return new TreeSet<>(tags); + } + + @Override + public String getName() { + return "Filtered: " + view.getName(); + } + +} \ No newline at end of file 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/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-core/src/main/java/com/structurizr/view/ImageView.java b/structurizr-core/src/main/java/com/structurizr/view/ImageView.java new file mode 100644 index 000000000..bd66943d2 --- /dev/null +++ b/structurizr-core/src/main/java/com/structurizr/view/ImageView.java @@ -0,0 +1,152 @@ +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 extends View { + + private Element element; + private String elementId; + private String content; + private String contentLight; + private String contentDark; + private String contentType; + + 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; + } + + /** + * 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)) { + if (colorScheme == ColorScheme.Dark) { + this.contentDark = null; + } else if (colorScheme == ColorScheme.Light) { + this.contentLight = null; + } else { + this.content = null; + } + } else { + ImageUtils.validateImage(content); + content = content.trim(); + + if (colorScheme == ColorScheme.Dark) { + this.contentDark = content; + } else if (colorScheme == ColorScheme.Light) { + this.contentLight = content; + } else { + this.content = content; + } + } + } + + /** + * Gets 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; + } + + @Override + 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/main/java/com/structurizr/view/LayoutMergeStrategy.java b/structurizr-core/src/main/java/com/structurizr/view/LayoutMergeStrategy.java new file mode 100644 index 000000000..ec17d9634 --- /dev/null +++ b/structurizr-core/src/main/java/com/structurizr/view/LayoutMergeStrategy.java @@ -0,0 +1,19 @@ +package com.structurizr.view; + +import javax.annotation.Nonnull; + +/** + * A pluggable strategy that can be used to copy layout information from one version of a view to another. + */ +public interface LayoutMergeStrategy { + + /** + * Attempts to copy the visual layout information (e.g. x,y coordinates) of elements and relationships + * from the specified source view into the specified destination view. + * + * @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 ModelView sourceView, @Nonnull ModelView destinationView); + +} \ No newline at end of file diff --git a/structurizr-core/src/main/java/com/structurizr/view/LineStyle.java b/structurizr-core/src/main/java/com/structurizr/view/LineStyle.java new file mode 100644 index 000000000..56fc365a2 --- /dev/null +++ b/structurizr-core/src/main/java/com/structurizr/view/LineStyle.java @@ -0,0 +1,9 @@ +package com.structurizr.view; + +public enum LineStyle { + + Dashed, + Dotted, + Solid + +} \ No newline at end of file diff --git a/structurizr-core/src/main/java/com/structurizr/view/MetadataSymbols.java b/structurizr-core/src/main/java/com/structurizr/view/MetadataSymbols.java new file mode 100644 index 000000000..a8f6f247a --- /dev/null +++ b/structurizr-core/src/main/java/com/structurizr/view/MetadataSymbols.java @@ -0,0 +1,15 @@ +package com.structurizr.view; + +/** + * The type of symbols to use when rendering metadata. + */ +public enum MetadataSymbols { + + SquareBrackets, + RoundBrackets, + CurlyBrackets, + AngleBrackets, + DoubleAngleBrackets, + None + +} \ 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 new file mode 100644 index 000000000..90f0a5bdc --- /dev/null +++ b/structurizr-core/src/main/java/com/structurizr/view/ModelView.java @@ -0,0 +1,506 @@ +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.Optional; +import java.util.Set; +import java.util.TreeSet; +import java.util.stream.Collectors; + +/** + * The superclass for all views that show elements/relationships from the model, namely: + * + * - Custom views + * - 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 TreeSet<>(); + private Set relationshipViews = new TreeSet<>(); + + 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; + } + + /** + * 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. + * + * @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); + } + } + + /** + * 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. + * + * @return a Set of ElementView objects + */ + public Set getElements() { + return new TreeSet<>(elementViews); + } + + void setElements(Set elementViews) { + if (elementViews != null) { + this.elementViews = new TreeSet<>(elementViews); + } + } + + /** + * Gets the set of relationships in this view. + * + * @return a Set of RelationshipView objects + */ + public Set getRelationships() { + return new TreeSet<>(this.relationshipViews); + } + + void setRelationships(Set relationshipViews) { + if (relationshipViews != null) { + this.relationshipViews = new TreeSet<>(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 + */ + public 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/main/java/com/structurizr/view/PaperSize.java b/structurizr-core/src/main/java/com/structurizr/view/PaperSize.java new file mode 100644 index 000000000..e58ebc2e8 --- /dev/null +++ b/structurizr-core/src/main/java/com/structurizr/view/PaperSize.java @@ -0,0 +1,96 @@ +package com.structurizr.view; + +import java.util.ArrayList; +import java.util.List; + +/** + * These represent paper sizes in pixels at 300dpi. + */ +public enum PaperSize { + + A6_Portrait("A6", Orientation.Portrait, 1240, 1748), + A6_Landscape("A6", Orientation.Landscape, 1748, 1240), + + A5_Portrait("A5", Orientation.Portrait, 1748, 2480), + A5_Landscape("A5", Orientation.Landscape, 2480, 1748), + + A4_Portrait("A4", Orientation.Portrait, 2480, 3508), + A4_Landscape("A4", Orientation.Landscape, 3508, 2480), + + A3_Portrait("A3", Orientation.Portrait, 3508, 4961), + A3_Landscape("A3", Orientation.Landscape, 4961, 3508), + + A2_Portrait("A2", Orientation.Portrait, 4961, 7016), + A2_Landscape("A2", Orientation.Landscape, 7016, 4961), + + A1_Portrait("A1", Orientation.Portrait, 7016, 9933), + A1_Landscape("A1", Orientation.Landscape, 9933, 7016), + + A0_Portrait("A0", Orientation.Portrait, 9933, 14043), + A0_Landscape("A0", Orientation.Landscape, 14043, 9933), + + Letter_Portrait("Letter", Orientation.Portrait, 2550, 3300), + Letter_Landscape("Letter", Orientation.Landscape, 3300, 2550), + + Legal_Portrait("Legal", Orientation.Portrait, 2550, 4200), + Legal_Landscape("Legal", Orientation.Landscape, 4200, 2550), + + Slide_4_3("Slide 4:3", Orientation.Landscape, 3306, 2480), + Slide_16_9("Slide 16:9", Orientation.Landscape, 3508, 1973), + Slide_16_10("Slide 16:10", Orientation.Landscape, 3508, 2193); + + private String name; + private Orientation orientation; + private int width; + private int height; + + private PaperSize(String name, Orientation orientation, int width, int height) { + this.name = name; + this.orientation = orientation; + this.width = width; + this.height = height; + } + + public String getName() { + return name; + } + + public Orientation getOrientation() { + return orientation; + } + + public int getWidth() { + return width; + } + + public int getHeight() { + return height; + } + + public enum Orientation { + Portrait, + Landscape + } + + public static List getOrderedPaperSizes() { + List paperSizes = new ArrayList<>(); + + paperSizes.addAll(getOrderedPaperSizes(Orientation.Landscape)); + paperSizes.addAll(getOrderedPaperSizes(Orientation.Portrait)); + + return paperSizes; + } + + public static List getOrderedPaperSizes(Orientation orientation) { + List paperSizes = new ArrayList<>(); + + for (PaperSize paperSize : values()) { + if (paperSize.getOrientation() == orientation) { + paperSizes.add(paperSize); + } + } + + return paperSizes; + } + +} \ No newline at end of file diff --git a/structurizr-core/src/main/java/com/structurizr/view/ParallelSequenceCounter.java b/structurizr-core/src/main/java/com/structurizr/view/ParallelSequenceCounter.java new file mode 100644 index 000000000..a5c57b6b3 --- /dev/null +++ b/structurizr-core/src/main/java/com/structurizr/view/ParallelSequenceCounter.java @@ -0,0 +1,10 @@ +package com.structurizr.view; + +class ParallelSequenceCounter extends SequenceCounter { + + ParallelSequenceCounter(SequenceCounter parent) { + super(parent); + setSequence(parent.getSequence()); + } + +} \ No newline at end of file diff --git a/structurizr-core/src/main/java/com/structurizr/view/RelationshipStyle.java b/structurizr-core/src/main/java/com/structurizr/view/RelationshipStyle.java new file mode 100644 index 000000000..5f77360b4 --- /dev/null +++ b/structurizr-core/src/main/java/com/structurizr/view/RelationshipStyle.java @@ -0,0 +1,269 @@ +package com.structurizr.view; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.structurizr.util.StringUtils; + +public final class RelationshipStyle extends AbstractStyle { + + private static final int START_OF_LINE = 0; + private static final int END_OF_LINE = 100; + + /** the thickness of the line, in pixels */ + @JsonInclude(value = JsonInclude.Include.NON_NULL) + private Integer thickness; + + /** the colour of the line, as a HTML hex value (e.g. #123456) */ + @JsonInclude(value = JsonInclude.Include.NON_NULL) + private String color; + + /** the font size of the annotation, in pixels */ + @JsonInclude(value = JsonInclude.Include.NON_NULL) + private Integer fontSize; + + /** the width of the annotation, in pixels */ + @JsonInclude(value = JsonInclude.Include.NON_NULL) + private Integer width; + + /** whether the line should be dashed or not */ + @JsonInclude(value = JsonInclude.Include.NON_NULL) + private Boolean dashed; + + /** the line style used when rendering lines */ + @JsonInclude(value = JsonInclude.Include.NON_NULL) + private LineStyle style; + + /** the routing algorithm used when rendering lines */ + @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; + + /** the opacity of the line/text; 0 to 100 */ + @JsonInclude(value = JsonInclude.Include.NON_NULL) + private Integer opacity; + + RelationshipStyle() { + } + + public RelationshipStyle(String tag) { + super(tag); + } + + RelationshipStyle(String tag, ColorScheme colorScheme) { + super(tag, colorScheme); + } + + public Integer getThickness() { + return thickness; + } + + public void setThickness(Integer thickness) { + this.thickness = thickness; + } + + public RelationshipStyle thickness(int thickness) { + setThickness(thickness); + return this; + } + + public String getColor() { + return color; + } + + public void setColor(String color) { + if (Color.isHexColorCode(color)) { + this.color = color.toLowerCase(); + } else { + 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."); + } + } + } + + public RelationshipStyle color(String color) { + setColor(color); + return this; + } + + public Boolean getDashed() { + return dashed; + } + + public void setDashed(Boolean dashed) { + this.dashed = dashed; + } + + public RelationshipStyle dashed(boolean dashed) { + setDashed(dashed); + return this; + } + + public LineStyle getStyle() { + return style; + } + + public void setStyle(LineStyle style) { + this.style = style; + } + + public RelationshipStyle style(LineStyle style) { + setStyle(style); + return this; + } + + public Routing getRouting() { + return routing; + } + + public void setRouting(Routing routing) { + this.routing = routing; + } + + public RelationshipStyle routing(Routing routing) { + setRouting(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; + } + + public void setFontSize(Integer fontSize) { + this.fontSize = fontSize; + } + + public RelationshipStyle fontSize(int fontSize) { + setFontSize(fontSize); + return this; + } + + public Integer getWidth() { + return width; + } + + public void setWidth(Integer width) { + this.width = width; + } + + public RelationshipStyle width(int width) { + setWidth(width); + return this; + } + + public Integer getPosition() { + return position; + } + + public void setPosition(Integer position) { + if (position == null) { + this.position = null; + } else if (position < START_OF_LINE) { + this.position = START_OF_LINE; + } else if (position > END_OF_LINE) { + this.position = END_OF_LINE; + } else { + this.position = position; + } + } + + public RelationshipStyle position(int position) { + setPosition(position); + return this; + } + + /** + * Gets the opacity used when rendering the relationship. + * + * @return the opacity, as an integer between 0 and 100. + */ + public Integer getOpacity() { + return opacity; + } + + public void setOpacity(Integer opacity) { + if (opacity != null) { + if (opacity < 0) { + this.opacity = 0; + } else if (opacity > 100) { + this.opacity = 100; + } else { + this.opacity = opacity; + } + } + } + + public RelationshipStyle opacity(int opacity) { + setOpacity(opacity); + return this; + } + + void copyFrom(RelationshipStyle relationshipStyle) { + if (relationshipStyle.getThickness() != null) { + this.setThickness(relationshipStyle.getThickness()); + } + + if (!StringUtils.isNullOrEmpty(relationshipStyle.getColor())) { + this.setColor(relationshipStyle.getColor()); + } + + if (relationshipStyle.getDashed() != null) { + this.setDashed(relationshipStyle.getDashed()); + } + + if (relationshipStyle.getStyle() != null) { + this.setStyle(relationshipStyle.getStyle()); + } + + if (relationshipStyle.getRouting() != null) { + this.setRouting(relationshipStyle.getRouting()); + } + + if (relationshipStyle.getJump() != null) { + this.setJump(relationshipStyle.getJump()); + } + + if (relationshipStyle.getFontSize() != null) { + this.setFontSize(relationshipStyle.getFontSize()); + } + + if (relationshipStyle.getWidth() != null) { + this.setWidth(relationshipStyle.getWidth()); + } + + if (relationshipStyle.getPosition() != null) { + this.setPosition(relationshipStyle.getPosition()); + } + + 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/src/main/java/com/structurizr/view/RelationshipView.java b/structurizr-core/src/main/java/com/structurizr/view/RelationshipView.java new file mode 100644 index 000000000..b9305de3e --- /dev/null +++ b/structurizr-core/src/main/java/com/structurizr/view/RelationshipView.java @@ -0,0 +1,319 @@ +package com.structurizr.view; + +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; + +import java.util.*; + +/** + * This class represents an instance of a Relationship on a View. + */ +public final class RelationshipView implements PropertyHolder, Comparable { + + private static final int START_OF_LINE = 0; + private static final int END_OF_LINE = 100; + + private Relationship relationship; + private String id; + private String description; + private Map properties = new HashMap<>(); + private String url; + private String order; + private Boolean response; + private List vertices = new ArrayList<>(); + + @JsonInclude(value = JsonInclude.Include.NON_NULL) + private Routing routing; + + @JsonInclude(value = JsonInclude.Include.NON_NULL) + private Boolean jump; + + @JsonInclude(value = JsonInclude.Include.NON_NULL) + private Integer position; + + RelationshipView() { + } + + RelationshipView(Relationship relationship) { + this.relationship = relationship; + } + + /** + * Gets the ID of the relationship this RelationshipView represents. + * + * @return the ID, as a String + */ + public String getId() { + if (relationship != null) { + return relationship.getId(); + } else { + return this.id; + } + } + + void setId(String id) { + this.id = id; + } + + /** + * Gets the relationship that this RelationshipView represents. + * + * @return a Relationship instance + */ + @JsonIgnore + public Relationship getRelationship() { + return relationship; + } + + void setRelationship(Relationship relationship) { + this.relationship = relationship; + } + + /** + * Gets the description of this relationship (used in dynamic views only). + * + * @return the description, as a String + * or an empty string if a description has not been set + */ + public String getDescription() { + return description != null ? description : ""; + } + + /** + * Sets the description of this relationship (used in dynamic views only). + * + * @param description the description, as a String + */ + 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. + * + * @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.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; + } 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). + * + * @return the order, as a String + */ + public String getOrder() { + return order != null ? order : ""; + } + + /** + * Sets the order of this relationship (used in dynamic views only; e.g. 1.0, 1.1, 2.0, etc). + * + * @param order the order, as a String + */ + public void setOrder(String order) { + this.order = order; + } + + /** + * Gets whether this relationship view represents a response (used in dynamic views only). + * + * @return true if a response, false or null otherwise + */ + public Boolean isResponse() { + return response; + } + + void setResponse(Boolean response) { + this.response = response; + } + + /** + * Gets the set of vertices used to render the relationship. + * + * @return a collection of Vertex objects + */ + public Collection getVertices() { + return new ArrayList<>(vertices); + } + + /** + * Sets the collection of vertices used when rendering this relationship. + * + * @param vertices a Collection of Vertex instances + */ + public void setVertices(Collection vertices) { + if (vertices != null) { + this.vertices = new ArrayList<>(vertices); + } + } + + /** + * Gets the routing algorithm used when rendering this relationship. + * + * @return a Routing instance, or null if not explicitly set + */ + public Routing getRouting() { + return routing; + } + + /** + * Sets the routing algorithm used when rendering this relationship. + * + * @param routing a Routing instance, or null to not explicitly set this property + */ + public void setRouting(Routing routing) { + this.routing = routing; + } + + /** + * Gets whether this relationship should "jump" when crossing others. + * + * @return true if jumping is enabled, false otherwise + */ + public Boolean getJump() { + return jump; + } + + /** + * Sets whether this relationship should "jump" when crossing others. + * + * @param jump true if enabled, false otherwise + */ + public void setJump(Boolean jump) { + this.jump = jump; + } + + /** + * Gets the position of the annotation along the line. + * + * @return an integer between 0 (start of the line) to 100 (end of the line) inclusive + */ + public Integer getPosition() { + return position; + } + + /** + * Sets the position of the annotation along the line. + * + * @param position the position, as an integer between 0 (start of the line) to 100 (end of the line) inclusive + */ + public void setPosition(Integer position) { + if (position == null) { + this.position = null; + } else if (position < START_OF_LINE) { + this.position = START_OF_LINE; + } else if (position > END_OF_LINE) { + this.position = END_OF_LINE; + } else { + this.position = position; + } + } + + void copyLayoutInformationFrom(RelationshipView source) { + if (source != null) { + setVertices(source.getVertices()); + setPosition(source.getPosition()); + setRouting(source.getRouting()); + setJump(source.getJump()); + } + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + RelationshipView that = (RelationshipView) o; + + if (description != null ? !description.equals(that.description) : that.description != null) return false; + if (!getId().equals(that.getId())) return false; + if (order != null ? !order.equals(that.order) : that.order != null) return false; + + return true; + } + + @Override + public int hashCode() { + int result = getId().hashCode(); + result = 31 * result + (description != null ? description.hashCode() : 0); + result = 31 * result + (order != null ? order.hashCode() : 0); + return result; + } + + @Override + public String toString() { + if (relationship != null) { + return (order != null ? order + ": " : "") + (description != null ? description + " " : "") + relationship.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/com/structurizr/view/Routing.java b/structurizr-core/src/main/java/com/structurizr/view/Routing.java similarity index 86% rename from structurizr-core/src/com/structurizr/view/Routing.java rename to structurizr-core/src/main/java/com/structurizr/view/Routing.java index 118618a4a..0e83e8fa4 100644 --- a/structurizr-core/src/com/structurizr/view/Routing.java +++ b/structurizr-core/src/main/java/com/structurizr/view/Routing.java @@ -3,6 +3,7 @@ public enum Routing { Direct, + Curved, Orthogonal -} +} \ No newline at end of file diff --git a/structurizr-core/src/main/java/com/structurizr/view/SequenceCounter.java b/structurizr-core/src/main/java/com/structurizr/view/SequenceCounter.java new file mode 100644 index 000000000..8f8d52314 --- /dev/null +++ b/structurizr-core/src/main/java/com/structurizr/view/SequenceCounter.java @@ -0,0 +1,42 @@ +package com.structurizr.view; + +class SequenceCounter implements Cloneable { + + private SequenceCounter parent; + private int sequence = 0; + private boolean incremented = false; + + SequenceCounter() { + } + + SequenceCounter(SequenceCounter parent) { + this.parent = parent; + } + + void increment() { + this.sequence++; + incremented = true; + } + + boolean incremented() { + return incremented; + } + + int getSequence() { + return this.sequence; + } + + void setSequence(int sequence) { + this.sequence = sequence; + } + + SequenceCounter getParent() { + return this.parent; + } + + @Override + public String toString() { + return "" + getSequence(); + } + +} \ No newline at end of file diff --git a/structurizr-core/src/main/java/com/structurizr/view/SequenceNumber.java b/structurizr-core/src/main/java/com/structurizr/view/SequenceNumber.java new file mode 100644 index 000000000..259bb98a0 --- /dev/null +++ b/structurizr-core/src/main/java/com/structurizr/view/SequenceNumber.java @@ -0,0 +1,38 @@ +package com.structurizr.view; + +class SequenceNumber { + + private SequenceCounter counter = new SequenceCounter(); + + SequenceNumber() { + } + + String getNext() { + counter.increment(); + return counter.toString(); + } + + void startParallelSequence() { + this.counter = new ParallelSequenceCounter(this.counter); + } + + void endParallelSequence(boolean endAllParallelSequencesAndContinueNumbering) { + if (endAllParallelSequencesAndContinueNumbering) { + 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(); + } + } + +} \ No newline at end of file diff --git a/structurizr-core/src/main/java/com/structurizr/view/Shape.java b/structurizr-core/src/main/java/com/structurizr/view/Shape.java new file mode 100644 index 000000000..40b2c1b34 --- /dev/null +++ b/structurizr-core/src/main/java/com/structurizr/view/Shape.java @@ -0,0 +1,25 @@ +package com.structurizr.view; + +public enum Shape { + + Box, + RoundedBox, + Circle, + Ellipse, + Hexagon, + Diamond, + Cylinder, + Bucket, + Pipe, + Person, + Robot, + Folder, + WebBrowser, + Window, + Terminal, + Shell, + MobileDevicePortrait, + MobileDeviceLandscape, + Component + +} \ No newline at end of file diff --git a/structurizr-core/src/main/java/com/structurizr/view/StaticView.java b/structurizr-core/src/main/java/com/structurizr/view/StaticView.java new file mode 100644 index 000000000..d5934af05 --- /dev/null +++ b/structurizr-core/src/main/java/com/structurizr/view/StaticView.java @@ -0,0 +1,279 @@ +package com.structurizr.view; + +import com.structurizr.model.*; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * The superclass for all static views (system landscape, system context, container and component views). + */ +public abstract class StaticView extends ModelView implements AnimatedView { + + @Nonnull + private List animations = new ArrayList<>(); + + StaticView() { + } + + StaticView(SoftwareSystem softwareSystem, String key, String description) { + super(softwareSystem, key, description); + } + + /** + * 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. + */ + public void addAllSoftwareSystems() { + getModel().getSoftwareSystems().forEach(ss -> { + try { + add(ss); + } catch (ElementNotPermittedInViewException e) { + // ignore + } + }); + } + + /** + * Adds the given software system to this view, including relationships to/from that software system. + * + * @param softwareSystem the SoftwareSystem to add + */ + public void add(@Nonnull SoftwareSystem softwareSystem) { + add(softwareSystem, true); + } + + /** + * Adds the given software system to this view. + * + * @param softwareSystem the SoftwareSystem to add + * @param addRelationships whether to add relationships to/from the software system + */ + public void add(@Nonnull SoftwareSystem softwareSystem, boolean addRelationships) { + addElement(softwareSystem, addRelationships); + } + + /** + * Removes the given software system from this view. + * + * @param softwareSystem the SoftwareSystem to remove + */ + public void remove(@Nonnull SoftwareSystem softwareSystem) { + removeElement(softwareSystem); + } + + /** + * Adds all people in the model to this view. + */ + public void addAllPeople() { + getModel().getPeople().forEach(this::add); + } + + /** + * Adds the given person to this view, including relationships to/from that person. + * + * @param person the Person to add + */ + public void add(@Nonnull Person person) { + add(person, true); + } + + /** + * Adds the given person to this view. + * + * @param person the Person to add + * @param addRelationships whether to add relationships to/from the person + */ + public void add(@Nonnull Person person, boolean addRelationships) { + addElement(person, addRelationships); + } + + /** + * Removes the given person from this view. + * + * @param person the Person to add + */ + public void remove(@Nonnull Person person) { + removeElement(person); + } + + /** + * Adds a specific relationship to this view. + * + * @param relationship the Relationship to be added + * @return a RelationshipView object representing the relationship added + */ + public RelationshipView add(@Nonnull Relationship relationship) { + return addRelationship(relationship); + } + + /** + * Adds all of the permitted elements to this view. + */ + public abstract void addAllElements(); + + /** + * Adds all of the permitted elements, which are directly connected to the specified element, to this view. + * + * @param element an Element + */ + public abstract void addNearestNeighbours(@Nonnull Element element); + + /** + * Removes all elements that cannot be reached by traversing the graph of relationships + * starting with the specified element. + * + * @param element the starting element + */ + public void removeElementsThatAreUnreachableFrom(Element element) { + if (element != null) { + Set elementsToShow = new HashSet<>(); + Set elementsVisited = new HashSet<>(); + findElementsToShow(element, element, elementsToShow, elementsVisited); + + for (ElementView elementView : getElements()) { + if (!elementsToShow.contains(elementView.getElement()) && canBeRemoved(elementView.getElement())) { + removeElement(elementView.getElement()); + } + } + } + } + + private void findElementsToShow(Element startingElement, Element element, Set elementsToShow, Set elementsVisited) { + if (!elementsVisited.contains(element) && getElements().contains(new ElementView(element))) { + elementsVisited.add(element); + elementsToShow.add(element); + + // check that we've not gone back to the starting point of the graph + if (!element.hasEfferentRelationshipWith(startingElement)) { + element.getRelationships().forEach(r -> findElementsToShow(startingElement, r.getDestination(), elementsToShow, elementsVisited)); + } + } + } + + /** + * Removes all {@link Element}s that have the given tag from this view. + * + * @param tag a tag + */ + public final void removeElementsWithTag(@Nonnull String tag) { + getElements().stream() + .map(ElementView::getElement) + .filter(e -> e.hasTag(tag)) + .forEach(this::removeElement); + } + + /** + * Removes all {@link Relationship}s that have the given tag from this view. + * + * @param tag a tag + */ + public final void removeRelationshipsWithTag(@Nonnull String tag) { + getRelationships().stream() + .map(RelationshipView::getRelationship) + .filter(r -> r.hasTag(tag)) + .forEach(this::remove); + } + + /** + * Adds an animation step, with the specified elements. + * + * @param elements the elements that should be shown in the animation step + */ + public void addAnimation(Element... elements) { + if (elements == null || elements.length == 0) { + throw new IllegalArgumentException("One or more elements must be specified."); + } + + Set elementIdsInPreviousAnimationSteps = new HashSet<>(); + + for (Animation animationStep : animations) { + elementIdsInPreviousAnimationSteps.addAll(animationStep.getElements()); + } + + Set elementsInThisAnimationStep = new HashSet<>(); + Set relationshipsInThisAnimationStep = new HashSet<>(); + + for (Element element : elements) { + if (isElementInView(element)) { + if (!elementIdsInPreviousAnimationSteps.contains(element.getId())) { + elementIdsInPreviousAnimationSteps.add(element.getId()); + elementsInThisAnimationStep.add(element); + } + } + } + + 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())) + ) { + relationshipsInThisAnimationStep.add(relationshipView.getRelationship()); + } + } + + animations.add(new Animation(animations.size() + 1, elementsInThisAnimationStep, relationshipsInThisAnimationStep)); + } + + @Nonnull + @Override + public List getAnimations() { + return new ArrayList<>(animations); + } + + void setAnimations(@Nullable List animations) { + if (animations != null) { + this.animations = new ArrayList<>(animations); + } else { + this.animations = new ArrayList<>(); + } + } + + /** + * 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); + } + +} diff --git a/structurizr-core/src/main/java/com/structurizr/view/Styles.java b/structurizr-core/src/main/java/com/structurizr/view/Styles.java new file mode 100644 index 000000000..2d2db79a7 --- /dev/null +++ b/structurizr-core/src/main/java/com/structurizr/view/Styles.java @@ -0,0 +1,429 @@ +package com.structurizr.view; + +import com.structurizr.model.*; +import com.structurizr.util.StringUtils; +import com.structurizr.util.TagUtils; + +import java.util.*; + +public final class Styles { + + public static final String DEFAULT_BACKGROUND_LIGHT = "#ffffff"; + public static final String DEFAULT_COLOR_LIGHT = "#444444"; + + public static final String DEFAULT_BACKGROUND_DARK = "#111111"; + public static final String DEFAULT_COLOR_DARK = "#cccccc"; + + private Collection elements = new TreeSet<>(); + private Collection relationships = new TreeSet<>(); + + private final List themes = new ArrayList<>(); + + public Collection getElements() { + return elements; + } + + public void add(ElementStyle elementStyle) { + if (elementStyle != null) { + if (StringUtils.isNullOrEmpty(elementStyle.getTag())) { + throw new IllegalArgumentException("A tag must be specified."); + } + + 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); + } + } + + public ElementStyle addElementStyle(String tag) { + return addElementStyle(tag, null); + } + + public ElementStyle addElementStyle(String tag, ColorScheme colorScheme) { + ElementStyle elementStyle = new ElementStyle(tag, colorScheme); + add(elementStyle); + + return elementStyle; + } + + /** + * Removes all element styles. + */ + public void clearElementStyles() { + this.elements = new LinkedList<>(); + } + + /** + * Removes all relationship styles. + */ + public void clearRelationshipStyles() { + this.relationships = new LinkedList<>(); + } + + public Collection getRelationships() { + return relationships; + } + + public void add(RelationshipStyle relationshipStyle) { + if (relationshipStyle != null) { + if (StringUtils.isNullOrEmpty(relationshipStyle.getTag())) { + throw new IllegalArgumentException("A tag must be specified."); + } + + 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); + } + } + + public RelationshipStyle addRelationshipStyle(String tag) { + return addRelationshipStyle(tag, null); + } + + public RelationshipStyle addRelationshipStyle(String tag, ColorScheme colorScheme) { + RelationshipStyle relationshipStyle = new RelationshipStyle(tag, colorScheme); + add(relationshipStyle); + + return relationshipStyle; + } + + /** + * 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 + */ + 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) && es.getColorScheme() == colorScheme).findFirst().orElse(null); + } + + /** + * 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; + } + + boolean elementStyleExists = false; + tag = tag.trim(); + ElementStyle style = new ElementStyle(tag); + + Collection elementStyles = new ArrayList<>(); + for (Theme theme : themes) { + elementStyles.addAll(theme.getElements()); + } + elementStyles.addAll(elements); + + for (ElementStyle elementStyle : elementStyles) { + if (elementStyle != null && elementStyle.getTag().equals(tag)) { + if (elementStyle.getColorScheme() == null || elementStyle.getColorScheme() == colorScheme) { + elementStyleExists = true; + style.copyFrom(elementStyle); + } + } + } + + if (elementStyleExists) { + return style; + } else { + return null; + } + } + + /** + * 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 + */ + 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) && rs.getColorScheme() == colorScheme).findFirst().orElse(null); + } + + /** + * 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; + } + + boolean relationshipStyleExists = false; + tag = tag.trim(); + RelationshipStyle style = new RelationshipStyle(tag); + + Collection relationshipStyles= new ArrayList<>(); + for (Theme theme : themes) { + relationshipStyles.addAll(theme.getRelationships()); + } + relationshipStyles.addAll(relationships); + + for (RelationshipStyle relationshipStyle : relationshipStyles) { + if (relationshipStyle != null && relationshipStyle.getTag().equals(tag)) { + if (relationshipStyle.getColorScheme() == null || relationshipStyle.getColorScheme() == colorScheme) { + style.copyFrom(relationshipStyle); + relationshipStyleExists = true; + } + } + } + + if (relationshipStyleExists) { + return style; + } else { + return null; + } + } + + /** + * 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, ColorScheme colorScheme) { + 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<>(); + tagsUsedToComposeStyle.add(Tags.ELEMENT); + String tags = element.getTags(); + + if (element instanceof SoftwareSystemInstance) { + SoftwareSystem ss = ((SoftwareSystemInstance)element).getSoftwareSystem(); + tags = ss.getTags() + "," + tags; + } else if (element instanceof ContainerInstance) { + Container c = ((ContainerInstance)element).getContainer(); + tags = c.getTags() + "," + tags; + } + + for (String tag : tags.split(",")) { + if (!StringUtils.isNullOrEmpty(tag)) { + ElementStyle elementStyle = findElementStyle(tag, colorScheme); + if (elementStyle != null) { + style.copyFrom(elementStyle); + tagsUsedToComposeStyle.add(elementStyle.getTag()); + } + } + } + + style.setTag(TagUtils.toString(tagsUsedToComposeStyle)); + } + + if (style.getBackground() == null) { + if (colorScheme == ColorScheme.Dark) { + style.background(DEFAULT_BACKGROUND_DARK); + if (style.getStroke() == null) { + style.stroke(DEFAULT_COLOR_DARK); + } + } else { + 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.getColor() == null) { + if (colorScheme == ColorScheme.Dark) { + style.color(DEFAULT_COLOR_DARK); + } else { + style.color(DEFAULT_COLOR_LIGHT); + } + } + + + return style; + } + + /** + * 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, 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<>(); + tagsUsedToComposeStyle.add(Tags.RELATIONSHIP); + String tags = relationship.getTags(); + String linkedRelationshipId = relationship.getLinkedRelationshipId(); + + while (!StringUtils.isNullOrEmpty(linkedRelationshipId)) { + // the "linked relationship ID" is used for: + // - container instance -> container instance relationships + // - implied relationships + Relationship linkedRelationship = relationship.getModel().getRelationship(linkedRelationshipId); + tags = linkedRelationship.getTags() + "," + tags; + linkedRelationshipId = linkedRelationship.getLinkedRelationshipId(); + } + + for (String tag : tags.split(",")) { + if (!StringUtils.isNullOrEmpty(tag)) { + RelationshipStyle relationshipStyle = findRelationshipStyle(tag, colorScheme); + if (relationshipStyle != null) { + style.copyFrom(relationshipStyle); + tagsUsedToComposeStyle.add(relationshipStyle.getTag()); + } + } + } + + 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; + } + + /** + * 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); + } + } + + /** + * Inlines the element and relationship styles from the specified theme, adding the styles into the workspace + * and overriding any properties already set. + * + * @param theme a Theme object + */ + public void inlineTheme(Theme theme) { + for (ElementStyle elementStyle : theme.getElements()) { + ElementStyle es = getElementStyle(elementStyle.getTag()); + if (es == null) { + es = addElementStyle(elementStyle.getTag()); + } + + es.copyFrom(elementStyle); + } + + for (RelationshipStyle relationshipStyle : theme.getRelationships()) { + RelationshipStyle rs = getRelationshipStyle(relationshipStyle.getTag()); + if (rs == null) { + rs = addRelationshipStyle(relationshipStyle.getTag()); + } + + rs.copyFrom(relationshipStyle); + } + } + +} \ No newline at end of file diff --git a/structurizr-core/src/main/java/com/structurizr/view/SystemContextView.java b/structurizr-core/src/main/java/com/structurizr/view/SystemContextView.java new file mode 100644 index 000000000..3c88717da --- /dev/null +++ b/structurizr-core/src/main/java/com/structurizr/view/SystemContextView.java @@ -0,0 +1,112 @@ +package com.structurizr.view; + +import com.structurizr.model.CustomElement; +import com.structurizr.model.Element; +import com.structurizr.model.Person; +import com.structurizr.model.SoftwareSystem; + +import javax.annotation.Nonnull; + +/** + * Represents the System Context view from the C4 model, showing how a software system fits into its environment, + * in terms of the users (people) and other software system dependencies. + */ +public final class SystemContextView extends StaticView { + + private boolean enterpriseBoundaryVisible = true; + + SystemContextView() { + } + + SystemContextView(SoftwareSystem softwareSystem, String key, String description) { + super(softwareSystem, key, description); + + addElement(softwareSystem, true); + } + + /** + * Gets the (computed) name of this view. + * + * @return the name, as a String + */ + @Override + public String getName() { + return "System Context View: " + getSoftwareSystem().getName(); + } + + /** + * Adds the default set of elements to this view. + */ + @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()); + } + } + + /** + * Adds all software systems and all people. + */ + @Override + public void addAllElements() { + addAllSoftwareSystems(); + addAllPeople(); + } + + /** + * Adds all software systems and people that are directly connected to the specified element. + * + * @param element an Element + */ + @Override + public void addNearestNeighbours(@Nonnull Element element) { + if (element == null) { + throw new IllegalArgumentException("An element must be specified."); + } + + if (element instanceof Person || element instanceof SoftwareSystem) { + super.addNearestNeighbours(element, Person.class); + super.addNearestNeighbours(element, SoftwareSystem.class); + } else { + throw new IllegalArgumentException("A person or software system must be specified."); + } + } + + @Deprecated + public boolean isEnterpriseBoundaryVisible() { + return enterpriseBoundaryVisible; + } + + @Deprecated + void setEnterpriseBoundaryVisible(boolean enterpriseBoundaryVisible) { + this.enterpriseBoundaryVisible = enterpriseBoundaryVisible; + } + + @Override + protected void checkElementCanBeAdded(Element element) { + if (element instanceof CustomElement || element instanceof Person || element instanceof SoftwareSystem) { + // all good + } else { + throw new ElementNotPermittedInViewException("Only people and software systems can be added to a system context view."); + } + } + + @Override + protected boolean canBeRemoved(Element element) { + return !getSoftwareSystem().equals(element); + } + +} \ No newline at end of file diff --git a/structurizr-core/src/main/java/com/structurizr/view/SystemLandscapeView.java b/structurizr-core/src/main/java/com/structurizr/view/SystemLandscapeView.java new file mode 100644 index 000000000..2284e575c --- /dev/null +++ b/structurizr-core/src/main/java/com/structurizr/view/SystemLandscapeView.java @@ -0,0 +1,124 @@ +package com.structurizr.view; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.structurizr.model.*; + +import javax.annotation.Nonnull; + +/** + * Represents a System Landscape view that sits "above" the C4 model, + * showing the software systems and people in a given environment. + */ +public final class SystemLandscapeView extends StaticView { + + private Model model; + + private boolean enterpriseBoundaryVisible = true; + + SystemLandscapeView() { + } + + SystemLandscapeView(Model model, String key, String description) { + super(null, key, description); + + this.model = model; + } + + /** + * Gets the (computed) name of this view. + * + * @return the name, as a String + */ + @Override + public String getName() { + return "System Landscape View"; + } + + /** + * Gets the model that this view belongs to. + * + * @return a Model object + */ + @JsonIgnore + @Override + public Model getModel() { + return this.model; + } + + void setModel(Model model) { + this.model = model; + } + + /** + * Adds the default set of elements to this view. + */ + @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(); + + getElements().stream().map(ElementView::getElement).forEach(e -> addNearestNeighbours(e, CustomElement.class)); + } + + /** + * Adds all software systems and all people to this view. + */ + @Override + public void addAllElements() { + addAllSoftwareSystems(); + addAllPeople(); + } + + /** + * Adds all software systems and people that are directly connected to the specified element. + * + * @param element an Element + */ + @Override + public void addNearestNeighbours(@Nonnull Element element) { + if (element == null) { + throw new IllegalArgumentException("An element must be specified."); + } + + if (element instanceof Person || element instanceof SoftwareSystem) { + super.addNearestNeighbours(element, Person.class); + super.addNearestNeighbours(element, SoftwareSystem.class); + } else { + throw new IllegalArgumentException("A person or software system must be specified."); + } + } + + @Deprecated + public boolean isEnterpriseBoundaryVisible() { + return enterpriseBoundaryVisible; + } + + @Deprecated + void setEnterpriseBoundaryVisible(boolean enterpriseBoundaryVisible) { + this.enterpriseBoundaryVisible = enterpriseBoundaryVisible; + } + + @Override + protected void checkElementCanBeAdded(Element element) { + if (element instanceof CustomElement || element instanceof Person || element instanceof SoftwareSystem) { + // all good + } else { + throw new ElementNotPermittedInViewException("Only people and software systems can be added to a system landscape view."); + } + } + + @Override + protected boolean canBeRemoved(Element element) { + return true; + } + +} \ No newline at end of file diff --git a/structurizr-core/src/main/java/com/structurizr/view/Terminology.java b/structurizr-core/src/main/java/com/structurizr/view/Terminology.java new file mode 100644 index 000000000..575a16a5e --- /dev/null +++ b/structurizr-core/src/main/java/com/structurizr/view/Terminology.java @@ -0,0 +1,145 @@ +package com.structurizr.view; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.structurizr.model.*; +import com.structurizr.util.StringUtils; + +/** + * Provides a way for the terminology on diagrams, etc to be modified (e.g. language translations). + */ +public final class Terminology { + + @JsonInclude(value = JsonInclude.Include.NON_EMPTY) + private String enterprise = ""; + + @JsonInclude(value = JsonInclude.Include.NON_EMPTY) + private String person = ""; + + @JsonInclude(value = JsonInclude.Include.NON_EMPTY) + private String softwareSystem = ""; + + @JsonInclude(value = JsonInclude.Include.NON_EMPTY) + private String container = ""; + + @JsonInclude(value = JsonInclude.Include.NON_EMPTY) + private String component = ""; + + @JsonInclude(value = JsonInclude.Include.NON_EMPTY) + private String code = ""; + + @JsonInclude(value = JsonInclude.Include.NON_EMPTY) + private String deploymentNode = ""; + + @JsonInclude(value = JsonInclude.Include.NON_EMPTY) + private String infrastructureNode = ""; + + @JsonInclude(value = JsonInclude.Include.NON_EMPTY) + private String relationship = ""; + + public String getEnterprise() { + return enterprise; + } + + @Deprecated + void setEnterprise(String enterprise) { + this.enterprise = enterprise; + } + + public String getPerson() { + return person; + } + + public void setPerson(String person) { + this.person = person; + } + + public String getSoftwareSystem() { + return softwareSystem; + } + + public void setSoftwareSystem(String softwareSystem) { + this.softwareSystem = softwareSystem; + } + + public String getContainer() { + return container; + } + + public void setContainer(String container) { + this.container = container; + } + + public String getComponent() { + return component; + } + + public void setComponent(String component) { + this.component = component; + } + + public String getCode() { + return code; + } + + public void setCode(String code) { + this.code = code; + } + + public String getDeploymentNode() { + return deploymentNode; + } + + public void setDeploymentNode(String deploymentNode) { + this.deploymentNode = deploymentNode; + } + + public String getInfrastructureNode() { + return infrastructureNode; + } + + public void setInfrastructureNode(String infrastructureNode) { + this.infrastructureNode = infrastructureNode; + } + + public String getRelationship() { + return relationship; + } + + public void setRelationship(String relationship) { + this.relationship = relationship; + } + + /** + * Finds the terminology that can be used to describe/label the specified model item. + * + * @param modelItem an Element or Relationship + * @return the default or overridden terminology for the specified model item + */ + public String findTerminology(ModelItem modelItem) { + if (modelItem instanceof StaticStructureElementInstance) { + modelItem = ((StaticStructureElementInstance)modelItem).getElement(); + } + + if (modelItem instanceof Relationship) { + return !StringUtils.isNullOrEmpty(getRelationship()) ? getRelationship() : "Relationship"; + } else if (modelItem instanceof Person) { + return !StringUtils.isNullOrEmpty(getPerson()) ? getPerson() : "Person"; + } else if (modelItem instanceof SoftwareSystem) { + return !StringUtils.isNullOrEmpty(getSoftwareSystem()) ? getSoftwareSystem() : "Software System"; + } else if (modelItem instanceof Container) { + return !StringUtils.isNullOrEmpty(getContainer()) ? getContainer() : "Container"; + } else if (modelItem instanceof Component) { + return !StringUtils.isNullOrEmpty(getComponent()) ? getComponent() : "Component"; + } else if (modelItem instanceof DeploymentNode) { + 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."); + } + +} \ No newline at end of file diff --git a/structurizr-core/src/main/java/com/structurizr/view/Theme.java b/structurizr-core/src/main/java/com/structurizr/view/Theme.java new file mode 100644 index 000000000..1d30657f6 --- /dev/null +++ b/structurizr-core/src/main/java/com/structurizr/view/Theme.java @@ -0,0 +1,99 @@ +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; + +final class Theme { + + private String name; + private String description; + private Collection elements = new LinkedList<>(); + private Collection relationships = new LinkedList<>(); + private String logo; + private Font font; + + Theme() { + } + + Theme(Collection elements, Collection relationships) { + this.elements = elements; + this.relationships = relationships; + } + + Theme(String name, String description, Collection elements, Collection relationships) { + this.name = name; + this.description = description; + this.elements = elements; + this.relationships = relationships; + } + + public String getName() { + return name; + } + + void setName(String name) { + this.name = name; + } + + public String getDescription() { + return description; + } + + void setDescription(String description) { + this.description = description; + } + + @JsonGetter + Collection getElements() { + return elements; + } + + void setElements(Collection elements) { + this.elements = elements; + } + + @JsonGetter + Collection getRelationships() { + return relationships; + } + + 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 diff --git a/structurizr-core/src/main/java/com/structurizr/view/Vertex.java b/structurizr-core/src/main/java/com/structurizr/view/Vertex.java new file mode 100644 index 000000000..eda796e23 --- /dev/null +++ b/structurizr-core/src/main/java/com/structurizr/view/Vertex.java @@ -0,0 +1,66 @@ +package com.structurizr.view; + +import java.util.Objects; + +/** + * The X, Y coordinate of a bend in a line. + */ +public final class Vertex { + + private int x; + private int y; + + Vertex() { + } + + public Vertex(int x, int y) { + this.x = x; + this.y = y; + } + + /** + * Gets the horizontal position of the vertex when rendered. + * + * @return the X coordinate, as an int + */ + public int getX() { + return x; + } + + public void setX(int x) { + this.x = x; + } + + /** + * Gets the vertical position of the vertex when rendered. + * + * @return the Y coordinate, as an int + */ + public int getY() { + return y; + } + + public void setY(int y) { + this.y = y; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + + if (o == null || getClass() != o.getClass()) { + return false; + } + + Vertex vertex = (Vertex)o; + return x == vertex.x && y == vertex.y; + } + + @Override + public int hashCode() { + return Objects.hash(x, y); + } + +} \ 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 new file mode 100644 index 000000000..b4132bcd7 --- /dev/null +++ b/structurizr-core/src/main/java/com/structurizr/view/View.java @@ -0,0 +1,178 @@ +package com.structurizr.view; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.structurizr.PropertyHolder; + +import javax.annotation.Nonnull; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +/** + * The superclass for all views. + */ +public abstract class View implements PropertyHolder, Comparable { + + private String key; + private boolean generatedKey = false; + + private int order; + private String title; + private String description; + private Map properties = new HashMap<>(); + + private ViewSet viewSet; + + View() { + } + + /** + * Gets the description of this view. + * + * @return the description, as a String + */ + public String getDescription() { + return description; + } + + 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; + } + + /** + * Returns true if this view has an automatically generated view key, false otherwise. + * + * @return true if this view has an automatically generated view key, false otherwise + */ + @JsonInclude(JsonInclude.Include.NON_DEFAULT) + public boolean isGeneratedKey() { + return generatedKey; + } + + void setGeneratedKey(boolean generatedKey) { + this.generatedKey = generatedKey; + } + + /** + * Gets the order of this view. + * + * @return a positive integer + */ + public int getOrder() { + return order; + } + + /** + * Sets the order of this view. + * + * @param order a positive integer + */ + public 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) { + this.title = title; + } + + /** + * Gets the (computed) name of this view. + * + * @return the name, as a String + */ + public abstract String getName(); + + 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; + } + + /** + * Gets the collection of name-value property pairs associated with this 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 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); + } + } + + @Override + public int compareTo(View view) { + int result = getOrder() - view.getOrder(); + if (result == 0) { + result = getKey().compareToIgnoreCase(view.getKey()); + } + + return result; + } + +} \ 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 new file mode 100644 index 000000000..c91c3c4e3 --- /dev/null +++ b/structurizr-core/src/main/java/com/structurizr/view/ViewSet.java @@ -0,0 +1,1288 @@ +package com.structurizr.view; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonSetter; +import com.structurizr.WorkspaceValidationException; +import com.structurizr.model.*; +import com.structurizr.util.StringUtils; +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; + +/** + * A set of views onto a software architecture model. + */ +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"; + public static final String FILTERED_VIEW_TYPE = "Filtered"; + public static final String IMAGE_VIEW_TYPE = "Image"; + public static final String CUSTOM_VIEW_TYPE = "Custom"; + + private Model model; + + 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 TreeSet<>(); + + private Configuration configuration = new Configuration(); + + ViewSet() { + } + + ViewSet(Model model) { + 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. + * + * @param key the key for the view (must be unique) + * @param title a title of the view + * @param description a description of the view + * @return a CustomView object + * @throws IllegalArgumentException if the key is not unique + */ + public CustomView createCustomView(String key, String title, String description) { + boolean keyIsAutomaticallyGenerated = false; + + if (StringUtils.isNullOrEmpty(key)) { + key = generateViewKey(CUSTOM_VIEW_TYPE); + keyIsAutomaticallyGenerated = true; + } + + assertThatTheViewKeyIsSpecifiedAndUnique(key); + + CustomView view = new CustomView(model, key, title, description); + view.setGeneratedKey(keyIsAutomaticallyGenerated); + view.setOrder(getNextOrder()); + view.setViewSet(this); + customViews.add(view); + 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. + * + * @param key the key for the view (must be unique) + * @param description a description of the view + * @return a SystemLandscapeView object + * @throws IllegalArgumentException if the key is not unique + */ + public SystemLandscapeView createSystemLandscapeView(String key, String description) { + boolean keyIsAutomaticallyGenerated = false; + + if (StringUtils.isNullOrEmpty(key)) { + key = generateViewKey(SYSTEM_LANDSCAPE_VIEW_TYPE); + keyIsAutomaticallyGenerated = true; + } + + assertThatTheViewKeyIsSpecifiedAndUnique(key); + + SystemLandscapeView view = new SystemLandscapeView(model, key, description); + view.setGeneratedKey(keyIsAutomaticallyGenerated); + view.setOrder(getNextOrder()); + view.setViewSet(this); + systemLandscapeViews.add(view); + + 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. + * + * @param softwareSystem the SoftwareSystem object representing the scope of the view + * @param key the key for the view (must be unique) + * @param description a description of the view + * @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, String description) { + boolean keyIsAutomaticallyGenerated = false; + + if (StringUtils.isNullOrEmpty(key)) { + key = generateViewKey(SYSTEM_CONTEXT_VIEW_TYPE); + keyIsAutomaticallyGenerated = true; + } + + assertThatTheSoftwareSystemIsNotNull(softwareSystem); + assertThatTheViewKeyIsSpecifiedAndUnique(key); + + SystemContextView view = new SystemContextView(softwareSystem, key, description); + view.setGeneratedKey(keyIsAutomaticallyGenerated); + view.setOrder(getNextOrder()); + view.setViewSet(this); + systemContextViews.add(view); + 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. + * + * @param softwareSystem the SoftwareSystem object representing the scope of the view + * @param key the key for the view (must be unique) + * @param description a description of the view + * @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, String description) { + boolean keyIsAutomaticallyGenerated = false; + + if (StringUtils.isNullOrEmpty(key)) { + key = generateViewKey(CONTAINER_VIEW_TYPE); + keyIsAutomaticallyGenerated = true; + } + + assertThatTheSoftwareSystemIsNotNull(softwareSystem); + assertThatTheViewKeyIsSpecifiedAndUnique(key); + + ContainerView view = new ContainerView(softwareSystem, key, description); + view.setGeneratedKey(keyIsAutomaticallyGenerated); + view.setOrder(getNextOrder()); + view.setViewSet(this); + containerViews.add(view); + 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. + * + * @param container the Container object representing the scope of the view + * @param key the key for the view (must be unique) + * @param description a description of the view + * @return a ContainerView object + * @throws IllegalArgumentException if the container is null or the key is not unique + */ + public ComponentView createComponentView(Container container, String key, String description) { + boolean keyIsAutomaticallyGenerated = false; + + if (StringUtils.isNullOrEmpty(key)) { + key = generateViewKey(COMPONENT_VIEW_TYPE); + keyIsAutomaticallyGenerated = true; + } + + assertThatTheContainerIsNotNull(container); + assertThatTheViewKeyIsSpecifiedAndUnique(key); + + ComponentView view = new ComponentView(container, key, description); + view.setGeneratedKey(keyIsAutomaticallyGenerated); + view.setOrder(getNextOrder()); + view.setViewSet(this); + componentViews.add(view); + 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. + * + * @param key the key for the view (must be unique) + * @param description a description of the view + * @return a DynamicView object + * @throws IllegalArgumentException if the key is not unique + */ + public DynamicView createDynamicView(String key, String description) { + boolean keyIsAutomaticallyGenerated = false; + + if (StringUtils.isNullOrEmpty(key)) { + key = generateViewKey(DYNAMIC_VIEW_TYPE); + keyIsAutomaticallyGenerated = true; + } + + assertThatTheViewKeyIsSpecifiedAndUnique(key); + + DynamicView view = new DynamicView(model, key, description); + view.setGeneratedKey(keyIsAutomaticallyGenerated); + view.setOrder(getNextOrder()); + view.setViewSet(this); + dynamicViews.add(view); + 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: + * + *
    + *
  • 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) + * @param description a description of the view + * @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, String description) { + boolean keyIsAutomaticallyGenerated = false; + + if (StringUtils.isNullOrEmpty(key)) { + key = generateViewKey(DYNAMIC_VIEW_TYPE); + keyIsAutomaticallyGenerated = true; + } + + assertThatTheSoftwareSystemIsNotNull(softwareSystem); + assertThatTheViewKeyIsSpecifiedAndUnique(key); + + DynamicView view = new DynamicView(softwareSystem, key, description); + view.setGeneratedKey(keyIsAutomaticallyGenerated); + view.setOrder(getNextOrder()); + view.setViewSet(this); + dynamicViews.add(view); + 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: + * + *
    + *
  • 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) + * @param description a description of the view + * @return a DynamicView object + * @throws IllegalArgumentException if the container is null or the key is not unique + */ + public DynamicView createDynamicView(Container container, String key, String description) { + boolean keyIsAutomaticallyGenerated = false; + + if (StringUtils.isNullOrEmpty(key)) { + key = generateViewKey(DYNAMIC_VIEW_TYPE); + keyIsAutomaticallyGenerated = true; + } + + assertThatTheContainerIsNotNull(container); + assertThatTheViewKeyIsSpecifiedAndUnique(key); + + DynamicView view = new DynamicView(container, key, description); + view.setGeneratedKey(keyIsAutomaticallyGenerated); + view.setOrder(getNextOrder()); + view.setViewSet(this); + dynamicViews.add(view); + 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. + * + * @param key the key for the deployment view (must be unique) + * @param description a description of the view + * @return a DeploymentView object + * @throws IllegalArgumentException if the key is not unique + */ + public DeploymentView createDeploymentView(String key, String description) { + boolean keyIsAutomaticallyGenerated = false; + + if (StringUtils.isNullOrEmpty(key)) { + key = generateViewKey(DEPLOYMENT_VIEW_TYPE); + keyIsAutomaticallyGenerated = true; + } + + assertThatTheViewKeyIsSpecifiedAndUnique(key); + + DeploymentView view = new DeploymentView(model, key, description); + view.setGeneratedKey(keyIsAutomaticallyGenerated); + view.setOrder(getNextOrder()); + view.setViewSet(this); + deploymentViews.add(view); + 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. + * + * @param softwareSystem the SoftwareSystem object representing the scope of the view + * @param key the key for the deployment view (must be unique) + * @param description a description of the view + * @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, String description) { + boolean keyIsAutomaticallyGenerated = false; + + if (StringUtils.isNullOrEmpty(key)) { + key = generateViewKey(DEPLOYMENT_VIEW_TYPE); + keyIsAutomaticallyGenerated = true; + } + + assertThatTheSoftwareSystemIsNotNull(softwareSystem); + assertThatTheViewKeyIsSpecifiedAndUnique(key); + + DeploymentView view = new DeploymentView(softwareSystem, key, description); + view.setGeneratedKey(keyIsAutomaticallyGenerated); + view.setOrder(getNextOrder()); + view.setViewSet(this); + deploymentViews.add(view); + 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. + * + * @param view the static 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(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)) { + key = generateViewKey(FILTERED_VIEW_TYPE); + keyIsAutomaticallyGenerated = true; + } + + assertThatTheViewIsNotNull(view); + assertThatTheViewKeyIsSpecifiedAndUnique(key); + + FilteredView filteredView = new FilteredView(view, key, description, mode, tags); + filteredView.setGeneratedKey(keyIsAutomaticallyGenerated); + filteredView.setOrder(getNextOrder()); + filteredView.setViewSet(this); + filteredViews.add(filteredView); + 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) { + boolean keyIsAutomaticallyGenerated = false; + + if (StringUtils.isNullOrEmpty(key)) { + key = generateViewKey(IMAGE_VIEW_TYPE); + keyIsAutomaticallyGenerated = true; + } + + assertThatTheViewKeyIsSpecifiedAndUnique(key); + + ImageView view = new ImageView(key); + view.setGeneratedKey(keyIsAutomaticallyGenerated); + view.setOrder(getNextOrder()); + view.setViewSet(this); + 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) { + boolean keyIsAutomaticallyGenerated = false; + + if (StringUtils.isNullOrEmpty(key)) { + key = generateViewKey(IMAGE_VIEW_TYPE); + keyIsAutomaticallyGenerated = true; + } + + if (element == null) { + throw new IllegalArgumentException("An element must be specified."); + } + assertThatTheViewKeyIsSpecifiedAndUnique(key); + + ImageView view = new ImageView(element, key); + view.setGeneratedKey(keyIsAutomaticallyGenerated); + view.setOrder(getNextOrder()); + view.setViewSet(this); + 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 || getImageViewWithKey(key) != null) { + throw new IllegalArgumentException("A view with the key " + key + " already exists."); + } + } + + private void assertThatTheSoftwareSystemIsNotNull(SoftwareSystem softwareSystem) { + if (softwareSystem == null) { + throw new IllegalArgumentException("A software system must be specified."); + } + } + + private void assertThatTheContainerIsNotNull(Container container) { + if (container == null) { + throw new IllegalArgumentException("A container must be specified."); + } + } + + private void assertThatTheViewIsNotNull(View view) { + if (view == null) { + throw new IllegalArgumentException("A view must be specified."); + } + } + + /** + * Finds the view with the specified key, or null if the view does not exist. + * + * @param key the key + * @return a View object, or null if a view with the specified key could not be found + */ + public View getViewWithKey(String key) { + if (key == null) { + throw new IllegalArgumentException("A key must be specified."); + } + + 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. + * + * @param key the key + * @return a FilteredView object, or null if a view with the specified key could not be found + */ + FilteredView getFilteredViewWithKey(String key) { + if (key == null) { + throw new IllegalArgumentException("A key must be specified."); + } + + 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. + * + * @return a Collection of CustomView objects + */ + public Collection getCustomViews() { + return new TreeSet<>(customViews); + } + + void setCustomViews(Set customViews) { + if (customViews != null) { + this.customViews = new TreeSet<>(customViews); + } + } + + /** + * Gets the set of system landscape views. + * + * @return a Collection of SystemLandscapeView objects + */ + public Collection getSystemLandscapeViews() { + return new TreeSet<>(systemLandscapeViews); + } + + void setSystemLandscapeViews(Set systemLandscapeViews) { + if (systemLandscapeViews != null) { + this.systemLandscapeViews = new TreeSet<>(systemLandscapeViews); + } + } + + /** + * (this is for backwards compatibility) + */ + @JsonSetter("enterpriseContextViews") + void setEnterpriseContextViews(Collection enterpriseContextViews) { + if (enterpriseContextViews != null) { + this.systemLandscapeViews = new TreeSet<>(enterpriseContextViews); + } + } + + /** + * Gets the set of system context views. + * + * @return a Collection of SystemContextView objects + */ + public Collection getSystemContextViews() { + return new TreeSet<>(systemContextViews); + } + + void setSystemContextViews(Set systemContextViews) { + if (systemContextViews != null) { + this.systemContextViews = new TreeSet<>(systemContextViews); + } + } + + /** + * Gets the set of container views. + * + * @return a Collection of ContainerView objects + */ + public Collection getContainerViews() { + return new TreeSet<>(containerViews); + } + + void setContainerViews(Set containerViews) { + if (containerViews != null) { + this.containerViews = new TreeSet<>(containerViews); + } + } + + /** + * Gets the set of component views. + * + * @return a Collection of ComponentView objects + */ + public Collection getComponentViews() { + return new TreeSet<>(componentViews); + } + + void setComponentViews(Set componentViews) { + if (componentViews != null) { + this.componentViews = new TreeSet<>(componentViews); + } + } + + /** + * Gets the set of dynamic views. + * + * @return a Collection of DynamicView objects + */ + public Collection getDynamicViews() { + return new TreeSet<>(dynamicViews); + } + + void setDynamicViews(Set dynamicViews) { + if (dynamicViews != null) { + this.dynamicViews = new TreeSet<>(dynamicViews); + } + } + + public Collection getFilteredViews() { + return new TreeSet<>(filteredViews); + } + + void setFilteredViews(Set filteredViews) { + if (filteredViews != null) { + this.filteredViews = new TreeSet<>(filteredViews); + } + } + + /** + * Gets the set of deployment views. + * + * @return a Collection of DeploymentView objects + */ + public Collection getDeploymentViews() { + return new TreeSet<>(deploymentViews); + } + + void setDeploymentViews(Set deploymentViews) { + if (deploymentViews != null) { + this.deploymentViews = new TreeSet<>(deploymentViews); + } + } + + /** + * Gets the set of image views. + * + * @return a Collection of ImageView objects + */ + public Collection getImageViews() { + return new TreeSet<>(imageViews); + } + + void setImageViews(Set imageViews) { + if (imageViews != null) { + this.imageViews = new TreeSet<>(imageViews); + } + } + + /** + * Gets the set of all views. + * + * @return a Collection of View objects + */ + @JsonIgnore + public Collection getViews() { + Set views = new TreeSet<>(); + + 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; + } + + void hydrate(Model model) { + this.model = model; + + checkViewKeysAreUnique(); + + for (CustomView view : customViews) { + view.setModel(model); + hydrateView(view); + } + + for (SystemLandscapeView view : systemLandscapeViews) { + view.setModel(model); + hydrateView(view); + } + + for (SystemContextView view : systemContextViews) { + SoftwareSystem softwareSystem = model.getSoftwareSystemWithId(view.getSoftwareSystemId()); + if (softwareSystem == null) { + throw new WorkspaceValidationException( + String.format("The system context view with key \"%s\" is associated with a software system (id=%s), but that element does not exist in the model.", + view.getKey(), view.getSoftwareSystemId()) + ); + } + + view.setSoftwareSystem(softwareSystem); + hydrateView(view); + } + + for (ContainerView view : containerViews) { + SoftwareSystem softwareSystem = model.getSoftwareSystemWithId(view.getSoftwareSystemId()); + if (softwareSystem == null) { + throw new WorkspaceValidationException( + String.format("The container view with key \"%s\" is associated with a software system (id=%s), but that element does not exist in the model.", + view.getKey(), view.getSoftwareSystemId()) + ); + } + + view.setSoftwareSystem(softwareSystem); + hydrateView(view); + } + + for (ComponentView view : componentViews) { + Container container = (Container)model.getElement(view.getContainerId()); + if (container == null) { + throw new WorkspaceValidationException( + String.format("The component view with key \"%s\" is associated with a container (id=%s), but that element does not exist in the model.", + view.getKey(), view.getContainerId()) + ); + } + + view.setContainer(container); + view.setSoftwareSystem(container.getSoftwareSystem()); + hydrateView(view); + } + + for (DynamicView view : dynamicViews) { + if (!isNullOrEmpty(view.getElementId())) { + Element element = model.getElement(view.getElementId()); + if (element == null) { + throw new WorkspaceValidationException( + String.format("The dynamic 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); + } + + view.setModel(model); + hydrateView(view); + } + + for (DeploymentView view : deploymentViews) { + if (!isNullOrEmpty(view.getSoftwareSystemId())) { + SoftwareSystem softwareSystem = model.getSoftwareSystemWithId(view.getSoftwareSystemId()); + if (softwareSystem == null) { + throw new WorkspaceValidationException( + String.format("The deployment view with key \"%s\" is associated with a software system (id=%s), but that element does not exist in the model.", + view.getKey(), view.getSoftwareSystemId()) + ); + } + + view.setSoftwareSystem(softwareSystem); + } + + view.setModel(model); + hydrateView(view); + } + + for (FilteredView filteredView : filteredViews) { + View view = getViewWithKey(filteredView.getBaseViewKey()); + if (view == null) { + throw new WorkspaceValidationException( + String.format("The filtered view with key \"%s\" is based upon a view (key=%s), but that view does not exist in the workspace.", + filteredView.getKey(), filteredView.getBaseViewKey()) + ); + } + + 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 or deployment view.", + filteredView.getKey(), filteredView.getBaseViewKey()) + ); + } + } + + 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(ModelView view) { + view.setViewSet(this); + + for (ElementView elementView : view.getElements()) { + Element element = model.getElement(elementView.getId()); + if (element == null) { + throw new WorkspaceValidationException( + String.format("The view with key \"%s\" references an element (id=%s), but that element does not exist in the model.", + view.getKey(), elementView.getId()) + ); + } + + elementView.setElement(element); + } + + for (RelationshipView relationshipView : view.getRelationships()) { + Relationship relationship = model.getRelationship(relationshipView.getId()); + if (relationship == null) { + throw new WorkspaceValidationException( + String.format("The view with key \"%s\" references a relationship (id=%s), but that relationship does not exist in the model.", + view.getKey(), relationshipView.getId()) + ); + } + + relationshipView.setRelationship(relationship); + } + } + + private void checkViewKeysAreUnique() { + Set keys = new HashSet<>(); + + for (View view : getViews()) { + if (keys.contains(view.getKey())) { + throw new WorkspaceValidationException("A view with the key " + view.getKey() + " already exists."); + } else { + keys.add(view.getKey()); + } + } + } + + private synchronized int getNextOrder() { + return getViews().stream().max(Comparator.comparingInt(View::getOrder)).map(View::getOrder).orElse(0) + 1; + } + + /** + * Gets the configuration object associated with this set of views. + * + * @return a Configuration object + */ + public Configuration getConfiguration() { + return configuration; + } + + public void copyLayoutInformationFrom(ViewSet source) { + for (CustomView view : customViews) { + if (view.getAutomaticLayout() == null && view.getMergeFromRemote() == true) { + CustomView sourceView = findView(source.getCustomViews(), view); + if (sourceView != null) { + view.copyLayoutInformationFrom(sourceView); + } else { + log.warn("Could not find a matching view for \"" + view.getName() + "\" ... diagram layout information may be lost."); + } + } + } + + for (SystemLandscapeView view : systemLandscapeViews) { + if (view.getAutomaticLayout() == null && view.getMergeFromRemote() == true) { + SystemLandscapeView sourceView = findView(source.getSystemLandscapeViews(), view); + if (sourceView != null) { + view.copyLayoutInformationFrom(sourceView); + } else { + log.warn("Could not find a matching view for \"" + view.getName() + "\" ... diagram layout information may be lost."); + } + } + } + + for (SystemContextView view : systemContextViews) { + if (view.getAutomaticLayout() == null && view.getMergeFromRemote() == true) { + SystemContextView sourceView = findView(source.getSystemContextViews(), view); + if (sourceView != null) { + view.copyLayoutInformationFrom(sourceView); + } else { + log.warn("Could not find a matching view for \"" + view.getName() + "\" ... diagram layout information may be lost."); + } + } + } + + for (ContainerView view : containerViews) { + if (view.getAutomaticLayout() == null && view.getMergeFromRemote() == true) { + ContainerView sourceView = findView(source.getContainerViews(), view); + if (sourceView != null) { + view.copyLayoutInformationFrom(sourceView); + } else { + log.warn("Could not find a matching view for \"" + view.getName() + "\" ... diagram layout information may be lost."); + } + } + } + + for (ComponentView view : componentViews) { + if (view.getAutomaticLayout() == null && view.getMergeFromRemote() == true) { + ComponentView sourceView = findView(source.getComponentViews(), view); + if (sourceView != null) { + view.copyLayoutInformationFrom(sourceView); + } else { + log.warn("Could not find a matching view for \"" + view.getName() + "\" ... diagram layout information may be lost."); + } + } + } + + for (DynamicView view : dynamicViews) { + if (view.getAutomaticLayout() == null && view.getMergeFromRemote() == true) { + DynamicView sourceView = findView(source.getDynamicViews(), view); + if (sourceView != null) { + view.copyLayoutInformationFrom(sourceView); + } else { + log.warn("Could not find a matching view for \"" + view.getName() + "\" ... diagram layout information may be lost."); + } + } + } + + for (DeploymentView view : deploymentViews) { + if (view.getAutomaticLayout() == null && view.getMergeFromRemote() == true) { + DeploymentView sourceView = findView(source.getDeploymentViews(), view); + if (sourceView != null) { + view.copyLayoutInformationFrom(sourceView); + } else { + log.warn("Could not find a matching view for \"" + view.getName() + "\" ... diagram layout information may be lost."); + } + } + } + } + + private T findView(Collection views, T sourceView) { + for (T view : views) { + if (view.getKey() != null && view.getKey().equals(sourceView.getKey())) { + return view; + } + } + + for (T view : views) { + if (view.getName().equals(sourceView.getName())) { + if (view.getDescription() != null) { + if (view.getDescription().equals(sourceView.getDescription())) { + return view; + } + } else { + return view; + } + } + } + + return null; + } + + @JsonIgnore + public boolean isEmpty() { + return customViews.isEmpty() && systemLandscapeViews.isEmpty() && systemContextViews.isEmpty() && containerViews.isEmpty() && componentViews.isEmpty() && dynamicViews.isEmpty() && deploymentViews.isEmpty() && filteredViews.isEmpty() && imageViews.isEmpty(); + } + + private 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); + } + + 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("", ""); + systemLandscapeView.addDefaultElements(); + systemLandscapeView.enableAutomaticLayout(AutomaticLayout.RankDirection.TopBottom, 300, 300); + + if (!model.getSoftwareSystems().isEmpty()) { + List softwareSystems = new ArrayList<>(model.getSoftwareSystems()); + softwareSystems.sort(Comparator.comparing(Element::getName)); + + // and a system context view plus container view for each software system + for (SoftwareSystem softwareSystem : softwareSystems) { + SystemContextView systemContextView = createSystemContextView(softwareSystem, "", ""); + systemContextView.addDefaultElements(); + systemContextView.enableAutomaticLayout(AutomaticLayout.RankDirection.TopBottom, 300, 300); + + if (softwareSystem.getContainers().size() > 0) { + List containers = new ArrayList<>(softwareSystem.getContainers()); + containers.sort(Comparator.comparing(Element::getName)); + + ContainerView containerView = createContainerView(softwareSystem, "", ""); + containerView.addDefaultElements(); + containerView.enableAutomaticLayout(AutomaticLayout.RankDirection.TopBottom, 300, 300); + + for (Container container : containers) { + if (container.getComponents().size() > 0) { + ComponentView componentView = createComponentView(container, "", ""); + componentView.addDefaultElements(); + componentView.enableAutomaticLayout(AutomaticLayout.RankDirection.TopBottom, 300, 300); + } + } + } + } + } + + // and deployment views for each environment and software system pair + List deploymentEnvironments = new ArrayList<>(); + for (DeploymentNode deploymentNode : model.getDeploymentNodes()) { + String environment = deploymentNode.getEnvironment(); + if (!deploymentEnvironments.contains(environment)) { + deploymentEnvironments.add(environment); + } + } + deploymentEnvironments.sort(String::compareTo); + + for (String deploymentEnvironment : deploymentEnvironments) { + List softwareSystems = new ArrayList<>(); + for (DeploymentNode deploymentNode : model.getDeploymentNodes()) { + if (deploymentNode.getEnvironment().equals(deploymentEnvironment)) { + Set softwareSystemInstances = getSoftwareSystemInstances(deploymentNode); + for (SoftwareSystemInstance softwareSystemInstance : softwareSystemInstances) { + SoftwareSystem softwareSystem = softwareSystemInstance.getSoftwareSystem(); + if (!softwareSystems.contains(softwareSystem)) { + softwareSystems.add(softwareSystem); + } + } + + Set containerInstances = getContainerInstances(deploymentNode); + for (ContainerInstance containerInstance : containerInstances) { + SoftwareSystem softwareSystem = containerInstance.getContainer().getSoftwareSystem(); + if (!softwareSystems.contains(softwareSystem)) { + softwareSystems.add(softwareSystem); + } + } + } + } + + 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))) { + DeploymentView deploymentView = createDeploymentView("", ""); + deploymentView.setEnvironment(deploymentEnvironment); + deploymentView.addDefaultElements(); + deploymentView.enableAutomaticLayout(AutomaticLayout.RankDirection.TopBottom, 300, 300); + } + } else { + softwareSystems.sort(Comparator.comparing(Element::getName)); + + for (SoftwareSystem softwareSystem : softwareSystems) { + DeploymentView deploymentView = createDeploymentView(softwareSystem, "", ""); + deploymentView.setEnvironment(deploymentEnvironment); + deploymentView.addDefaultElements(); + deploymentView.enableAutomaticLayout(AutomaticLayout.RankDirection.TopBottom, 300, 300); + } + } + } + } + + private Set getSoftwareSystemInstances(DeploymentNode deploymentNode) { + Set softwareSystemInstances = new TreeSet<>(deploymentNode.getSoftwareSystemInstances()); + + for (DeploymentNode child : deploymentNode.getChildren()) { + softwareSystemInstances.addAll(getSoftwareSystemInstances(child)); + } + + return softwareSystemInstances; + } + + private Set getContainerInstances(DeploymentNode deploymentNode) { + Set containerInstances = new TreeSet<>(deploymentNode.getContainerInstances()); + + for (DeploymentNode child : deploymentNode.getChildren()) { + containerInstances.addAll(getContainerInstances(child)); + } + + return containerInstances; + } + + /** + * Removes all views and configuration. + */ + public void clear() { + 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(); + } + +} \ No newline at end of file diff --git a/structurizr-core/src/main/java/com/structurizr/view/ViewSortOrder.java b/structurizr-core/src/main/java/com/structurizr/view/ViewSortOrder.java new file mode 100644 index 000000000..b36add005 --- /dev/null +++ b/structurizr-core/src/main/java/com/structurizr/view/ViewSortOrder.java @@ -0,0 +1,19 @@ +package com.structurizr.view; + +/** + *

+ * Allows the sort order of views to be customized as follows: + *

+ *
    + *
  • Default: Views are grouped by the software system they are associated with, and then sorted by type (System Landscape, System Context, Container, Component, Dynamic and Deployment) within these groups.
  • + *
  • Type: Views are sorted by type (System Landscape, System Context, Container, Component, Dynamic and Deployment).
  • + *
  • Key: Views are sorted by the view key (alphabetical, ascending).
  • + *
+ */ +public enum ViewSortOrder { + + Default, + Type, + Key + +} \ No newline at end of file diff --git a/structurizr-core/src/test/java/com/structurizr/AbstractWorkspaceTestBase.java b/structurizr-core/src/test/java/com/structurizr/AbstractWorkspaceTestBase.java new file mode 100644 index 000000000..91be93b65 --- /dev/null +++ b/structurizr-core/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-core/src/test/java/com/structurizr/WorkspaceTests.java b/structurizr-core/src/test/java/com/structurizr/WorkspaceTests.java new file mode 100644 index 000000000..ad55331ad --- /dev/null +++ b/structurizr-core/src/test/java/com/structurizr/WorkspaceTests.java @@ -0,0 +1,331 @@ +package com.structurizr; + +import com.structurizr.documentation.Decision; +import com.structurizr.documentation.Format; +import com.structurizr.model.*; +import com.structurizr.view.SystemLandscapeView; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +public class WorkspaceTests { + + private Workspace workspace = new Workspace("Name", "Description"); + + @Test + void isEmpty_ReturnsTrue_WhenThereAreNoElementsViewsOrDocumentation() { + workspace = new Workspace("Name", "Description"); + assertTrue(workspace.isEmpty()); + } + + @Test + void isEmpty_ReturnsFalse_WhenThereAreElements() { + workspace = new Workspace("Name", "Description"); + workspace.getModel().addPerson("Name", "Description"); + assertFalse(workspace.isEmpty()); + } + + @Test + void isEmpty_ReturnsFalse_WhenThereAreViews() { + workspace = new Workspace("Name", "Description"); + workspace.getViews().createSystemLandscapeView("key", "Description"); + assertFalse(workspace.isEmpty()); + } + + @Test + void isEmpty_ReturnsFalse_WhenThereIsDocumentation() throws Exception { + workspace = new Workspace("Name", "Description"); + Decision d = new Decision("1"); + d.setTitle("Title"); + d.setContent("Content"); + d.setStatus("Proposed"); + d.setFormat(Format.Markdown); + workspace.getDocumentation().addDecision(d); + assertFalse(workspace.isEmpty()); + } + + @Test + void hydrate_DoesNotCrash() { + Workspace workspace = new Workspace("Name", "Description"); + assertNotNull(workspace.getViews()); + assertNotNull(workspace.getDocumentation()); + + // check that the hydrate method doesn't crash (it includes some method calls via reflection) + 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()); + assertEquals(0, workspace.getModel().getRelationships().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)); + } + + @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()); + } + + @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 diff --git a/structurizr-core/src/test/java/com/structurizr/configuration/UserTests.java b/structurizr-core/src/test/java/com/structurizr/configuration/UserTests.java new file mode 100644 index 000000000..efacd7398 --- /dev/null +++ b/structurizr-core/src/test/java/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/src/test/java/com/structurizr/configuration/WorkspaceConfigurationTests.java b/structurizr-core/src/test/java/com/structurizr/configuration/WorkspaceConfigurationTests.java new file mode 100644 index 000000000..bdc982e9a --- /dev/null +++ b/structurizr-core/src/test/java/com/structurizr/configuration/WorkspaceConfigurationTests.java @@ -0,0 +1,92 @@ +package com.structurizr.configuration; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +public class WorkspaceConfigurationTests { + + @Test + void addUser_ThrowsAnException_WhenANullUsernameIsSpecified() { + try { + WorkspaceConfiguration configuration = new WorkspaceConfiguration(); + configuration.addUser(null, Role.ReadWrite); + fail(); + } catch (IllegalArgumentException iae) { + assertEquals("A username must be specified.", iae.getMessage()); + } + } + + @Test + void addUser_ThrowsAnException_WhenAnEmptyUsernameIsSpecified() { + try { + WorkspaceConfiguration configuration = new WorkspaceConfiguration(); + configuration.addUser(" ", Role.ReadWrite); + fail(); + } catch (IllegalArgumentException iae) { + assertEquals("A username must be specified.", iae.getMessage()); + } + } + + @Test + void addUser_ThrowsAnException_WhenANullRoleIsSpecified() { + try { + WorkspaceConfiguration configuration = new WorkspaceConfiguration(); + configuration.addUser("user@example.com", null); + fail(); + } catch (IllegalArgumentException iae) { + assertEquals("A role must be specified.", iae.getMessage()); + } + } + + @Test + void addUser_AddsAUser() { + WorkspaceConfiguration configuration = new WorkspaceConfiguration(); + configuration.addUser("user1@example.com", Role.ReadOnly); + + assertEquals(1, configuration.getUsers().size()); + 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 + 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(); + assertNull(configuration.getVisibility()); + + configuration.setVisibility(Visibility.Private); + assertEquals(Visibility.Private, configuration.getVisibility()); + + configuration.setVisibility(null); + assertNull(configuration.getVisibility()); + } + + @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 diff --git a/structurizr-core/src/test/java/com/structurizr/documentation/DecisionTests.java b/structurizr-core/src/test/java/com/structurizr/documentation/DecisionTests.java new file mode 100644 index 000000000..82efda419 --- /dev/null +++ b/structurizr-core/src/test/java/com/structurizr/documentation/DecisionTests.java @@ -0,0 +1,23 @@ +package com.structurizr.documentation; + +import com.structurizr.AbstractWorkspaceTestBase; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class DecisionTests extends AbstractWorkspaceTestBase { + + @Test + void hasLinkTo() { + Decision d1 = new Decision("1"); + Decision d2 = new Decision("2"); + Decision d3 = new Decision("3"); + + d1.addLink(d2, "Type"); + + assertTrue(d1.hasLinkTo(d2)); + assertFalse(d1.hasLinkTo(d3)); + } + +} \ No newline at end of file diff --git a/structurizr-core/src/test/java/com/structurizr/documentation/DocumentationTests.java b/structurizr-core/src/test/java/com/structurizr/documentation/DocumentationTests.java new file mode 100644 index 000000000..187a85870 --- /dev/null +++ b/structurizr-core/src/test/java/com/structurizr/documentation/DocumentationTests.java @@ -0,0 +1,132 @@ +package com.structurizr.documentation; + +import com.structurizr.AbstractWorkspaceTestBase; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +public class DocumentationTests extends AbstractWorkspaceTestBase { + + private Documentation documentation; + + @BeforeEach + public void setUp() { + documentation = workspace.getDocumentation(); + } + + @Test + void addSection_ThrowsAnException_WhenTheFormatIsNotSpecified() { + try { + Section section = new Section(); + section.setContent("Content"); + + documentation.addSection(section); + fail(); + } catch (IllegalArgumentException iae) { + assertEquals("A format must be specified.", iae.getMessage()); + } + } + + @Test + void addSection() { + Section section = new Section(); + section.setContent("Content"); + section.setFormat(Format.Markdown); + + documentation.addSection(section); + + assertEquals(1, documentation.getSections().size()); + assertTrue(documentation.getSections().contains(section)); + assertEquals(Format.Markdown, section.getFormat()); + assertEquals("Content", section.getContent()); + assertEquals(1, section.getOrder()); + } + + @Test + void addSection_IncrementsTheSectionOrderNumber() { + 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); + documentation.addSection(section3); + + assertEquals(1, section1.getOrder()); + assertEquals(2, section2.getOrder()); + assertEquals(3, section3.getOrder()); + } + + @Test + void addDecision_ThrowsAnException_WhenTheTitleIsNotSpecified() { + try { + Decision decision = new Decision("1"); + + documentation.addDecision(decision); + fail(); + } catch (IllegalArgumentException iae) { + assertEquals("A title must be specified.", iae.getMessage()); + } + } + + @Test + void addDecision_ThrowsAnException_WhenTheContentIsNotSpecified() { + try { + Decision decision = new Decision("1"); + decision.setTitle("Title"); + + documentation.addDecision(decision); + fail(); + } catch (IllegalArgumentException iae) { + assertEquals("Content must be specified.", iae.getMessage()); + } + } + + @Test + void addDecision_ThrowsAnException_WhenTheStatusIsNotSpecified() { + try { + Decision decision = new Decision("1"); + decision.setTitle("Title"); + decision.setContent("Content"); + + documentation.addDecision(decision); + fail(); + } catch (IllegalArgumentException iae) { + assertEquals("A status must be specified.", iae.getMessage()); + } + } + + @Test + void addDecision_ThrowsAnException_WhenTheFormatIsNotSpecified() { + try { + Decision decision = new Decision("1"); + decision.setTitle("Title"); + decision.setContent("Content"); + decision.setStatus("Accepted"); + + documentation.addDecision(decision); + fail(); + } catch (IllegalArgumentException iae) { + assertEquals("A format must be specified.", iae.getMessage()); + } + } + + @Test + void addDecision_ThrowsAnException_WhenADecisionExistsWithTheSameId() { + try { + Decision decision = new Decision("1"); + decision.setTitle("Title"); + decision.setContent("Content"); + decision.setFormat(Format.Markdown); + decision.setStatus("Accepted"); + + documentation.addDecision(decision); + documentation.addDecision(decision); + fail(); + } catch (IllegalArgumentException iae) { + assertEquals("A decision with an ID of 1 already exists in this scope.", iae.getMessage()); + } + } + +} \ No newline at end of file diff --git a/structurizr-core/src/test/java/com/structurizr/documentation/SectionTests.java b/structurizr-core/src/test/java/com/structurizr/documentation/SectionTests.java new file mode 100644 index 000000000..0f1705d44 --- /dev/null +++ b/structurizr-core/src/test/java/com/structurizr/documentation/SectionTests.java @@ -0,0 +1,17 @@ +package com.structurizr.documentation; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class SectionTests { + + @Test + void construction() { + Section section = new Section(Format.Markdown, "Content"); + + assertEquals(Format.Markdown, section.getFormat()); + assertEquals("Content", section.getContent()); + } + +} \ No newline at end of file diff --git a/structurizr-core/src/test/java/com/structurizr/model/ComponentTests.java b/structurizr-core/src/test/java/com/structurizr/model/ComponentTests.java new file mode 100644 index 000000000..d38adc7ba --- /dev/null +++ b/structurizr-core/src/test/java/com/structurizr/model/ComponentTests.java @@ -0,0 +1,66 @@ +package com.structurizr.model; + +import com.structurizr.AbstractWorkspaceTestBase; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +public class ComponentTests extends AbstractWorkspaceTestBase { + + private SoftwareSystem softwareSystem = model.addSoftwareSystem("System", "Description"); + private Container container = softwareSystem.addContainer("Container", "Description", "Some technology"); + + @Test + void getName_ReturnsTheGivenName_WhenANameIsGiven() { + Component component = new Component(); + component.setName("Some name"); + assertEquals("Some name", component.getName()); + } + + @Test + void getCanonicalName() { + Component component = container.addComponent("Component", "Description"); + assertEquals("Component://System.Container.Component", component.getCanonicalName()); + } + + @Test + void getCanonicalName_WhenNameContainsSlashAndDotCharacters() { + Component component = container.addComponent("Name1/.Name2", "Description"); + assertEquals("Component://System.Container.Name1Name2", component.getCanonicalName()); + } + + @Test + void getParent_ReturnsTheParentContainer() { + Component component = container.addComponent("Component", "Description"); + assertEquals(container, component.getParent()); + } + + @Test + void getContainer_ReturnsTheParentContainer() { + Component component = container.addComponent("Name", "Description"); + assertEquals(container, component.getContainer()); + } + + @Test + void removeTags_DoesNotRemoveRequiredTags() { + Component component = new Component(); + assertTrue(component.getTags().contains(Tags.ELEMENT)); + assertTrue(component.getTags().contains(Tags.COMPONENT)); + + component.removeTag(Tags.COMPONENT); + component.removeTag(Tags.ELEMENT); + + assertTrue(component.getTags().contains(Tags.ELEMENT)); + assertTrue(component.getTags().contains(Tags.COMPONENT)); + } + + @Test + void technologyProperty() { + Component component = new Component(); + assertNull(component.getTechnology()); + + component.setTechnology("Spring Bean"); + assertEquals("Spring Bean", component.getTechnology()); + } + +} \ No newline at end of file diff --git a/structurizr-core/src/test/java/com/structurizr/model/ContainerInstanceTests.java b/structurizr-core/src/test/java/com/structurizr/model/ContainerInstanceTests.java new file mode 100644 index 000000000..1f0b150b7 --- /dev/null +++ b/structurizr-core/src/test/java/com/structurizr/model/ContainerInstanceTests.java @@ -0,0 +1,206 @@ +package com.structurizr.model; + +import com.structurizr.AbstractWorkspaceTestBase; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +public class ContainerInstanceTests extends AbstractWorkspaceTestBase { + + 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"); + + @Test + void construction() { + ContainerInstance instance = deploymentNode.add(database); + + assertSame(database, instance.getContainer()); + assertEquals(database.getId(), instance.getContainerId()); + assertEquals(1, instance.getInstanceId()); + } + + @Test + void getContainerId() { + ContainerInstance instance = deploymentNode.add(database); + + assertEquals(database.getId(), instance.getContainerId()); + instance.setContainer(null); + instance.setContainerId("1234"); + assertEquals("1234", instance.getContainerId()); + } + + @Test + void getName() { + ContainerInstance instance = deploymentNode.add(database); + + assertEquals("Database Schema", instance.getName()); + + instance.setName("foo"); + assertEquals("Database Schema", instance.getName()); + } + + @Test + void getCanonicalName() { + ContainerInstance instance = deploymentNode.add(database); + + assertEquals("ContainerInstance://Default/Deployment Node/System.Database Schema[1]", instance.getCanonicalName()); + } + + @Test + void getParent_ReturnsTheParentDeploymentNode() { + ContainerInstance instance = deploymentNode.add(database); + + assertEquals(deploymentNode, instance.getParent()); + } + + @Test + void getRequiredTags() { + ContainerInstance instance = deploymentNode.add(database); + + assertTrue(instance.getDefaultTags().isEmpty()); + } + + @Test + void getTags() { + database.addTags("Database"); + ContainerInstance instance = deploymentNode.add(database); + instance.addTags("Primary Instance"); + + assertEquals("Container Instance,Primary Instance", instance.getTags()); + } + + @Test + void removeTags_DoesNotRemoveAnyTags() { + ContainerInstance instance = deploymentNode.add(database); + + assertTrue(instance.getTags().contains(Tags.CONTAINER_INSTANCE)); + + instance.removeTag(Tags.CONTAINER_INSTANCE); + + assertTrue(instance.getTags().contains(Tags.CONTAINER_INSTANCE)); + } + + @Test + void getDeploymentGroups_WhenNoGroupsHaveBeenSpecified() { + ContainerInstance instance = deploymentNode.add(database); + + assertEquals(1, instance.getDeploymentGroups().size()); + assertTrue(instance.getDeploymentGroups().contains("Default")); + } + + @Test + void getDeploymentGroups_WhenOneGroupHasBeenSpecified() { + ContainerInstance instance = deploymentNode.add(database, "Group 1"); + + assertEquals(1, instance.getDeploymentGroups().size()); + assertTrue(instance.getDeploymentGroups().contains("Group 1")); + } + + @Test + void getDeploymentGroups_WhenMultipleGroupsAreSpecified() { + ContainerInstance instance = deploymentNode.add(database, "Group 1", "Group 2"); + + assertEquals(2, instance.getDeploymentGroups().size()); + assertTrue(instance.getDeploymentGroups().contains("Group 1")); + assertTrue(instance.getDeploymentGroups().contains("Group 2")); + } + + @Test + void addHealthCheck() { + ContainerInstance instance = deploymentNode.add(database); + assertTrue(instance.getHealthChecks().isEmpty()); + + HttpHealthCheck healthCheck = instance.addHealthCheck("Test web application is working", "http://localhost:8080"); + assertEquals("Test web application is working", healthCheck.getName()); + assertEquals("http://localhost:8080", healthCheck.getUrl()); + assertEquals(60, healthCheck.getInterval()); + assertEquals(0, healthCheck.getTimeout()); + assertEquals(1, instance.getHealthChecks().size()); + } + + @Test + void addHealthCheck_ThrowsAnException_WhenTheNameIsNull() { + ContainerInstance instance = deploymentNode.add(database); + + try { + instance.addHealthCheck(null, "http://localhost"); + fail(); + } catch (IllegalArgumentException iae) { + assertEquals("The name must not be null or empty.", iae.getMessage()); + } + } + + @Test + void addHealthCheck_ThrowsAnException_WhenTheNameIsEmpty() { + ContainerInstance instance = deploymentNode.add(database); + + try { + instance.addHealthCheck(" ", "http://localhost"); + fail(); + } catch (IllegalArgumentException iae) { + assertEquals("The name must not be null or empty.", iae.getMessage()); + } + } + + @Test + void addHealthCheck_ThrowsAnException_WhenTheUrlIsNull() { + ContainerInstance instance = deploymentNode.add(database); + + try { + instance.addHealthCheck("Name", null); + fail(); + } catch (IllegalArgumentException iae) { + assertEquals("The URL must not be null or empty.", iae.getMessage()); + } + } + + @Test + void addHealthCheck_ThrowsAnException_WhenTheUrlIsEmpty() { + ContainerInstance instance = deploymentNode.add(database); + + try { + instance.addHealthCheck("Name", " "); + fail(); + } catch (IllegalArgumentException iae) { + assertEquals("The URL must not be null or empty.", iae.getMessage()); + } + } + + @Test + void addHealthCheck_ThrowsAnException_WhenTheUrlIsInvalid() { + ContainerInstance instance = deploymentNode.add(database); + + try { + instance.addHealthCheck("Name", "localhost"); + fail(); + } catch (IllegalArgumentException iae) { + assertEquals("localhost is not a valid URL.", iae.getMessage()); + } + } + + @Test + void addHealthCheck_ThrowsAnException_WhenTheIntervalIsLessThanZero() { + ContainerInstance instance = deploymentNode.add(database); + + try { + instance.addHealthCheck("Name", "https://localhost", -1, 0); + fail(); + } catch (IllegalArgumentException iae) { + assertEquals("The polling interval must be zero or a positive integer.", iae.getMessage()); + } + } + + @Test + void addHealthCheck_ThrowsAnException_WhenTheTimeoutIsLessThanZero() { + ContainerInstance instance = deploymentNode.add(database); + + try { + instance.addHealthCheck("Name", "https://localhost", 60, -1); + fail(); + } catch (IllegalArgumentException iae) { + assertEquals("The timeout must be zero or a positive integer.", iae.getMessage()); + } + } + +} \ No newline at end of file diff --git a/structurizr-core/src/test/java/com/structurizr/model/ContainerTests.java b/structurizr-core/src/test/java/com/structurizr/model/ContainerTests.java new file mode 100644 index 000000000..f9599c2c1 --- /dev/null +++ b/structurizr-core/src/test/java/com/structurizr/model/ContainerTests.java @@ -0,0 +1,120 @@ +package com.structurizr.model; + +import com.structurizr.AbstractWorkspaceTestBase; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +public class ContainerTests extends AbstractWorkspaceTestBase { + + private SoftwareSystem softwareSystem = model.addSoftwareSystem("System", "Description"); + private Container container = softwareSystem.addContainer("Container", "Description", "Some technology"); + + @Test + void technologyProperty() { + assertEquals("Some technology", container.getTechnology()); + + container.setTechnology("Some other technology"); + assertEquals("Some other technology", container.getTechnology()); + } + + @Test + void getCanonicalName() { + assertEquals("Container://System.Container", container.getCanonicalName()); + } + + @Test + void getCanonicalName_WhenNameContainsSlashAndDotCharacters() { + container = softwareSystem.addContainer("Name1/.Name2", "Description", "Some technology"); + + assertEquals("Container://System.Name1Name2", container.getCanonicalName()); + } + + @Test + void getParent_ReturnsTheParentSoftwareSystem() { + assertEquals(softwareSystem, container.getParent()); + } + + @Test + void getSoftwareSystem_ReturnsTheParentSoftwareSystem() { + assertEquals(softwareSystem, container.getSoftwareSystem()); + } + + @Test + void removeTags_DoesNotRemoveRequiredTags() { + assertTrue(container.getTags().contains(Tags.ELEMENT)); + assertTrue(container.getTags().contains(Tags.CONTAINER)); + + container.removeTag(Tags.CONTAINER); + container.removeTag(Tags.ELEMENT); + + assertTrue(container.getTags().contains(Tags.ELEMENT)); + assertTrue(container.getTags().contains(Tags.CONTAINER)); + } + + @Test + void addComponent_ThrowsAnException_WhenANullNameIsSpecified() { + assertThrows(IllegalArgumentException.class, () -> { + container.addComponent(null, ""); + }); + } + + @Test + void addComponent_ThrowsAnException_WhenAnEmptyNameIsSpecified() { + assertThrows(IllegalArgumentException.class, () -> { + container.addComponent(" ", ""); + }); + } + + @Test + void addComponent_ThrowsAnException_WhenAComponentWithTheSameNameAlreadyExists() { + container.addComponent("Component 1", ""); + try { + container.addComponent("Component 1", ""); + fail(); + } catch (IllegalArgumentException iae) { + assertEquals("A component named 'Component 1' already exists for this container.", iae.getMessage()); + } + } + + @Test + void addComponent_AddsAComponentWithTheSpecifiedNameAndDescription() { + Component component = container.addComponent("Name", "Description"); + assertTrue(container.getComponents().contains(component)); + assertEquals("Name", component.getName()); + assertEquals("Description", component.getDescription()); + assertNull(component.getTechnology()); + assertSame(container, component.getParent()); + } + + @Test + void addComponent_AddsAComponentWithTheSpecifiedNameAndDescriptionAndTechnology() { + Component component = container.addComponent("Name", "Description", "Technology"); + assertTrue(container.getComponents().contains(component)); + assertEquals("Name", component.getName()); + assertEquals("Description", component.getDescription()); + assertEquals("Technology", component.getTechnology()); + assertSame(container, component.getParent()); + } + + @Test + void getComponentWithName_ThrowsAnException_WhenANullNameIsSpecified() { + try { + container.getComponentWithName(null); + fail(); + } catch (IllegalArgumentException iae) { + assertEquals("A component name must be provided.", iae.getMessage()); + } + } + + @Test + void getComponentWithName_ThrowsAnException_WhenAnEmptyNameIsSpecified() { + try { + container.getComponentWithName(" "); + fail(); + } catch (IllegalArgumentException iae) { + assertEquals("A component name must be provided.", iae.getMessage()); + } + } + +} \ No newline at end of file diff --git a/structurizr-core/src/test/java/com/structurizr/model/CreateImpliedRelationshipsUnlessAnyRelationshipExistsStrategyTests.java b/structurizr-core/src/test/java/com/structurizr/model/CreateImpliedRelationshipsUnlessAnyRelationshipExistsStrategyTests.java new file mode 100644 index 000000000..aeb8b6839 --- /dev/null +++ b/structurizr-core/src/test/java/com/structurizr/model/CreateImpliedRelationshipsUnlessAnyRelationshipExistsStrategyTests.java @@ -0,0 +1,98 @@ +package com.structurizr.model; + +import com.structurizr.AbstractWorkspaceTestBase; +import org.junit.jupiter.api.Test; + +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.*; + +public class CreateImpliedRelationshipsUnlessAnyRelationshipExistsStrategyTests extends AbstractWorkspaceTestBase { + + @Test + void impliedRelationshipsAreCreated() { + SoftwareSystem a = model.addSoftwareSystem("A", ""); + Container aa = a.addContainer("AA", "", ""); + Component aaa = aa.addComponent("AAA", "", ""); + + SoftwareSystem b = model.addSoftwareSystem("B", ""); + Container bb = b.addContainer("BB", "", ""); + Component bbb = bb.addComponent("BBB", "", ""); + + model.setImpliedRelationshipsStrategy(new CreateImpliedRelationshipsUnlessAnyRelationshipExistsStrategy()); + + 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")); + + // AAA->BBB implies AAA->BB AAA->B AA->BBB AA->BB AA->B A->BBB A->BB A->B + assertTrue(aaa.hasEfferentRelationshipWith(bb, "Uses 1")); + assertTrue(aaa.hasEfferentRelationshipWith(b, "Uses 1")); + + assertTrue(aa.hasEfferentRelationshipWith(bbb, "Uses 1")); + assertTrue(aa.hasEfferentRelationshipWith(bb, "Uses 1")); + assertTrue(aa.hasEfferentRelationshipWith(b, "Uses 1")); + + assertTrue(a.hasEfferentRelationshipWith(bbb, "Uses 1")); + assertTrue(a.hasEfferentRelationshipWith(bb, "Uses 1")); + assertTrue(a.hasEfferentRelationshipWith(b, "Uses 1")); + + // all implied relationships with have a linked relationship, technology, and other properties unset + Set impliedRelationships = model.getRelationships(); + impliedRelationships.remove(explicitRelationship); + for (Relationship r : impliedRelationships) { + assertEquals(explicitRelationship.getId(), r.getLinkedRelationshipId()); + assertEquals("Technology", r.getTechnology()); + assertNull(r.getInteractionStyle()); + assertTrue(r.getTagsAsSet().isEmpty()); + } + + // and add another relationship with a different description + aaa.uses(bbb, "Uses 2"); + assertEquals(10, model.getRelationships().size()); // no change + } + + @Test + void impliedRelationshipsAreCreated_UnlessAnyRelationshipExists() { + SoftwareSystem a = model.addSoftwareSystem("A", ""); + Container aa = a.addContainer("AA", "", ""); + Component aaa = aa.addComponent("AAA", "", ""); + + SoftwareSystem b = model.addSoftwareSystem("B", ""); + Container bb = b.addContainer("BB", "", ""); + Component bbb = bb.addComponent("BBB", "", ""); + + model.setImpliedRelationshipsStrategy(new CreateImpliedRelationshipsUnlessAnyRelationshipExistsStrategy()); + + // add some higher level relationships + aa.uses(bb, "Uses"); + + assertEquals(4, model.getRelationships().size()); + assertTrue(aa.hasEfferentRelationshipWith(bb, "Uses")); + + // AA->BB implies AA->B A->BB A->B + assertTrue(aa.hasEfferentRelationshipWith(b, "Uses")); + assertTrue(a.hasEfferentRelationshipWith(bb, "Uses")); + assertTrue(a.hasEfferentRelationshipWith(b, "Uses")); + + // and now a lower level relationship, which will be propagated to parents that don't already have relationships between them + aaa.uses(bbb, "Uses 1"); + + assertEquals(9, model.getRelationships().size()); + assertTrue(aaa.hasEfferentRelationshipWith(bbb, "Uses 1")); + + // AAA->BBB implies AAA->BB AAA->B AA->BBB AA->BB AA->B A->BBB A->BB A->B + assertTrue(aaa.hasEfferentRelationshipWith(bb, "Uses 1")); + assertTrue(aaa.hasEfferentRelationshipWith(b, "Uses 1")); + + assertTrue(aa.hasEfferentRelationshipWith(bbb, "Uses 1")); + assertTrue(aa.hasEfferentRelationshipWith(bb, "Uses")); // existing relationship + assertTrue(aa.hasEfferentRelationshipWith(b, "Uses")); // existing relationship + + assertTrue(a.hasEfferentRelationshipWith(bbb, "Uses 1")); + assertTrue(a.hasEfferentRelationshipWith(bb, "Uses")); // existing relationship + assertTrue(a.hasEfferentRelationshipWith(b, "Uses")); // existing relationship + } + +} \ No newline at end of file diff --git a/structurizr-core/src/test/java/com/structurizr/model/CreateImpliedRelationshipsUnlessSameRelationshipExistsStrategyTests.java b/structurizr-core/src/test/java/com/structurizr/model/CreateImpliedRelationshipsUnlessSameRelationshipExistsStrategyTests.java new file mode 100644 index 000000000..1b8fda1ce --- /dev/null +++ b/structurizr-core/src/test/java/com/structurizr/model/CreateImpliedRelationshipsUnlessSameRelationshipExistsStrategyTests.java @@ -0,0 +1,70 @@ +package com.structurizr.model; + +import com.structurizr.AbstractWorkspaceTestBase; +import org.junit.jupiter.api.Test; + +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.*; + +public class CreateImpliedRelationshipsUnlessSameRelationshipExistsStrategyTests extends AbstractWorkspaceTestBase { + + @Test + void impliedRelationships_WhenNoSummaryRelationshipsExist() { + SoftwareSystem a = model.addSoftwareSystem("A", ""); + Container aa = a.addContainer("AA", "", ""); + Component aaa = aa.addComponent("AAA", "", ""); + + SoftwareSystem b = model.addSoftwareSystem("B", ""); + Container bb = b.addContainer("BB", "", ""); + Component bbb = bb.addComponent("BBB", "", ""); + + model.setImpliedRelationshipsStrategy(new CreateImpliedRelationshipsUnlessSameRelationshipExistsStrategy()); + + 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")); + + // all implied relationships with have a linked relationship, technology, and other properties unset + Set impliedRelationships = model.getRelationships(); + impliedRelationships.remove(explicitRelationship); + for (Relationship r : impliedRelationships) { + assertEquals(explicitRelationship.getId(), r.getLinkedRelationshipId()); + assertEquals("Technology", r.getTechnology()); + assertNull(r.getInteractionStyle()); + assertTrue(r.getTagsAsSet().isEmpty()); + } + + // AAA->BBB implies AAA->BB AAA->B AA->BBB AA->BB AA->B A->BBB A->BB A->B + assertTrue(aaa.hasEfferentRelationshipWith(bb, "Uses 1")); + assertTrue(aaa.hasEfferentRelationshipWith(b, "Uses 1")); + + assertTrue(aa.hasEfferentRelationshipWith(bbb, "Uses 1")); + assertTrue(aa.hasEfferentRelationshipWith(bb, "Uses 1")); + assertTrue(aa.hasEfferentRelationshipWith(b, "Uses 1")); + + assertTrue(a.hasEfferentRelationshipWith(bbb, "Uses 1")); + assertTrue(a.hasEfferentRelationshipWith(bb, "Uses 1")); + assertTrue(a.hasEfferentRelationshipWith(b, "Uses 1")); + + // and add another relationship with a different description + aaa.uses(bbb, "Uses 2"); + + assertEquals(18, model.getRelationships().size()); + assertTrue(aaa.hasEfferentRelationshipWith(bbb, "Uses 2")); + + // AAA->BBB implies AAA->BB AAA->B AA->BBB AA->BB AA->B A->BBB A->BB A->B + assertTrue(aaa.hasEfferentRelationshipWith(bb, "Uses 2")); + assertTrue(aaa.hasEfferentRelationshipWith(b, "Uses 2")); + + assertTrue(aa.hasEfferentRelationshipWith(bbb, "Uses 2")); + assertTrue(aa.hasEfferentRelationshipWith(bb, "Uses 2")); + assertTrue(aa.hasEfferentRelationshipWith(b, "Uses 2")); + + assertTrue(a.hasEfferentRelationshipWith(bbb, "Uses 2")); + assertTrue(a.hasEfferentRelationshipWith(bb, "Uses 2")); + assertTrue(a.hasEfferentRelationshipWith(b, "Uses 2")); + } + +} \ No newline at end of file diff --git a/structurizr-core/src/test/java/com/structurizr/model/CustomElementTests.java b/structurizr-core/src/test/java/com/structurizr/model/CustomElementTests.java new file mode 100644 index 000000000..f90ef36a2 --- /dev/null +++ b/structurizr-core/src/test/java/com/structurizr/model/CustomElementTests.java @@ -0,0 +1,112 @@ +package com.structurizr.model; + +import com.structurizr.AbstractWorkspaceTestBase; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +public class CustomElementTests extends AbstractWorkspaceTestBase { + + @Test + void basicProperties() { + CustomElement element = model.addCustomElement("Name", "Type", "Description"); + assertEquals("Name", element.getName()); + assertEquals("Type", element.getMetadata()); + assertEquals("Description", element.getDescription()); + } + + @Test + void getCanonicalName() { + CustomElement element = model.addCustomElement("Name", "Type", "Description"); + assertEquals("Custom://Name", element.getCanonicalName()); + } + + @Test + void getCanonicalName_WhenNameContainsSlashAndDotCharacters() { + CustomElement element = model.addCustomElement("Name", "Type", "Description"); + element.setName("Name1/.Name2"); + assertEquals("Custom://Name1Name2", element.getCanonicalName()); + } + + @Test + void getParent_ReturnsNull() { + CustomElement element = model.addCustomElement("Name", "Type", "Description"); + assertNull(element.getParent()); + } + + @Test + void removeTags_DoesNotRemoveRequiredTags() { + CustomElement element = model.addCustomElement("Name", "Type", "Description"); + assertTrue(element.getTags().contains(Tags.ELEMENT)); + + element.removeTag(Tags.ELEMENT); + + assertTrue(element.getTags().contains(Tags.ELEMENT)); + } + + @Test + void uses_AddsARelationshipWhenTheDescriptionIsSpecified() { + CustomElement element1 = model.addCustomElement("Box 1"); + CustomElement element2 = model.addCustomElement("Box 2"); + + element1.uses(element2, "Uses"); + assertEquals(1, element1.getRelationships().size()); + + Relationship relationship = element1.getRelationships().iterator().next(); + assertSame(element1, relationship.getSource()); + assertSame(element2, relationship.getDestination()); + assertEquals("Uses", relationship.getDescription()); + assertNull(relationship.getTechnology()); + assertNull(relationship.getInteractionStyle()); + } + + @Test + void uses_AddsARelationshipWhenTheDescriptionAndTechnologyAreSpecified() { + CustomElement element1 = model.addCustomElement("Box 1"); + CustomElement element2 = model.addCustomElement("Box 2"); + + element1.uses(element2, "Uses", "Technology"); + assertEquals(1, element1.getRelationships().size()); + + Relationship relationship = element1.getRelationships().iterator().next(); + assertSame(element1, relationship.getSource()); + assertSame(element2, relationship.getDestination()); + assertEquals("Uses", relationship.getDescription()); + assertEquals("Technology", relationship.getTechnology()); + assertNull(relationship.getInteractionStyle()); + } + + @Test + void uses_AddsARelationshipWhenTheDescriptionAndTechnologyAndInteractionStyleAreSpecified() { + CustomElement element1 = model.addCustomElement("Box 1"); + CustomElement element2 = model.addCustomElement("Box 2"); + + element1.uses(element2, "Uses", "Technology", InteractionStyle.Asynchronous); + assertEquals(1, element1.getRelationships().size()); + + Relationship relationship = element1.getRelationships().iterator().next(); + assertSame(element1, relationship.getSource()); + assertSame(element2, relationship.getDestination()); + assertEquals("Uses", relationship.getDescription()); + assertEquals("Technology", relationship.getTechnology()); + assertEquals(InteractionStyle.Asynchronous, relationship.getInteractionStyle()); + } + + @Test + void 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"}); + assertEquals(1, element1.getRelationships().size()); + + Relationship relationship = element1.getRelationships().iterator().next(); + assertSame(element1, relationship.getSource()); + assertSame(element2, relationship.getDestination()); + assertEquals("Uses", relationship.getDescription()); + assertEquals("Technology", relationship.getTechnology()); + assertEquals(InteractionStyle.Asynchronous, relationship.getInteractionStyle()); + assertEquals("Relationship,Asynchronous,Tag 1,Tag 2", relationship.getTags()); + } + +} \ No newline at end of file diff --git a/structurizr-core/src/test/java/com/structurizr/model/DefaultImpliedRelationshipsStrategyTests.java b/structurizr-core/src/test/java/com/structurizr/model/DefaultImpliedRelationshipsStrategyTests.java new file mode 100644 index 000000000..7f0e261ae --- /dev/null +++ b/structurizr-core/src/test/java/com/structurizr/model/DefaultImpliedRelationshipsStrategyTests.java @@ -0,0 +1,31 @@ +package com.structurizr.model; + +import com.structurizr.AbstractWorkspaceTestBase; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class DefaultImpliedRelationshipsStrategyTests extends AbstractWorkspaceTestBase { + + @Test + void createImpliedRelationships_DoesNothing() { + SoftwareSystem a = model.addSoftwareSystem("A", ""); + Container aa = a.addContainer("AA", "", ""); + Component aaa = aa.addComponent("AAA", "", ""); + + SoftwareSystem b = model.addSoftwareSystem("B", ""); + Container bb = b.addContainer("BB", "", ""); + Component bbb = bb.addComponent("BBB", "", ""); + + model.setImpliedRelationshipsStrategy(new DefaultImpliedRelationshipsStrategy()); + + aaa.uses(bbb, "Uses 1"); + aaa.uses(bbb, "Uses 2"); + + assertEquals(2, model.getRelationships().size()); + assertTrue(aaa.hasEfferentRelationshipWith(bbb, "Uses 1")); + assertTrue(aaa.hasEfferentRelationshipWith(bbb, "Uses 2")); + } + +} \ No newline at end of file diff --git a/structurizr-core/src/test/java/com/structurizr/model/DeploymentNodeTests.java b/structurizr-core/src/test/java/com/structurizr/model/DeploymentNodeTests.java new file mode 100644 index 000000000..303d3ded6 --- /dev/null +++ b/structurizr-core/src/test/java/com/structurizr/model/DeploymentNodeTests.java @@ -0,0 +1,280 @@ +package com.structurizr.model; + +import com.structurizr.AbstractWorkspaceTestBase; +import com.structurizr.util.MapUtils; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +public class DeploymentNodeTests extends AbstractWorkspaceTestBase { + + @Test + void getCanonicalName_WhenTheDeploymentNodeHasNoParent() { + DeploymentNode deploymentNode = model.addDeploymentNode("Ubuntu Server", "", ""); + + assertEquals("DeploymentNode://Default/Ubuntu Server", deploymentNode.getCanonicalName()); + } + + @Test + void getCanonicalName_WhenTheDeploymentNodeHasAParent() { + DeploymentNode l1 = model.addDeploymentNode("Level 1", "", ""); + DeploymentNode l2 = l1.addDeploymentNode("Level 2", "", ""); + DeploymentNode l3 = l2.addDeploymentNode("Level 3", "", ""); + + assertEquals("DeploymentNode://Default/Level 1", l1.getCanonicalName()); + assertEquals("DeploymentNode://Default/Level 1/Level 2", l2.getCanonicalName()); + assertEquals("DeploymentNode://Default/Level 1/Level 2/Level 3", l3.getCanonicalName()); + } + + @Test + void getParent_ReturnsTheParentDeploymentNode() { + DeploymentNode parent = model.addDeploymentNode("Parent", "", ""); + assertNull(parent.getParent()); + + DeploymentNode child = parent.addDeploymentNode("Child", "", ""); + child.setParent(parent); + assertSame(parent, child.getParent()); + } + + @Test + void getRequiredTags() { + DeploymentNode deploymentNode = new DeploymentNode(); + assertEquals(2, deploymentNode.getDefaultTags().size()); + assertTrue(deploymentNode.getDefaultTags().contains(Tags.ELEMENT)); + assertTrue(deploymentNode.getDefaultTags().contains(Tags.DEPLOYMENT_NODE)); + } + + @Test + void getTags() { + DeploymentNode deploymentNode = new DeploymentNode(); + deploymentNode.addTags("Tag 1", "Tag 2"); + assertEquals("Element,Deployment Node,Tag 1,Tag 2", deploymentNode.getTags()); + } + + @Test + void add_ThrowsAnException_WhenASoftwareSystemIsNotSpecified() { + try { + DeploymentNode deploymentNode = model.addDeploymentNode("Deployment Node", "Description", "Technology"); + deploymentNode.add((SoftwareSystem) null); + fail(); + } catch (IllegalArgumentException iae) { + assertEquals("A software system must be specified.", iae.getMessage()); + } + } + + @Test + void add_ThrowsAnException_WhenAContainerIsNotSpecified() { + try { + DeploymentNode deploymentNode = model.addDeploymentNode("Deployment Node", "Description", "Technology"); + deploymentNode.add((Container) null); + fail(); + } catch (IllegalArgumentException iae) { + assertEquals("A container must be specified.", iae.getMessage()); + } + } + + @Test + void add_AddsAContainerInstance_WhenAContainerIsSpecified() { + SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System", ""); + Container container = softwareSystem.addContainer("Container", "", ""); + DeploymentNode deploymentNode = model.addDeploymentNode("Deployment Node", "", ""); + ContainerInstance containerInstance = deploymentNode.add(container); + + assertNotNull(containerInstance); + assertSame(container, containerInstance.getContainer()); + assertTrue(deploymentNode.getContainerInstances().contains(containerInstance)); + assertEquals("ContainerInstance://Default/Deployment Node/Software System.Container[1]", containerInstance.getCanonicalName()); + } + + @Test + void addDeploymentNode_ThrowsAnException_WhenANameIsNotSpecified() { + try { + DeploymentNode parent = model.addDeploymentNode("Parent", "", ""); + parent.addDeploymentNode(null, "", ""); + fail(); + } catch (IllegalArgumentException iae) { + assertEquals("A name must be specified.", iae.getMessage()); + } + } + + @Test + void addDeploymentNode_AddsAChildDeploymentNode_WhenANameIsSpecified() { + DeploymentNode parent = model.addDeploymentNode("Parent", "", ""); + + DeploymentNode child = parent.addDeploymentNode("Child 1", "Description", "Technology"); + assertNotNull(child); + assertEquals("Child 1", child.getName()); + assertEquals("Description", child.getDescription()); + assertEquals("Technology", child.getTechnology()); + assertEquals("Default", child.getEnvironment()); + assertEquals("1", child.getInstances()); + assertTrue(child.getProperties().isEmpty()); + assertTrue(parent.getChildren().contains(child)); + + child = parent.addDeploymentNode("Child 2", "Description", "Technology", 4); + assertNotNull(child); + assertEquals("Child 2", child.getName()); + assertEquals("Description", child.getDescription()); + assertEquals("Technology", child.getTechnology()); + assertEquals("Default", child.getEnvironment()); + assertEquals("4", child.getInstances()); + assertTrue(child.getProperties().isEmpty()); + assertTrue(parent.getChildren().contains(child)); + + child = parent.addDeploymentNode("Child 3", "Description", "Technology", 4, MapUtils.create("name=value")); + assertNotNull(child); + assertEquals("Child 3", child.getName()); + assertEquals("Description", child.getDescription()); + assertEquals("Technology", child.getTechnology()); + assertEquals("Default", child.getEnvironment()); + assertEquals("4", child.getInstances()); + assertEquals(1, child.getProperties().size()); + assertEquals("value", child.getProperties().get("name")); + assertTrue(parent.getChildren().contains(child)); + } + + @Test + void uses_ThrowsAnException_WhenANullDestinationIsSpecified() { + try { + DeploymentNode deploymentNode = model.addDeploymentNode("Deployment Node", "", ""); + deploymentNode.uses((DeploymentNode) null, "", ""); + fail(); + } catch (IllegalArgumentException iae) { + assertEquals("The destination must be specified.", iae.getMessage()); + } + } + + @Test + 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"); + + assertNotNull(relationship); + assertSame(primaryNode, relationship.getSource()); + assertSame(secondaryNode, relationship.getDestination()); + assertEquals("Replicates data to", relationship.getDescription()); + assertEquals("Some technology", relationship.getTechnology()); + } + + @Test + void getDeploymentNodeWithName_ThrowsAnException_WhenANameIsNotSpecified() { + try { + DeploymentNode deploymentNode = new DeploymentNode(); + deploymentNode.getDeploymentNodeWithName(null); + fail(); + } catch (IllegalArgumentException iae) { + assertEquals("A name must be specified.", iae.getMessage()); + } + } + + @Test + void getDeploymentNodeWithName_ReturnsNull_WhenThereIsNoDeploymentNodeWithTheSpecifiedName() { + DeploymentNode deploymentNode = new DeploymentNode(); + assertNull(deploymentNode.getDeploymentNodeWithName("foo")); + } + + @Test + void getDeploymentNodeWithName_ReturnsTheNamedDeploymentNode_WhenThereIsADeploymentNodeWithTheSpecifiedName() { + DeploymentNode parent = model.addDeploymentNode("parent", "", ""); + DeploymentNode child = parent.addDeploymentNode("child", "", ""); + assertSame(child, parent.getDeploymentNodeWithName("child")); + } + + @Test + void getInfrastructureNodeWithName_ReturnsNull_WhenThereIsNoInfrastructureNodeWithTheSpecifiedName() { + DeploymentNode deploymentNode = new DeploymentNode(); + assertNull(deploymentNode.getInfrastructureNodeWithName("foo")); + } + + @Test + void getInfrastructureNodeWithName_ReturnsTheNamedDeploymentNode_WhenThereIsAInfrastructureNodeWithTheSpecifiedName() { + DeploymentNode parent = model.addDeploymentNode("parent", "", ""); + InfrastructureNode child = parent.addInfrastructureNode("child", "", ""); + assertSame(child, parent.getInfrastructureNodeWithName("child")); + } + + @Test + 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_WhenANegativeNumberIsSpecified() { + try { + DeploymentNode deploymentNode = new DeploymentNode(); + deploymentNode.setInstances("-1"); + fail(); + } catch (IllegalArgumentException iae) { + assertEquals("Number of instances must be a positive integer or a range.", iae.getMessage()); + } + } + + @Test + 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("x..N"); + fail(); + } catch (IllegalArgumentException iae) { + 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 diff --git a/structurizr-core/src/test/java/com/structurizr/model/ElementTests.java b/structurizr-core/src/test/java/com/structurizr/model/ElementTests.java new file mode 100644 index 000000000..05a151574 --- /dev/null +++ b/structurizr-core/src/test/java/com/structurizr/model/ElementTests.java @@ -0,0 +1,285 @@ +package com.structurizr.model; + +import com.structurizr.AbstractWorkspaceTestBase; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +public class ElementTests extends AbstractWorkspaceTestBase { + + @Test + void construction() { + Element element = model.addSoftwareSystem("Name", "Description"); + assertEquals("Name", element.getName()); + assertEquals("Description", element.getDescription()); + } + + @Test + void setName_ThrowsAnException_WhenANullValueIsSpecified() { + Element element = model.addSoftwareSystem("Name", "Description"); + try { + element.setName(null); + fail(); + } catch (Exception e) { + assertEquals("The name of an element must not be null or empty.", e.getMessage()); + } + } + + @Test + void setName_ThrowsAnException_WhenAnEmptyValueIsSpecified() { + Element element = model.addSoftwareSystem("Name", "Description"); + try { + element.setName(" "); + fail(); + } catch (Exception e) { + assertEquals("The name of an element must not be null or empty.", e.getMessage()); + } + } + + @Test + void hasEfferentRelationshipWith_ReturnsFalse_WhenANullElementIsSpecified() { + SoftwareSystem softwareSystem1 = model.addSoftwareSystem("System 1", ""); + assertFalse(softwareSystem1.hasEfferentRelationshipWith(null)); + } + + @Test + void hasEfferentRelationshipWith_ReturnsFalse_WhenTheSameElementIsSpecifiedAndNoCyclicRelationshipExists() { + SoftwareSystem softwareSystem1 = model.addSoftwareSystem("System 1", ""); + assertFalse(softwareSystem1.hasEfferentRelationshipWith(softwareSystem1)); + } + + @Test + void hasEfferentRelationshipWith_ReturnsTrue_WhenTheSameElementIsSpecifiedAndACyclicRelationshipExists() { + SoftwareSystem softwareSystem1 = model.addSoftwareSystem("System 1", ""); + softwareSystem1.uses(softwareSystem1, "uses"); + assertTrue(softwareSystem1.hasEfferentRelationshipWith(softwareSystem1)); + } + + @Test + void hasEfferentRelationshipWith_ReturnsTrue_WhenThereIsARelationship() { + SoftwareSystem softwareSystem1 = model.addSoftwareSystem("System 1", ""); + SoftwareSystem softwareSystem2 = model.addSoftwareSystem("System 2", ""); + softwareSystem1.uses(softwareSystem2, "uses"); + assertTrue(softwareSystem1.hasEfferentRelationshipWith(softwareSystem2)); + } + + @Test + void hasEfferentRelationshipWithElementAndDescription_ReturnsFalse_WhenANullElementIsSpecified() { + SoftwareSystem softwareSystem1 = model.addSoftwareSystem("System 1", ""); + assertFalse(softwareSystem1.hasEfferentRelationshipWith(null, null)); + } + + @Test + void hasEfferentRelationshipWithElementAndDescription_ReturnsFalse_WhenThereIsNotAMatchingRelationship() { + SoftwareSystem softwareSystem1 = model.addSoftwareSystem("System 1", ""); + SoftwareSystem softwareSystem2 = model.addSoftwareSystem("System 2", ""); + softwareSystem1.uses(softwareSystem2, "Uses"); + + assertFalse(softwareSystem1.hasEfferentRelationshipWith(softwareSystem2, "Does something with")); + } + + @Test + void hasEfferentRelationshipWithElementAndDescription_ReturnsTrue_WhenThereIsAMatchingRelationship() { + SoftwareSystem softwareSystem1 = model.addSoftwareSystem("System 1", ""); + SoftwareSystem softwareSystem2 = model.addSoftwareSystem("System 2", ""); + softwareSystem1.uses(softwareSystem2, "Uses"); + + assertTrue(softwareSystem1.hasEfferentRelationshipWith(softwareSystem2, "Uses")); + } + + @Test + void getEfferentRelationshipWith_ReturnsNull_WhenANullElementIsSpecified() { + SoftwareSystem softwareSystem1 = model.addSoftwareSystem("System 1", ""); + assertNull(softwareSystem1.getEfferentRelationshipWith(null)); + } + + @Test + void getEfferentRelationshipWith_ReturnsNull_WhenTheSameElementIsSpecifiedAndNoCyclicRelationshipExists() { + SoftwareSystem softwareSystem1 = model.addSoftwareSystem("System 1", ""); + assertNull(softwareSystem1.getEfferentRelationshipWith(softwareSystem1)); + } + + @Test + void getEfferentRelationshipWith_ReturnsCyclicRelationship_WhenTheSameElementIsSpecifiedAndACyclicRelationshipExists() { + SoftwareSystem softwareSystem1 = model.addSoftwareSystem("System 1", ""); + softwareSystem1.uses(softwareSystem1, "uses"); + + Relationship relationship = softwareSystem1.getEfferentRelationshipWith(softwareSystem1); + assertSame(softwareSystem1, relationship.getSource()); + assertEquals("uses", relationship.getDescription()); + assertSame(softwareSystem1, relationship.getDestination()); + } + + @Test + void getEfferentRelationshipWith_ReturnsTheRelationship_WhenThereIsARelationship() { + SoftwareSystem softwareSystem1 = model.addSoftwareSystem("System 1", ""); + SoftwareSystem softwareSystem2 = model.addSoftwareSystem("System 2", ""); + softwareSystem1.uses(softwareSystem2, "uses"); + + Relationship relationship = softwareSystem1.getEfferentRelationshipWith(softwareSystem2); + assertSame(softwareSystem1, relationship.getSource()); + assertEquals("uses", relationship.getDescription()); + assertSame(softwareSystem2, relationship.getDestination()); + } + + @Test + void hasAfferentRelationships_ReturnsFalse_WhenThereAreNoIncomingRelationships() { + SoftwareSystem softwareSystem1 = model.addSoftwareSystem("System 1", ""); + SoftwareSystem softwareSystem2 = model.addSoftwareSystem("System 2", ""); + softwareSystem1.uses(softwareSystem2, "Uses"); + + assertFalse(softwareSystem1.hasAfferentRelationships()); + } + + @Test + void hasAfferentRelationships_ReturnsTrue_WhenThereAreIncomingRelationships() { + SoftwareSystem softwareSystem1 = model.addSoftwareSystem("System 1", ""); + SoftwareSystem softwareSystem2 = model.addSoftwareSystem("System 2", ""); + softwareSystem1.uses(softwareSystem2, "Uses"); + + assertTrue(softwareSystem2.hasAfferentRelationships()); + } + + @Test + void addRelationship_DoesNothing_WhenTheSameRelationshipWithASoftwareSystemIsAddedMoreThanOnce() { + SoftwareSystem a = model.addSoftwareSystem("A", ""); + SoftwareSystem b = model.addSoftwareSystem("B", ""); + + Relationship relationship = a.uses(b, "Uses"); + assertNotNull(relationship); + assertEquals(1, model.getRelationships().size()); + + assertNull(a.uses(b, "Uses")); + assertEquals(1, model.getRelationships().size()); + + assertNull(a.uses(b, "Uses", "Technology")); + assertEquals(1, model.getRelationships().size()); + + assertNull(a.uses(b, "Uses", "Technology", InteractionStyle.Synchronous)); + assertEquals(1, model.getRelationships().size()); + } + + @Test + void addRelationship_DoesNothing_WhenTheSameRelationshipWithAContainerIsAddedMoreThanOnce() { + SoftwareSystem a = model.addSoftwareSystem("A", ""); + SoftwareSystem b = model.addSoftwareSystem("B", ""); + Container bb = b.addContainer("BB", "", ""); + + Relationship relationship = a.uses(bb, "Uses"); + assertNotNull(relationship); + assertEquals(1, model.getRelationships().size()); + + assertNull(a.uses(bb, "Uses")); + assertEquals(1, model.getRelationships().size()); + + assertNull(a.uses(bb, "Uses", "Technology")); + assertEquals(1, model.getRelationships().size()); + + assertNull(a.uses(bb, "Uses", "Technology", InteractionStyle.Synchronous)); + assertEquals(1, model.getRelationships().size()); + } + + @Test + void addRelationship_DoesNothing_WhenTheSameRelationshipWithAComponentIsAddedMoreThanOnce() { + SoftwareSystem a = model.addSoftwareSystem("A", ""); + SoftwareSystem b = model.addSoftwareSystem("B", ""); + Container bb = b.addContainer("BB", "", ""); + Component bbb = bb.addComponent("BBB", "", ""); + + Relationship relationship = a.uses(bbb, "Uses"); + assertNotNull(relationship); + assertEquals(1, model.getRelationships().size()); + + assertNull(a.uses(bbb, "Uses")); + assertEquals(1, model.getRelationships().size()); + + assertNull(a.uses(bbb, "Uses", "Technology")); + assertEquals(1, model.getRelationships().size()); + + assertNull(a.uses(bbb, "Uses", "Technology", InteractionStyle.Synchronous)); + assertEquals(1, model.getRelationships().size()); + } + + @Test + void addRelationship_DoesNothing_WhenTheSameRelationshipWithAPersonIsAddedMoreThanOnce() { + SoftwareSystem a = model.addSoftwareSystem("A", ""); + Person b = model.addPerson("B", ""); + + Relationship relationship = a.delivers(b, "Sends e-mail to"); + assertNotNull(relationship); + assertEquals(1, model.getRelationships().size()); + + assertNull(a.delivers(b, "Sends e-mail to")); + assertEquals(1, model.getRelationships().size()); + + assertNull(a.delivers(b, "Sends e-mail to", "Technology")); + assertEquals(1, model.getRelationships().size()); + + assertNull(a.delivers(b, "Sends e-mail to", "Technology", InteractionStyle.Synchronous)); + assertEquals(1, model.getRelationships().size()); + } + + @Test + void equals_ReturnsFalse_WhenTestedAgainstNull() { + SoftwareSystem softwareSystem = model.addSoftwareSystem("Name", "Description"); + assertNotEquals(softwareSystem, null); + } + + @Test + void equals_ReturnsFalse_WhenTheTwoObjectsAreDifferentTypes() { + SoftwareSystem softwareSystem = model.addSoftwareSystem("Name", "Description"); + assertNotEquals(softwareSystem, "hello world"); + } + + @Test + void equals_ReturnsTrue_WhenTestedAgainstItself() { + SoftwareSystem softwareSystem = model.addSoftwareSystem("Name", "Description"); + assertEquals(softwareSystem, softwareSystem); + } + + @Test + void equals_ReturnsFalse_WhenTheTwoObjectsAreDifferent() { + SoftwareSystem softwareSystemA = model.addSoftwareSystem("A", "Description"); + SoftwareSystem softwareSystemB = model.addSoftwareSystem("B", "Description"); + assertNotEquals(softwareSystemA, softwareSystemB); + } + + @Test + void equals_ReturnsFalse_WhenTheTwoElementsHaveTheSameCanonicalNameButAreDifferentTypes() { + SoftwareSystem softwareSystem = model.addSoftwareSystem("Name", "Description"); + Person person = model.addPerson("Name", "Description"); + assertNotEquals(softwareSystem, person); + } + + @Test + void setUrl() { + Element element = model.addSoftwareSystem("Name", "Description"); + element.setUrl("https://structurizr.com"); + assertEquals("https://structurizr.com", element.getUrl()); + } + + @Test + void setUrl_ThrowsAnIllegalArgumentException_WhenAnInvalidUrlIsSpecified() { + assertThrows(IllegalArgumentException.class, () -> { + Element element = model.addSoftwareSystem("Name", "Description"); + element.setUrl("htt://blah"); + }); + } + + @Test + void setUrl_ResetsTheUrl_WhenANullUrlIsSpecified() { + Element element = model.addSoftwareSystem("Name", "Description"); + element.setUrl("https://structurizr.com"); + element.setUrl(null); + assertNull(element.getUrl()); + } + + @Test + void setUrl_ResetsTheUrl_WhenAnEmptyUrlIsSpecified() { + Element element = model.addSoftwareSystem("Name", "Description"); + element.setUrl("https://structurizr.com"); + element.setUrl(" "); + assertNull(element.getUrl()); + } + +} \ No newline at end of file diff --git a/structurizr-core/src/test/java/com/structurizr/model/GroupableElementTests.java b/structurizr-core/src/test/java/com/structurizr/model/GroupableElementTests.java new file mode 100644 index 000000000..3c981f390 --- /dev/null +++ b/structurizr-core/src/test/java/com/structurizr/model/GroupableElementTests.java @@ -0,0 +1,48 @@ +package com.structurizr.model; + +import com.structurizr.AbstractWorkspaceTestBase; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +public class GroupableElementTests extends AbstractWorkspaceTestBase { + + @Test + void getGroup_ReturnsNullByDefault() { + Person element = model.addPerson("Person"); + assertNull(element.getGroup()); + } + + @Test + void setGroup() { + Person element = model.addPerson("Person"); + element.setGroup("Group"); + assertEquals("Group", element.getGroup()); + } + + @Test + void setGroup_TrimsWhiteSpace() { + Person element = model.addPerson("Person"); + element.setGroup(" Group "); + assertEquals("Group", element.getGroup()); + } + + @Test + void setGroup_HandlesEmptyAndNullValues() { + Person element = model.addPerson("Person"); + element.setGroup("Group"); + + element.setGroup(null); + assertNull(element.getGroup()); + + element.setGroup("Group"); + element.setGroup(""); + assertNull(element.getGroup()); + + element.setGroup("Group"); + element.setGroup(" "); + assertNull(element.getGroup()); + } + +} \ No newline at end of file diff --git a/structurizr-core/src/test/java/com/structurizr/model/HttpHealthCheckTests.java b/structurizr-core/src/test/java/com/structurizr/model/HttpHealthCheckTests.java new file mode 100644 index 000000000..57935806b --- /dev/null +++ b/structurizr-core/src/test/java/com/structurizr/model/HttpHealthCheckTests.java @@ -0,0 +1,74 @@ +package com.structurizr.model; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; + +public class HttpHealthCheckTests { + + private HttpHealthCheck healthCheck; + + @Test + void defaultConstructorExists() { + // the default constructor is used when deserializing from JSON + healthCheck = new HttpHealthCheck(); + } + + @Test + void construction() { + healthCheck = new HttpHealthCheck("Name", "http://localhost", 120, 1000); + assertEquals("Name", healthCheck.getName()); + assertEquals("http://localhost", healthCheck.getUrl()); + assertEquals(120, healthCheck.getInterval()); + assertEquals(1000, healthCheck.getTimeout()); + } + + @Test + void addHeader() { + healthCheck = new HttpHealthCheck(); + healthCheck.addHeader("Name", "Value"); + assertEquals("Value", healthCheck.getHeaders().get("Name")); + } + + @Test + void addHeader_ThrowsAnException_WhenTheHeaderNameIsNull() { + healthCheck = new HttpHealthCheck(); + try { + healthCheck.addHeader(null, "value"); + fail(); + } catch (IllegalArgumentException iae) { + assertEquals("The header name must not be null or empty.", iae.getMessage()); + } + } + + @Test + void addHeader_ThrowsAnException_WhenTheHeaderNameIsEmpty() { + healthCheck = new HttpHealthCheck(); + try { + healthCheck.addHeader(" ", "value"); + fail(); + } catch (IllegalArgumentException iae) { + assertEquals("The header name must not be null or empty.", iae.getMessage()); + } + } + + @Test + void addHeader_ThrowsAnException_WhenTheHeaderValueIsNull() { + healthCheck = new HttpHealthCheck(); + try { + healthCheck.addHeader("Name", null); + fail(); + } catch (IllegalArgumentException iae) { + assertEquals("The header value must not be null.", iae.getMessage()); + } + } + + @Test + void addHeader_DoesNotThrowAnException_WhenTheHeaderValueIsEmpty() { + healthCheck = new HttpHealthCheck(); + healthCheck.addHeader("Name", ""); + assertEquals("", healthCheck.getHeaders().get("Name")); + } + +} \ No newline at end of file diff --git a/structurizr-core/src/test/java/com/structurizr/model/InfrastructureNodeTests.java b/structurizr-core/src/test/java/com/structurizr/model/InfrastructureNodeTests.java new file mode 100644 index 000000000..8ec8bb364 --- /dev/null +++ b/structurizr-core/src/test/java/com/structurizr/model/InfrastructureNodeTests.java @@ -0,0 +1,41 @@ +package com.structurizr.model; + +import com.structurizr.AbstractWorkspaceTestBase; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +public class InfrastructureNodeTests extends AbstractWorkspaceTestBase { + + @Test + void getCanonicalName() { + DeploymentNode deploymentNode = model.addDeploymentNode("Amazon Web Services", "", ""); + InfrastructureNode infrastructureNode = deploymentNode.addInfrastructureNode("Route 53", "", ""); + + assertEquals("InfrastructureNode://Default/Amazon Web Services/Route 53", infrastructureNode.getCanonicalName()); + } + + @Test + void getParent_ReturnsTheParentDeploymentNode() { + DeploymentNode parent = model.addDeploymentNode("Parent", "", ""); + InfrastructureNode child = parent.addInfrastructureNode("Child", "", ""); + child.setParent(parent); + assertSame(parent, child.getParent()); + } + + @Test + void getRequiredTags() { + InfrastructureNode infrastructureNode = new InfrastructureNode(); + assertEquals(2, infrastructureNode.getDefaultTags().size()); + assertTrue(infrastructureNode.getDefaultTags().contains(Tags.ELEMENT)); + assertTrue(infrastructureNode.getDefaultTags().contains(Tags.INFRASTRUCTURE_NODE)); + } + + @Test + void getTags() { + InfrastructureNode infrastructureNode = new InfrastructureNode(); + infrastructureNode.addTags("Tag 1", "Tag 2"); + assertEquals("Element,Infrastructure Node,Tag 1,Tag 2", infrastructureNode.getTags()); + } + +} \ No newline at end of file diff --git a/structurizr-core/src/test/java/com/structurizr/model/ModelItemTests.java b/structurizr-core/src/test/java/com/structurizr/model/ModelItemTests.java new file mode 100644 index 000000000..783a1c548 --- /dev/null +++ b/structurizr-core/src/test/java/com/structurizr/model/ModelItemTests.java @@ -0,0 +1,257 @@ +package com.structurizr.model; + +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 ModelItemTests extends AbstractWorkspaceTestBase { + + @Test + void construction() { + Element element = model.addSoftwareSystem("Name", "Description"); + assertEquals("Name", element.getName()); + assertEquals("Description", element.getDescription()); + } + + @Test + void getTags_WhenThereAreNoTags() { + Element element = model.addSoftwareSystem("Name", "Description"); + assertEquals("Element,Software System", element.getTags()); + } + + @Test + 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 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 setTags_DoesNotDoAnything_WhenPassedNull() { + Element element = model.addSoftwareSystem("Name", "Description"); + element.setTags(null); + assertEquals("Element,Software System", element.getTags()); + } + + @Test + void addTags_DoesNotDoAnything_WhenPassedNull() { + Element element = model.addSoftwareSystem("Name", "Description"); + element.addTags((String) null); + assertEquals("Element,Software System", element.getTags()); + + element.addTags(null, null, null); + assertEquals("Element,Software System", element.getTags()); + } + + @Test + 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 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 removeTags() { + Element element = model.addSoftwareSystem("Name", "Description"); + element.addTags("tag1", "tag2"); + assertTrue(element.removeTag("tag1"), "Remove an existing tag returns true"); + assertFalse(element.hasTag("tag1"), "Tag has been removed"); + + assertFalse(element.removeTag("no-such-tag"), "Remove a non-existing tag returns false"); + assertFalse(element.removeTag("Element"), "Remove a required tag returns false"); + } + + @Test + void getProperties_ReturnsAnEmptyList_WhenNoPropertiesHaveBeenAdded() { + Element element = model.addSoftwareSystem("Name", "Description"); + assertEquals(0, element.getProperties().size()); + } + + @Test + void addProperty_ThrowsAnException_WhenTheNameIsNull() { + try { + Element element = model.addSoftwareSystem("Name", "Description"); + element.addProperty(null, "value"); + fail(); + } catch (IllegalArgumentException e) { + assertEquals("A property name must be specified.", e.getMessage()); + } + } + + @Test + void addProperty_ThrowsAnException_WhenTheNameIsEmpty() { + try { + Element element = model.addSoftwareSystem("Name", "Description"); + element.addProperty(" ", "value"); + fail(); + } catch (IllegalArgumentException e) { + assertEquals("A property name must be specified.", e.getMessage()); + } + } + + @Test + void addProperty_ThrowsAnException_WhenTheValueIsNull() { + try { + Element element = model.addSoftwareSystem("Name", "Description"); + element.addProperty("name", null); + fail(); + } catch (IllegalArgumentException e) { + assertEquals("A property value must be specified.", e.getMessage()); + } + } + + @Test + void addProperty_ThrowsAnException_WhenTheValueIsEmpty() { + try { + Element element = model.addSoftwareSystem("Name", "Description"); + element.addProperty("name", " "); + fail(); + } catch (IllegalArgumentException e) { + assertEquals("A property value must be specified.", e.getMessage()); + } + } + + @Test + 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 setProperties_DoesNothing_WhenNullIsSpecified() { + Element element = model.addSoftwareSystem("Name", "Description"); + element.setProperties(null); + assertEquals(0, element.getProperties().size()); + } + + @Test + void setProperties_SetsTheProperties_WhenANonEmptyMapIsSpecified() { + Element element = model.addSoftwareSystem("Name", "Description"); + Map properties = new HashMap<>(); + properties.put("name", "value"); + element.setProperties(properties); + assertEquals(1, element.getProperties().size()); + assertEquals("value", element.getProperties().get("name")); + } + + @Test + void addPerspective_ThrowsAnException_WhenANameIsNotSpecified() { + try { + Element element = model.addSoftwareSystem("Name", "Description"); + element.addPerspective(null, null); + fail(); + } catch (IllegalArgumentException iae) { + assertEquals("A name must be specified.", iae.getMessage()); + } + } + + @Test + void addPerspective_ThrowsAnException_WhenAnEmptyNameIsSpecified() { + try { + Element element = model.addSoftwareSystem("Name", "Description"); + element.addPerspective(" ", null); + fail(); + } catch (IllegalArgumentException iae) { + assertEquals("A name must be specified.", iae.getMessage()); + } + } + + @Test + void addPerspective_ThrowsAnException_WhenADescriptionIsNotSpecified() { + try { + Element element = model.addSoftwareSystem("Name", "Description"); + element.addPerspective("Security", null); + fail(); + } catch (IllegalArgumentException iae) { + assertEquals("A description must be specified.", iae.getMessage()); + } + } + + @Test + void addPerspective_ThrowsAnException_WhenAnEmptyDescriptionIsSpecified() { + try { + Element element = model.addSoftwareSystem("Name", "Description"); + element.addPerspective("Security", " "); + fail(); + } catch (IllegalArgumentException iae) { + assertEquals("A description must be specified.", iae.getMessage()); + } + } + + @Test + void addPerspective_AddsAPerspective() { + Element element = model.addSoftwareSystem("Name", "Description"); + + 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 + void addPerspective_ThrowsAnException_WhenTheNamedPerspectiveAlreadyExists() { + try { + Element element = model.addSoftwareSystem("Name", "Description"); + element.addPerspective("Security", "Data is encrypted at rest."); + element.addPerspective("Security", "Data is encrypted at rest."); + fail(); + } catch (IllegalArgumentException iae) { + assertEquals("A perspective named \"Security\" already exists.", iae.getMessage()); + } + } + + @Test + void setUrl_AcceptsAUrl() { + Element element = model.addSoftwareSystem("Name"); + element.setUrl("https://structurizr.com"); + assertEquals("https://structurizr.com", element.getUrl()); + } + + @Test + 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 diff --git a/structurizr-core/src/test/java/com/structurizr/model/ModelTests.java b/structurizr-core/src/test/java/com/structurizr/model/ModelTests.java new file mode 100644 index 000000000..3bda19147 --- /dev/null +++ b/structurizr-core/src/test/java/com/structurizr/model/ModelTests.java @@ -0,0 +1,829 @@ +package com.structurizr.model; + +import com.structurizr.AbstractWorkspaceTestBase; +import org.junit.jupiter.api.Test; + +import java.util.Collections; + +import static org.junit.jupiter.api.Assertions.*; + +public class ModelTests extends AbstractWorkspaceTestBase { + + @Test + void addSoftwareSystem_ThrowsAnException_WhenANullNameIsSpecified() { + assertThrows(IllegalArgumentException.class, () -> { + model.addSoftwareSystem(null, ""); + }); + } + + @Test + void addSoftwareSystem_ThrowsAnException_WhenAnEmptyNameIsSpecified() { + assertThrows(IllegalArgumentException.class, () -> { + model.addSoftwareSystem(" ", ""); + }); + } + + @Test + void addPerson_ThrowsAnException_WhenANullNameIsSpecified() { + assertThrows(IllegalArgumentException.class, () -> { + model.addPerson(null, ""); + }); + } + + @Test + void addPerson_ThrowsAnException_WhenAnEmptyNameIsSpecified() { + assertThrows(IllegalArgumentException.class, () -> { + model.addPerson(" ", ""); + }); + } + + @Test + void addSoftwareSystem_AddsTheSoftwareSystem_WhenASoftwareSystemDoesNotExistWithTheSameName() { + assertTrue(model.getSoftwareSystems().isEmpty()); + SoftwareSystem softwareSystem = model.addSoftwareSystem("System A", "Some description"); + assertEquals(1, model.getSoftwareSystems().size()); + + assertEquals("System A", softwareSystem.getName()); + assertEquals("Some description", softwareSystem.getDescription()); + assertEquals("1", softwareSystem.getId()); + assertSame(softwareSystem, model.getSoftwareSystems().iterator().next()); + } + + @Test + void addSoftwareSystem_ThrowsAnException_WhenASoftwareSystemExistsWithTheSameName() { + SoftwareSystem softwareSystem = model.addSoftwareSystem("System A", "Some description"); + assertEquals(1, model.getSoftwareSystems().size()); + + try { + model.addSoftwareSystem("System A", "Description"); + fail(); + } catch (Exception e) { + assertEquals("A top-level element named 'System A' already exists.", e.getMessage()); + } + } + + @Test + void addSoftwareSystemWithoutSpecifyingLocation_AddsTheSoftwareSystem_WhenASoftwareSystemDoesNotExistWithTheSameName() { + assertTrue(model.getSoftwareSystems().isEmpty()); + SoftwareSystem softwareSystem = model.addSoftwareSystem("System A", "Some description"); + assertEquals(1, model.getSoftwareSystems().size()); + + assertEquals("System A", softwareSystem.getName()); + assertEquals("Some description", softwareSystem.getDescription()); + assertEquals("1", softwareSystem.getId()); + assertSame(softwareSystem, model.getSoftwareSystems().iterator().next()); + } + + @Test + void addPerson_AddsThePerson_WhenAPersonDoesNotExistWithTheSameName() { + assertTrue(model.getPeople().isEmpty()); + Person person = model.addPerson("Some internal user", "Some description"); + assertEquals(1, model.getPeople().size()); + + assertEquals("Some internal user", person.getName()); + assertEquals("Some description", person.getDescription()); + assertEquals("1", person.getId()); + assertSame(person, model.getPeople().iterator().next()); + } + + @Test + void addPerson_ThrowsAnException_WhenAPersonExistsWithTheSameName() { + Person person = model.addPerson("Admin User", "Description"); + assertEquals(1, model.getPeople().size()); + + try { + model.addPerson("Admin User", "Description"); + fail(); + } catch (Exception e) { + assertEquals("A top-level element named 'Admin User' already exists.", e.getMessage()); + } + } + + @Test + void addPerson_AddsThePersonWithoutSpecifyingTheLocation_WhenAPersonDoesNotExistWithTheSameName() { + assertTrue(model.getPeople().isEmpty()); + Person person = model.addPerson("Some internal user", "Some description"); + assertEquals(1, model.getPeople().size()); + + assertEquals("Some internal user", person.getName()); + assertEquals("Some description", person.getDescription()); + assertEquals("1", person.getId()); + assertSame(person, model.getPeople().iterator().next()); + } + + @Test + void getElement_ReturnsNull_WhenAnElementWithTheSpecifiedIdDoesNotExist() { + assertNull(model.getElement("100")); + } + + @Test + void getElement_ReturnsAnElement_WhenAnElementWithTheSpecifiedIdDoesExist() { + 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("Name", "Description"); + assertFalse(model.contains(softwareSystem)); + } + + @Test + void contains_ReturnsTrue_WhenTheSpecifiedElementIsInTheModel() { + SoftwareSystem softwareSystem = model.addSoftwareSystem("Name", "Description"); + assertTrue(model.contains(softwareSystem)); + } + + @Test + void getSoftwareSystemWithName_ReturnsNull_WhenASoftwareSystemWithTheSpecifiedNameDoesNotExist() { + assertNull(model.getSoftwareSystemWithName("System X")); + } + + @Test + void getSoftwareSystemWithName_ReturnsASoftwareSystem_WhenASoftwareSystemWithTheSpecifiedNameExists() { + SoftwareSystem softwareSystem = model.addSoftwareSystem("System A", "Description"); + assertSame(softwareSystem, model.getSoftwareSystemWithName("System A")); + } + + @Test + void getSoftwareSystemWithId_ThrowsAnException_WhenPassedANullId() { + try { + model.getSoftwareSystemWithId(null); + fail(); + } catch (IllegalArgumentException iae) { + assertEquals("A software system ID must be specified.", iae.getMessage()); + } + } + + @Test + void getSoftwareSystemWithId_ThrowsAnException_WhenPassedAnEmptyId() { + try { + model.getSoftwareSystemWithId(" "); + fail(); + } catch (IllegalArgumentException iae) { + assertEquals("A software system ID must be specified.", iae.getMessage()); + } + } + + @Test + void getSoftwareSystemWithId_ReturnsNull_WhenASoftwareSystemWithTheSpecifiedIdDoesNotExist() { + assertNull(model.getSoftwareSystemWithId("100")); + } + + @Test + void getSoftwareSystemWithId_ReturnsASoftwareSystem_WhenASoftwareSystemWithTheSpecifiedIdDoesExist() { + SoftwareSystem softwareSystem = model.addSoftwareSystem("System A", "Description"); + assertSame(softwareSystem, model.getSoftwareSystemWithId(softwareSystem.getId())); + } + + @Test + void getPersonWithName_ReturnsNull_WhenAPersonWithTheSpecifiedNameDoesNotExist() { + assertNull(model.getPersonWithName("Admin User")); + } + + @Test + void getPersonWithName_ReturnsAPerson_WhenAPersonWithTheSpecifiedNameExists() { + Person person = model.addPerson("Admin User", "Description"); + assertSame(person, model.getPersonWithName("Admin User")); + } + + @Test + void getRelationship_ThrowsAnException_WhenPassedANullId() { + try { + model.getRelationship(null); + fail(); + } catch (IllegalArgumentException iae) { + assertEquals("A relationship ID must be specified.", iae.getMessage()); + } + } + + @Test + void getRelationship_ThrowsAnException_WhenPassedAnEmptyId() { + try { + model.getRelationship(" "); + fail(); + } catch (IllegalArgumentException iae) { + assertEquals("A relationship ID must be specified.", iae.getMessage()); + } + } + + @Test + void addRelationship_AddsARelationshipWithTheSpecifiedDescriptionAndTechnologyAndInteractionStyle() { + SoftwareSystem a = model.addSoftwareSystem("A", ""); + SoftwareSystem b = model.addSoftwareSystem("B", ""); + Relationship relationship = model.addRelationship(a, b, "Uses", "HTTPS", InteractionStyle.Asynchronous); + + assertSame(a, relationship.getSource()); + assertSame(b, relationship.getDestination()); + assertEquals("Uses", relationship.getDescription()); + assertEquals("HTTPS", relationship.getTechnology()); + assertEquals(InteractionStyle.Asynchronous, relationship.getInteractionStyle()); + + assertTrue(model.getRelationships().contains(relationship)); + } + + @Test + void addRelationship_DisallowsTheSameRelationshipToBeAddedMoreThanOnce() { + SoftwareSystem element1 = model.addSoftwareSystem("Element 1", "Description"); + SoftwareSystem element2 = model.addSoftwareSystem("Element 2", "Description"); + Relationship relationship1 = element1.uses(element2, "Uses", ""); + Relationship relationship2 = element1.uses(element2, "Uses", ""); + assertTrue(element1.has(relationship1)); + assertNull(relationship2); + assertEquals(1, element1.getRelationships().size()); + } + + @Test + 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", ""); + Relationship relationship2 = element1.uses(element2, "Uses in another way", ""); + assertTrue(element1.has(relationship1)); + assertTrue(element1.has(relationship2)); + assertEquals(2, element1.getRelationships().size()); + } + + @Test + void addRelationship_ThrowsAnException_WhenTheDestinationIsAChildOfTheSource() { + SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System", ""); + Container container = softwareSystem.addContainer("Container", "", ""); + Component component = container.addComponent("Component", "", ""); + + try { + softwareSystem.uses(container, "Uses"); + fail(); + } catch (IllegalArgumentException iae) { + assertEquals("Relationships cannot be added between parents and children.", iae.getMessage()); + assertEquals(0, softwareSystem.getRelationships().size()); + } + + try { + container.uses(component, "Uses"); + fail(); + } catch (IllegalArgumentException iae) { + assertEquals("Relationships cannot be added between parents and children.", iae.getMessage()); + assertEquals(0, softwareSystem.getRelationships().size()); + } + + try { + softwareSystem.uses(component, "Uses"); + fail(); + } catch (IllegalArgumentException iae) { + assertEquals("Relationships cannot be added between parents and children.", iae.getMessage()); + assertEquals(0, softwareSystem.getRelationships().size()); + } + } + + @Test + void addRelationship_ThrowsAnException_WhenTheSourceIsAChildOfTheDestination() { + SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System", ""); + Container container = softwareSystem.addContainer("Container", "", ""); + Component component = container.addComponent("Component", "", ""); + + try { + container.uses(softwareSystem, "Uses"); + fail(); + } catch (IllegalArgumentException iae) { + assertEquals("Relationships cannot be added between parents and children.", iae.getMessage()); + assertEquals(0, softwareSystem.getRelationships().size()); + } + + try { + component.uses(container, "Uses"); + fail(); + } catch (IllegalArgumentException iae) { + assertEquals("Relationships cannot be added between parents and children.", iae.getMessage()); + assertEquals(0, softwareSystem.getRelationships().size()); + } + + try { + component.uses(softwareSystem, "Uses"); + fail(); + } catch (IllegalArgumentException iae) { + assertEquals("Relationships cannot be added between parents and children.", iae.getMessage()); + assertEquals(0, softwareSystem.getRelationships().size()); + } + } + + @Test + void modifyRelationship_ThrowsAnException_WhenARelationshipIsNotSpecified() { + try { + model.modifyRelationship(null, "Uses", "Technology"); + fail(); + } catch (IllegalArgumentException iae) { + assertEquals("A relationship must be specified.", iae.getMessage()); + } + } + + @Test + void modifyRelationship_ModifiesAnExistingRelationship_WhenThatRelationshipDoesNotAlreadyExist() { + SoftwareSystem element1 = model.addSoftwareSystem("Element 1", "Description"); + SoftwareSystem element2 = model.addSoftwareSystem("Element 2", "Description"); + Relationship relationship = element1.uses(element2, "", ""); + + model.modifyRelationship(relationship, "Uses", "Technology"); + assertEquals("Uses", relationship.getDescription()); + assertEquals("Technology", relationship.getTechnology()); + } + + @Test + 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"); + + try { + model.modifyRelationship(relationship, "Uses", "Technology"); + fail(); + } catch (IllegalArgumentException iae) { + assertEquals("A relationship named \"Uses\" between \"Element 1\" and \"Element 2\" already exists.", iae.getMessage()); + } + } + + @Test + void addSoftwareSystemInstance_ThrowsAnException_WhenANullSoftwareSystemIsSpecified() { + try { + DeploymentNode deploymentNode = model.addDeploymentNode("Deployment Node", "Description", "Technology"); + deploymentNode.add((SoftwareSystem) null); + fail(); + } catch (Exception e) { + assertEquals("A software system must be specified.", e.getMessage()); + } + } + + @Test + void addContainerInstance_ThrowsAnException_WhenANullContainerIsSpecified() { + try { + DeploymentNode deploymentNode = model.addDeploymentNode("Deployment Node", "Description", "Technology"); + deploymentNode.add((Container) null); + fail(); + } catch (Exception e) { + assertEquals("A container must be specified.", e.getMessage()); + } + } + + @Test + void addDeploymentNode_AddsADeploymentNode_WhenADeploymentEnvironmentIsNotSpecified() { + DeploymentNode deploymentNode = model.addDeploymentNode("Deployment Node", "Description", "Technology"); + + assertEquals("Deployment Node", deploymentNode.getName()); + assertEquals("Description", deploymentNode.getDescription()); + assertEquals("Technology", deploymentNode.getTechnology()); + assertEquals("Default", deploymentNode.getEnvironment()); + } + + @Test + void addDeploymentNode_AddsADeploymentNode_WhenADeploymentEnvironmentIsSpecified() { + DeploymentNode deploymentNode = model.addDeploymentNode("Development", "Deployment Node", "Description", "Technology"); + + assertEquals("Deployment Node", deploymentNode.getName()); + assertEquals("Description", deploymentNode.getDescription()); + assertEquals("Technology", deploymentNode.getTechnology()); + assertEquals("Development", deploymentNode.getEnvironment()); + } + + @Test + void addElementInstance_AddsElementInstancesAndReplicatesRelationshipsWithinTheDeploymentEnvironment() { + SoftwareSystem softwareSystem1 = model.addSoftwareSystem("Software System 1", "Description"); + Container container1 = softwareSystem1.addContainer("Container 1", "Description", "Technology"); + + SoftwareSystem softwareSystem2 = model.addSoftwareSystem("Software System 2", "Description"); + Container container2 = softwareSystem2.addContainer("Container 2", "Description", "Technology"); + + SoftwareSystem softwareSystem3 = model.addSoftwareSystem("Software System 3", "Description"); + Container container3 = softwareSystem3.addContainer("Container 3", "Description", "Technology"); + + SoftwareSystem softwareSystem4 = model.addSoftwareSystem("Software System 4", "Description"); + + container1.uses(container2, "Uses 1", "Technology 1", InteractionStyle.Synchronous); + container2.uses(container3, "Uses 2", "Technology 2", InteractionStyle.Asynchronous); + container3.uses(softwareSystem4, "Uses"); + + DeploymentNode developmentDeploymentNode = model.addDeploymentNode("Development", "Deployment Node", "Description", "Technology"); + ContainerInstance containerInstance1 = developmentDeploymentNode.add(container1); + ContainerInstance containerInstance2 = developmentDeploymentNode.add(container2); + ContainerInstance containerInstance3 = developmentDeploymentNode.add(container3); + SoftwareSystemInstance softwareSystemInstance = developmentDeploymentNode.add(softwareSystem4); + + // the following live element instances should not affect the relationships of the development element instances + DeploymentNode liveDeploymentNode = model.addDeploymentNode("Live", "Deployment Node", "Description", "Technology"); + liveDeploymentNode.add(container1); + liveDeploymentNode.add(container2); + liveDeploymentNode.add(container3); + liveDeploymentNode.add(softwareSystem4); + + assertEquals(1, containerInstance1.getRelationships().size()); + Relationship relationship = containerInstance1.getRelationships().iterator().next(); + assertSame(containerInstance1, relationship.getSource()); + assertSame(containerInstance2, relationship.getDestination()); + assertEquals("Uses 1", relationship.getDescription()); + assertEquals("Technology 1", relationship.getTechnology()); + assertEquals(InteractionStyle.Synchronous, relationship.getInteractionStyle()); + assertEquals("", relationship.getTags()); + + assertEquals(1, containerInstance2.getRelationships().size()); + relationship = containerInstance2.getRelationships().iterator().next(); + assertSame(containerInstance2, relationship.getSource()); + assertSame(containerInstance3, relationship.getDestination()); + assertEquals("Uses 2", relationship.getDescription()); + assertEquals("Technology 2", relationship.getTechnology()); + assertEquals(InteractionStyle.Asynchronous, relationship.getInteractionStyle()); + assertEquals("", relationship.getTags()); + + assertEquals(1, containerInstance3.getRelationships().size()); + relationship = containerInstance3.getRelationships().iterator().next(); + assertSame(containerInstance3, relationship.getSource()); + assertSame(softwareSystemInstance, relationship.getDestination()); + assertEquals("Uses", relationship.getDescription()); + assertNull(relationship.getTechnology()); + assertNull(relationship.getInteractionStyle()); + assertEquals("", relationship.getTags()); + } + + @Test + void addElementInstance_AddsElementInstancesAndReplicatesRelationshipsWithinTheDeploymentEnvironmentAndDefaultGroup() { + SoftwareSystem softwareSystem1 = model.addSoftwareSystem("Software System"); + Container api = softwareSystem1.addContainer("API"); + Container database = softwareSystem1.addContainer("Database"); + api.uses(database, "Uses"); + + DeploymentNode liveDeploymentNode = model.addDeploymentNode("Live", "Deployment Node", "Description", "Technology"); + ContainerInstance apiInstance1 = liveDeploymentNode.add(api); + ContainerInstance databaseInstance1 = liveDeploymentNode.add(database); + + ContainerInstance apiInstance2 = liveDeploymentNode.add(api); + ContainerInstance databaseInstance2 = liveDeploymentNode.add(database); + + assertEquals(2, apiInstance1.getRelationships().size()); + assertEquals(2, apiInstance2.getRelationships().size()); + + // apiInstance1 -> databaseInstance1 + Relationship relationship = apiInstance1.getEfferentRelationshipWith(databaseInstance1); + assertEquals("Uses", relationship.getDescription()); + + // apiInstance1 -> databaseInstance2 + relationship = apiInstance1.getEfferentRelationshipWith(databaseInstance2); + assertEquals("Uses", relationship.getDescription()); + + // apiInstance2 -> databaseInstance1 + relationship = apiInstance2.getEfferentRelationshipWith(databaseInstance1); + assertEquals("Uses", relationship.getDescription()); + + // apiInstance2 -> databaseInstance2 + relationship = apiInstance2.getEfferentRelationshipWith(databaseInstance2); + assertEquals("Uses", relationship.getDescription()); + } + + @Test + 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 + + SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System"); + Container api = softwareSystem.addContainer("API"); + Container database = softwareSystem.addContainer("Database"); + api.uses(database, "Uses"); + + DeploymentNode liveDeploymentNode = model.addDeploymentNode("Live", "Deployment Node", "Description", "Technology"); + ContainerInstance apiInstance1 = liveDeploymentNode.add(api, "Instance 1"); + ContainerInstance databaseInstance1 = liveDeploymentNode.add(database, "Instance 1"); + + ContainerInstance apiInstance2 = liveDeploymentNode.add(api, "Instance 2"); + ContainerInstance databaseInstance2 = liveDeploymentNode.add(database, "Instance 2"); + + assertEquals(1, apiInstance1.getRelationships().size()); + assertEquals(1, apiInstance2.getRelationships().size()); + + // apiInstance1 -> databaseInstance1 + Relationship relationship = apiInstance1.getEfferentRelationshipWith(databaseInstance1); + assertEquals("Uses", relationship.getDescription()); + + // apiInstance2 -> databaseInstance2 + relationship = apiInstance2.getEfferentRelationshipWith(databaseInstance2); + assertEquals("Uses", relationship.getDescription()); + } + + @Test + 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" + // - a shared container is added to "Shared" + + SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System"); + Container api = softwareSystem.addContainer("API"); + Container database = softwareSystem.addContainer("Database"); + Container cache = softwareSystem.addContainer("Cache"); + api.uses(database, "Uses"); + api.uses(cache, "Uses"); + + DeploymentNode liveDeploymentNode = model.addDeploymentNode("Live"); + ContainerInstance apiInstance1 = liveDeploymentNode.add(api, "Instance 1", "Shared"); + ContainerInstance databaseInstance1 = liveDeploymentNode.add(database, "Instance 1"); + + ContainerInstance apiInstance2 = liveDeploymentNode.add(api, "Instance 2", "Shared"); + ContainerInstance databaseInstance2 = liveDeploymentNode.add(database, "Instance 2"); + + ContainerInstance cacheInstance = liveDeploymentNode.add(cache, "Shared"); + + assertEquals(2, apiInstance1.getRelationships().size()); + assertTrue(apiInstance1.hasEfferentRelationshipWith(databaseInstance1)); + assertTrue(apiInstance1.hasEfferentRelationshipWith(cacheInstance)); + + assertEquals(2, apiInstance2.getRelationships().size()); + assertTrue(apiInstance2.hasEfferentRelationshipWith(databaseInstance2)); + assertTrue(apiInstance2.hasEfferentRelationshipWith(cacheInstance)); + + // apiInstance1 -> databaseInstance1 + Relationship relationship = apiInstance1.getEfferentRelationshipWith(databaseInstance1); + assertEquals("Uses", relationship.getDescription()); + + // apiInstance2 -> databaseInstance2 + relationship = apiInstance2.getEfferentRelationshipWith(databaseInstance2); + assertEquals("Uses", relationship.getDescription()); + } + + @Test + void getElement_ThrowsAnException_WhenANullIdIsSpecified() { + try { + model.getElement(null); + } catch (IllegalArgumentException iae) { + assertEquals("An element ID must be specified.", iae.getMessage()); + } + } + + @Test + void getElement_ThrowsAnException_WhenAnEmptyIdIsSpecified() { + try { + model.getElement(" "); + } catch (IllegalArgumentException iae) { + assertEquals("An element ID must be specified.", iae.getMessage()); + } + } + + @Test + void getElementWithCanonicalName_ThrowsAnException_WhenANullCanonicalNameIsSpecified() { + try { + model.getElementWithCanonicalName(null); + } catch (IllegalArgumentException iae) { + assertEquals("A canonical name must be specified.", iae.getMessage()); + } + } + + @Test + void getElementWithCanonicalName_ThrowsAnException_WhenAnEmptyCanonicalNameIsSpecified() { + try { + model.getElementWithCanonicalName(" "); + } catch (IllegalArgumentException iae) { + assertEquals("A canonical name must be specified.", iae.getMessage()); + } + } + + @Test + void getElementWithCanonicalName_ReturnsNull_WhenAnElementWithTheSpecifiedCanonicalNameDoesNotExist() { + assertNull(model.getElementWithCanonicalName("SoftwareSystem://A")); + } + + @Test + void getElementWithCanonicalName_ReturnsTheElement_WhenAnElementWithTheSpecifiedCanonicalNameExists() { + SoftwareSystem a = model.addSoftwareSystem("A"); + Container b = a.addContainer("B"); + + 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 + void addDeploymentNode_ThrowsAnException_WhenADeploymentNodeWithTheSameNameAlreadyExists() { + model.addDeploymentNode("Amazon AWS", "Description", "Technology"); + try { + model.addDeploymentNode("Amazon AWS", "Description", "Technology"); + fail(); + } catch (IllegalArgumentException iae) { + assertEquals("A deployment/infrastructure node named 'Amazon AWS' already exists.", iae.getMessage()); + } + } + + @Test + void addDeploymentNode_ThrowsAnException_WhenAChildDeploymentNodeWithTheSameNameAlreadyExists() { + DeploymentNode deploymentNode = model.addDeploymentNode("Amazon Web Services"); + deploymentNode.addDeploymentNode("AWS Region"); + try { + deploymentNode.addDeploymentNode("AWS Region"); + fail(); + } catch (IllegalArgumentException iae) { + assertEquals("A deployment/infrastructure node named 'AWS Region' already exists.", iae.getMessage()); + } + } + + @Test + void addDeploymentNode_ThrowsAnException_WhenAChildInfrastructureNodeWithTheSameNameAlreadyExists() { + DeploymentNode deploymentNode = model.addDeploymentNode("Amazon Web Services"); + deploymentNode.addInfrastructureNode("Node"); + try { + deploymentNode.addDeploymentNode("Node"); + fail(); + } catch (IllegalArgumentException iae) { + assertEquals("A deployment/infrastructure node named 'Node' already exists.", iae.getMessage()); + } + } + + @Test + void addInfrastructureNode_ThrowsAnException_WhenAChildDeploymentNodeWithTheSameNameAlreadyExists() { + DeploymentNode deploymentNode = model.addDeploymentNode("Amazon Web Services"); + deploymentNode.addDeploymentNode("Node"); + try { + deploymentNode.addInfrastructureNode("Node"); + fail(); + } catch (IllegalArgumentException iae) { + assertEquals("A deployment/infrastructure node named 'Node' already exists.", iae.getMessage()); + } + } + + @Test + void addInfrastructureNode_ThrowsAnException_WhenAChildInfrastructureNodeWithTheSameNameAlreadyExists() { + DeploymentNode deploymentNode = model.addDeploymentNode("Amazon Web Services"); + deploymentNode.addInfrastructureNode("Node"); + try { + deploymentNode.addInfrastructureNode("Node"); + fail(); + } catch (IllegalArgumentException iae) { + assertEquals("A deployment/infrastructure node named 'Node' already exists.", iae.getMessage()); + } + } + + @Test + void setIdGenerator_ThrowsAnException_WhenANullIdGeneratorIsSpecified() { + try { + model.setIdGenerator(null); + fail(); + } catch (IllegalArgumentException iae) { + assertEquals("An ID generator must be provided.", iae.getMessage()); + } + } + + @Test + void hydrate() { + Person person = new Person(); + person.setId("1"); + person.setName("Person"); + model.setPeople(Collections.singleton(person)); + + SoftwareSystem softwareSystem = new SoftwareSystem(); + softwareSystem.setId("2"); + softwareSystem.setName("Software System"); + model.setSoftwareSystems(Collections.singleton(softwareSystem)); + + Container container = new Container(); + container.setId("3"); + container.setName("Container"); + softwareSystem.setContainers(Collections.singleton(container)); + + Component component = new Component(); + component.setId("4"); + component.setName("Component"); + container.setComponents(Collections.singleton(component)); + + DeploymentNode deploymentNodeParent = new DeploymentNode(); + deploymentNodeParent.setId("5"); + deploymentNodeParent.setName("Deployment Node - Parent"); + model.setDeploymentNodes(Collections.singleton(deploymentNodeParent)); + + DeploymentNode deploymentNodeChild = new DeploymentNode(); + deploymentNodeChild.setId("6"); + deploymentNodeChild.setName("Deployment Node - Child"); + deploymentNodeParent.setChildren(Collections.singleton(deploymentNodeChild)); + + ContainerInstance containerInstance = new ContainerInstance(); + containerInstance.setId("7"); + containerInstance.setContainerId("3"); + deploymentNodeChild.setContainerInstances(Collections.singleton(containerInstance)); + + Relationship relationship = new Relationship(); + relationship.setId("8"); + relationship.setSourceId("1"); + relationship.setDestinationId("2"); + person.setRelationships(Collections.singleton(relationship)); + + model.hydrate(); + + assertSame(person, model.getElement("1")); + assertSame(model, person.getModel()); + + assertSame(softwareSystem, model.getElement("2")); + assertSame(model, softwareSystem.getModel()); + + assertSame(container, model.getElement("3")); + assertSame(model, container.getModel()); + assertSame(softwareSystem, container.getParent()); + + assertSame(component, model.getElement("4")); + assertSame(model, component.getModel()); + assertSame(container, component.getParent()); + + assertSame(deploymentNodeParent, model.getElement("5")); + assertSame(model, deploymentNodeParent.getModel()); + assertNull(deploymentNodeParent.getParent()); + + assertSame(deploymentNodeChild, model.getElement("6")); + assertSame(model, deploymentNodeChild.getModel()); + assertSame(deploymentNodeParent, deploymentNodeChild.getParent()); + + assertSame(containerInstance, model.getElement("7")); + assertSame(model, containerInstance.getModel()); + assertSame(container, containerInstance.getContainer()); + + assertSame(relationship, model.getRelationship("8")); + assertSame(person, relationship.getSource()); + assertSame(softwareSystem, relationship.getDestination()); + + // test that new elements take the next ID + Element element = model.addPerson("New element", "Description"); + assertEquals("9", element.getId()); + } + + @Test + void impliedRelationshipStrategy() { + // default strategy initially + assertTrue(model.getImpliedRelationshipsStrategy() instanceof DefaultImpliedRelationshipsStrategy); + + model.setImpliedRelationshipsStrategy(new CreateImpliedRelationshipsUnlessAnyRelationshipExistsStrategy()); + assertTrue(model.getImpliedRelationshipsStrategy() instanceof CreateImpliedRelationshipsUnlessAnyRelationshipExistsStrategy); + } + + @Test + void setImpliedRelationshipStrategy_ResetsToTheDefaultStrategy_WhenPassedNull() { + model.setImpliedRelationshipsStrategy(new CreateImpliedRelationshipsUnlessAnyRelationshipExistsStrategy()); + model.setImpliedRelationshipsStrategy(null); + + assertTrue(model.getImpliedRelationshipsStrategy() instanceof DefaultImpliedRelationshipsStrategy); + } + + @Test + void addSoftwareSystemInstance_AllocatesInstanceIdsPerDeploymentNode() { + SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System", ""); + DeploymentNode deploymentNodeA = model.addDeploymentNode("Deployment Node A", "", ""); + DeploymentNode deploymentNodeB = model.addDeploymentNode("Deployment Node B", "", ""); + SoftwareSystemInstance softwareSystemInstanceA1 = deploymentNodeA.add(softwareSystem); + SoftwareSystemInstance softwareSystemInstanceA2 = deploymentNodeA.add(softwareSystem); + SoftwareSystemInstance softwareSystemInstanceB1 = deploymentNodeB.add(softwareSystem); + SoftwareSystemInstance softwareSystemInstanceB2 = deploymentNodeB.add(softwareSystem); + + assertEquals("SoftwareSystemInstance://Default/Deployment Node A/Software System[1]", softwareSystemInstanceA1.getCanonicalName()); + assertEquals("SoftwareSystemInstance://Default/Deployment Node A/Software System[2]", softwareSystemInstanceA2.getCanonicalName()); + assertEquals("SoftwareSystemInstance://Default/Deployment Node B/Software System[1]", softwareSystemInstanceB1.getCanonicalName()); + assertEquals("SoftwareSystemInstance://Default/Deployment Node B/Software System[2]", softwareSystemInstanceB2.getCanonicalName()); + } + + @Test + void addContainerInstance_AllocatesInstanceIdsPerDeploymentNode() { + SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System", ""); + Container container = softwareSystem.addContainer("Container", "", ""); + DeploymentNode deploymentNodeA = model.addDeploymentNode("Deployment Node A", "", ""); + DeploymentNode deploymentNodeB = model.addDeploymentNode("Deployment Node B", "", ""); + ContainerInstance containerInstanceA1 = deploymentNodeA.add(container); + ContainerInstance containerInstanceA2 = deploymentNodeA.add(container); + ContainerInstance containerInstanceB1 = deploymentNodeB.add(container); + ContainerInstance containerInstanceB2 = deploymentNodeB.add(container); + + assertEquals("ContainerInstance://Default/Deployment Node A/Software System.Container[1]", containerInstanceA1.getCanonicalName()); + assertEquals("ContainerInstance://Default/Deployment Node A/Software System.Container[2]", containerInstanceA2.getCanonicalName()); + assertEquals("ContainerInstance://Default/Deployment Node B/Software System.Container[1]", containerInstanceB1.getCanonicalName()); + assertEquals("ContainerInstance://Default/Deployment Node B/Software System.Container[2]", containerInstanceB2.getCanonicalName()); + } + +} \ No newline at end of file diff --git a/structurizr-core/src/test/java/com/structurizr/model/PersonTests.java b/structurizr-core/src/test/java/com/structurizr/model/PersonTests.java new file mode 100644 index 000000000..63fd8a6ee --- /dev/null +++ b/structurizr-core/src/test/java/com/structurizr/model/PersonTests.java @@ -0,0 +1,90 @@ +package com.structurizr.model; + +import com.structurizr.AbstractWorkspaceTestBase; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +public class PersonTests extends AbstractWorkspaceTestBase { + + @Test + void getCanonicalName() { + Person person = model.addPerson("Person", "Description"); + assertEquals("Person://Person", person.getCanonicalName()); + } + + @Test + void getCanonicalName_WhenNameContainsSlashAndDotCharacters() { + Person person = model.addPerson("Person", "Description"); + person.setName("Name1/.Name2"); + assertEquals("Person://Name1Name2", person.getCanonicalName()); + } + + @Test + void getParent_ReturnsNull() { + Person person = model.addPerson("Person", "Description"); + assertNull(person.getParent()); + } + + @Test + void removeTags_DoesNotRemoveRequiredTags() { + Person person = model.addPerson("Person", "Description"); + assertTrue(person.getTags().contains(Tags.ELEMENT)); + assertTrue(person.getTags().contains(Tags.PERSON)); + + person.removeTag(Tags.PERSON); + person.removeTag(Tags.ELEMENT); + + assertTrue(person.getTags().contains(Tags.ELEMENT)); + assertTrue(person.getTags().contains(Tags.PERSON)); + } + + @Test + void interactsWith_AddsARelationshipWhenTheDescriptionIsSpecified() { + Person person1 = model.addPerson("Person 1", "Description"); + Person person2 = model.addPerson("Person 2", "Description"); + + person1.interactsWith(person2, "Sends an e-mail to"); + assertEquals(1, person1.getRelationships().size()); + + Relationship relationship = person1.getRelationships().iterator().next(); + assertSame(person1, relationship.getSource()); + assertSame(person2, relationship.getDestination()); + assertEquals("Sends an e-mail to", relationship.getDescription()); + assertNull(relationship.getTechnology()); + assertNull(relationship.getInteractionStyle()); + } + + @Test + void interactsWith_AddsARelationshipWhenTheDescriptionAndTechnologyAreSpecified() { + Person person1 = model.addPerson("Person 1", "Description"); + Person person2 = model.addPerson("Person 2", "Description"); + + person1.interactsWith(person2, "Sends a message to", "E-mail"); + assertEquals(1, person1.getRelationships().size()); + + Relationship relationship = person1.getRelationships().iterator().next(); + assertSame(person1, relationship.getSource()); + assertSame(person2, relationship.getDestination()); + assertEquals("Sends a message to", relationship.getDescription()); + assertEquals("E-mail", relationship.getTechnology()); + assertNull(relationship.getInteractionStyle()); + } + + @Test + void interactsWith_AddsARelationshipWhenTheDescriptionAndTechnologyAndInteractionStyleAreSpecified() { + Person person1 = model.addPerson("Person 1", "Description"); + Person person2 = model.addPerson("Person 2", "Description"); + + person1.interactsWith(person2, "Sends a message to", "E-mail", InteractionStyle.Asynchronous); + assertEquals(1, person1.getRelationships().size()); + + Relationship relationship = person1.getRelationships().iterator().next(); + assertSame(person1, relationship.getSource()); + assertSame(person2, relationship.getDestination()); + assertEquals("Sends a message to", relationship.getDescription()); + assertEquals("E-mail", relationship.getTechnology()); + assertEquals(InteractionStyle.Asynchronous, relationship.getInteractionStyle()); + } + +} \ No newline at end of file diff --git a/structurizr-core/src/test/java/com/structurizr/model/RelationshipTests.java b/structurizr-core/src/test/java/com/structurizr/model/RelationshipTests.java new file mode 100644 index 000000000..be7c631dd --- /dev/null +++ b/structurizr-core/src/test/java/com/structurizr/model/RelationshipTests.java @@ -0,0 +1,135 @@ +package com.structurizr.model; + +import com.structurizr.AbstractWorkspaceTestBase; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +public class RelationshipTests extends AbstractWorkspaceTestBase { + + private SoftwareSystem softwareSystem1, softwareSystem2; + + @BeforeEach + public void setUp() { + softwareSystem1 = model.addSoftwareSystem("Name1", "Description"); + softwareSystem2 = model.addSoftwareSystem("Name2", "Description"); + } + + @Test + void getDescription_NeverReturnsNull() { + Relationship relationship = softwareSystem1.uses(softwareSystem2, null); + assertEquals("", relationship.getDescription()); + } + + @Test + void getTags_WhenThereAreNoTags() { + Relationship relationship = softwareSystem1.uses(softwareSystem2, "uses"); + assertEquals("Relationship", relationship.getTags()); + } + + @Test + void getTags_ReturnsTheListOfTags_WhenThereAreSomeTags() { + Relationship relationship = softwareSystem1.uses(softwareSystem2, "uses"); + relationship.addTags("tag1", "tag2", "tag3"); + assertEquals("Relationship,tag1,tag2,tag3", relationship.getTags()); + } + + @Test + void setTags_ClearsTheTags_WhenPassedNull() { + Relationship relationship = softwareSystem1.uses(softwareSystem2, "uses"); + relationship.addTags("Tag 1", "Tag 2"); + assertEquals("Relationship,Tag 1,Tag 2", relationship.getTags()); + relationship.setTags(null); + assertEquals("Relationship", relationship.getTags()); + } + + @Test + void addTags_DoesNotDoAnything_WhenPassedNull() { + Relationship relationship = softwareSystem1.uses(softwareSystem2, "uses"); + relationship.addTags((String) null); + assertEquals("Relationship", relationship.getTags()); + + relationship.addTags(null, null, null); + assertEquals("Relationship", relationship.getTags()); + } + + @Test + void addTags_AddsTags_WhenPassedSomeTags() { + Relationship relationship = softwareSystem1.uses(softwareSystem2, "uses"); + relationship.addTags(null, "tag1", null, "tag2"); + assertEquals("Relationship,tag1,tag2", relationship.getTags()); + } + + @Test + void getInteractionStyle_ReturnsNull_WhenNotExplicitlySet() { + Relationship relationship = softwareSystem1.uses(softwareSystem2, "uses"); + assertNull(relationship.getInteractionStyle()); + } + + @Test + void getTags_IncludesTheInteractionStyleWhenSpecified() { + Relationship relationship = softwareSystem1.uses(softwareSystem2, "Uses 1", "Technology"); + assertFalse(relationship.getTags().contains(Tags.SYNCHRONOUS)); + assertFalse(relationship.getTags().contains(Tags.ASYNCHRONOUS)); + + relationship.setInteractionStyle(InteractionStyle.Synchronous); + assertTrue(relationship.getTags().contains(Tags.SYNCHRONOUS)); + assertFalse(relationship.getTags().contains(Tags.ASYNCHRONOUS)); + + relationship.setInteractionStyle(InteractionStyle.Asynchronous); + assertFalse(relationship.getTags().contains(Tags.SYNCHRONOUS)); + assertTrue(relationship.getTags().contains(Tags.ASYNCHRONOUS)); + } + + @Test + void setUrl() { + Relationship relationship = softwareSystem1.uses(softwareSystem2, "Uses 1", "Technology"); + relationship.setUrl("https://structurizr.com"); + assertEquals("https://structurizr.com", relationship.getUrl()); + } + + @Test + void setUrl_ThrowsAnIllegalArgumentException_WhenAnInvalidUrlIsSpecified() { + assertThrows(IllegalArgumentException.class, () -> { + Relationship relationship = softwareSystem1.uses(softwareSystem2, "Uses 1", "Technology"); + relationship.setUrl("htt://blah"); + }); + } + + @Test + void setUrl_ResetsTheUrl_WhenANullUrlIsSpecified() { + Relationship relationship = softwareSystem1.uses(softwareSystem2, "Uses 1", "Technology"); + relationship.setUrl("https://structurizr.com"); + relationship.setUrl(null); + assertNull(relationship.getUrl()); + } + + @Test + void setUrl_ResetsTheUrl_WhenAnEmptyUrlIsSpecified() { + Relationship relationship = softwareSystem1.uses(softwareSystem2, "Uses 1", "Technology"); + relationship.setUrl("https://structurizr.com"); + relationship.setUrl(" "); + assertNull(relationship.getUrl()); + } + + @Test + void interactionStyle_CanBeSetToNull() { + Relationship relationship = softwareSystem1.uses(softwareSystem2, "Uses 1", "Technology", null); + + assertNull(relationship.getInteractionStyle()); + assertFalse(relationship.getTagsAsSet().contains(Tags.ASYNCHRONOUS)); + 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 diff --git a/structurizr-core/src/test/java/com/structurizr/model/SoftwareSystemInstanceTests.java b/structurizr-core/src/test/java/com/structurizr/model/SoftwareSystemInstanceTests.java new file mode 100644 index 000000000..6795bee01 --- /dev/null +++ b/structurizr-core/src/test/java/com/structurizr/model/SoftwareSystemInstanceTests.java @@ -0,0 +1,205 @@ +package com.structurizr.model; + +import com.structurizr.AbstractWorkspaceTestBase; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +public class SoftwareSystemInstanceTests extends AbstractWorkspaceTestBase { + + private SoftwareSystem softwareSystem = model.addSoftwareSystem("System", "Description"); + private DeploymentNode deploymentNode = model.addDeploymentNode("Deployment Node", "Description", "Technology"); + + @Test + void construction() { + SoftwareSystemInstance instance = deploymentNode.add(softwareSystem); + + assertSame(softwareSystem, instance.getSoftwareSystem()); + assertEquals(softwareSystem.getId(), instance.getSoftwareSystemId()); + assertEquals(1, instance.getInstanceId()); + } + + @Test + void getSoftwareSystemId() { + SoftwareSystemInstance instance = deploymentNode.add(softwareSystem); + + assertEquals(softwareSystem.getId(), instance.getSoftwareSystemId()); + instance.setSoftwareSystem(null); + instance.setSoftwareSystemId("1234"); + assertEquals("1234", instance.getSoftwareSystemId()); + } + + @Test + void getName_CannotBeChanged() { + SoftwareSystemInstance instance = deploymentNode.add(softwareSystem); + + assertEquals("System", instance.getName()); + + instance.setName("foo"); + assertEquals("System", instance.getName()); + } + + @Test + void getCanonicalName() { + SoftwareSystemInstance instance = deploymentNode.add(softwareSystem); + + assertEquals("SoftwareSystemInstance://Default/Deployment Node/System[1]", instance.getCanonicalName()); + } + + @Test + void getParent_ReturnsTheParentDeploymentNode() { + SoftwareSystemInstance instance = deploymentNode.add(softwareSystem); + + assertEquals(deploymentNode, instance.getParent()); + } + + @Test + void getRequiredTags() { + SoftwareSystemInstance instance = deploymentNode.add(softwareSystem); + + assertTrue(instance.getDefaultTags().isEmpty()); + } + + @Test + void getTags() { + softwareSystem.addTags("Tag 1"); + SoftwareSystemInstance instance = deploymentNode.add(softwareSystem); + instance.addTags("Primary Instance"); + + assertEquals("Software System Instance,Primary Instance", instance.getTags()); + } + + @Test + void removeTags_DoesNotRemoveAnyTags() { + SoftwareSystemInstance instance = deploymentNode.add(softwareSystem); + + assertTrue(instance.getTags().contains(Tags.SOFTWARE_SYSTEM_INSTANCE)); + + instance.removeTag(Tags.SOFTWARE_SYSTEM_INSTANCE); + + assertTrue(instance.getTags().contains(Tags.SOFTWARE_SYSTEM_INSTANCE)); + } + + @Test + void addHealthCheck() { + SoftwareSystemInstance instance = deploymentNode.add(softwareSystem); + assertTrue(instance.getHealthChecks().isEmpty()); + + HttpHealthCheck healthCheck = instance.addHealthCheck("Test web application is working", "http://localhost:8080"); + assertEquals("Test web application is working", healthCheck.getName()); + assertEquals("http://localhost:8080", healthCheck.getUrl()); + assertEquals(60, healthCheck.getInterval()); + assertEquals(0, healthCheck.getTimeout()); + assertEquals(1, instance.getHealthChecks().size()); + } + + @Test + void addHealthCheck_ThrowsAnException_WhenTheNameIsNull() { + SoftwareSystemInstance instance = deploymentNode.add(softwareSystem); + + try { + instance.addHealthCheck(null, "http://localhost"); + fail(); + } catch (IllegalArgumentException iae) { + assertEquals("The name must not be null or empty.", iae.getMessage()); + } + } + + @Test + void addHealthCheck_ThrowsAnException_WhenTheNameIsEmpty() { + SoftwareSystemInstance instance = deploymentNode.add(softwareSystem); + + try { + instance.addHealthCheck(" ", "http://localhost"); + fail(); + } catch (IllegalArgumentException iae) { + assertEquals("The name must not be null or empty.", iae.getMessage()); + } + } + + @Test + void addHealthCheck_ThrowsAnException_WhenTheUrlIsNull() { + SoftwareSystemInstance instance = deploymentNode.add(softwareSystem); + + try { + instance.addHealthCheck("Name", null); + fail(); + } catch (IllegalArgumentException iae) { + assertEquals("The URL must not be null or empty.", iae.getMessage()); + } + } + + @Test + void addHealthCheck_ThrowsAnException_WhenTheUrlIsEmpty() { + SoftwareSystemInstance instance = deploymentNode.add(softwareSystem); + + try { + instance.addHealthCheck("Name", " "); + fail(); + } catch (IllegalArgumentException iae) { + assertEquals("The URL must not be null or empty.", iae.getMessage()); + } + } + + @Test + void addHealthCheck_ThrowsAnException_WhenTheUrlIsInvalid() { + SoftwareSystemInstance instance = deploymentNode.add(softwareSystem); + + try { + instance.addHealthCheck("Name", "localhost"); + fail(); + } catch (IllegalArgumentException iae) { + assertEquals("localhost is not a valid URL.", iae.getMessage()); + } + } + + @Test + void addHealthCheck_ThrowsAnException_WhenTheIntervalIsLessThanZero() { + SoftwareSystemInstance instance = deploymentNode.add(softwareSystem); + + try { + instance.addHealthCheck("Name", "https://localhost", -1, 0); + fail(); + } catch (IllegalArgumentException iae) { + assertEquals("The polling interval must be zero or a positive integer.", iae.getMessage()); + } + } + + @Test + void addHealthCheck_ThrowsAnException_WhenTheTimeoutIsLessThanZero() { + SoftwareSystemInstance instance = deploymentNode.add(softwareSystem); + + try { + instance.addHealthCheck("Name", "https://localhost", 60, -1); + fail(); + } catch (IllegalArgumentException iae) { + assertEquals("The timeout must be zero or a positive integer.", iae.getMessage()); + } + } + + @Test + void getDeploymentGroups_WhenNoGroupsHaveBeenSpecified() { + SoftwareSystemInstance instance = deploymentNode.add(softwareSystem); + + assertEquals(1, instance.getDeploymentGroups().size()); + assertTrue(instance.getDeploymentGroups().contains("Default")); + } + + @Test + void getDeploymentGroups_WhenOneGroupHasBeenSpecified() { + SoftwareSystemInstance instance = deploymentNode.add(softwareSystem, "Group 1"); + + assertEquals(1, instance.getDeploymentGroups().size()); + assertTrue(instance.getDeploymentGroups().contains("Group 1")); + } + + @Test + void getDeploymentGroups_WhenMultipleGroupsAreSpecified() { + SoftwareSystemInstance instance = deploymentNode.add(softwareSystem, "Group 1", "Group 2"); + + assertEquals(2, instance.getDeploymentGroups().size()); + assertTrue(instance.getDeploymentGroups().contains("Group 1")); + assertTrue(instance.getDeploymentGroups().contains("Group 2")); + } + +} \ No newline at end of file diff --git a/structurizr-core/src/test/java/com/structurizr/model/SoftwareSystemTests.java b/structurizr-core/src/test/java/com/structurizr/model/SoftwareSystemTests.java new file mode 100644 index 000000000..51b97f3e7 --- /dev/null +++ b/structurizr-core/src/test/java/com/structurizr/model/SoftwareSystemTests.java @@ -0,0 +1,238 @@ +package com.structurizr.model; + +import com.structurizr.AbstractWorkspaceTestBase; +import org.junit.jupiter.api.Test; + +import java.util.Iterator; + +import static org.junit.jupiter.api.Assertions.*; + +public class SoftwareSystemTests extends AbstractWorkspaceTestBase { + + private SoftwareSystem softwareSystem = model.addSoftwareSystem("Name", "Description"); + + @Test + void addContainer_ThrowsAnException_WhenANullNameIsSpecified() { + assertThrows(IllegalArgumentException.class, () -> { + softwareSystem.addContainer(null, "", ""); + }); + } + + @Test + void addContainer_ThrowsAnException_WhenAnEmptyNameIsSpecified() { + assertThrows(IllegalArgumentException.class, () -> { + softwareSystem.addContainer(" ", "", ""); + }); + } + + @Test + void addContainer_AddsAContainer_WhenAContainerWithTheSameNameDoesNotExist() { + Container container = softwareSystem.addContainer("Web Application", "Description", "Spring MVC"); + assertEquals("Web Application", container.getName()); + assertEquals("Description", container.getDescription()); + assertEquals("Spring MVC", container.getTechnology()); + assertEquals("2", container.getId()); + assertEquals(1, softwareSystem.getContainers().size()); + assertSame(container, softwareSystem.getContainers().iterator().next()); + } + + @Test + void addContainer_ThrowsAnException_WhenAContainerWithTheSameNameAlreadyExists() { + Container container = softwareSystem.addContainer("Web Application", "Description", "Spring MVC"); + assertEquals(1, softwareSystem.getContainers().size()); + + try { + softwareSystem.addContainer("Web Application", "Description", "Spring MVC"); + fail(); + } catch (Exception e) { + assertEquals("A container named 'Web Application' already exists for this software system.", e.getMessage()); + } + } + + @Test + void getContainerWithName_ReturnsNull_WhenAContainerWithTheSpecifiedNameDoesNotExist() { + assertNull(softwareSystem.getContainerWithName("Web Application")); + } + + @Test + void GetContainerWithName_ReturnsAContainer_WhenAContainerWithTheSpecifiedNameDoesExist() { + Container container = softwareSystem.addContainer("Web Application", "Description", "Spring MVC"); + assertSame(container, softwareSystem.getContainerWithName("Web Application")); + } + + @Test + void getContainerWithId_ReturnsNull_WhenAContainerWithTheSpecifiedIdDoesNotExist() { + assertNull(softwareSystem.getContainerWithId("100")); + } + + @Test + void GetContainerWithId_ReturnsAContainer_WhenAContainerWithTheSpecifiedIdDoesExist() { + Container container = softwareSystem.addContainer("Web Application", "Description", "Spring MVC"); + assertSame(container, softwareSystem.getContainerWithId(container.getId())); + } + + @Test + void uses_AddsAUnidirectionalRelationshipBetweenTwoSoftwareSystems() { + 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()); + assertEquals(0, systemB.getRelationships().size()); + Relationship relationship = systemA.getRelationships().iterator().next(); + assertEquals(systemA, relationship.getSource()); + assertEquals(systemB, relationship.getDestination()); + assertEquals("Gets some data from", relationship.getDescription()); + } + + @Test + void uses_AddsAUnidirectionalRelationshipBetweenTwoSoftwareSystems_WhenADifferentRelationshipAlreadyExists() { + 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"); + + Iterator it = systemA.getRelationships().iterator(); + assertEquals(2, systemA.getRelationships().size()); + Relationship relationship = it.next(); + assertEquals(systemA, relationship.getSource()); + assertEquals(systemB, relationship.getDestination()); + assertEquals("Gets data using the REST API", relationship.getDescription()); + + relationship = it.next(); + assertEquals(systemA, relationship.getSource()); + assertEquals(systemB, relationship.getDestination()); + assertEquals("Subscribes to updates using the Streaming API", relationship.getDescription()); + } + + @Test + void uses_DoesNotAddAUnidirectionalRelationshipBetweenTwoSoftwareSystems_WhenTheSameRelationshipAlreadyExists() { + 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"); + + assertEquals(1, systemA.getRelationships().size()); + } + + @Test + void delivers_AddsAUnidirectionalRelationshipBetweenASoftwareSystemAndAPerson() { + SoftwareSystem system = model.addSoftwareSystem("System", "Description"); + Person person = model.addPerson("User", "Description"); + system.delivers(person, "E-mails results to"); + + assertEquals(1, system.getRelationships().size()); + assertEquals(0, person.getRelationships().size()); + Relationship relationship = system.getRelationships().iterator().next(); + assertEquals(system, relationship.getSource()); + assertEquals(person, relationship.getDestination()); + assertEquals("E-mails results to", relationship.getDescription()); + } + + @Test + void delivers_AddsAUnidirectionalRelationshipBetweenASoftwareSystemAndAPerson_WhenADifferentRelationshipAlreadyExists() { + 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"); + + Iterator it = system.getRelationships().iterator(); + assertEquals(2, system.getRelationships().size()); + assertEquals(0, person.getRelationships().size()); + Relationship relationship = it.next(); + assertEquals(system, relationship.getSource()); + assertEquals(person, relationship.getDestination()); + assertEquals("E-mails results to", relationship.getDescription()); + + relationship = it.next(); + assertEquals(system, relationship.getSource()); + assertEquals(person, relationship.getDestination()); + assertEquals("Text messages results to", relationship.getDescription()); + } + + @Test + void delivers_DoesNotAddAUnidirectionalRelationshipBetweenASoftwareSystemAndAPerson_WhenTheSameRelationshipAlreadyExists() { + 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"); + + assertEquals(1, system.getRelationships().size()); + } + + @Test + void getTags_IncludesSoftwareSystemByDefault() { + SoftwareSystem system = model.addSoftwareSystem("System", "Description"); + assertEquals("Element,Software System", system.getTags()); + } + + @Test + void getCanonicalName() { + SoftwareSystem system = model.addSoftwareSystem("System", "Description"); + assertEquals("SoftwareSystem://System", system.getCanonicalName()); + } + + @Test + void getCanonicalName_WhenNameContainsSlashAndDotCharacters() { + SoftwareSystem softwareSystem = model.addSoftwareSystem("Name1/.Name2", "Description"); + assertEquals("SoftwareSystem://Name1Name2", softwareSystem.getCanonicalName()); + } + + @Test + void getParent_ReturnsNull() { + assertNull(softwareSystem.getParent()); + } + + @Test + void removeTags_DoesNotRemoveRequiredTags() { + assertTrue(softwareSystem.getTags().contains(Tags.ELEMENT)); + assertTrue(softwareSystem.getTags().contains(Tags.SOFTWARE_SYSTEM)); + + softwareSystem.removeTag(Tags.SOFTWARE_SYSTEM); + softwareSystem.removeTag(Tags.ELEMENT); + + assertTrue(softwareSystem.getTags().contains(Tags.ELEMENT)); + assertTrue(softwareSystem.getTags().contains(Tags.SOFTWARE_SYSTEM)); + } + + @Test + void getContainerWithName_ThrowsAnException_WhenANullNameIsSpecified() { + try { + softwareSystem.getContainerWithName(null); + fail(); + } catch (IllegalArgumentException iae) { + assertEquals("A container name must be provided.", iae.getMessage()); + } + } + + @Test + void getContainerWithName_ThrowsAnException_WhenAnEmptyNameIsSpecified() { + try { + softwareSystem.getContainerWithName(" "); + fail(); + } catch (IllegalArgumentException iae) { + assertEquals("A container name must be provided.", iae.getMessage()); + } + } + + @Test + void getContainerWithId_ThrowsAnException_WhenANullIdIsSpecified() { + try { + softwareSystem.getContainerWithId(null); + fail(); + } catch (IllegalArgumentException iae) { + assertEquals("A container ID must be provided.", iae.getMessage()); + } + } + + @Test + void getContainerWithId_ThrowsAnException_WhenAnEmptyIdIsSpecified() { + try { + softwareSystem.getContainerWithId(" "); + fail(); + } catch (IllegalArgumentException iae) { + assertEquals("A container ID must be provided.", iae.getMessage()); + } + } + +} \ 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 new file mode 100644 index 000000000..5f2a700bc --- /dev/null +++ b/structurizr-core/src/test/java/com/structurizr/util/ImageUtilsTests.java @@ -0,0 +1,215 @@ +package com.structurizr.util; + +import org.junit.jupiter.api.Test; + +import java.io.File; + +import static org.junit.jupiter.api.Assertions.*; + +public class ImageUtilsTests { + + @Test + void getContentType_ThrowsAnException_WhenANullFileIsSpecified() throws Exception { + try { + ImageUtils.getContentType((File)null); + fail(); + } catch (IllegalArgumentException iae) { + assertEquals("A file must be specified.", iae.getMessage()); + } + } + + @Test + void getContentType_ThrowsAnException_WhenAFileIsSpecifiedButItIsNotAFile() throws Exception { + try { + ImageUtils.getContentType(new File("../structurizr-core")); + fail(); + } catch (IllegalArgumentException iae) { + iae.printStackTrace(); + assertTrue(iae.getMessage().endsWith("structurizr-core is not a file.")); + } + } + + @Test + void getContentType_ThrowsAnException_WhenAFileIsSpecifiedButItIsNotAnImage() throws Exception { + try { + ImageUtils.getContentType(new File("../build.gradle")); + fail(); + } catch (IllegalArgumentException iae) { + iae.printStackTrace(); + assertTrue(iae.getMessage().endsWith("build.gradle is not a supported image file.")); + } + } + + @Test + void getContentType_ThrowsAnException_WhenAFileIsSpecifiedButItDoesNotExist() throws Exception { + try { + ImageUtils.getContentType(new File("./foo.xml")); + fail(); + } catch (IllegalArgumentException iae) { + assertTrue(iae.getMessage().endsWith("foo.xml does not exist.")); + } + } + + @Test + void getContentType_ReturnsTheContentType_WhenAPNGFileIsSpecified() throws Exception { + String contentType = ImageUtils.getContentType(new File("./src/test/resources/image.png")); + assertEquals("image/png", contentType); + } + + @Test + void getContentType_ReturnsTheContentType_WhenASVGFileIsSpecified() throws Exception { + String contentType = ImageUtils.getContentType(new File("./src/test/resources/image.svg")); + assertEquals("image/svg+xml", 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 { + ImageUtils.getImageAsBase64(null); + fail(); + } catch (IllegalArgumentException iae) { + assertEquals("A file must be specified.", iae.getMessage()); + } + } + + @Test + void getImageAsBase64_ThrowsAnException_WhenAFileIsSpecifiedButItIsNotAFile() throws Exception { + try { + ImageUtils.getImageAsBase64(new File("../structurizr-core")); + fail(); + } catch (IllegalArgumentException iae) { + iae.printStackTrace(); + assertTrue(iae.getMessage().endsWith("structurizr-core is not a file.")); + } + } + + @Test + void getImageAsBase64_ThrowsAnException_WhenAFileIsSpecifiedButItIsNotAnImage() throws Exception { + try { + ImageUtils.getImageAsBase64(new File("../build.gradle")); + fail(); + } catch (IllegalArgumentException iae) { + iae.printStackTrace(); + assertTrue(iae.getMessage().endsWith("build.gradle is not a supported image file.")); + } + } + + @Test + void getImageAsBase64_ThrowsAnException_WhenAFileIsSpecifiedButItDoesNotExist() throws Exception { + try { + ImageUtils.getImageAsBase64(new File("./foo.xml")); + fail(); + } catch (IllegalArgumentException iae) { + assertTrue(iae.getMessage().endsWith("foo.xml does not exist.")); + } + } + + @Test + void getImageAsBase64_ReturnsTheImageAsABase64EncodedString_WhenAPNGFileIsSpecified() throws Exception { + String imageAsBase64 = ImageUtils.getImageAsBase64(new File("./src/test/resources/image.png")); + assertEquals("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", imageAsBase64); + } + + @Test + void getImageAsBase64_ReturnsTheImageAsABase64EncodedString_WhenASVGFileIsSpecified() throws Exception { + String imageAsBase64 = ImageUtils.getImageAsBase64(new File("./src/test/resources/image.svg")); + assertEquals("PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXMtYXNjaWkiIHN0YW5kYWxvbmU9Im5vIj8+PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiBjb250ZW50U3R5bGVUeXBlPSJ0ZXh0L2NzcyIgaGVpZ2h0PSIxMjBweCIgcHJlc2VydmVBc3BlY3RSYXRpbz0ibm9uZSIgc3R5bGU9IndpZHRoOjExM3B4O2hlaWdodDoxMjBweDtiYWNrZ3JvdW5kOiNGRkZGRkY7IiB2ZXJzaW9uPSIxLjEiIHZpZXdCb3g9IjAgMCAxMTMgMTIwIiB3aWR0aD0iMTEzcHgiIHpvb21BbmRQYW49Im1hZ25pZnkiPjxkZWZzLz48Zz48bGluZSBzdHlsZT0ic3Ryb2tlOiMxODE4MTg7c3Ryb2tlLXdpZHRoOjAuNTtzdHJva2UtZGFzaGFycmF5OjUuMCw1LjA7IiB4MT0iMjYiIHgyPSIyNiIgeTE9IjM2LjI5NjkiIHkyPSI4NS40Mjk3Ii8+PGxpbmUgc3R5bGU9InN0cm9rZTojMTgxODE4O3N0cm9rZS13aWR0aDowLjU7c3Ryb2tlLWRhc2hhcnJheTo1LjAsNS4wOyIgeDE9IjgyIiB4Mj0iODIiIHkxPSIzNi4yOTY5IiB5Mj0iODUuNDI5NyIvPjxyZWN0IGZpbGw9IiNFMkUyRjAiIGhlaWdodD0iMzAuMjk2OSIgcng9IjIuNSIgcnk9IjIuNSIgc3R5bGU9InN0cm9rZTojMTgxODE4O3N0cm9rZS13aWR0aDowLjU7IiB3aWR0aD0iNDMiIHg9IjUiIHk9IjUiLz48dGV4dCBmaWxsPSIjMDAwMDAwIiBmb250LWZhbWlseT0ic2Fucy1zZXJpZiIgZm9udC1zaXplPSIxNCIgbGVuZ3RoQWRqdXN0PSJzcGFjaW5nIiB0ZXh0TGVuZ3RoPSIyOSIgeD0iMTIiIHk9IjI0Ljk5NTEiPkJvYjwvdGV4dD48cmVjdCBmaWxsPSIjRTJFMkYwIiBoZWlnaHQ9IjMwLjI5NjkiIHJ4PSIyLjUiIHJ5PSIyLjUiIHN0eWxlPSJzdHJva2U6IzE4MTgxODtzdHJva2Utd2lkdGg6MC41OyIgd2lkdGg9IjQzIiB4PSI1IiB5PSI4NC40Mjk3Ii8+PHRleHQgZmlsbD0iIzAwMDAwMCIgZm9udC1mYW1pbHk9InNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMTQiIGxlbmd0aEFkanVzdD0ic3BhY2luZyIgdGV4dExlbmd0aD0iMjkiIHg9IjEyIiB5PSIxMDQuNDI0OCI+Qm9iPC90ZXh0PjxyZWN0IGZpbGw9IiNFMkUyRjAiIGhlaWdodD0iMzAuMjk2OSIgcng9IjIuNSIgcnk9IjIuNSIgc3R5bGU9InN0cm9rZTojMTgxODE4O3N0cm9rZS13aWR0aDowLjU7IiB3aWR0aD0iNDkiIHg9IjU4IiB5PSI1Ii8+PHRleHQgZmlsbD0iIzAwMDAwMCIgZm9udC1mYW1pbHk9InNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMTQiIGxlbmd0aEFkanVzdD0ic3BhY2luZyIgdGV4dExlbmd0aD0iMzUiIHg9IjY1IiB5PSIyNC45OTUxIj5BbGljZTwvdGV4dD48cmVjdCBmaWxsPSIjRTJFMkYwIiBoZWlnaHQ9IjMwLjI5NjkiIHJ4PSIyLjUiIHJ5PSIyLjUiIHN0eWxlPSJzdHJva2U6IzE4MTgxODtzdHJva2Utd2lkdGg6MC41OyIgd2lkdGg9IjQ5IiB4PSI1OCIgeT0iODQuNDI5NyIvPjx0ZXh0IGZpbGw9IiMwMDAwMDAiIGZvbnQtZmFtaWx5PSJzYW5zLXNlcmlmIiBmb250LXNpemU9IjE0IiBsZW5ndGhBZGp1c3Q9InNwYWNpbmciIHRleHRMZW5ndGg9IjM1IiB4PSI2NSIgeT0iMTA0LjQyNDgiPkFsaWNlPC90ZXh0Pjxwb2x5Z29uIGZpbGw9IiMxODE4MTgiIHBvaW50cz0iNzAuNSw2My40Mjk3LDgwLjUsNjcuNDI5Nyw3MC41LDcxLjQyOTcsNzQuNSw2Ny40Mjk3IiBzdHlsZT0ic3Ryb2tlOiMxODE4MTg7c3Ryb2tlLXdpZHRoOjEuMDsiLz48bGluZSBzdHlsZT0ic3Ryb2tlOiMxODE4MTg7c3Ryb2tlLXdpZHRoOjEuMDsiIHgxPSIyNi41IiB4Mj0iNzYuNSIgeTE9IjY3LjQyOTciIHkyPSI2Ny40Mjk3Ii8+PHRleHQgZmlsbD0iIzAwMDAwMCIgZm9udC1mYW1pbHk9InNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMTMiIGxlbmd0aEFkanVzdD0ic3BhY2luZyIgdGV4dExlbmd0aD0iMzAiIHg9IjMzLjUiIHk9IjYyLjM2MzgiPmhlbGxvPC90ZXh0PjwhLS1TUkM9W1N5ZkZLajJyS3QzQ29LbkVMUjFJbzRaRG9TYTcwMDAwXS0tPjwvZz48L3N2Zz4=", imageAsBase64); + } + + @Test + void getImageAsDataUri_ThrowsAnException_WhenANullFileIsSpecified() throws Exception { + try { + ImageUtils.getImageAsDataUri(null); + fail(); + } catch (IllegalArgumentException iae) { + assertEquals("A file must be specified.", iae.getMessage()); + } + } + + @Test + void getImageAsDataUri_ThrowsAnException_WhenAFileIsSpecifiedButItIsNotAFile() throws Exception { + try { + ImageUtils.getImageAsDataUri(new File("../structurizr-core")); + fail(); + } catch (IllegalArgumentException iae) { + iae.printStackTrace(); + assertTrue(iae.getMessage().endsWith("structurizr-core is not a file.")); + } + } + + @Test + void getImageAsDataUri_ThrowsAnException_WhenAFileIsSpecifiedButItIsNotAnImage() throws Exception { + try { + ImageUtils.getImageAsDataUri(new File("../build.gradle")); + fail(); + } catch (IllegalArgumentException iae) { + iae.printStackTrace(); + assertTrue(iae.getMessage().endsWith("build.gradle is not a supported image file.")); + } + } + + @Test + void getImageAsDataUri_ThrowsAnException_WhenAFileIsSpecifiedButItDoesNotExist() throws Exception { + try { + ImageUtils.getImageAsDataUri(new File("./foo.xml")); + fail(); + } catch (IllegalArgumentException iae) { + assertTrue(iae.getMessage().endsWith("foo.xml does not exist.")); + } + } + + @Test + void getImageAsDataUri_ReturnsTheImageAsADataUri_WhenAFileIsSpecified() throws Exception { + String imageAsDataUri = ImageUtils.getImageAsDataUri(new File("./src/test/resources/image.png")); + assertTrue(imageAsDataUri.startsWith("data:image/png;base64,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")); + } + + @Test + void validateImage() { + // allowed + 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"); + ImageUtils.validateImage("image.svg"); + ImageUtils.validateImage("data:image/svg+xml;utf8,iVBORw0KGg"); + + //disallowed + try { + ImageUtils.validateImage("data:image/other"); + fail(); + } catch (Exception e) { + assertEquals("Only PNG, JPG, and SVG data URIs are supported: data:image/other", e.getMessage()); + } + } + + @Test + void isSupportedDataUri() { + assertTrue(ImageUtils.isSupportedDataUri("data:image/png;base64,iVBORw0KGg")); + assertTrue(ImageUtils.isSupportedDataUri("data:image/jpeg;base64,iVBORw0KGg")); + assertTrue(ImageUtils.isSupportedDataUri("data:image/svg+xml;utf8, { + branding.setLogo("htt://blah"); + }); } @Test - public void test_setLogo_DoesNothing_WhenANullUrlIsSpecified() { + void setLogo_DoesNothing_WhenANullUrlIsSpecified() { branding.setLogo(null); assertNull(branding.getLogo()); } @Test - public void test_setLogo_DoesNothing_WhenAnEmptyUrlIsSpecified() { + void setLogo_DoesNothing_WhenAnEmptyUrlIsSpecified() { branding.setLogo(" "); assertNull(branding.getLogo()); } diff --git a/structurizr-core/src/test/java/com/structurizr/view/ColorTests.java b/structurizr-core/src/test/java/com/structurizr/view/ColorTests.java new file mode 100644 index 000000000..660cfd40e --- /dev/null +++ b/structurizr-core/src/test/java/com/structurizr/view/ColorTests.java @@ -0,0 +1,48 @@ +package com.structurizr.view; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +public class ColorTests { + + @Test + void isHexColorCode_ReturnsFalse_WhenPassedNull() { + assertFalse(Color.isHexColorCode(null)); + } + + @Test + void isHexColorCode_ReturnsFalse_WhenPassedAnEmptyString() { + assertFalse(Color.isHexColorCode("")); + } + + @Test + void isHexColorCode_ReturnsFalse_WhenPassedAnInvalidString() { + assertFalse(Color.isHexColorCode("ffffff")); + assertFalse(Color.isHexColorCode("#fffff")); + assertFalse(Color.isHexColorCode("#gggggg")); + } + + @Test + void isHexColorCode_ReturnsTrue_WhenPassedAnValidString() { + assertTrue(Color.isHexColorCode("#abcdef")); + assertTrue(Color.isHexColorCode("#ABCDEF")); + assertTrue(Color.isHexColorCode("#123456")); + } + + @Test + void fromColorNameToHexColorCode_ReturnsNull_WhenPassedAnInvalidName() { + assertNull(Color.fromColorNameToHexColorCode("hello world")); + } + + @Test + 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 diff --git a/structurizr-core/src/test/java/com/structurizr/view/ComponentViewTests.java b/structurizr-core/src/test/java/com/structurizr/view/ComponentViewTests.java new file mode 100644 index 000000000..a1408b08b --- /dev/null +++ b/structurizr-core/src/test/java/com/structurizr/view/ComponentViewTests.java @@ -0,0 +1,819 @@ +package com.structurizr.view; + +import com.structurizr.AbstractWorkspaceTestBase; +import com.structurizr.model.*; +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.jupiter.api.Assertions.*; + +public class ComponentViewTests extends AbstractWorkspaceTestBase { + + private SoftwareSystem softwareSystem; + private Container webApplication; + private ComponentView view; + + @BeforeEach + public void setUp() { + softwareSystem = model.addSoftwareSystem("The System", "Description"); + webApplication = softwareSystem.addContainer("Web Application", "Does something", "Apache Tomcat"); + view = new ComponentView(webApplication, "Key", "Some description"); + } + + @Test + void construction() { + assertEquals("Component View: The System - Web Application", view.getName()); + assertEquals("Some description", view.getDescription()); + assertEquals(0, view.getElements().size()); + assertSame(softwareSystem, view.getSoftwareSystem()); + assertEquals(softwareSystem.getId(), view.getSoftwareSystemId()); + assertEquals(webApplication.getId(), view.getContainerId()); + assertSame(model, view.getModel()); + } + + @Test + void addAllSoftwareSystems_DoesNothing_WhenThereAreNoOtherSoftwareSystems() { + assertEquals(0, view.getElements().size()); + view.addAllSoftwareSystems(); + assertEquals(0, view.getElements().size()); + } + + @Test + void addAllSoftwareSystems_AddsAllSoftwareSystems_WhenThereAreSomeSoftwareSystemsInTheModel() { + SoftwareSystem softwareSystemA = model.addSoftwareSystem("System A", "Description"); + SoftwareSystem softwareSystemB = model.addSoftwareSystem("System B", "Description"); + + view.addAllSoftwareSystems(); + + assertEquals(2, view.getElements().size()); + assertTrue(view.getElements().contains(new ElementView(softwareSystemA))); + assertTrue(view.getElements().contains(new ElementView(softwareSystemB))); + } + + @Test + void addAllPeople_DoesNothing_WhenThereAreNoPeople() { + assertEquals(0, view.getElements().size()); + view.addAllPeople(); + assertEquals(0, view.getElements().size()); + } + + @Test + void addAllPeople_AddsAllPeople_WhenThereAreSomePeopleInTheModel() { + Person userA = model.addPerson("User A", "Description"); + Person userB = model.addPerson("User B", "Description"); + + view.addAllPeople(); + + assertEquals(2, view.getElements().size()); + assertTrue(view.getElements().contains(new ElementView(userA))); + assertTrue(view.getElements().contains(new ElementView(userB))); + } + + @Test + void addAllElements_DoesNothing_WhenThereAreNoSoftwareSystemsOrPeople() { + assertEquals(0, view.getElements().size()); + view.addAllElements(); + assertEquals(0, view.getElements().size()); + } + + @Test + void addAllElements_AddsAllSoftwareSystemsAndPeopleAndContainersAndComponents_WhenThereAreSomeSoftwareSystemsAndPeopleAndContainersAndComponentsInTheModel() { + 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"); + + view.addAllElements(); + + assertEquals(7, view.getElements().size()); + assertTrue(view.getElements().contains(new ElementView(softwareSystemA))); + assertTrue(view.getElements().contains(new ElementView(softwareSystemB))); + assertTrue(view.getElements().contains(new ElementView(userA))); + assertTrue(view.getElements().contains(new ElementView(userB))); + assertTrue(view.getElements().contains(new ElementView(database))); + assertTrue(view.getElements().contains(new ElementView(componentA))); + assertTrue(view.getElements().contains(new ElementView(componentB))); + } + + @Test + void addAllContainers_DoesNothing_WhenThereAreNoContainers() { + assertEquals(0, view.getElements().size()); + view.addAllContainers(); + assertEquals(0, view.getElements().size()); + } + + @Test + void addAllContainers_AddsAllContainers_WhenThereAreSomeContainers() { + Container database = softwareSystem.addContainer("Database", "Stores something", "MySQL"); + Container fileSystem = softwareSystem.addContainer("File System", "Stores something else", ""); + + view.addAllContainers(); + + assertEquals(2, view.getElements().size()); + assertTrue(view.getElements().contains(new ElementView(database))); + assertTrue(view.getElements().contains(new ElementView(fileSystem))); + } + + @Test + void addAllComponents_DoesNothing_WhenThereAreNoComponents() { + assertEquals(0, view.getElements().size()); + view.addAllComponents(); + assertEquals(0, view.getElements().size()); + } + + @Test + void addAllComponents_AddsAllComponents_WhenThereAreSomeComponents() { + Component componentA = webApplication.addComponent("Component A", "Does something", "Java"); + Component componentB = webApplication.addComponent("Component B", "Does something", "Java"); + + view.addAllComponents(); + + assertEquals(2, view.getElements().size()); + assertTrue(view.getElements().contains(new ElementView(componentA))); + assertTrue(view.getElements().contains(new ElementView(componentB))); + } + + @Test + void add_ThrowsAnException_WhenANullContainerIsSpecified() { + assertEquals(0, view.getElements().size()); + + try { + view.add((Container) null); + fail(); + } catch (IllegalArgumentException iae) { + assertEquals("An element must be specified.", iae.getMessage()); + } + } + + @Test + void add_AddsTheContainer_WhenTheContainerIsNoInTheViewAlready() { + Container database = softwareSystem.addContainer("Database", "Stores something", "MySQL"); + + assertEquals(0, view.getElements().size()); + view.add(database); + assertEquals(1, view.getElements().size()); + assertTrue(view.getElements().contains(new ElementView(database))); + } + + @Test + void add_DoesNothing_WhenTheSpecifiedContainerIsAlreadyInTheView() { + Container database = softwareSystem.addContainer("Database", "Stores something", "MySQL"); + view.add(database); + assertEquals(1, view.getElements().size()); + assertTrue(view.getElements().contains(new ElementView(database))); + + view.add(database); + assertEquals(1, view.getElements().size()); + } + + @Test + void remove_ThrowsAndException_WhenANullContainerIsPassed() { + try { + view.remove((Container) null); + fail(); + } catch (IllegalArgumentException iae) { + assertEquals("An element must be specified.", iae.getMessage()); + } + } + + @Test + void remove_RemovesTheContainer_WhenTheContainerIsInTheView() { + Container database = softwareSystem.addContainer("Database", "Stores something", "MySQL"); + view.add(database); + assertEquals(1, view.getElements().size()); + assertTrue(view.getElements().contains(new ElementView(database))); + + view.remove(database); + assertEquals(0, view.getElements().size()); + } + + @Test + void remove_DoesNothing_WhenTheContainerIsNotInTheView() { + Container database = softwareSystem.addContainer("Database", "Stores something", "MySQL"); + Container fileSystem = softwareSystem.addContainer("File System", "Stores something else", ""); + + view.add(database); + assertEquals(1, view.getElements().size()); + assertTrue(view.getElements().contains(new ElementView(database))); + + view.remove(fileSystem); + assertEquals(1, view.getElements().size()); + assertTrue(view.getElements().contains(new ElementView(database))); + } + + @Test + void add_DoesNothing_WhenANullComponentIsSpecified() { + assertEquals(0, view.getElements().size()); + view.add((Component) null); + assertEquals(0, view.getElements().size()); + } + + @Test + void add_AddsTheComponent_WhenTheComponentIsNotInTheViewAlready() { + Component componentA = webApplication.addComponent("Component A", "Does something", "Java"); + + assertEquals(0, view.getElements().size()); + view.add(componentA); + assertEquals(1, view.getElements().size()); + assertTrue(view.getElements().contains(new ElementView(componentA))); + } + + @Test + void add_DoesNothing_WhenTheSpecifiedComponentIsAlreadyInTheView() { + Component componentA = webApplication.addComponent("Component A", "Does something", "Java"); + view.add(componentA); + assertEquals(1, view.getElements().size()); + assertTrue(view.getElements().contains(new ElementView(componentA))); + + view.add(componentA); + assertEquals(1, view.getElements().size()); + } + + @Test + void add_ThrowsAnException_WhenTheSpecifiedComponentIsInADifferentContainer() { + try { + SoftwareSystem softwareSystemA = model.addSoftwareSystem("System A", "Description"); + + final Container containerA1 = softwareSystemA.addContainer("Container A1", "Description", "Tec"); + + final Container containerA2 = softwareSystemA.addContainer("Container A2", "Description", "Tec"); + final Component componentA2_1 = containerA2.addComponent("Component A2-1", "Description"); + + view = new ComponentView(containerA1, "components", "Description"); + view.add(componentA2_1); + } catch (Exception e) { + assertEquals("Only components belonging to Container A1 can be added to this view.", e.getMessage()); + } + } + + @Test + void add_ThrowsAnException_WhenTheContainerOfTheViewIsAdded() { + try { + view.add(webApplication); + fail(); + } catch (ElementNotPermittedInViewException e) { + assertEquals("The container in scope cannot be added to a component view.", e.getMessage()); + } + } + + @Test + void add_DoesNothing_WhenTheContainerOfTheViewIsAddedViaDependency() { + final SoftwareSystem softwareSystem = model.addSoftwareSystem("Some other system", "external system that uses our web application"); + + final Relationship relationshipFromExternalSystem = softwareSystem.uses(webApplication, ""); + + 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(0, view.getElements().stream().map(e -> e.getElement()).filter(e -> e.equals(webApplication)).count(), "the container itself is not added to the view"); + } + + @Test + void remove_DoesNothing_WhenANullComponentIsPassed() { + try { + view.remove((Component) null); + fail(); + } catch (IllegalArgumentException iae) { + assertEquals("An element must be specified.", iae.getMessage()); + } + } + + @Test + void remove_RemovesTheComponent_WhenTheComponentIsInTheView() { + Component componentA = webApplication.addComponent("Component A", "Does something", "Java"); + view.add(componentA); + assertEquals(1, view.getElements().size()); + assertTrue(view.getElements().contains(new ElementView(componentA))); + + view.remove(componentA); + assertEquals(0, view.getElements().size()); + } + + @Test + 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"); + + view.add(componentA); + view.add(componentB); + assertEquals(2, view.getElements().size()); + assertEquals(1, view.getRelationships().size()); + + view.remove(componentB); + assertEquals(1, view.getElements().size()); + assertEquals(0, view.getRelationships().size()); + } + + @Test + void remove_DoesNothing_WhenTheComponentIsNotInTheView() { + Component componentA = webApplication.addComponent("Component A", "Does something", "Java"); + Component componentB = webApplication.addComponent("Component B", "Does something", "Java"); + + view.add(componentA); + assertEquals(1, view.getElements().size()); + assertTrue(view.getElements().contains(new ElementView(componentA))); + + view.remove(componentB); + assertEquals(1, view.getElements().size()); + assertTrue(view.getElements().contains(new ElementView(componentA))); + } + + @Test + void addNearestNeightbours_DoesNothing_WhenANullElementIsSpecified() { + view.addNearestNeighbours(null); + + assertEquals(0, view.getElements().size()); + } + + @Test + void addNearestNeighbours_DoesNothing_WhenThereAreNoNeighbours() { + Component component = webApplication.addComponent("Component", "", ""); + view.add(component); + assertEquals(1, view.getElements().size()); + + view.addNearestNeighbours(component); + assertEquals(1, view.getElements().size()); + } + + @Test + 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"); + Person userB = model.addPerson("User B", "Description"); + + // userA -> systemA -> system -> systemB -> userB + userA.uses(softwareSystemA, ""); + softwareSystemA.uses(softwareSystem, ""); + softwareSystem.uses(softwareSystemB, ""); + softwareSystemB.delivers(userB, ""); + + // userA -> systemA -> web application -> systemB -> userB + // web application -> database + Container database = softwareSystem.addContainer("Database", "", ""); + softwareSystemA.uses(webApplication, ""); + webApplication.uses(softwareSystemB, ""); + webApplication.uses(database, ""); + + // userA -> systemA -> controller -> service -> repository -> database + Component controller = webApplication.addComponent("Controller", ""); + Component service = webApplication.addComponent("Service", ""); + Component repository = webApplication.addComponent("Repository", ""); + softwareSystemA.uses(controller, ""); + controller.uses(service, ""); + service.uses(repository, ""); + repository.uses(database, ""); + + // userA -> systemA -> controller -> service -> systemB -> userB + service.uses(softwareSystemB, ""); + + view = new ComponentView(webApplication, "components", "Description"); + view.addNearestNeighbours(softwareSystemA); + + assertEquals(3, view.getElements().size()); + assertTrue(view.getElements().contains(new ElementView(userA))); + assertTrue(view.getElements().contains(new ElementView(softwareSystemA))); + assertTrue(view.getElements().contains(new ElementView(controller))); + + view = new ComponentView(webApplication, "components", "Description"); + view.addNearestNeighbours(controller); + + assertEquals(3, view.getElements().size()); + assertTrue(view.getElements().contains(new ElementView(softwareSystemA))); + assertTrue(view.getElements().contains(new ElementView(controller))); + assertTrue(view.getElements().contains(new ElementView(service))); + + view = new ComponentView(webApplication, "components", "Description"); + view.addNearestNeighbours(service); + + assertEquals(4, view.getElements().size()); + assertTrue(view.getElements().contains(new ElementView(controller))); + assertTrue(view.getElements().contains(new ElementView(service))); + assertTrue(view.getElements().contains(new ElementView(repository))); + assertTrue(view.getElements().contains(new ElementView(softwareSystemB))); + } + + @Test + void addExternalDependencies_AddsOrphanedElements_WhenThereAreNoDirectRelationshipsWithAComponent() { + SoftwareSystem source = model.addSoftwareSystem("Source", ""); + SoftwareSystem destination = model.addSoftwareSystem("Destination", ""); + + SoftwareSystem a = model.addSoftwareSystem("A", ""); + Container aa = a.addContainer("AA", "", ""); + Component aaa = aa.addComponent("AAA", "", ""); + + source.uses(aa, ""); + aa.uses(destination, ""); + + view = new ComponentView(aa, "components", "Description"); + view.addAllComponents(); + view.addExternalDependencies(); + + // check that the view includes the desired elements + Set elementsInView = view.getElements().stream().map(ElementView::getElement).collect(Collectors.toSet()); + assertTrue(elementsInView.contains(aaa)); + + // but there are no relationships (because component AAA isn't directly related to anything) + assertEquals(0, view.getRelationships().size()); + } + + @Test + void addExternalDependencies_AddsTheContainer_WhenAComponentHasARelationshipToAContainerInTheSameSoftwareSystem() { + SoftwareSystem softwareSystemA = model.addSoftwareSystem("Software System A", ""); + Container containerA = softwareSystemA.addContainer("Container A", "", ""); + Component componentA = containerA.addComponent("Component A", "", ""); + Container containerB = softwareSystemA.addContainer("Container B", "", ""); + + componentA.uses(containerB, "uses"); + + view = new ComponentView(containerA, "key", "description"); + view.addAllComponents(); + view.addExternalDependencies(); + assertEquals(2, view.getElements().size()); + assertTrue(view.getElements().contains(new ElementView(componentA))); + assertTrue(view.getElements().contains(new ElementView(containerB))); + } + + @Test + void addExternalDependencies_AddsTheContainer_WhenAComponentHasARelationshipFromAContainerInTheSameSoftwareSystem() { + SoftwareSystem softwareSystemA = model.addSoftwareSystem("Software System A", ""); + Container containerA = softwareSystemA.addContainer("Container A", "", ""); + Component componentA = containerA.addComponent("Component A", "", ""); + Container containerB = softwareSystemA.addContainer("Container B", "", ""); + + containerB.uses(componentA, "uses"); + + view = new ComponentView(containerA, "key", "description"); + view.addAllComponents(); + view.addExternalDependencies(); + assertEquals(2, view.getElements().size()); + assertTrue(view.getElements().contains(new ElementView(componentA))); + assertTrue(view.getElements().contains(new ElementView(containerB))); + } + + @Test + void addExternalDependencies_AddsTheParentContainer_WhenAComponentHasARelationshipToAComponentInADifferentContainerInTheSameSoftwareSystem() { + SoftwareSystem softwareSystemA = model.addSoftwareSystem("Software System A", ""); + Container containerA = softwareSystemA.addContainer("Container A", "", ""); + Component componentA = containerA.addComponent("Component A", "", ""); + Container containerB = softwareSystemA.addContainer("Container B", "", ""); + Component componentB = containerB.addComponent("Component B", "", ""); + + componentA.uses(componentB, "uses"); + + view = new ComponentView(containerA, "key", "description"); + view.addAllComponents(); + view.addExternalDependencies(); + assertEquals(2, view.getElements().size()); + assertTrue(view.getElements().contains(new ElementView(componentA))); + assertTrue(view.getElements().contains(new ElementView(containerB))); + } + + @Test + void addExternalDependencies_AddsTheParentContainer_WhenAComponentHasARelationshipFromAComponentInADifferentContainerInTheSameSoftwareSystem() { + SoftwareSystem softwareSystemA = model.addSoftwareSystem("Software System A", ""); + Container containerA = softwareSystemA.addContainer("Container A", "", ""); + Component componentA = containerA.addComponent("Component A", "", ""); + Container containerB = softwareSystemA.addContainer("Container B", "", ""); + Component componentB = containerB.addComponent("Component B", "", ""); + + componentB.uses(componentA, "uses"); + + view = new ComponentView(containerA, "key", "description"); + view.addAllComponents(); + view.addExternalDependencies(); + assertEquals(2, view.getElements().size()); + assertTrue(view.getElements().contains(new ElementView(componentA))); + assertTrue(view.getElements().contains(new ElementView(containerB))); + } + + @Test + void addExternalDependencies_AddsTheParentSoftwareSystem_WhenAComponentHasARelationshipToAContainerInAnotherSoftwareSystem() { + SoftwareSystem softwareSystemA = model.addSoftwareSystem("Software System A", ""); + Container containerA = softwareSystemA.addContainer("Container A", "", ""); + Component componentA = containerA.addComponent("Component A", "", ""); + SoftwareSystem softwareSystemB = model.addSoftwareSystem("Software System B", ""); + Container containerB = softwareSystemB.addContainer("Container B", "", ""); + + componentA.uses(containerB, "uses"); + + view = new ComponentView(containerA, "key", "description"); + view.addAllComponents(); + view.addExternalDependencies(); + assertEquals(2, view.getElements().size()); + assertTrue(view.getElements().contains(new ElementView(componentA))); + assertTrue(view.getElements().contains(new ElementView(softwareSystemB))); + } + + @Test + void addExternalDependencies_AddsTheParentSoftwareSystem_WhenAComponentHasARelationshipFromAContainerInAnotherSoftwareSystem() { + SoftwareSystem softwareSystemA = model.addSoftwareSystem("Software System A", ""); + Container containerA = softwareSystemA.addContainer("Container A", "", ""); + Component componentA = containerA.addComponent("Component A", "", ""); + SoftwareSystem softwareSystemB = model.addSoftwareSystem("Software System B", ""); + Container containerB = softwareSystemB.addContainer("Container B", "", ""); + + containerB.uses(componentA, "uses"); + + view = new ComponentView(containerA, "key", "description"); + view.addAllComponents(); + view.addExternalDependencies(); + assertEquals(2, view.getElements().size()); + assertTrue(view.getElements().contains(new ElementView(componentA))); + assertTrue(view.getElements().contains(new ElementView(softwareSystemB))); + } + + @Test + void addExternalDependencies_AddsTheParentSoftwareSystem_WhenAComponentHasARelationshipToAComponentInAnotherSoftwareSystem() { + SoftwareSystem softwareSystemA = model.addSoftwareSystem("Software System A", ""); + Container containerA = softwareSystemA.addContainer("Container A", "", ""); + Component componentA = containerA.addComponent("Component A", "", ""); + SoftwareSystem softwareSystemB = model.addSoftwareSystem("Software System B", ""); + Container containerB = softwareSystemB.addContainer("Container B", "", ""); + Component componentB = containerB.addComponent("Component B", "", ""); + + componentA.uses(componentB, "uses"); + + view = new ComponentView(containerA, "key", "description"); + view.addAllComponents(); + view.addExternalDependencies(); + assertEquals(2, view.getElements().size()); + assertTrue(view.getElements().contains(new ElementView(componentA))); + assertTrue(view.getElements().contains(new ElementView(softwareSystemB))); + } + + @Test + void addExternalDependencies_AddsTheParentSoftwareSystem_WhenAComponentHasARelationshipFromAComponentInAnotherSoftwareSystem() { + SoftwareSystem softwareSystemA = model.addSoftwareSystem("Software System A", ""); + Container containerA = softwareSystemA.addContainer("Container A", "", ""); + Component componentA = containerA.addComponent("Component A", "", ""); + SoftwareSystem softwareSystemB = model.addSoftwareSystem("Software System B", ""); + Container containerB = softwareSystemB.addContainer("Container B", "", ""); + Component componentB = containerB.addComponent("Component B", "", ""); + + componentB.uses(componentA, "uses"); + + view = new ComponentView(containerA, "key", "description"); + view.addAllComponents(); + view.addExternalDependencies(); + assertEquals(2, view.getElements().size()); + assertTrue(view.getElements().contains(new ElementView(componentA))); + assertTrue(view.getElements().contains(new ElementView(softwareSystemB))); + } + + @Test + void addDefaultElements() { + model.setImpliedRelationshipsStrategy(new CreateImpliedRelationshipsUnlessAnyRelationshipExistsStrategy()); + + CustomElement element = model.addCustomElement("Custom"); + Person user1 = model.addPerson("User 1"); + Person user2 = model.addPerson("User 2"); + 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", "", ""); + + user1.uses(component1, "Uses"); + user2.uses(component2, "Uses"); + component1.uses(component2, "Uses"); + + view = new ComponentView(container1, "components", "Description"); + view.addDefaultElements(); + + assertEquals(3, view.getElements().size()); + assertFalse(view.getElements().contains(new ElementView(element))); + assertTrue(view.getElements().contains(new ElementView(user1))); + assertFalse(view.getElements().contains(new ElementView(user2))); + assertFalse(view.getElements().contains(new ElementView(softwareSystem1))); + assertTrue(view.getElements().contains(new ElementView(softwareSystem2))); + assertFalse(view.getElements().contains(new ElementView(container1))); + assertFalse(view.getElements().contains(new ElementView(container2))); + assertTrue(view.getElements().contains(new ElementView(component1))); + assertFalse(view.getElements().contains(new ElementView(component2))); + + element.uses(component1, "Uses"); + view.addDefaultElements(); + + assertEquals(4, view.getElements().size()); + assertTrue(view.getElements().contains(new ElementView(element))); + assertTrue(view.getElements().contains(new ElementView(user1))); + assertFalse(view.getElements().contains(new ElementView(user2))); + assertFalse(view.getElements().contains(new ElementView(softwareSystem1))); + assertTrue(view.getElements().contains(new ElementView(softwareSystem2))); + assertFalse(view.getElements().contains(new ElementView(container1))); + assertFalse(view.getElements().contains(new ElementView(container2))); + assertTrue(view.getElements().contains(new ElementView(component1))); + 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"); + Container container = softwareSystem.addContainer("Container"); + + view = new ComponentView(container, "components", "Description"); + try { + view.add(softwareSystem); + fail(); + } catch (ElementNotPermittedInViewException e) { + assertEquals("The software system in scope cannot be added to a component view.", e.getMessage()); + } + } + + @Test + void addContainer_ThrowsAnException_WhenTheContainerIsTheScopeOfTheView() { + SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System"); + Container container = softwareSystem.addContainer("Container"); + + view = new ComponentView(container, "components", "Description"); + try { + view.add(container); + fail(); + } catch (ElementNotPermittedInViewException e) { + assertEquals("The container in scope cannot be added to a component view.", e.getMessage()); + } + } + + @Test + void addSoftwareSystem_ThrowsAnException_WhenAChildContainerIsAlreadyAdded() { + try { + 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"); + + ComponentView view = views.createComponentView(container1, "key", "Description"); + + view.add(container2); + view.add(softwareSystem2); + + fail(); + } catch (ElementNotPermittedInViewException e) { + assertEquals("A child of Software System 2 is already in this view.", e.getMessage()); + } + } + + @Test + void addSoftwareSystem_ThrowsAnException_WhenAChildComponentIsAlreadyAdded() { + try { + 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"); + + ComponentView view = views.createComponentView(container1, "key", "Description"); + + view.add(component2); + view.add(softwareSystem2); + + fail(); + } catch (ElementNotPermittedInViewException e) { + assertEquals("A child of Software System 2 is already in this view.", e.getMessage()); + } + } + + @Test + void addContainer_ThrowsAnException_WhenAChildComponentIsAlreadyAdded() { + try { + 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"); + + ComponentView view = views.createComponentView(container1, "key", "Description"); + + view.add(component2); + view.add(container2); + + fail(); + } catch (ElementNotPermittedInViewException e) { + assertEquals("A child of Container 2 is already in this view.", e.getMessage()); + } + } + + @Test + void addContainer_ThrowsAnException_WhenTheParentIsAlreadyAdded() { + try { + 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"); + + ComponentView view = views.createComponentView(container1, "key", "Description"); + + view.add(softwareSystem2); + view.add(container2); + + fail(); + } catch (ElementNotPermittedInViewException e) { + assertEquals("A parent of Container 2 is already in this view.", e.getMessage()); + } + } + + @Test + void addComponent_ThrowsAnException_WhenTheParentIsAlreadyAdded() { + try { + 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"); + + ComponentView view = views.createComponentView(container1, "key", "Description"); + + view.add(softwareSystem2); + view.add(component2); + + fail(); + } catch (ElementNotPermittedInViewException e) { + assertEquals("A parent of Component 2 is already in this view.", e.getMessage()); + } + } + +} \ No newline at end of file diff --git a/structurizr-core/src/test/java/com/structurizr/view/ConfigurationTests.java b/structurizr-core/src/test/java/com/structurizr/view/ConfigurationTests.java new file mode 100644 index 000000000..5a15e24dc --- /dev/null +++ b/structurizr-core/src/test/java/com/structurizr/view/ConfigurationTests.java @@ -0,0 +1,71 @@ +package com.structurizr.view; + +import com.structurizr.AbstractWorkspaceTestBase; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +public class ConfigurationTests extends AbstractWorkspaceTestBase { + + @Test + void defaultView_DoesNothing_WhenPassedNull() { + Configuration configuration = new Configuration(); + configuration.setDefaultView((View) null); + assertNull(configuration.getDefaultView()); + } + + @Test + void defaultView() { + SystemLandscapeView view = views.createSystemLandscapeView("key", "Description"); + Configuration configuration = new Configuration(); + configuration.setDefaultView(view); + assertEquals("key", configuration.getDefaultView()); + } + + @Test + void copyConfigurationFrom() { + Configuration source = new Configuration(); + source.setLastSavedView("someKey"); + + Configuration destination = new Configuration(); + destination.copyConfigurationFrom(source); + assertEquals("someKey", destination.getLastSavedView()); + } + + @Test + void setTheme_WithAUrl() { + Configuration configuration = new Configuration(); + configuration.setTheme("https://example.com/theme.json"); + 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.getThemes()[0]); + } + + @Test + void setTheme_ThrowsAnIllegalArgumentException_WhenAnInvalidUrlIsSpecified() { + assertThrows(IllegalArgumentException.class, () -> { + Configuration configuration = new Configuration(); + configuration.setTheme("htt://blah"); + }); + } + + @Test + void setTheme_DoesNothing_WhenANullUrlIsSpecified() { + Configuration configuration = new Configuration(); + configuration.setTheme(null); + assertEquals(0, configuration.getThemes().length); + } + + @Test + void setTheme_DoesNothing_WhenAnEmptyUrlIsSpecified() { + Configuration configuration = new Configuration(); + configuration.setTheme(" "); + assertEquals(0, configuration.getThemes().length); + } + +} \ No newline at end of file diff --git a/structurizr-core/src/test/java/com/structurizr/view/ContainerViewTests.java b/structurizr-core/src/test/java/com/structurizr/view/ContainerViewTests.java new file mode 100644 index 000000000..7d877de13 --- /dev/null +++ b/structurizr-core/src/test/java/com/structurizr/view/ContainerViewTests.java @@ -0,0 +1,426 @@ +package com.structurizr.view; + +import com.structurizr.AbstractWorkspaceTestBase; +import com.structurizr.model.*; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +public class ContainerViewTests extends AbstractWorkspaceTestBase { + + private SoftwareSystem softwareSystem; + private ContainerView view; + + @BeforeEach + public void setUp() { + softwareSystem = model.addSoftwareSystem("The System", "Description"); + view = new ContainerView(softwareSystem, "containers", "Description"); + } + + @Test + void construction() { + assertEquals("Container View: The System", view.getName()); + assertEquals("Description", view.getDescription()); + assertEquals(0, view.getElements().size()); + assertSame(softwareSystem, view.getSoftwareSystem()); + assertEquals(softwareSystem.getId(), view.getSoftwareSystemId()); + assertSame(model, view.getModel()); + } + + @Test + void addAllSoftwareSystems_DoesNothing_WhenThereAreNoOtherSoftwareSystems() { + assertEquals(0, view.getElements().size()); + view.addAllSoftwareSystems(); + assertEquals(0, view.getElements().size()); + } + + @Test + void addAllSoftwareSystems_AddsAllSoftwareSystems_WhenThereAreSomeSoftwareSystemsInTheModel() { + SoftwareSystem softwareSystemA = model.addSoftwareSystem("System A", "Description"); + SoftwareSystem softwareSystemB = model.addSoftwareSystem("System B", "Description"); + + view.addAllSoftwareSystems(); + + assertEquals(2, view.getElements().size()); + assertTrue(view.getElements().contains(new ElementView(softwareSystemA))); + assertTrue(view.getElements().contains(new ElementView(softwareSystemB))); + } + + @Test + void addAllPeople_DoesNothing_WhenThereAreNoPeople() { + assertEquals(0, view.getElements().size()); + view.addAllPeople(); + assertEquals(0, view.getElements().size()); + } + + @Test + void addAllPeople_AddsAllPeople_WhenThereAreSomePeopleInTheModel() { + Person userA = model.addPerson("User A", "Description"); + Person userB = model.addPerson("User B", "Description"); + + view.addAllPeople(); + + assertEquals(2, view.getElements().size()); + assertTrue(view.getElements().contains(new ElementView(userA))); + assertTrue(view.getElements().contains(new ElementView(userB))); + } + + @Test + void addAllElements_DoesNothing_WhenThereAreNoSoftwareSystemsOrPeople() { + assertEquals(0, view.getElements().size()); + view.addAllElements(); + assertEquals(0, view.getElements().size()); + } + + @Test + void addAllElements_AddsAllSoftwareSystemsAndPeopleAndContainers_WhenThereAreSomeSoftwareSystemsAndPeopleAndContainersInTheModel() { + 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"); + + view.addAllElements(); + + assertEquals(6, view.getElements().size()); + assertTrue(view.getElements().contains(new ElementView(softwareSystemA))); + assertTrue(view.getElements().contains(new ElementView(softwareSystemB))); + assertTrue(view.getElements().contains(new ElementView(userA))); + assertTrue(view.getElements().contains(new ElementView(userB))); + assertTrue(view.getElements().contains(new ElementView(webApplication))); + assertTrue(view.getElements().contains(new ElementView(database))); + } + + @Test + void addAllContainers_DoesNothing_WhenThereAreNoContainers() { + assertEquals(0, view.getElements().size()); + view.addAllContainers(); + assertEquals(0, view.getElements().size()); + } + + @Test + void addAllContainers_AddsAllContainers_WhenThereAreSomeContainers() { + Container webApplication = softwareSystem.addContainer("Web Application", "Does something", "Apache Tomcat"); + Container database = softwareSystem.addContainer("Database", "Does something", "MySQL"); + + view.addAllContainers(); + + assertEquals(2, view.getElements().size()); + assertTrue(view.getElements().contains(new ElementView(webApplication))); + assertTrue(view.getElements().contains(new ElementView(database))); + } + + @Test + void addNearestNeightbours_DoesNothing_WhenANullElementIsSpecified() { + view.addNearestNeighbours(null); + + assertEquals(0, view.getElements().size()); + } + + @Test + void addNearestNeighbours_DoesNothing_WhenThereAreNoNeighbours() { + view.addNearestNeighbours(softwareSystem); + + assertEquals(0, view.getElements().size()); + } + + @Test + 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"); + Person userB = model.addPerson("User B", "Description"); + + // userA -> systemA -> system -> systemB -> userB + userA.uses(softwareSystemA, ""); + softwareSystemA.uses(softwareSystem, ""); + softwareSystem.uses(softwareSystemB, ""); + softwareSystemB.delivers(userB, ""); + + // userA -> systemA -> web application -> systemB -> userB + // web application -> database + Container webApplication = softwareSystem.addContainer("Web Application", "", ""); + Container database = softwareSystem.addContainer("Database", "", ""); + softwareSystemA.uses(webApplication, ""); + webApplication.uses(softwareSystemB, ""); + webApplication.uses(database, ""); + + // userA -> systemA -> controller -> service -> repository -> database + Component controller = webApplication.addComponent("Controller", ""); + Component service = webApplication.addComponent("Service", ""); + Component repository = webApplication.addComponent("Repository", ""); + softwareSystemA.uses(controller, ""); + controller.uses(service, ""); + service.uses(repository, ""); + repository.uses(database, ""); + + // userA -> systemA -> controller -> service -> systemB -> userB + service.uses(softwareSystemB, ""); + + view.addNearestNeighbours(webApplication); + + assertEquals(4, view.getElements().size()); + assertTrue(view.getElements().contains(new ElementView(softwareSystemA))); + assertTrue(view.getElements().contains(new ElementView(softwareSystemB))); + assertTrue(view.getElements().contains(new ElementView(webApplication))); + assertTrue(view.getElements().contains(new ElementView(database))); + + view = new ContainerView(softwareSystem, "containers", "Description"); + view.addNearestNeighbours(softwareSystemA); + + assertEquals(3, view.getElements().size()); + assertTrue(view.getElements().contains(new ElementView(userA))); + assertTrue(view.getElements().contains(new ElementView(softwareSystemA))); + assertTrue(view.getElements().contains(new ElementView(webApplication))); + + view = new ContainerView(softwareSystem, "containers", "Description"); + view.addNearestNeighbours(webApplication); + + assertEquals(4, view.getElements().size()); + assertTrue(view.getElements().contains(new ElementView(softwareSystemA))); + assertTrue(view.getElements().contains(new ElementView(webApplication))); + assertTrue(view.getElements().contains(new ElementView(database))); + assertTrue(view.getElements().contains(new ElementView(softwareSystemB))); + } + + @Test + void remove_RemovesContainer() { + Container webApplication = softwareSystem.addContainer("Web Application", "", ""); + Container database = softwareSystem.addContainer("Database", "", ""); + + view.addAllContainers(); + assertEquals(2, view.getElements().size()); + + view.remove(webApplication); + assertEquals(1, view.getElements().size()); + assertTrue(view.getElements().contains(new ElementView(database))); + } + + @Test + void remove_ElementsWithTag() { + final String TAG = "myTag"; + Container webApplication = softwareSystem.addContainer("Web Application", "", ""); + Container database = softwareSystem.addContainer("Database", "", ""); + database.addTags(TAG); + + view.addAllContainers(); + assertEquals(2, view.getElements().size()); + + view.removeElementsWithTag(TAG); + assertEquals(1, view.getElements().size()); + assertEquals(webApplication, view.getElements().iterator().next().getElement()); + } + + @Test + void remove_RelationshipWithTag() { + final String TAG = "myTag"; + Container webApplication = softwareSystem.addContainer("Web Application", "", ""); + Container database = softwareSystem.addContainer("Database", "", ""); + webApplication.uses(database, "").addTags(TAG); + + view.addAllContainers(); + assertEquals(2, view.getElements().size()); + assertEquals(1, view.getRelationships().size()); + + view.removeRelationshipsWithTag(TAG); + assertEquals(2, view.getElements().size()); + assertEquals(0, view.getRelationships().size()); + } + + @Test + void addDependentSoftwareSystem() { + assertEquals(0, view.getElements().size()); + assertEquals(0, view.getRelationships().size()); + + view.addDependentSoftwareSystems(); + + SoftwareSystem softwareSystem2 = model.addSoftwareSystem("SoftwareSystem 2", ""); + + view.addDependentSoftwareSystems(); + assertEquals(0, view.getElements().size()); + assertEquals(0, view.getRelationships().size()); + + softwareSystem2.uses(softwareSystem, ""); + + view.addDependentSoftwareSystems(); + assertEquals(1, view.getElements().size()); + } + + @Test + void addDependentSoftwareSystem2() { + Container container1a = softwareSystem.addContainer("Container 1A", "", ""); + + SoftwareSystem softwareSystem2 = model.addSoftwareSystem("SoftwareSystem 2", ""); + Container container2a = softwareSystem2.addContainer("Container 2-A", "", ""); + + model.setImpliedRelationshipsStrategy(new CreateImpliedRelationshipsUnlessAnyRelationshipExistsStrategy()); + container2a.uses(container1a, ""); + + view.addDependentSoftwareSystems(); + view.addAllContainers(); + + assertEquals(2, view.getElements().size()); + assertEquals(1, view.getRelationships().size()); + } + + @Test + void addDefaultElements() { + model.setImpliedRelationshipsStrategy(new CreateImpliedRelationshipsUnlessAnyRelationshipExistsStrategy()); + + CustomElement element = model.addCustomElement("Custom"); + Person user1 = model.addPerson("User 1"); + Person user2 = model.addPerson("User 2"); + SoftwareSystem softwareSystem1 = model.addSoftwareSystem("Software System 1"); + Container container1 = softwareSystem1.addContainer("Container 1", "", ""); + SoftwareSystem softwareSystem2 = model.addSoftwareSystem("Software System 2"); + Container container2 = softwareSystem2.addContainer("Container 2", "", ""); + + user1.uses(container1, "Uses"); + user2.uses(container2, "Uses"); + container1.uses(container2, "Uses"); + + view = new ContainerView(softwareSystem1, "containers", "Description"); + view.addDefaultElements(); + + assertEquals(3, view.getElements().size()); + assertFalse(view.getElements().contains(new ElementView(element))); + assertTrue(view.getElements().contains(new ElementView(user1))); + assertFalse(view.getElements().contains(new ElementView(user2))); + assertFalse(view.getElements().contains(new ElementView(softwareSystem1))); + assertTrue(view.getElements().contains(new ElementView(softwareSystem2))); + assertTrue(view.getElements().contains(new ElementView(container1))); + assertFalse(view.getElements().contains(new ElementView(container2))); + + element.uses(container1, "Uses"); + view.addDefaultElements(); + + assertEquals(4, view.getElements().size()); + assertTrue(view.getElements().contains(new ElementView(element))); + assertTrue(view.getElements().contains(new ElementView(user1))); + assertFalse(view.getElements().contains(new ElementView(user2))); + assertFalse(view.getElements().contains(new ElementView(softwareSystem1))); + assertTrue(view.getElements().contains(new ElementView(softwareSystem2))); + assertTrue(view.getElements().contains(new ElementView(container1))); + 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"); + + view = new ContainerView(softwareSystem, "containers", "Description"); + try { + view.add(softwareSystem); + fail(); + } catch (ElementNotPermittedInViewException e) { + assertEquals("The software system in scope cannot be added to a container view.", e.getMessage()); + } + } + + @Test + void addSoftwareSystem_ThrowsAnException_WhenAChildContainerIsAlreadyAdded() { + try { + SoftwareSystem softwareSystem1 = model.addSoftwareSystem("Software System 1"); + Container container1 = softwareSystem1.addContainer("Container 1"); + + SoftwareSystem softwareSystem2 = model.addSoftwareSystem("Software System 2"); + Container container2 = softwareSystem2.addContainer("Container 2"); + + ContainerView view = views.createContainerView(softwareSystem1, "key", "Description"); + + view.add(container1); + view.add(container2); + view.add(softwareSystem2); + + fail(); + } catch (ElementNotPermittedInViewException e) { + assertEquals("A child of Software System 2 is already in this view.", e.getMessage()); + } + } + + @Test + void addContainer_ThrowsAnException_WhenTheParentIsAlreadyAdded() { + try { + SoftwareSystem softwareSystem1 = model.addSoftwareSystem("Software System 1"); + Container container1 = softwareSystem1.addContainer("Container 1"); + + SoftwareSystem softwareSystem2 = model.addSoftwareSystem("Software System 2"); + Container container2 = softwareSystem2.addContainer("Container 2"); + + ContainerView view = views.createContainerView(softwareSystem1, "key", "Description"); + + view.add(container1); + view.add(softwareSystem2); + view.add(container2); + + fail(); + } catch (ElementNotPermittedInViewException e) { + assertEquals("A parent of Container 2 is already in this view.", e.getMessage()); + } + } + +} \ No newline at end of file diff --git a/structurizr-core/src/test/java/com/structurizr/view/DefaultLayoutMergeStrategyTests.java b/structurizr-core/src/test/java/com/structurizr/view/DefaultLayoutMergeStrategyTests.java new file mode 100644 index 000000000..fdb582f80 --- /dev/null +++ b/structurizr-core/src/test/java/com/structurizr/view/DefaultLayoutMergeStrategyTests.java @@ -0,0 +1,277 @@ +package com.structurizr.view; + +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 { + + @Test + void copyLayoutInformation_WhenCanonicalNamesHaveNotChanged() { + Workspace workspace1 = new Workspace("1", ""); + SoftwareSystem softwareSystem1 = workspace1.getModel().addSoftwareSystem("Software System"); + Container container1 = softwareSystem1.addContainer("Container", "", ""); + ContainerView view1 = workspace1.getViews().createContainerView(softwareSystem1, "key", ""); + view1.add(container1); + view1.getElementView(container1).setX(123); + view1.getElementView(container1).setY(456); + + Workspace workspace2 = new Workspace("2", ""); + SoftwareSystem softwareSystem2 = workspace2.getModel().addSoftwareSystem("Software System"); + Container container2 = softwareSystem2.addContainer("Container", "", ""); + ContainerView view2 = workspace2.getViews().createContainerView(softwareSystem2, "key", ""); + view2.add(container2); + + DefaultLayoutMergeStrategy strategy = new DefaultLayoutMergeStrategy(); + strategy.copyLayoutInformation(view1, view2); + + assertEquals(123, view2.getElementView(container2).getX()); + assertEquals(456, view2.getElementView(container2).getY()); + } + + @Test + void copyLayoutInformation_WhenAParentElementNameHasChanged() { + Workspace workspace1 = new Workspace("1", ""); + SoftwareSystem softwareSystem1 = workspace1.getModel().addSoftwareSystem("Software System"); + Container container1 = softwareSystem1.addContainer("Container", "", ""); + ContainerView view1 = workspace1.getViews().createContainerView(softwareSystem1, "key", ""); + view1.add(container1); + view1.getElementView(container1).setX(123); + view1.getElementView(container1).setY(456); + + Workspace workspace2 = new Workspace("2", ""); + SoftwareSystem softwareSystem2 = workspace2.getModel().addSoftwareSystem("Software System with a new name"); + Container container2 = softwareSystem2.addContainer("Container", "", ""); + ContainerView view2 = workspace2.getViews().createContainerView(softwareSystem2, "key", ""); + view2.add(container2); + + DefaultLayoutMergeStrategy strategy = new DefaultLayoutMergeStrategy(); + strategy.copyLayoutInformation(view1, view2); + + assertEquals(123, view2.getElementView(container2).getX()); + assertEquals(456, view2.getElementView(container2).getY()); + } + + @Test + void copyLayoutInformation_WhenAnElementNameHasChangedButTheDescriptionHasNotChanged() { + Workspace workspace1 = new Workspace("1", ""); + SoftwareSystem softwareSystem1 = workspace1.getModel().addSoftwareSystem("Software System"); + Container container1 = softwareSystem1.addContainer("Container", "Container description", ""); + ContainerView view1 = workspace1.getViews().createContainerView(softwareSystem1, "key", ""); + view1.add(container1); + view1.getElementView(container1).setX(123); + view1.getElementView(container1).setY(456); + + Workspace workspace2 = new Workspace("2", ""); + SoftwareSystem softwareSystem2 = workspace2.getModel().addSoftwareSystem("Software System"); + Container container2 = softwareSystem2.addContainer("Container with a new name", "Container description", ""); + ContainerView view2 = workspace2.getViews().createContainerView(softwareSystem2, "key", ""); + view2.add(container2); + + DefaultLayoutMergeStrategy strategy = new DefaultLayoutMergeStrategy(); + strategy.copyLayoutInformation(view1, view2); + + assertEquals(123, view2.getElementView(container2).getX()); + assertEquals(456, view2.getElementView(container2).getY()); + } + + @Test + void copyLayoutInformation_WhenAnElementNameAndDescriptionHaveChangedButTheIdHasNotChanged() { + Workspace workspace1 = new Workspace("1", ""); + SoftwareSystem softwareSystem1 = workspace1.getModel().addSoftwareSystem("Software System"); + Container container1 = softwareSystem1.addContainer("Container", "Container description", ""); + ContainerView view1 = workspace1.getViews().createContainerView(softwareSystem1, "key", ""); + view1.add(container1); + view1.getElementView(container1).setX(123); + view1.getElementView(container1).setY(456); + + Workspace workspace2 = new Workspace("2", ""); + SoftwareSystem softwareSystem2 = workspace2.getModel().addSoftwareSystem("Software System"); + Container container2 = softwareSystem2.addContainer("Container with a new name", "Container with a new description", ""); + ContainerView view2 = workspace2.getViews().createContainerView(softwareSystem2, "key", ""); + view2.add(container2); + + DefaultLayoutMergeStrategy strategy = new DefaultLayoutMergeStrategy(); + strategy.copyLayoutInformation(view1, view2); + + assertEquals(123, view2.getElementView(container2).getX()); + assertEquals(456, view2.getElementView(container2).getY()); + } + + @Test + void copyLayoutInformation_WhenAnElementNameAndDescriptionAndIdHaveChanged() { + Workspace workspace1 = new Workspace("1", ""); + SoftwareSystem softwareSystem1 = workspace1.getModel().addSoftwareSystem("Software System"); + Container container1 = softwareSystem1.addContainer("Container", "Container description", ""); + ContainerView view1 = workspace1.getViews().createContainerView(softwareSystem1, "key", ""); + view1.add(container1); + view1.getElementView(container1).setX(123); + view1.getElementView(container1).setY(456); + + Workspace workspace2 = new Workspace("2", ""); + SoftwareSystem softwareSystem2 = workspace2.getModel().addSoftwareSystem("Software System"); + softwareSystem2.addContainer("Web Application", "Description", ""); // this element has ID 2 + Container container2 = softwareSystem2.addContainer("Database", "Description", ""); + ContainerView view2 = workspace2.getViews().createContainerView(softwareSystem2, "key", ""); + view2.add(container2); + + DefaultLayoutMergeStrategy strategy = new DefaultLayoutMergeStrategy(); + strategy.copyLayoutInformation(view1, view2); + + assertEquals(0, view2.getElementView(container2).getX()); + assertEquals(0, view2.getElementView(container2).getY()); + } + + @Test + void copyLayoutInformation_WhenAnElementNameAndDescriptionAndIdHaveChangedAndDescriptionWasNull() { + Workspace workspace1 = new Workspace("1", ""); + SoftwareSystem softwareSystem1 = workspace1.getModel().addSoftwareSystem("Software System"); + Container container1 = softwareSystem1.addContainer("Container"); + container1.setDescription(null); + ContainerView view1 = workspace1.getViews().createContainerView(softwareSystem1, "key", ""); + view1.add(container1); + view1.getElementView(container1).setX(123); + view1.getElementView(container1).setY(456); + + Workspace workspace2 = new Workspace("2", ""); + SoftwareSystem softwareSystem2 = workspace2.getModel().addSoftwareSystem("Software System"); + softwareSystem2.addContainer("Web Application", "Description", ""); // this element has ID 2 + Container container2 = softwareSystem2.addContainer("Database", "Description", ""); + ContainerView view2 = workspace2.getViews().createContainerView(softwareSystem2, "key", ""); + view2.add(container2); + + DefaultLayoutMergeStrategy strategy = new DefaultLayoutMergeStrategy(); + strategy.copyLayoutInformation(view1, view2); + + assertEquals(0, view2.getElementView(container2).getX()); + assertEquals(0, view2.getElementView(container2).getY()); + } + + @Test + void copyLayoutInformation_DoesNotThrowAnExceptionWhenAddingAnElementToAView() { + Workspace workspace1 = new Workspace("1", ""); + SoftwareSystem softwareSystem1A = workspace1.getModel().addSoftwareSystem("Software System A"); + SoftwareSystem softwareSystem1B = workspace1.getModel().addSoftwareSystem("Software System B"); + softwareSystem1A.uses(softwareSystem1B, "Uses"); + SystemLandscapeView view1 = workspace1.getViews().createSystemLandscapeView("key", "description"); + view1.add(softwareSystem1A); + + Workspace workspace2 = new Workspace("2", ""); + SoftwareSystem softwareSystem2A = workspace2.getModel().addSoftwareSystem("Software System A"); + SoftwareSystem softwareSystem2B = workspace2.getModel().addSoftwareSystem("Software System B"); + softwareSystem2A.uses(softwareSystem2B, "Uses"); + SystemLandscapeView view2 = workspace2.getViews().createSystemLandscapeView("key", "description"); + view2.add(softwareSystem2A); + view2.add(softwareSystem2B); + + DefaultLayoutMergeStrategy strategy = new DefaultLayoutMergeStrategy(); + 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 diff --git a/structurizr-core/src/test/java/com/structurizr/view/DeploymentViewTests.java b/structurizr-core/src/test/java/com/structurizr/view/DeploymentViewTests.java new file mode 100644 index 000000000..56b0a0038 --- /dev/null +++ b/structurizr-core/src/test/java/com/structurizr/view/DeploymentViewTests.java @@ -0,0 +1,539 @@ +package com.structurizr.view; + +import com.structurizr.AbstractWorkspaceTestBase; +import com.structurizr.model.*; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +public class DeploymentViewTests extends AbstractWorkspaceTestBase { + + private DeploymentView deploymentView; + + @BeforeEach + public void setup() { + } + + @Test + void getName_WithNoSoftwareSystemAndNoEnvironment() { + deploymentView = views.createDeploymentView("deployment", "Description"); + assertEquals("Deployment View: Default", deploymentView.getName()); + } + + @Test + void getName_WithNoSoftwareSystemAndAnEnvironment() { + deploymentView = views.createDeploymentView("deployment", "Description"); + deploymentView.setEnvironment("Live"); + assertEquals("Deployment View: Live", deploymentView.getName()); + } + + @Test + void getName_WithASoftwareSystemAndNoEnvironment() { + SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System", ""); + deploymentView = views.createDeploymentView(softwareSystem, "deployment", "Description"); + assertEquals("Deployment View: Software System - Default", deploymentView.getName()); + } + + @Test + void getName_WithASoftwareSystemAndAnEnvironment() { + SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System", ""); + deploymentView = views.createDeploymentView(softwareSystem, "deployment", "Description"); + deploymentView.setEnvironment("Live"); + assertEquals("Deployment View: Software System - Live", deploymentView.getName()); + } + + @Test + void addDeploymentNode_ThrowsAnException_WhenPassedNull() { + try { + deploymentView = views.createDeploymentView("key", "Description"); + deploymentView.add((DeploymentNode) null); + fail(); + } catch (IllegalArgumentException iae) { + assertEquals("A deployment node must be specified.", iae.getMessage()); + } + } + + @Test + void addRelationship_ThrowsAnException_WhenPassedNull() { + try { + deploymentView = views.createDeploymentView("key", "Description"); + deploymentView.add((Relationship) null); + fail(); + } catch (IllegalArgumentException iae) { + assertEquals("A relationship must be specified.", iae.getMessage()); + } + } + + @Test + void addAllDeploymentNodes_DoesNothing_WhenThereAreNoTopLevelDeploymentNodes() { + deploymentView = views.createDeploymentView("deployment", "Description"); + + deploymentView.addAllDeploymentNodes(); + assertEquals(0, deploymentView.getElements().size()); + } + + @Test + void addAllDeploymentNodes_DoesNothing_WhenThereAreTopLevelDeploymentNodesButNoContainerInstances() { + deploymentView = views.createDeploymentView("deployment", "Description"); + model.addDeploymentNode("Deployment Node", "Description", "Technology"); + + deploymentView.addAllDeploymentNodes(); + assertEquals(0, deploymentView.getElements().size()); + } + + @Test + 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"); + ContainerInstance containerInstance = deploymentNode.add(container); + + deploymentView = views.createDeploymentView(softwareSystem, "deployment", "Description"); + deploymentView.setEnvironment("Live"); + deploymentView.addAllDeploymentNodes(); + assertEquals(0, deploymentView.getElements().size()); + } + + @Test + 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"); + ContainerInstance containerInstance = deploymentNode.add(container); + + deploymentView = views.createDeploymentView(softwareSystem, "deployment", "Description"); + deploymentView.addAllDeploymentNodes(); + assertEquals(2, deploymentView.getElements().size()); + assertTrue(deploymentView.getElements().contains(new ElementView(deploymentNode))); + assertTrue(deploymentView.getElements().contains(new ElementView(containerInstance))); + } + + @Test + 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"); + DeploymentNode deploymentNodeChild = deploymentNodeParent.addDeploymentNode("Deployment Node", "Description", "Technology"); + ContainerInstance containerInstance = deploymentNodeChild.add(container); + + deploymentView = views.createDeploymentView(softwareSystem, "deployment", "Description"); + deploymentView.addAllDeploymentNodes(); + assertEquals(3, deploymentView.getElements().size()); + assertTrue(deploymentView.getElements().contains(new ElementView(deploymentNodeParent))); + assertTrue(deploymentView.getElements().contains(new ElementView(deploymentNodeChild))); + assertTrue(deploymentView.getElements().contains(new ElementView(containerInstance))); + } + + @Test + 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"); + ContainerInstance containerInstance1 = deploymentNode1.add(container1); + + SoftwareSystem softwareSystem2 = model.addSoftwareSystem("Software System 2", ""); + Container container2 = softwareSystem2.addContainer("Container 2", "Description", "Technology"); + DeploymentNode deploymentNode2 = model.addDeploymentNode("Deployment Node 2", "Description", "Technology"); + ContainerInstance containerInstance2 = deploymentNode2.add(container2); + + // two containers from different software systems on the same deployment node + deploymentNode1.add(container2); + + deploymentView = views.createDeploymentView(softwareSystem1, "deployment", "Description"); + deploymentView.addAllDeploymentNodes(); + + assertEquals(2, deploymentView.getElements().size()); + assertTrue(deploymentView.getElements().contains(new ElementView(deploymentNode1))); + assertTrue(deploymentView.getElements().contains(new ElementView(containerInstance1))); + } + + @Test + void addDeploymentNode_AddsTheParentToo() { + SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System", ""); + Container container = softwareSystem.addContainer("Container", "Description", "Technology"); + DeploymentNode deploymentNodeParent = model.addDeploymentNode("Deployment Node", "Description", "Technology"); + DeploymentNode deploymentNodeChild = deploymentNodeParent.addDeploymentNode("Deployment Node", "Description", "Technology"); + ContainerInstance containerInstance = deploymentNodeChild.add(container); + + deploymentView = views.createDeploymentView(softwareSystem, "deployment", "Description"); + deploymentView.add(deploymentNodeChild); + assertEquals(3, deploymentView.getElements().size()); + assertTrue(deploymentView.getElements().contains(new ElementView(deploymentNodeParent))); + assertTrue(deploymentView.getElements().contains(new ElementView(deploymentNodeChild))); + assertTrue(deploymentView.getElements().contains(new ElementView(containerInstance))); + } + + @Test + 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"); + liveDeploymentNode.addInfrastructureNode("Load Balancer"); + + deploymentView = views.createDeploymentView("deployment", "Description"); + deploymentView.setEnvironment("Dev"); + deploymentView.add(devDeploymentNode); // should work + + try { + deploymentView.add(liveDeploymentNode); // should fail + fail(); + } catch (Exception e) { + assertEquals("Only elements in the Dev deployment environment can be added to this view.", e.getMessage()); + } + } + + @Test + void addSoftwareSystemInstance_ThrowsAnException_WhenTheSoftwareSystemInstanceIsTheSoftwareSystemInScope() { + SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System"); + DeploymentNode deploymentNode = model.addDeploymentNode("Deployment Node", "Description", "Technology"); + SoftwareSystemInstance softwareSystemInstance = deploymentNode.add(softwareSystem); + + deploymentView = views.createDeploymentView(softwareSystem, "deployment", "Description"); + deploymentView.add(deploymentNode); + + // the software system instance won't have been added (neither will the empty parent deployment node) + assertEquals(0, deploymentView.getElements().size()); + assertNull(deploymentView.getElementView(softwareSystemInstance)); + } + + @Test + void addSoftwareSystemInstance_DoesNotAddTheSoftwareSystemInstance_WhenAChildContainerInstanceHasAlreadyBeenAdded() { + SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System"); + Container container = softwareSystem.addContainer("Container"); + DeploymentNode deploymentNode1 = model.addDeploymentNode("Deployment Node 1", "Description", "Technology"); + DeploymentNode deploymentNode2 = model.addDeploymentNode("Deployment Node 2", "Description", "Technology"); + ContainerInstance containerInstance = deploymentNode1.add(container); + SoftwareSystemInstance softwareSystemInstance = deploymentNode2.add(softwareSystem); + + deploymentView = views.createDeploymentView("deployment", "Description"); + deploymentView.add(deploymentNode1); + deploymentView.add(deploymentNode2); + + // the software system instance won't have been added (neither will the empty parent deployment node) + assertEquals(2, deploymentView.getElements().size()); + assertNotNull(deploymentView.getElementView(containerInstance)); + assertNull(deploymentView.getElementView(softwareSystemInstance)); + } + + @Test + void addContainerInstance_DoesNotAddTheContainerInstance_WhenTheParentSoftwareSystemInstanceHasAlreadyBeenAdded() { + SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System"); + Container container = softwareSystem.addContainer("Container"); + DeploymentNode deploymentNode1 = model.addDeploymentNode("Deployment Node 1", "Description", "Technology"); + DeploymentNode deploymentNode2 = model.addDeploymentNode("Deployment Node 2", "Description", "Technology"); + SoftwareSystemInstance softwareSystemInstance = deploymentNode1.add(softwareSystem); + ContainerInstance containerInstance = deploymentNode2.add(container); + + deploymentView = views.createDeploymentView("deployment", "Description"); + deploymentView.add(deploymentNode1); + deploymentView.add(deploymentNode2); + + // the container instance won't have been added (neither will the empty parent deployment node) + assertEquals(2, deploymentView.getElements().size()); + assertNotNull(deploymentView.getElementView(softwareSystemInstance)); + assertNull(deploymentView.getElementView(containerInstance)); + } + + @Test + void addAnimationStep_ThrowsAnException_WhenNoElementInstancesAreSpecified() { + try { + deploymentView = views.createDeploymentView("deployment", "Description"); + deploymentView.addAnimation((ContainerInstance[]) null); + fail(); + } catch (IllegalArgumentException iae) { + assertEquals("One or more software system/container instances must be specified.", iae.getMessage()); + } + } + + @Test + void addAnimationStep_ThrowsAnException_WhenNoInfrastructureNodesAreSpecified() { + try { + deploymentView = views.createDeploymentView("deployment", "Description"); + deploymentView.addAnimation((InfrastructureNode[]) null); + fail(); + } catch (IllegalArgumentException iae) { + assertEquals("One or more infrastructure nodes must be specified.", iae.getMessage()); + } + } + + @Test + void addAnimationStep_ThrowsAnException_WhenNoElementInstancesOrInfrastructureNodesAreSpecified() { + try { + deploymentView = views.createDeploymentView("deployment", "Description"); + deploymentView.addAnimation(null, null); + fail(); + } catch (IllegalArgumentException iae) { + assertEquals("One or more software system/container instances and/or infrastructure nodes must be specified.", iae.getMessage()); + } + } + + @Test + void addAnimationStep() { + SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System", ""); + Container webApplication = softwareSystem.addContainer("Web Application", "Description", "Technology"); + Container database = softwareSystem.addContainer("Database", "Description", "Technology"); + webApplication.uses(database, "Reads from and writes to", "JDBC/HTTPS"); + + DeploymentNode developerLaptop = model.addDeploymentNode("Developer Laptop", "Description", "Technology"); + DeploymentNode apacheTomcat = developerLaptop.addDeploymentNode("Apache Tomcat", "Description", "Technology"); + DeploymentNode oracle = developerLaptop.addDeploymentNode("Oracle", "Description", "Technology"); + ContainerInstance webApplicationInstance = apacheTomcat.add(webApplication); + ContainerInstance databaseInstance = oracle.add(database); + + deploymentView = views.createDeploymentView(softwareSystem, "deployment", "Description"); + deploymentView.add(developerLaptop); + + deploymentView.addAnimation(webApplicationInstance); + deploymentView.addAnimation(databaseInstance); + + Animation step1 = deploymentView.getAnimations().stream().filter(step -> step.getOrder() == 1).findFirst().get(); + assertEquals(3, step1.getElements().size()); + assertTrue(step1.getElements().contains(developerLaptop.getId())); + assertTrue(step1.getElements().contains(apacheTomcat.getId())); + assertTrue(step1.getElements().contains(webApplicationInstance.getId())); + assertEquals(0, step1.getRelationships().size()); + + Animation step2 = deploymentView.getAnimations().stream().filter(step -> step.getOrder() == 2).findFirst().get(); + assertEquals(2, step2.getElements().size()); + assertTrue(step2.getElements().contains(oracle.getId())); + assertTrue(step2.getElements().contains(databaseInstance.getId())); + assertEquals(1, step2.getRelationships().size()); + assertTrue(step2.getRelationships().contains(webApplicationInstance.getRelationships().stream().findFirst().get().getId())); + } + + @Test + void addAnimationStep_IgnoresContainerInstancesThatDoNotExistInTheView() { + SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System", ""); + Container webApplication = softwareSystem.addContainer("Web Application", "Description", "Technology"); + Container database = softwareSystem.addContainer("Database", "Description", "Technology"); + webApplication.uses(database, "Reads from and writes to", "JDBC/HTTPS"); + + DeploymentNode developerLaptop = model.addDeploymentNode("Developer Laptop", "Description", "Technology"); + DeploymentNode apacheTomcat = developerLaptop.addDeploymentNode("Apache Tomcat", "Description", "Technology"); + DeploymentNode oracle = developerLaptop.addDeploymentNode("Oracle", "Description", "Technology"); + ContainerInstance webApplicationInstance = apacheTomcat.add(webApplication); + ContainerInstance databaseInstance = oracle.add(database); + + deploymentView = views.createDeploymentView(softwareSystem, "deployment", "Description"); + deploymentView.add(apacheTomcat); + + deploymentView.addAnimation(webApplicationInstance, databaseInstance); + + Animation step1 = deploymentView.getAnimations().stream().filter(step -> step.getOrder() == 1).findFirst().get(); + assertEquals(3, step1.getElements().size()); + assertTrue(step1.getElements().contains(developerLaptop.getId())); + assertTrue(step1.getElements().contains(apacheTomcat.getId())); + assertTrue(step1.getElements().contains(webApplicationInstance.getId())); + assertEquals(0, step1.getRelationships().size()); + } + + @Test + void addAnimationStep_ThrowsAnException_WhenContainerInstancesAreSpecifiedButNoneOfThemExistInTheView() { + try { + SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System", ""); + Container webApplication = softwareSystem.addContainer("Web Application", "Description", "Technology"); + Container database = softwareSystem.addContainer("Database", "Description", "Technology"); + webApplication.uses(database, "Reads from and writes to", "JDBC/HTTPS"); + + DeploymentNode developerLaptop = model.addDeploymentNode("Developer Laptop", "Description", "Technology"); + DeploymentNode apacheTomcat = developerLaptop.addDeploymentNode("Apache Tomcat", "Description", "Technology"); + DeploymentNode oracle = developerLaptop.addDeploymentNode("Oracle", "Description", "Technology"); + ContainerInstance webApplicationInstance = apacheTomcat.add(webApplication); + ContainerInstance databaseInstance = oracle.add(database); + + deploymentView = views.createDeploymentView(softwareSystem, "deployment", "Description"); + + deploymentView.addAnimation(webApplicationInstance, databaseInstance); + fail(); + } catch (IllegalArgumentException iae) { + assertEquals("None of the specified elements exist in this view.", iae.getMessage()); + } + } + + @Test + void remove_RemovesTheInfrastructureNode() { + SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System", ""); + Container container = softwareSystem.addContainer("Container", "Description", "Technology"); + DeploymentNode deploymentNodeParent = model.addDeploymentNode("Deployment Node", "Description", "Technology"); + DeploymentNode deploymentNodeChild = deploymentNodeParent.addDeploymentNode("Deployment Node", "Description", "Technology"); + InfrastructureNode infrastructureNode = deploymentNodeChild.addInfrastructureNode("Infrastructure Node"); + ContainerInstance containerInstance = deploymentNodeChild.add(container); + + deploymentView = views.createDeploymentView(softwareSystem, "deployment", "Description"); + deploymentView.addAllDeploymentNodes(); + assertEquals(4, deploymentView.getElements().size()); + + deploymentView.remove(infrastructureNode); + assertEquals(3, deploymentView.getElements().size()); + assertTrue(deploymentView.getElements().contains(new ElementView(deploymentNodeParent))); + assertTrue(deploymentView.getElements().contains(new ElementView(deploymentNodeChild))); + assertTrue(deploymentView.getElements().contains(new ElementView(containerInstance))); + } + + @Test + 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"); + InfrastructureNode infrastructureNode = deploymentNodeChild.addInfrastructureNode("Infrastructure Node"); + SoftwareSystemInstance softwareSystemInstance = deploymentNodeChild.add(softwareSystem); + + deploymentView = views.createDeploymentView("deployment", "Description"); + deploymentView.addAllDeploymentNodes(); + assertEquals(4, deploymentView.getElements().size()); + + deploymentView.remove(softwareSystemInstance); + assertEquals(3, deploymentView.getElements().size()); + assertTrue(deploymentView.getElements().contains(new ElementView(deploymentNodeParent))); + assertTrue(deploymentView.getElements().contains(new ElementView(deploymentNodeChild))); + assertTrue(deploymentView.getElements().contains(new ElementView(infrastructureNode))); + } + + @Test + void remove_RemovesTheContainerInstance() { + SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System"); + Container container = softwareSystem.addContainer("Container", "Description", "Technology"); + DeploymentNode deploymentNodeParent = model.addDeploymentNode("Deployment Node", "Description", "Technology"); + DeploymentNode deploymentNodeChild = deploymentNodeParent.addDeploymentNode("Deployment Node", "Description", "Technology"); + InfrastructureNode infrastructureNode = deploymentNodeChild.addInfrastructureNode("Infrastructure Node"); + ContainerInstance containerInstance = deploymentNodeChild.add(container); + + deploymentView = views.createDeploymentView(softwareSystem, "deployment", "Description"); + deploymentView.addAllDeploymentNodes(); + assertEquals(4, deploymentView.getElements().size()); + + deploymentView.remove(containerInstance); + assertEquals(3, deploymentView.getElements().size()); + assertTrue(deploymentView.getElements().contains(new ElementView(deploymentNodeParent))); + assertTrue(deploymentView.getElements().contains(new ElementView(deploymentNodeChild))); + assertTrue(deploymentView.getElements().contains(new ElementView(infrastructureNode))); + } + + @Test + void remove_RemovesTheDeploymentNodeAndChildren() { + SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System", ""); + Container container = softwareSystem.addContainer("Container", "Description", "Technology"); + DeploymentNode deploymentNodeParent = model.addDeploymentNode("Deployment Node", "Description", "Technology"); + DeploymentNode deploymentNodeChild = deploymentNodeParent.addDeploymentNode("Deployment Node", "Description", "Technology"); + InfrastructureNode infrastructureNode = deploymentNodeChild.addInfrastructureNode("Infrastructure Node"); + ContainerInstance containerInstance = deploymentNodeChild.add(container); + + deploymentView = views.createDeploymentView(softwareSystem, "deployment", "Description"); + deploymentView.addAllDeploymentNodes(); + assertEquals(4, deploymentView.getElements().size()); + + deploymentView.remove(deploymentNodeChild); + assertEquals(1, deploymentView.getElements().size()); + assertTrue(deploymentView.getElements().contains(new ElementView(deploymentNodeParent))); + } + + @Test + void remove_RemovesTheChildDeploymentNodeAndChildren() { + SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System", ""); + Container container = softwareSystem.addContainer("Container", "Description", "Technology"); + DeploymentNode deploymentNodeParent = model.addDeploymentNode("Deployment Node", "Description", "Technology"); + DeploymentNode deploymentNodeChild = deploymentNodeParent.addDeploymentNode("Deployment Node", "Description", "Technology"); + InfrastructureNode infrastructureNode = deploymentNodeChild.addInfrastructureNode("Infrastructure Node"); + ContainerInstance containerInstance = deploymentNodeChild.add(container); + + deploymentView = views.createDeploymentView(softwareSystem, "deployment", "Description"); + deploymentView.addAllDeploymentNodes(); + assertEquals(4, deploymentView.getElements().size()); + + deploymentView.remove(deploymentNodeParent); + assertEquals(0, deploymentView.getElements().size()); + } + + @Test + 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"); + InfrastructureNode infrastructureNode2 = deploymentNodeChild.addInfrastructureNode("Infrastructure Node 2"); + + deploymentView = views.createDeploymentView("deployment", "Description"); + deploymentView.add(infrastructureNode1); + + assertEquals(3, deploymentView.getElements().size()); + assertTrue(deploymentView.getElements().contains(new ElementView(deploymentNodeParent))); + assertTrue(deploymentView.getElements().contains(new ElementView(deploymentNodeChild))); + assertTrue(deploymentView.getElements().contains(new ElementView(infrastructureNode1))); + } + + @Test + 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"); + InfrastructureNode infrastructureNode = deploymentNodeChild.addInfrastructureNode("Infrastructure Node "); + SoftwareSystemInstance softwareSystemInstance = deploymentNodeChild.add(softwareSystem); + + deploymentView = views.createDeploymentView("deployment", "Description"); + deploymentView.add(softwareSystemInstance); + + assertEquals(3, deploymentView.getElements().size()); + assertTrue(deploymentView.getElements().contains(new ElementView(deploymentNodeParent))); + assertTrue(deploymentView.getElements().contains(new ElementView(deploymentNodeChild))); + assertTrue(deploymentView.getElements().contains(new ElementView(softwareSystemInstance))); + } + + @Test + void addSoftwareSystemInstance_ThrowsAnException_WhenAChildContainerInstanceHasAlreadyBeenAdded() { + SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System"); + Container container = softwareSystem.addContainer("Container"); + DeploymentNode deploymentNodeParent = model.addDeploymentNode("Deployment Node", "Description", "Technology"); + DeploymentNode deploymentNodeChild = deploymentNodeParent.addDeploymentNode("Deployment Node", "Description", "Technology"); + SoftwareSystemInstance softwareSystemInstance = deploymentNodeChild.add(softwareSystem); + ContainerInstance containerInstance = deploymentNodeChild.add(container); + + deploymentView = views.createDeploymentView("deployment", "Description"); + deploymentView.add(containerInstance); + + try { + deploymentView.add(softwareSystemInstance); + fail(); + } catch (ElementNotPermittedInViewException e) { + assertEquals("A child of Software System is already in this view.", e.getMessage()); + } + } + + @Test + void add_AddsTheContainerInstance() { + SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System"); + Container container = softwareSystem.addContainer("Container"); + DeploymentNode deploymentNodeParent = model.addDeploymentNode("Deployment Node", "Description", "Technology"); + DeploymentNode deploymentNodeChild = deploymentNodeParent.addDeploymentNode("Deployment Node", "Description", "Technology"); + InfrastructureNode infrastructureNode = deploymentNodeChild.addInfrastructureNode("Infrastructure Node "); + ContainerInstance containerInstance = deploymentNodeChild.add(container); + + deploymentView = views.createDeploymentView("deployment", "Description"); + deploymentView.add(containerInstance); + + assertEquals(3, deploymentView.getElements().size()); + assertTrue(deploymentView.getElements().contains(new ElementView(deploymentNodeParent))); + assertTrue(deploymentView.getElements().contains(new ElementView(deploymentNodeChild))); + assertTrue(deploymentView.getElements().contains(new ElementView(containerInstance))); + } + + @Test + void addContainerInstance_ThrowsAnException_WhenTheParentSoftwareSystemInstanceHasAlreadyBeenAdded() { + SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System"); + Container container = softwareSystem.addContainer("Container"); + DeploymentNode deploymentNodeParent = model.addDeploymentNode("Deployment Node", "Description", "Technology"); + DeploymentNode deploymentNodeChild = deploymentNodeParent.addDeploymentNode("Deployment Node", "Description", "Technology"); + SoftwareSystemInstance softwareSystemInstance = deploymentNodeChild.add(softwareSystem); + ContainerInstance containerInstance = deploymentNodeChild.add(container); + + deploymentView = views.createDeploymentView("deployment", "Description"); + deploymentView.add(softwareSystemInstance); + + try { + deploymentView.add(containerInstance); + fail(); + } catch (ElementNotPermittedInViewException e) { + assertEquals("A parent of Container is already in this view.", e.getMessage()); + } + } + +} \ No newline at end of file diff --git a/structurizr-core/src/test/java/com/structurizr/view/DimensionsTests.java b/structurizr-core/src/test/java/com/structurizr/view/DimensionsTests.java new file mode 100644 index 000000000..da073721b --- /dev/null +++ b/structurizr-core/src/test/java/com/structurizr/view/DimensionsTests.java @@ -0,0 +1,40 @@ +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 DimensionsTests { + + @Test + void construction() { + Dimensions dimensions = new Dimensions(123, 456); + + assertEquals(123, dimensions.getWidth()); + assertEquals(456, dimensions.getHeight()); + } + + @Test + void setWidth_ThrowsAnException_WhenANegativeIntegerIsSpecified() { + try { + Dimensions dimensions = new Dimensions(); + dimensions.setWidth(-100); + fail(); + } catch (IllegalArgumentException iae) { + assertEquals("The width must be a positive integer.", iae.getMessage()); + } + } + + @Test + void setHeight_ThrowsAnException_WhenANegativeIntegerIsSpecified() { + try { + Dimensions dimensions = new Dimensions(); + dimensions.setHeight(-100); + fail(); + } catch (IllegalArgumentException iae) { + assertEquals("The height must be a positive integer.", iae.getMessage()); + } + } + +} \ No newline at end of file diff --git a/structurizr-core/src/test/java/com/structurizr/view/DynamicViewTests.java b/structurizr-core/src/test/java/com/structurizr/view/DynamicViewTests.java new file mode 100644 index 000000000..bcbea49d6 --- /dev/null +++ b/structurizr-core/src/test/java/com/structurizr/view/DynamicViewTests.java @@ -0,0 +1,497 @@ +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.ArrayList; +import java.util.LinkedList; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +public class DynamicViewTests extends AbstractWorkspaceTestBase { + + private Person person; + private SoftwareSystem softwareSystemA; + private Container containerA1; + private Container containerA2; + private Container containerA3; + private Component componentA1; + private Component componentA2; + + private SoftwareSystem softwareSystemB; + private Container containerB1; + private Component componentB1; + + private Relationship relationship; + + @BeforeEach + public void setup() { + person = model.addPerson("Person", ""); + softwareSystemA = model.addSoftwareSystem("Software System A", ""); + containerA1 = softwareSystemA.addContainer("Container A1", "", ""); + componentA1 = containerA1.addComponent("Component A1", ""); + containerA2 = softwareSystemA.addContainer("Container A2", "", ""); + componentA2 = containerA2.addComponent("Component A2", ""); + containerA3 = softwareSystemA.addContainer("Container A3", "", ""); + relationship = containerA1.uses(containerA2, "uses"); + + softwareSystemB = model.addSoftwareSystem("Software System B", ""); + 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 { + DynamicView dynamicView = workspace.getViews().createDynamicView("key", "Description"); + dynamicView.add((StaticStructureElement)null, softwareSystemA); + fail(); + } catch (IllegalArgumentException iae) { + assertEquals("A source element must be specified.", iae.getMessage()); + } + } + + @Test + void add_ThrowsAnException_WhenPassedANullDestinationElement() { + try { + DynamicView dynamicView = workspace.getViews().createDynamicView("key", "Description"); + dynamicView.add(person, (StaticStructureElement)null); + fail(); + } catch (IllegalArgumentException iae) { + assertEquals("A destination element must be specified.", iae.getMessage()); + } + } + + @Test + void add_ThrowsAnException_WhenTheScopeOfTheDynamicViewIsNotSpecifiedButAContainerIsAdded() { + try { + DynamicView dynamicView = workspace.getViews().createDynamicView("key", "Description"); + dynamicView.add(containerA1, containerA1); + fail(); + } catch (ElementNotPermittedInViewException iae) { + assertEquals("Only people and software systems can be added to this dynamic view.", iae.getMessage()); + } + } + + @Test + void add_ThrowsAnException_WhenTheScopeOfTheDynamicViewIsNotSpecifiedButAComponentIsAdded() { + try { + DynamicView dynamicView = workspace.getViews().createDynamicView("key", "Description"); + dynamicView.add(componentA1, componentA1); + fail(); + } catch (ElementNotPermittedInViewException iae) { + assertEquals("Only people and software systems can be added to this dynamic view.", iae.getMessage()); + } + } + + @Test + void add_ThrowsAnException_WhenTheScopeOfTheDynamicViewIsASoftwareSystemButAComponentIsAdded() { + try { + DynamicView dynamicView = workspace.getViews().createDynamicView(softwareSystemA, "key", "Description"); + dynamicView.add(componentA1, containerA1); + fail(); + } catch (Exception e) { + assertEquals("Components can't be added to a dynamic view when the scope is a software system.", e.getMessage()); + } + } + + @Test + void add_ThrowsAnException_WhenTheScopeOfTheDynamicViewIsASoftwareSystemAndTheSameSoftwareSystemIsAdded() { + try { + DynamicView dynamicView = workspace.getViews().createDynamicView(softwareSystemA, "key", "Description"); + dynamicView.add(softwareSystemA, containerA1); + fail(); + } catch (Exception e) { + assertEquals("Software System A is already the scope of this view and cannot be added to it.", e.getMessage()); + } + } + + @Test + void add_ThrowsAnException_WhenTheScopeOfTheDynamicViewIsAContainerAndTheSameContainerIsAdded() { + try { + DynamicView dynamicView = workspace.getViews().createDynamicView(containerA1, "key", "Description"); + dynamicView.add(containerA1, containerA2); + fail(); + } catch (Exception e) { + assertEquals("Container A1 is already the scope of this view and cannot be added to it.", e.getMessage()); + } + } + + @Test + void add_ThrowsAnException_WhenTheScopeOfTheDynamicViewIsAContainerAndTheParentSoftwareSystemIsAdded() { + try { + DynamicView dynamicView = workspace.getViews().createDynamicView(containerA1, "key", "Description"); + dynamicView.add(softwareSystemA, containerA2); + fail(); + } catch (Exception e) { + assertEquals("Software System A is already the scope of this view and cannot be added to it.", e.getMessage()); + } + } + + @Test + void add_ThrowsAnException_WhenTheParentOfAnElementHasAlreadyBeenAdded() { + try { + SoftwareSystem softwareSystem = workspace.getModel().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(container2, "Uses"); + component1.uses(component2, "Uses"); + + DynamicView dynamicView = workspace.getViews().createDynamicView(container1, "key", "Description"); + dynamicView.add(component1, container2); + dynamicView.add(component1, component2); + fail(); + } catch (Exception e) { + assertEquals("A parent of Component 2 is already in this view.", e.getMessage()); + } + } + + @Test + void add_ThrowsAnException_WhenTheChildOfAnElementHasAlreadyBeenAdded() { + try { + SoftwareSystem softwareSystem = workspace.getModel().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, "Uses"); + component1.uses(container2, "Uses"); + + DynamicView dynamicView = workspace.getViews().createDynamicView(container1, "key", "Description"); + dynamicView.add(component1, component2); + dynamicView.add(component1, container2); + fail(); + } catch (Exception e) { + assertEquals("A child of Container 2 is already in this view.", e.getMessage()); + } + } + + @Test + void add_ThrowsAnException_WhenARelationshipBetweenTheSourceAndDestinationElementsDoesNotExist() { + try { + DynamicView dynamicView = workspace.getViews().createDynamicView("key", "Description"); + SoftwareSystem ss1 = workspace.getModel().addSoftwareSystem("Software System 1", ""); + SoftwareSystem ss2 = workspace.getModel().addSoftwareSystem("Software System 2", ""); + dynamicView.add(ss1, ss2); + fail(); + } catch (Exception e) { + assertEquals("A relationship between Software System 1 and Software System 2 does not exist in model.", e.getMessage()); + } + } + + @Test + void add_ThrowsAnException_WhenARelationshipBetweenTheSourceAndDestinationElementsWithTheSpecifiedTechnologyDoesNotExist() { + try { + workspace = new Workspace("Name", "Description"); + model = workspace.getModel(); + + SoftwareSystem ss1 = workspace.getModel().addSoftwareSystem("Software System 1", ""); + SoftwareSystem ss2 = workspace.getModel().addSoftwareSystem("Software System 2", ""); + ss1.uses(ss2, "Uses 1", "Tech 1"); + + DynamicView view = workspace.getViews().createDynamicView("key", "Description"); + + view.add(ss1, "Uses", "Tech 1", ss2); + view.add(ss1, "Uses", "Tech 2", ss2); + fail(); + } catch (Exception e) { + assertEquals("A relationship between Software System 1 and Software System 2 with technology Tech 2 does not exist in model.", e.getMessage()); + } + } + + @Test + void 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 + void 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 + void add_AddsTheSourceAndDestinationElements_WhenARelationshipBetweenThemExists() { + final DynamicView dynamicView = workspace.getViews().createDynamicView(softwareSystemA, "key", "Description"); + dynamicView.add(containerA1, containerA2); + assertEquals(2, dynamicView.getElements().size()); + } + + @Test + void add_AddsTheSourceAndDestinationElements_WhenARelationshipBetweenThemExistsAndTheDestinationIsAnExternalSoftwareSystem() { + DynamicView dynamicView = workspace.getViews().createDynamicView(softwareSystemA, "key", "Description"); + containerA2.uses(softwareSystemB, "", ""); + dynamicView.add(containerA2, softwareSystemB); + assertEquals(2, dynamicView.getElements().size()); + } + + @Test + void normalSequence() { + workspace = new Workspace("Name", "Description"); + model = workspace.getModel(); + + SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System", "Description"); + Container container1 = softwareSystem.addContainer("Container 1", "Description", "Technology"); + Container container2 = softwareSystem.addContainer("Container 2", "Description", "Technology"); + Container container3 = softwareSystem.addContainer("Container 3", "Description", "Technology"); + + container1.uses(container2, "Uses"); + container1.uses(container3, "Uses"); + + DynamicView view = workspace.getViews().createDynamicView(softwareSystem, "key", "Description"); + + view.add(container1, container2); + view.add(container1, container3); + + assertSame(container2, view.getRelationships().stream().filter(r -> r.getOrder().equals("1")).findFirst().get().getRelationship().getDestination()); + assertSame(container3, view.getRelationships().stream().filter(r -> r.getOrder().equals("2")).findFirst().get().getRelationship().getDestination()); + } + + @Test + void 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 + void normalSequence_WhenThereAreMultipleTechnologies() { + 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", "Tech 1"); + Relationship r2 = ss1.uses(ss2, "Uses 2", "Tech 2"); + + DynamicView view = workspace.getViews().createDynamicView("key", "Description"); + + RelationshipView rv1 = view.add(ss1, "Uses", "Tech 1", ss2); + RelationshipView rv2 = view.add(ss1, "Uses", "Tech 2", ss2); + + assertSame(r1, rv1.getRelationship()); + assertSame(r2, rv2.getRelationship()); + } + + @Test + void parallelSequence() { + workspace = new Workspace("Name", "Description"); + model = workspace.getModel(); + SoftwareSystem softwareSystemA = model.addSoftwareSystem("A", ""); + SoftwareSystem softwareSystemB = model.addSoftwareSystem("B", ""); + SoftwareSystem softwareSystemC1 = model.addSoftwareSystem("C1", ""); + SoftwareSystem softwareSystemC2 = model.addSoftwareSystem("C2", ""); + SoftwareSystem softwareSystemD = model.addSoftwareSystem("D", ""); + SoftwareSystem softwareSystemE = model.addSoftwareSystem("E", ""); + + // A -> B -> C1 -> D -> E + // A -> B -> C2 -> D -> E + softwareSystemA.uses(softwareSystemB, "uses"); + softwareSystemB.uses(softwareSystemC1, "uses"); + softwareSystemC1.uses(softwareSystemD, "uses"); + softwareSystemB.uses(softwareSystemC2, "uses"); + softwareSystemC2.uses(softwareSystemD, "uses"); + softwareSystemD.uses(softwareSystemE, "uses"); + + DynamicView view = workspace.getViews().createDynamicView("key", "Description"); + + view.add(softwareSystemA, softwareSystemB); + view.startParallelSequence(); + view.add(softwareSystemB, softwareSystemC1); + view.add(softwareSystemC1, softwareSystemD); + view.endParallelSequence(); + view.startParallelSequence(); + view.add(softwareSystemB, softwareSystemC2); + view.add(softwareSystemC2, softwareSystemD); + view.endParallelSequence(true); + view.add(softwareSystemD, softwareSystemE); + + assertEquals(1, view.getRelationships().stream().filter(r -> r.getOrder().equals("1")).count()); + assertEquals(2, view.getRelationships().stream().filter(r -> r.getOrder().equals("2")).count()); + assertEquals(2, view.getRelationships().stream().filter(r -> r.getOrder().equals("3")).count()); + 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"); + DynamicView view = workspace.getViews().createDynamicView(softwareSystemA, "key", "Description"); + for (int i = 0; i < 10; i++) { + view.add(containerA1, containerA2); + } + + List relationships = new LinkedList<>(view.getRelationships()); + assertEquals("1", relationships.get(0).getOrder()); + assertEquals("2", relationships.get(1).getOrder()); + assertEquals("3", relationships.get(2).getOrder()); + assertEquals("4", relationships.get(3).getOrder()); + assertEquals("5", relationships.get(4).getOrder()); + assertEquals("6", relationships.get(5).getOrder()); + assertEquals("7", relationships.get(6).getOrder()); + assertEquals("8", relationships.get(7).getOrder()); + assertEquals("9", relationships.get(8).getOrder()); + assertEquals("10", relationships.get(9).getOrder()); + } + + @Test + void getRelationships_WhenTheOrderPropertyIsADecimal() { + containerA1.uses(containerA2, "uses"); + DynamicView view = workspace.getViews().createDynamicView(softwareSystemA, "key", "Description"); + for (int i = 0; i < 10; i++) { + RelationshipView relationshipView = view.add(containerA1, containerA2); + relationshipView.setOrder("1." + i); + } + + List relationships = new LinkedList<>(view.getRelationships()); + assertEquals("1.0", relationships.get(0).getOrder()); + assertEquals("1.1", relationships.get(1).getOrder()); + assertEquals("1.2", relationships.get(2).getOrder()); + assertEquals("1.3", relationships.get(3).getOrder()); + assertEquals("1.4", relationships.get(4).getOrder()); + assertEquals("1.5", relationships.get(5).getOrder()); + assertEquals("1.6", relationships.get(6).getOrder()); + assertEquals("1.7", relationships.get(7).getOrder()); + assertEquals("1.8", relationships.get(8).getOrder()); + assertEquals("1.9", relationships.get(9).getOrder()); + } + + @Test + void getRelationships_WhenTheOrderPropertyIsAString() { + String characters = "abcdefghij"; + containerA1.uses(containerA2, "uses"); + DynamicView view = workspace.getViews().createDynamicView(softwareSystemA, "key", "Description"); + for (int i = 0; i < 10; i++) { + RelationshipView relationshipView = view.add(containerA1, containerA2); + relationshipView.setOrder("1" + characters.charAt(i)); + } + + List relationships = new LinkedList<>(view.getRelationships()); + assertEquals("1a", relationships.get(0).getOrder()); + assertEquals("1b", relationships.get(1).getOrder()); + assertEquals("1c", relationships.get(2).getOrder()); + assertEquals("1d", relationships.get(3).getOrder()); + assertEquals("1e", relationships.get(4).getOrder()); + assertEquals("1f", relationships.get(5).getOrder()); + assertEquals("1g", relationships.get(6).getOrder()); + assertEquals("1h", relationships.get(7).getOrder()); + assertEquals("1i", relationships.get(8).getOrder()); + assertEquals("1j", relationships.get(9).getOrder()); + } + + @Test + void 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 + + List list = new ArrayList<>(view.getRelationships()); + RelationshipView relationshipView = list.get(0); + assertSame(relationship, relationshipView.getRelationship()); + assertEquals("Asks for X", relationshipView.getDescription()); + assertFalse(relationshipView.isResponse()); + + relationshipView = list.get(1); + assertSame(relationship, relationshipView.getRelationship()); + assertEquals("Returns X", relationshipView.getDescription()); + assertTrue(relationshipView.isResponse()); + } + +} \ No newline at end of file diff --git a/structurizr-core/src/test/java/com/structurizr/view/ElementStyleTests.java b/structurizr-core/src/test/java/com/structurizr/view/ElementStyleTests.java new file mode 100644 index 000000000..c26c44509 --- /dev/null +++ b/structurizr-core/src/test/java/com/structurizr/view/ElementStyleTests.java @@ -0,0 +1,375 @@ +package com.structurizr.view; + +import org.junit.jupiter.api.Test; + +import java.util.HashMap; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.*; + +public class ElementStyleTests { + + @Test + void setOpacity() { + ElementStyle style = new ElementStyle(); + assertNull(style.getOpacity()); + + style.setOpacity(-1); + assertEquals(0, style.getOpacity().intValue()); + + style.setOpacity(0); + assertEquals(0, style.getOpacity().intValue()); + + style.setOpacity(50); + assertEquals(50, style.getOpacity().intValue()); + + style.setOpacity(100); + assertEquals(100, style.getOpacity().intValue()); + + style.setOpacity(101); + assertEquals(100, style.getOpacity().intValue()); + } + + @Test + void opacity() { + ElementStyle style = new ElementStyle(); + assertNull(style.getOpacity()); + + style.opacity(-1); + assertEquals(0, style.getOpacity().intValue()); + + style.opacity(0); + assertEquals(0, style.getOpacity().intValue()); + + style.opacity(50); + assertEquals(50, style.getOpacity().intValue()); + + style.opacity(100); + assertEquals(100, style.getOpacity().intValue()); + + style.opacity(101); + assertEquals(100, style.getOpacity().intValue()); + } + + @Test + void setColor_SetsTheColorProperty_WhenAValidHexColorCodeIsSpecified() { + ElementStyle style = new ElementStyle(); + style.setColor("#ffffff"); + assertEquals("#ffffff", style.getColor()); + + style.setColor("#FFFFFF"); + assertEquals("#ffffff", style.getColor()); + + style.setColor("#123456"); + assertEquals("#123456", style.getColor()); + } + + @Test + void color_SetsTheColorProperty_WhenAValidHexColorCodeIsSpecified() { + ElementStyle style = new ElementStyle(); + style.color("#ffffff"); + assertEquals("#ffffff", style.getColor()); + + style.color("#FFFFFF"); + assertEquals("#ffffff", style.getColor()); + + style.color("#123456"); + 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("hello"); + }); + } + + @Test + void color_ThrowsAnException_WhenAnInvalidHexColorCodeIsSpecified() { + assertThrows(IllegalArgumentException.class, () -> { + ElementStyle style = new ElementStyle(); + style.color("hello"); + }); + } + + @Test + void setBackground_SetsTheBackgroundProperty_WhenAValidHexColorCodeIsSpecified() { + ElementStyle style = new ElementStyle(); + style.setBackground("#ffffff"); + assertEquals("#ffffff", style.getBackground()); + + style.setBackground("#FFFFFF"); + assertEquals("#ffffff", style.getBackground()); + + style.setBackground("#123456"); + assertEquals("#123456", style.getBackground()); + } + + @Test + void background_SetsTheBackgroundProperty_WhenAValidHexColorCodeIsSpecified() { + ElementStyle style = new ElementStyle(); + style.background("#ffffff"); + assertEquals("#ffffff", style.getBackground()); + + style.background("#FFFFFF"); + assertEquals("#ffffff", style.getBackground()); + + style.background("#123456"); + 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("hello"); + }); + } + + @Test + void background_ThrowsAnException_WhenAnInvalidHexColorCodeIsSpecified() { + assertThrows(IllegalArgumentException.class, () -> { + ElementStyle style = new ElementStyle(); + style.background("hello"); + }); + } + + @Test + 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 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 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 setIcon_ThrowsAnIllegalArgumentException_WhenAnInvalidUrlIsSpecified() { + assertThrows(IllegalArgumentException.class, () -> { + ElementStyle style = new ElementStyle(); + style.setIcon("htt://blah"); + }); + } + + @Test + void setIcon_DoesNothing_WhenANullUrlIsSpecified() { + ElementStyle style = new ElementStyle(); + style.setIcon(null); + assertNull(style.getIcon()); + } + + @Test + void setIcon_DoesNothing_WhenAnEmptyUrlIsSpecified() { + ElementStyle style = new ElementStyle(); + style.setIcon(" "); + assertNull(style.getIcon()); + } + + @Test + void setStroke_SetsTheStrokeProperty_WhenAValidHexColorCodeIsSpecified() { + ElementStyle style = new ElementStyle(); + style.setStroke("#ffffff"); + assertEquals("#ffffff", style.getStroke()); + + style.setStroke("#FFFFFF"); + assertEquals("#ffffff", style.getStroke()); + + style.setStroke("#123456"); + assertEquals("#123456", style.getStroke()); + } + + @Test + void Stroke_SetsTheStrokeProperty_WhenAValidHexColorCodeIsSpecified() { + ElementStyle style = new ElementStyle(); + style.stroke("#ffffff"); + assertEquals("#ffffff", style.getStroke()); + + style.stroke("#FFFFFF"); + assertEquals("#ffffff", style.getStroke()); + + style.stroke("#123456"); + 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("hello"); + }); + } + + @Test + void Stroke_ThrowsAnException_WhenAnInvalidHexColorCodeIsSpecified() { + assertThrows(IllegalArgumentException.class, () -> { + ElementStyle style = new ElementStyle(); + style.stroke("hello"); + }); + } + + @Test + void getProperties_ReturnsAnEmptyList_WhenNoPropertiesHaveBeenAdded() { + ElementStyle style = new ElementStyle(); + assertEquals(0, style.getProperties().size()); + } + + @Test + void addProperty_ThrowsAnException_WhenTheNameIsNull() { + try { + ElementStyle style = new ElementStyle(); + style.addProperty(null, "value"); + fail(); + } catch (IllegalArgumentException e) { + assertEquals("A property name must be specified.", e.getMessage()); + } + } + + @Test + void addProperty_ThrowsAnException_WhenTheNameIsEmpty() { + try { + ElementStyle style = new ElementStyle(); + style.addProperty(" ", "value"); + fail(); + } catch (IllegalArgumentException e) { + assertEquals("A property name must be specified.", e.getMessage()); + } + } + + @Test + void addProperty_ThrowsAnException_WhenTheValueIsNull() { + try { + ElementStyle style = new ElementStyle(); + style.addProperty("name", null); + fail(); + } catch (IllegalArgumentException e) { + assertEquals("A property value must be specified.", e.getMessage()); + } + } + + @Test + void addProperty_ThrowsAnException_WhenTheValueIsEmpty() { + try { + ElementStyle style = new ElementStyle(); + style.addProperty("name", " "); + fail(); + } catch (IllegalArgumentException e) { + assertEquals("A property value must be specified.", e.getMessage()); + } + } + + @Test + void addProperty_AddsTheProperty_WhenANameAndValueAreSpecified() { + ElementStyle style = new ElementStyle(); + style.addProperty("name", "value"); + assertEquals("value", style.getProperties().get("name")); + } + + @Test + void setProperties_DoesNothing_WhenNullIsSpecified() { + ElementStyle style = new ElementStyle(); + style.setProperties(null); + assertEquals(0, style.getProperties().size()); + } + + @Test + void setProperties_SetsTheProperties_WhenANonEmptyMapIsSpecified() { + ElementStyle style = new ElementStyle(); + Map properties = new HashMap<>(); + properties.put("name", "value"); + style.setProperties(properties); + assertEquals(1, style.getProperties().size()); + 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/src/test/java/com/structurizr/view/ElementViewTests.java b/structurizr-core/src/test/java/com/structurizr/view/ElementViewTests.java new file mode 100644 index 000000000..661308e4e --- /dev/null +++ b/structurizr-core/src/test/java/com/structurizr/view/ElementViewTests.java @@ -0,0 +1,34 @@ +package com.structurizr.view; + +import com.structurizr.AbstractWorkspaceTestBase; +import com.structurizr.model.Element; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class ElementViewTests extends AbstractWorkspaceTestBase { + + @Test + void copyLayoutInformationFrom_DoesNothing_WhenNullIsPassed() { + Element element = model.addSoftwareSystem("SystemA", ""); + ElementView elementView = new ElementView(element); + elementView.copyLayoutInformationFrom(null); + } + + @Test + void copyLayoutInformationFrom_CopiesXAndY_WhenANonNullElementViewIsPassed() { + Element element = model.addSoftwareSystem("SystemA", ""); + ElementView elementView1 = new ElementView(element); + assertEquals(0, elementView1.getX()); + assertEquals(0, elementView1.getY()); + + ElementView elementView2 = new ElementView(element); + elementView2.setX(123); + elementView2.setY(456); + + elementView1.copyLayoutInformationFrom(elementView2); + assertEquals(123, elementView1.getX()); + assertEquals(456, elementView1.getY()); + } + +} 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 91% rename from structurizr-core/test/unit/com/structurizr/view/FilteredViewTests.java rename to structurizr-core/src/test/java/com/structurizr/view/FilteredViewTests.java index 83f4ee965..a21074f97 100644 --- a/structurizr-core/test/unit/com/structurizr/view/FilteredViewTests.java +++ b/structurizr-core/src/test/java/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 construction() { SoftwareSystem softwareSystem = model.addSoftwareSystem("Name", "Description"); SystemContextView systemContextView = views.createSystemContextView(softwareSystem, "SystemContext", "Description"); FilteredView filteredView = views.createFilteredView( diff --git a/structurizr-core/src/test/java/com/structurizr/view/FontTests.java b/structurizr-core/src/test/java/com/structurizr/view/FontTests.java new file mode 100644 index 000000000..b5cee1446 --- /dev/null +++ b/structurizr-core/src/test/java/com/structurizr/view/FontTests.java @@ -0,0 +1,55 @@ +package com.structurizr.view; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +public class FontTests { + + private Font font; + + @BeforeEach + public void setUp() { + this.font = new Font(); + } + + @Test + void construction_WithANameOnly() { + this.font = new Font("Times New Roman"); + assertEquals("Times New Roman", font.getName()); + } + + @Test + 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 + 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 setUrl_ThrowsAnIllegalArgumentException_WhenAnInvalidUrlIsSpecified() { + assertThrows(IllegalArgumentException.class, () -> { + font.setUrl("htt://blah"); + }); + } + + @Test + void setUrl_DoesNothing_WhenANullUrlIsSpecified() { + font.setUrl(null); + assertNull(font.getUrl()); + } + + @Test + void setUrl_DoesNothing_WhenAnEmptyUrlIsSpecified() { + font.setUrl(" "); + assertNull(font.getUrl()); + } + +} diff --git a/structurizr-core/src/test/java/com/structurizr/view/ImageViewTests.java b/structurizr-core/src/test/java/com/structurizr/view/ImageViewTests.java new file mode 100644 index 000000000..97056284e --- /dev/null +++ b/structurizr-core/src/test/java/com/structurizr/view/ImageViewTests.java @@ -0,0 +1,57 @@ +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()); + } + + @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-core/src/test/java/com/structurizr/view/PaperSizeTests.java b/structurizr-core/src/test/java/com/structurizr/view/PaperSizeTests.java new file mode 100644 index 000000000..bf4af3932 --- /dev/null +++ b/structurizr-core/src/test/java/com/structurizr/view/PaperSizeTests.java @@ -0,0 +1,79 @@ +package com.structurizr.view; + +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class PaperSizeTests { + + @Test + void getOrderedPaperSizes_WhenOrientationIsLandscape() { + List paperSizes = PaperSize.getOrderedPaperSizes(PaperSize.Orientation.Landscape); + assertEquals(12, paperSizes.size()); + + assertEquals(PaperSize.A6_Landscape, paperSizes.get(0)); + assertEquals(PaperSize.A5_Landscape, paperSizes.get(1)); + assertEquals(PaperSize.A4_Landscape, paperSizes.get(2)); + assertEquals(PaperSize.A3_Landscape, paperSizes.get(3)); + assertEquals(PaperSize.A2_Landscape, paperSizes.get(4)); + assertEquals(PaperSize.A1_Landscape, paperSizes.get(5)); + assertEquals(PaperSize.A0_Landscape, paperSizes.get(6)); + + assertEquals(PaperSize.Letter_Landscape, paperSizes.get(7)); + assertEquals(PaperSize.Legal_Landscape, paperSizes.get(8)); + assertEquals(PaperSize.Slide_4_3, paperSizes.get(9)); + assertEquals(PaperSize.Slide_16_9, paperSizes.get(10)); + assertEquals(PaperSize.Slide_16_10, paperSizes.get(11)); + } + + @Test + void getOrderedPaperSizes_WhenOrientationIsPortrait() { + List paperSizes = PaperSize.getOrderedPaperSizes(PaperSize.Orientation.Portrait); + assertEquals(9, paperSizes.size()); + + assertEquals(PaperSize.A6_Portrait, paperSizes.get(0)); + assertEquals(PaperSize.A5_Portrait, paperSizes.get(1)); + assertEquals(PaperSize.A4_Portrait, paperSizes.get(2)); + assertEquals(PaperSize.A3_Portrait, paperSizes.get(3)); + assertEquals(PaperSize.A2_Portrait, paperSizes.get(4)); + assertEquals(PaperSize.A1_Portrait, paperSizes.get(5)); + assertEquals(PaperSize.A0_Portrait, paperSizes.get(6)); + + assertEquals(PaperSize.Letter_Portrait, paperSizes.get(7)); + assertEquals(PaperSize.Legal_Portrait, paperSizes.get(8)); + } + + @Test + void getOrderedPaperSizes() { + List paperSizes = PaperSize.getOrderedPaperSizes(); + assertEquals(21, paperSizes.size()); + + assertEquals(PaperSize.A6_Landscape, paperSizes.get(0)); + assertEquals(PaperSize.A5_Landscape, paperSizes.get(1)); + assertEquals(PaperSize.A4_Landscape, paperSizes.get(2)); + assertEquals(PaperSize.A3_Landscape, paperSizes.get(3)); + assertEquals(PaperSize.A2_Landscape, paperSizes.get(4)); + assertEquals(PaperSize.A1_Landscape, paperSizes.get(5)); + assertEquals(PaperSize.A0_Landscape, paperSizes.get(6)); + + assertEquals(PaperSize.Letter_Landscape, paperSizes.get(7)); + assertEquals(PaperSize.Legal_Landscape, paperSizes.get(8)); + assertEquals(PaperSize.Slide_4_3, paperSizes.get(9)); + assertEquals(PaperSize.Slide_16_9, paperSizes.get(10)); + assertEquals(PaperSize.Slide_16_10, paperSizes.get(11)); + + assertEquals(PaperSize.A6_Portrait, paperSizes.get(12)); + assertEquals(PaperSize.A5_Portrait, paperSizes.get(13)); + assertEquals(PaperSize.A4_Portrait, paperSizes.get(14)); + assertEquals(PaperSize.A3_Portrait, paperSizes.get(15)); + assertEquals(PaperSize.A2_Portrait, paperSizes.get(16)); + assertEquals(PaperSize.A1_Portrait, paperSizes.get(17)); + assertEquals(PaperSize.A0_Portrait, paperSizes.get(18)); + + assertEquals(PaperSize.Letter_Portrait, paperSizes.get(19)); + assertEquals(PaperSize.Legal_Portrait, paperSizes.get(20)); + } + +} \ No newline at end of file diff --git a/structurizr-core/src/test/java/com/structurizr/view/RelationshipStyleTests.java b/structurizr-core/src/test/java/com/structurizr/view/RelationshipStyleTests.java new file mode 100644 index 000000000..e34cff717 --- /dev/null +++ b/structurizr-core/src/test/java/com/structurizr/view/RelationshipStyleTests.java @@ -0,0 +1,223 @@ +package com.structurizr.view; + +import org.junit.jupiter.api.Test; + +import java.util.HashMap; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.*; + +public class RelationshipStyleTests { + + private RelationshipStyle relationshipStyle = new RelationshipStyle("tag"); + + @Test + void setPosition_SetsPositionToNull_WhenNullIsSpecified() { + relationshipStyle.setPosition(null); + assertNull(relationshipStyle.getPosition()); + } + + @Test + void setPosition_SetsPositionToZero_WhenANegativeNumberIsSpecified() { + relationshipStyle.setPosition(-1); + assertEquals(Integer.valueOf(0), relationshipStyle.getPosition()); + } + + @Test + void setPosition_SetsPositionToOneHundred_WhenANumberGreaterThanOneHundredIsSpecified() { + relationshipStyle.setPosition(101); + assertEquals(Integer.valueOf(100), relationshipStyle.getPosition()); + } + + @Test + void setPosition_SetsPosition_WhenANumberBetweenZeroAndOneHundredIsSpecified() { + relationshipStyle.setPosition(0); + assertEquals(Integer.valueOf(0), relationshipStyle.getPosition()); + + relationshipStyle.setPosition(1); + assertEquals(Integer.valueOf(1), relationshipStyle.getPosition()); + + relationshipStyle.setPosition(50); + assertEquals(Integer.valueOf(50), relationshipStyle.getPosition()); + + + relationshipStyle.setPosition(99); + assertEquals(Integer.valueOf(99), relationshipStyle.getPosition()); + + relationshipStyle.setPosition(100); + assertEquals(Integer.valueOf(100), relationshipStyle.getPosition()); + } + + @Test + void setOpacity() { + RelationshipStyle style = new RelationshipStyle(); + assertNull(style.getOpacity()); + + style.setOpacity(-1); + assertEquals(0, style.getOpacity().intValue()); + + style.setOpacity(0); + assertEquals(0, style.getOpacity().intValue()); + + style.setOpacity(50); + assertEquals(50, style.getOpacity().intValue()); + + style.setOpacity(100); + assertEquals(100, style.getOpacity().intValue()); + + style.setOpacity(101); + assertEquals(100, style.getOpacity().intValue()); + } + + @Test + void opacity() { + RelationshipStyle style = new RelationshipStyle(); + assertNull(style.getOpacity()); + + style.opacity(-1); + assertEquals(0, style.getOpacity().intValue()); + + style.opacity(0); + assertEquals(0, style.getOpacity().intValue()); + + style.opacity(50); + assertEquals(50, style.getOpacity().intValue()); + + style.opacity(100); + assertEquals(100, style.getOpacity().intValue()); + + style.opacity(101); + assertEquals(100, style.getOpacity().intValue()); + } + + @Test + void setColor_SetsTheColorProperty_WhenAValidHexColorCodeIsSpecified() { + RelationshipStyle style = new RelationshipStyle(); + style.setColor("#ffffff"); + assertEquals("#ffffff", style.getColor()); + + style.setColor("#FFFFFF"); + assertEquals("#ffffff", style.getColor()); + + style.setColor("#123456"); + assertEquals("#123456", style.getColor()); + } + + @Test + void color_SetsTheColorProperty_WhenAValidHexColorCodeIsSpecified() { + RelationshipStyle style = new RelationshipStyle(); + style.color("#ffffff"); + assertEquals("#ffffff", style.getColor()); + + style.color("#FFFFFF"); + assertEquals("#ffffff", style.getColor()); + + style.color("#123456"); + assertEquals("#123456", style.getColor()); + } + + @Test + 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("hello"); + }); + } + + @Test + void color_ThrowsAnException_WhenAnInvalidColorIsSpecified() { + assertThrows(IllegalArgumentException.class, () -> { + RelationshipStyle style = new RelationshipStyle(); + style.color("hello"); + }); + } + + @Test + void getProperties_ReturnsAnEmptyList_WhenNoPropertiesHaveBeenAdded() { + RelationshipStyle style = new RelationshipStyle(); + assertEquals(0, style.getProperties().size()); + } + + @Test + void addProperty_ThrowsAnException_WhenTheNameIsNull() { + try { + RelationshipStyle style = new RelationshipStyle(); + style.addProperty(null, "value"); + fail(); + } catch (IllegalArgumentException e) { + assertEquals("A property name must be specified.", e.getMessage()); + } + } + + @Test + void addProperty_ThrowsAnException_WhenTheNameIsEmpty() { + try { + RelationshipStyle style = new RelationshipStyle(); + style.addProperty(" ", "value"); + fail(); + } catch (IllegalArgumentException e) { + assertEquals("A property name must be specified.", e.getMessage()); + } + } + + @Test + void addProperty_ThrowsAnException_WhenTheValueIsNull() { + try { + RelationshipStyle style = new RelationshipStyle(); + style.addProperty("name", null); + fail(); + } catch (IllegalArgumentException e) { + assertEquals("A property value must be specified.", e.getMessage()); + } + } + + @Test + void addProperty_ThrowsAnException_WhenTheValueIsEmpty() { + try { + RelationshipStyle style = new RelationshipStyle(); + style.addProperty("name", " "); + fail(); + } catch (IllegalArgumentException e) { + assertEquals("A property value must be specified.", e.getMessage()); + } + } + + @Test + void addProperty_AddsTheProperty_WhenANameAndValueAreSpecified() { + RelationshipStyle style = new RelationshipStyle(); + style.addProperty("name", "value"); + assertEquals("value", style.getProperties().get("name")); + } + + @Test + void setProperties_DoesNothing_WhenNullIsSpecified() { + RelationshipStyle style = new RelationshipStyle(); + style.setProperties(null); + assertEquals(0, style.getProperties().size()); + } + + @Test + void setProperties_SetsTheProperties_WhenANonEmptyMapIsSpecified() { + RelationshipStyle style = new RelationshipStyle(); + Map properties = new HashMap<>(); + properties.put("name", "value"); + style.setProperties(properties); + assertEquals(1, style.getProperties().size()); + assertEquals("value", style.getProperties().get("name")); + } + +} 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-core/src/test/java/com/structurizr/view/SequenceCounterTests.java b/structurizr-core/src/test/java/com/structurizr/view/SequenceCounterTests.java new file mode 100644 index 000000000..3b8242421 --- /dev/null +++ b/structurizr-core/src/test/java/com/structurizr/view/SequenceCounterTests.java @@ -0,0 +1,21 @@ +package com.structurizr.view; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class SequenceCounterTests { + + @Test + void increment_IncrementsTheCounter_WhenThereIsNoParent() { + SequenceCounter counter = new SequenceCounter(); + assertEquals("0", counter.toString()); + + counter.increment(); + assertEquals("1", counter.toString()); + + counter.increment(); + assertEquals("2", counter.toString()); + } + +} \ No newline at end of file diff --git a/structurizr-core/src/test/java/com/structurizr/view/SequenceNumberTests.java b/structurizr-core/src/test/java/com/structurizr/view/SequenceNumberTests.java new file mode 100644 index 000000000..6735450c6 --- /dev/null +++ b/structurizr-core/src/test/java/com/structurizr/view/SequenceNumberTests.java @@ -0,0 +1,52 @@ +package com.structurizr.view; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class SequenceNumberTests { + + @Test + void increment() { + SequenceNumber sequenceNumber = new SequenceNumber(); + assertEquals("1", sequenceNumber.getNext()); + assertEquals("2", sequenceNumber.getNext()); + } + + @Test + void parallelSequences_1() { + SequenceNumber sequenceNumber = new SequenceNumber(); + assertEquals("1", sequenceNumber.getNext()); + + sequenceNumber.startParallelSequence(); + assertEquals("2", sequenceNumber.getNext()); + sequenceNumber.endParallelSequence(false); + + sequenceNumber.startParallelSequence(); + assertEquals("2", sequenceNumber.getNext()); + sequenceNumber.endParallelSequence(true); + + 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()); + } + +} diff --git a/structurizr-core/src/test/java/com/structurizr/view/StaticViewTests.java b/structurizr-core/src/test/java/com/structurizr/view/StaticViewTests.java new file mode 100644 index 000000000..722bd5c39 --- /dev/null +++ b/structurizr-core/src/test/java/com/structurizr/view/StaticViewTests.java @@ -0,0 +1,84 @@ +package com.structurizr.view; + +import com.structurizr.AbstractWorkspaceTestBase; +import com.structurizr.model.Relationship; +import com.structurizr.model.SoftwareSystem; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +public class StaticViewTests extends AbstractWorkspaceTestBase { + + @Test + void addAnimationStep_ThrowsAnException_WhenNoElementsAreSpecified() { + try { + SystemLandscapeView view = workspace.getViews().createSystemLandscapeView("key", "Description"); + view.addAnimation(); + fail(); + } catch (IllegalArgumentException iae) { + assertEquals("One or more elements must be specified.", iae.getMessage()); + } + } + + @Test + void addAnimationStep() { + SoftwareSystem element1 = model.addSoftwareSystem("Software System 1", ""); + SoftwareSystem element2 = model.addSoftwareSystem("Software System 2", ""); + SoftwareSystem element3 = model.addSoftwareSystem("Software System 3", ""); + + Relationship relationship1_2 = element1.uses(element2, "uses"); + Relationship relationship2_3 = element2.uses(element3, "uses"); + + SystemLandscapeView view = workspace.getViews().createSystemLandscapeView("key", "Description"); + view.addAllElements(); + + view.addAnimation(element1); + view.addAnimation(element2); + view.addAnimation(element3); + + Animation step1 = view.getAnimations().stream().filter(step -> step.getOrder() == 1).findFirst().get(); + assertEquals(1, step1.getElements().size()); + assertTrue(step1.getElements().contains(element1.getId())); + assertEquals(0, step1.getRelationships().size()); + + Animation step2 = view.getAnimations().stream().filter(step -> step.getOrder() == 2).findFirst().get(); + assertEquals(1, step2.getElements().size()); + assertTrue(step2.getElements().contains(element2.getId())); + assertEquals(1, step2.getRelationships().size()); + assertTrue(step2.getRelationships().contains(relationship1_2.getId())); + + Animation step3 = view.getAnimations().stream().filter(step -> step.getOrder() == 3).findFirst().get(); + assertEquals(1, step3.getElements().size()); + assertTrue(step3.getElements().contains(element3.getId())); + assertEquals(1, step3.getRelationships().size()); + assertTrue(step3.getRelationships().contains(relationship2_3.getId())); + } + + @Test + void addAnimationStep_IgnoresElementsThatDoNotExistInTheView() { + SoftwareSystem element1 = model.addSoftwareSystem("Software System 1", ""); + SoftwareSystem element2 = model.addSoftwareSystem("Software System 2", ""); + + SystemLandscapeView view = workspace.getViews().createSystemLandscapeView("key", "Description"); + view.add(element1); + view.addAnimation(element1, element2); + + Animation step1 = view.getAnimations().stream().filter(step -> step.getOrder() == 1).findFirst().get(); + assertEquals(1, step1.getElements().size()); + assertTrue(step1.getElements().contains(element1.getId())); + } + + @Test + void addAnimationStep_ThrowsAnException_WhenElementsAreSpecifiedButNoneOfThemExistInTheView() { + try { + SoftwareSystem element1 = model.addSoftwareSystem("Software System 1", ""); + + SystemLandscapeView view = workspace.getViews().createSystemLandscapeView("key", "Description"); + view.addAnimation(element1); + fail(); + } catch (IllegalArgumentException iae) { + assertEquals("None of the specified elements exist in this view.", iae.getMessage()); + } + } + +} \ No newline at end of file diff --git a/structurizr-core/src/test/java/com/structurizr/view/StylesTests.java b/structurizr-core/src/test/java/com/structurizr/view/StylesTests.java new file mode 100644 index 000000000..befbcebcd --- /dev/null +++ b/structurizr-core/src/test/java/com/structurizr/view/StylesTests.java @@ -0,0 +1,505 @@ +package com.structurizr.view; + +import com.structurizr.AbstractWorkspaceTestBase; +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.*; + +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); + assertNull(style.getWidth()); + assertNull(style.getHeight()); + 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 findElementStyle_ReturnsTheDefaultStyle_WhenNoStylesAreDefined() { + SoftwareSystem element = model.addSoftwareSystem("Name", "Description"); + ElementStyle style = styles.findElementStyle(element); + assertNull(style.getWidth()); + assertNull(style.getHeight()); + 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); + assertNull(style.getWidth()); + assertNull(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()); + assertNull(style.getStrokeWidth()); + assertEquals(Integer.valueOf(100), style.getOpacity()); + assertEquals(true, style.getMetadata()); + assertEquals(true, style.getDescription()); + } + + @Test + void findElementStyle_ReturnsTheCorrectStyle_WhenStylesAreDefined() { + SoftwareSystem element = model.addSoftwareSystem("Name", "Description"); + element.addTags("Some Tag"); + + styles.addElementStyle(Tags.SOFTWARE_SYSTEM).background("#ff0000").color("#ffffff"); + 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()); + assertEquals(Integer.valueOf(456), style.getHeight()); + assertEquals("#ff0000", style.getBackground()); + assertEquals("#0000ff", style.getColor()); + assertEquals(Integer.valueOf(24), style.getFontSize()); + assertEquals(Shape.RoundedBox, style.getShape()); + 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()); + } + + @Test + void findElementStyle_ReturnsTheCorrectStyleForAnElementInstance_WhenStylesAreDefined() { + SoftwareSystem softwareSystem = model.addSoftwareSystem("Name"); + softwareSystem.addTags("Some Tag"); + + DeploymentNode deploymentNode = model.addDeploymentNode("Server"); + 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).addProperty("name", "value"); + + ElementStyle style = styles.findElementStyle(softwareSystemInstance); + assertEquals(Integer.valueOf(123), style.getWidth()); + assertEquals(Integer.valueOf(456), style.getHeight()); + assertEquals("#ff0000", style.getBackground()); + assertEquals("#0000ff", style.getColor()); + assertEquals(Integer.valueOf(24), style.getFontSize()); + assertEquals(Shape.RoundedBox, style.getShape()); + 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()); + assertEquals("value", style.getProperties().get("name")); + } + + @Test + 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_ForDarkColorScheme() { + RelationshipStyle style = styles.findRelationshipStyle((Relationship) null, ColorScheme.Dark); + assertEquals(Integer.valueOf(2), style.getThickness()); + assertEquals("#cccccc", 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_WhenNoStylesAreDefined() { + SoftwareSystem element = model.addSoftwareSystem("Name", "Description"); + Relationship relationship = element.uses(element, "Uses"); + RelationshipStyle style = styles.findRelationshipStyle(relationship); + 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_ReturnsTheCorrectStyle_WhenStylesAreDefined() { + SoftwareSystem element = model.addSoftwareSystem("Name", "Description"); + Relationship relationship = element.uses(element, "Uses"); + relationship.addTags("Some Tag"); + + styles.addRelationshipStyle(Tags.RELATIONSHIP).color("#ff0000"); + styles.addRelationshipStyle("Some Tag").color("#0000ff").addProperty("name", "value"); + + RelationshipStyle style = styles.findRelationshipStyle(relationship); + assertEquals(Integer.valueOf(2), style.getThickness()); + assertEquals("#0000ff", 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()); + assertEquals("value", style.getProperties().get("name")); + } + + @Test + 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"); + + Relationship relationship = container1.uses(container2, "Uses"); + relationship.addTags("Tag"); + styles.addRelationshipStyle("Tag").color("#0000ff"); + + RelationshipStyle style = styles.findRelationshipStyle(relationship); + assertEquals("#0000ff", style.getColor()); + + DeploymentNode deploymentNode = model.addDeploymentNode("Server"); + ContainerInstance containerInstance1 = deploymentNode.add(container1); + ContainerInstance containerInstance2 = deploymentNode.add(container2); + + Relationship relationshipInstance = containerInstance1.getEfferentRelationshipWith(containerInstance2); + + style = styles.findRelationshipStyle(relationshipInstance); + assertEquals("#0000ff", style.getColor()); + } + + @Test + void findRelationshipStyle_ReturnsTheCorrectStyle_WhenThereIsALinkedRelationshipBasedUponAnImpliedRelationship() { + model.setImpliedRelationshipsStrategy(new CreateImpliedRelationshipsUnlessAnyRelationshipExistsStrategy()); + SoftwareSystem softwareSystem = model.addSoftwareSystem("Name", "Description"); + 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, "Uses"); + relationship.addTags("Tag"); + styles.addRelationshipStyle("Tag").color("#0000ff"); + + RelationshipStyle style = styles.findRelationshipStyle(relationship); + assertEquals("#0000ff", style.getColor()); + + DeploymentNode deploymentNode = model.addDeploymentNode("Server"); + ContainerInstance containerInstance1 = deploymentNode.add(container1); + ContainerInstance containerInstance2 = deploymentNode.add(container2); + + Relationship relationshipInstance = containerInstance1.getEfferentRelationshipWith(containerInstance2); + + style = styles.findRelationshipStyle(relationshipInstance); + assertEquals("#0000ff", style.getColor()); + } + + @Test + void addElementStyle_ThrowsAnException_WhenATagIsNotSpecified() { + try { + styles.addElementStyle(""); + fail(); + } catch (IllegalArgumentException iae) { + assertEquals("A tag must be specified.", iae.getMessage()); + } + + try { + styles.addElementStyle(" "); + fail(); + } catch (IllegalArgumentException iae) { + assertEquals("A tag must be specified.", iae.getMessage()); + } + + try { + styles.addElementStyle(null); + fail(); + } catch (IllegalArgumentException iae) { + assertEquals("A tag must be specified.", iae.getMessage()); + } + } + + @Test + void addElementStyleByTag_ThrowsAnException_WhenAStyleWithTheSameTagExistsAlready() { + try { + styles.addElementStyle(Tags.SOFTWARE_SYSTEM).color("#ff0000"); + styles.addElementStyle(Tags.SOFTWARE_SYSTEM).color("#ff0000"); + + fail(); + } catch (IllegalArgumentException iae) { + assertEquals("An element style for the tag \"Software System\" already exists.", iae.getMessage()); + } + } + + @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 { + ElementStyle style = styles.addElementStyle(Tags.SOFTWARE_SYSTEM).color("#ff0000"); + styles.add(style); + + fail(); + } catch (IllegalArgumentException iae) { + assertEquals("An element style for the tag \"Software System\" already exists.", iae.getMessage()); + } + } + + @Test + void addRelationshipStyle_ThrowsAnException_WhenATagIsNotSpecified() { + try { + styles.addRelationshipStyle(""); + fail(); + } catch (IllegalArgumentException iae) { + assertEquals("A tag must be specified.", iae.getMessage()); + } + + try { + styles.addRelationshipStyle(" "); + fail(); + } catch (IllegalArgumentException iae) { + assertEquals("A tag must be specified.", iae.getMessage()); + } + + try { + styles.addRelationshipStyle(null); + fail(); + } catch (IllegalArgumentException iae) { + assertEquals("A tag must be specified.", iae.getMessage()); + } + } + + @Test + void addRelationshipStyleByTag_ThrowsAnException_WhenAStyleWithTheSameTagExistsAlready() { + try { + styles.addRelationshipStyle(Tags.RELATIONSHIP).color("#ff0000"); + styles.addRelationshipStyle(Tags.RELATIONSHIP).color("#ff0000"); + + fail(); + } catch (IllegalArgumentException iae) { + assertEquals("A relationship style for the tag \"Relationship\" already exists.", iae.getMessage()); + } + } + + @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 { + RelationshipStyle style = styles.addRelationshipStyle(Tags.RELATIONSHIP).color("#ff0000"); + styles.add(style); + + fail(); + } catch (IllegalArgumentException iae) { + assertEquals("A relationship style for the tag \"Relationship\" already exists.", iae.getMessage()); + } + } + + @Test + void clearElementStyles_RemovesAllElementStyles() { + styles.addElementStyle(Tags.SOFTWARE_SYSTEM).color("#ff0000"); + assertEquals(1, styles.getElements().size()); + + styles.clearElementStyles(); + assertEquals(0, styles.getElements().size()); + } + + @Test + void clearRelationshipStyles_RemovesAllRelationshipStyles() { + styles.addRelationshipStyle(Tags.RELATIONSHIP).color("#ff0000"); + assertEquals(1, styles.getRelationships().size()); + + styles.clearRelationshipStyles(); + 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()); + } + + @Test + void inlineTheme() { + Theme theme = new Theme( + Set.of(new ElementStyle("Tag").background("#ff0000")), + Set.of(new RelationshipStyle("Tag").color("#00ff00")) + ); + + styles.addElementStyle("Tag").background("#ffffff"); + styles.addRelationshipStyle("Tag").color("#ffffff"); + styles.inlineTheme(theme); // this will override the existing styles + + assertEquals("#ff0000", styles.getElementStyle("Tag").getBackground()); + assertEquals("#00ff00", styles.getRelationshipStyle("Tag").getColor()); + } + +} \ 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 new file mode 100644 index 000000000..b35075f43 --- /dev/null +++ b/structurizr-core/src/test/java/com/structurizr/view/SystemContextViewTests.java @@ -0,0 +1,346 @@ +package com.structurizr.view; + +import com.structurizr.AbstractWorkspaceTestBase; +import com.structurizr.model.*; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +public class SystemContextViewTests extends AbstractWorkspaceTestBase { + + private SoftwareSystem softwareSystem; + private SystemContextView view; + + @BeforeEach + public void setUp() { + softwareSystem = model.addSoftwareSystem( "The System", "Description"); + view = new SystemContextView(softwareSystem, "context", "Description"); + } + + @Test + void construction() { + assertEquals("System Context View: The System", view.getName()); + assertEquals(1, view.getElements().size()); + assertSame(view.getElements().iterator().next().getElement(), softwareSystem); + assertSame(softwareSystem, view.getSoftwareSystem()); + assertEquals(softwareSystem.getId(), view.getSoftwareSystemId()); + assertSame(model, view.getModel()); + } + + @Test + void addAllSoftwareSystems_DoesNothing_WhenThereAreNoOtherSoftwareSystems() { + assertEquals(1, view.getElements().size()); + view.addAllSoftwareSystems(); + assertEquals(1, view.getElements().size()); + } + + @Test + void addAllSoftwareSystems_AddsAllSoftwareSystems_WhenThereAreSomeSoftwareSystemsInTheModel() { + SoftwareSystem softwareSystemA = model.addSoftwareSystem("System A", "Description"); + SoftwareSystem softwareSystemB = model.addSoftwareSystem("System B", "Description"); + + view.addAllSoftwareSystems(); + + assertEquals(3, view.getElements().size()); + assertTrue(view.getElements().contains(new ElementView(softwareSystem))); + assertTrue(view.getElements().contains(new ElementView(softwareSystemA))); + assertTrue(view.getElements().contains(new ElementView(softwareSystemB))); + } + + @Test + void addAllPeople_DoesNothing_WhenThereAreNoPeople() { + assertEquals(1, view.getElements().size()); + view.addAllPeople(); + assertEquals(1, view.getElements().size()); + } + + @Test + void addAllPeople_AddsAllPeople_WhenThereAreSomePeopleInTheModel() { + Person userA = model.addPerson( "User A", "Description"); + Person userB = model.addPerson( "User B", "Description"); + + view.addAllPeople(); + + assertEquals(3, view.getElements().size()); + assertTrue(view.getElements().contains(new ElementView(softwareSystem))); + assertTrue(view.getElements().contains(new ElementView(userA))); + assertTrue(view.getElements().contains(new ElementView(userB))); + } + + @Test + void addAllElements_DoesNothing_WhenThereAreNoSoftwareSystemsOrPeople() { + assertEquals(1, view.getElements().size()); + view.addAllElements(); + assertEquals(1, view.getElements().size()); + } + + @Test + void addAllElements_AddsAllSoftwareSystemsAndPeople_WhenThereAreSomeSoftwareSystemsAndPeopleInTheModel() { + 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(); + + assertEquals(5, view.getElements().size()); + assertTrue(view.getElements().contains(new ElementView(softwareSystem))); + assertTrue(view.getElements().contains(new ElementView(softwareSystemA))); + assertTrue(view.getElements().contains(new ElementView(softwareSystemB))); + assertTrue(view.getElements().contains(new ElementView(userA))); + assertTrue(view.getElements().contains(new ElementView(userB))); + } + + @Test + void addNearestNeighbours_ThrowsAnException_WhenANullElementIsSpecified() { + try { + view.addNearestNeighbours(null); + fail(); + } catch (IllegalArgumentException iae) { + assertEquals("An element must be specified.", iae.getMessage()); + } + } + + @Test + void addNearestNeighbours_ThrowsAnException_WhenAnElementThatIsNotAPersonOrSoftwareSystemIsSpecified() { + Container container = softwareSystem.addContainer("Container", "Description", "Technology"); + try { + view.addNearestNeighbours(container); + fail(); + } catch (IllegalArgumentException iae) { + assertEquals("A person or software system must be specified.", iae.getMessage()); + } + } + + @Test + void addNearestNeighbours_DoesNothing_WhenThereAreNoNeighbours() { + view.addNearestNeighbours(softwareSystem); + + assertEquals(1, view.getElements().size()); + } + + @Test + void addNearestNeighbours_AddsNearestNeighbours_WhenThereAreSomeNearestNeighbours() { + SoftwareSystem softwareSystemA = model.addSoftwareSystem("A", "Description"); + SoftwareSystem softwareSystemB = model.addSoftwareSystem("B", "Description"); + Person userA = model.addPerson("User A", "Description"); + Person userB = model.addPerson("User B", "Description"); + + // userA -> systemA -> system -> systemB -> userB + userA.uses(softwareSystemA, ""); + softwareSystemA.uses(softwareSystem, ""); + softwareSystem.uses(softwareSystemB, ""); + softwareSystemB.delivers(userB, ""); + + // userA -> systemA -> web application -> systemB -> userB + // web application -> database + Container webApplication = softwareSystem.addContainer("Web Application", "", ""); + Container database = softwareSystem.addContainer("Database", "", ""); + softwareSystemA.uses(webApplication, ""); + webApplication.uses(softwareSystemB, ""); + webApplication.uses(database, ""); + + // userA -> systemA -> controller -> service -> repository -> database + Component controller = webApplication.addComponent("Controller", ""); + Component service = webApplication.addComponent("Service", ""); + Component repository = webApplication.addComponent("Repository", ""); + softwareSystemA.uses(controller, ""); + controller.uses(service, ""); + service.uses(repository, ""); + repository.uses(database, ""); + + // userA -> systemA -> controller -> service -> systemB -> userB + service.uses(softwareSystemB, ""); + + view.addNearestNeighbours(softwareSystem); + + assertEquals(3, view.getElements().size()); + assertTrue(view.getElements().contains(new ElementView(softwareSystemA))); + assertTrue(view.getElements().contains(new ElementView(softwareSystem))); + assertTrue(view.getElements().contains(new ElementView(softwareSystemB))); + + view = new SystemContextView(softwareSystem, "context", "Description"); + view.addNearestNeighbours(softwareSystemA); + + assertEquals(3, view.getElements().size()); + assertTrue(view.getElements().contains(new ElementView(userA))); + assertTrue(view.getElements().contains(new ElementView(softwareSystemA))); + assertTrue(view.getElements().contains(new ElementView(softwareSystem))); + } + + @Test + void removeSoftwareSystem_ThrowsAnException_WhenPassedNull() { + try { + view.remove((SoftwareSystem) null); + fail(); + } catch (IllegalArgumentException iae) { + assertEquals("An element must be specified.", iae.getMessage()); + } + } + + @Test + void removeSoftwareSystem_DoesNothing_WhenTheSoftwareSystemIsNotInTheView() { + SoftwareSystem anotherSoftwareSystem = model.addSoftwareSystem("Another software system", ""); + assertEquals(1, view.getElements().size()); + + view.remove(anotherSoftwareSystem); + assertEquals(1, view.getElements().size()); + } + + @Test + void removeSoftwareSystem_DoesNotRemoveTheSoftwareSystemInFocus() { + try { + view.remove(softwareSystem); + fail(); + } catch (IllegalArgumentException iae) { + assertEquals("The element named 'The System' cannot be removed from this view.", iae.getMessage()); + } + } + + @Test + void removeSoftwareSystem_RemovesTheSoftwareSystemAndRelationshipsFromTheView() { + SoftwareSystem softwareSystem1 = model.addSoftwareSystem("Software system 1", ""); + SoftwareSystem softwareSystem2 = model.addSoftwareSystem("Software system 2", ""); + softwareSystem1.uses(softwareSystem2, "uses"); + softwareSystem2.uses(softwareSystem1, "uses"); + view = views.createSystemContextView(softwareSystem1, "key", "description"); + view.add(softwareSystem2); + assertEquals(2, view.getElements().size()); + assertEquals(2, view.getRelationships().size()); + + view.remove(softwareSystem2); + assertEquals(1, view.getElements().size()); + assertEquals(0, view.getRelationships().size()); + } + + @Test + void removePerson_ThrowsAnException_WhenPassedNull() { + try { + view.remove((Person) null); + fail(); + } catch (IllegalArgumentException iae) { + assertEquals("An element must be specified.", iae.getMessage()); + } + } + + @Test + void removePerson_DoesNothing_WhenThePersonIsNotInTheView() { + Person person = model.addPerson("Person", ""); + assertEquals(1, view.getElements().size()); + + view.remove(person); + assertEquals(1, view.getElements().size()); + } + + @Test + void removePerson_RemovesThePersonAndRelationshipsFromTheView() { + Person person = model.addPerson("Person", ""); + person.uses(softwareSystem, "uses"); + softwareSystem.delivers(person, "delivers something to"); + view = views.createSystemContextView(softwareSystem, "key", "description"); + view.add(person); + assertEquals(2, view.getElements().size()); + assertEquals(2, view.getRelationships().size()); + + view.remove(person); + assertEquals(1, view.getElements().size()); + assertEquals(0, view.getRelationships().size()); + } + + @Test + void addSoftwareSystemWithoutRelationships_DoesNotAddRelationships() { + SoftwareSystem softwareSystem1 = model.addSoftwareSystem("Software system 1", ""); + SoftwareSystem softwareSystem2 = model.addSoftwareSystem("Software system 2", ""); + softwareSystem1.uses(softwareSystem2, "uses"); + view = views.createSystemContextView(softwareSystem1, "key", "description"); + view.add(softwareSystem2, false); + + assertEquals(2, view.getElements().size()); + assertEquals(0, view.getRelationships().size()); + } + + @Test + void addPersonWithoutRelationships_DoesNotAddRelationships() { + Person user = model.addPerson("User", ""); + SoftwareSystem softwareSystem = model.addSoftwareSystem("Software system 2", ""); + user.uses(softwareSystem, "uses"); + view = views.createSystemContextView(softwareSystem, "key", "description"); + view.add(user, false); + + assertEquals(2, view.getElements().size()); + assertEquals(0, view.getRelationships().size()); + } + + @Test + void addDefaultElements() { + CustomElement element = model.addCustomElement("Custom"); + Person user1 = model.addPerson("User 1"); + Person user2 = model.addPerson("User 2"); + SoftwareSystem softwareSystem1 = model.addSoftwareSystem("Software System 1"); + SoftwareSystem softwareSystem2 = model.addSoftwareSystem("Software System 2"); + + user1.uses(softwareSystem1, ""); + softwareSystem1.uses(softwareSystem2, ""); + user2.uses(softwareSystem2, ""); + + view = views.createSystemContextView(softwareSystem1, "key", "description"); + view.addDefaultElements(); + + assertEquals(3, view.getElements().size()); + assertFalse(view.getElements().contains(new ElementView(element))); + assertTrue(view.getElements().contains(new ElementView(user1))); + assertFalse(view.getElements().contains(new ElementView(user2))); + assertTrue(view.getElements().contains(new ElementView(softwareSystem1))); + assertTrue(view.getElements().contains(new ElementView(softwareSystem2))); + + element.uses(softwareSystem1, "Uses"); + view.addDefaultElements(); + + assertEquals(4, view.getElements().size()); + assertTrue(view.getElements().contains(new ElementView(element))); + assertTrue(view.getElements().contains(new ElementView(user1))); + assertFalse(view.getElements().contains(new ElementView(user2))); + assertTrue(view.getElements().contains(new ElementView(softwareSystem1))); + 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-core/src/test/java/com/structurizr/view/SystemLandscapeViewTests.java b/structurizr-core/src/test/java/com/structurizr/view/SystemLandscapeViewTests.java new file mode 100644 index 000000000..481c1c77c --- /dev/null +++ b/structurizr-core/src/test/java/com/structurizr/view/SystemLandscapeViewTests.java @@ -0,0 +1,189 @@ +package com.structurizr.view; + +import com.structurizr.AbstractWorkspaceTestBase; +import com.structurizr.model.*; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +public class SystemLandscapeViewTests extends AbstractWorkspaceTestBase { + + private SystemLandscapeView view; + + @BeforeEach + public void setUp() { + view = new SystemLandscapeView(model, "context", "Description"); + } + + @Test + void construction() { + assertEquals(0, view.getElements().size()); + assertSame(model, view.getModel()); + } + + @Test + void getName() { + assertEquals("System Landscape View", view.getName()); + } + + @Test + void addAllSoftwareSystems_DoesNothing_WhenThereAreNoOtherSoftwareSystems() { + view.addAllSoftwareSystems(); + assertEquals(0, view.getElements().size()); + } + + @Test + void addAllSoftwareSystems_AddsAllSoftwareSystems_WhenThereAreSomeSoftwareSystemsInTheModel() { + SoftwareSystem softwareSystemA = model.addSoftwareSystem("System A", "Description"); + SoftwareSystem softwareSystemB = model.addSoftwareSystem("System B", "Description"); + + view.addAllSoftwareSystems(); + + assertEquals(2, view.getElements().size()); + assertTrue(view.getElements().contains(new ElementView(softwareSystemA))); + assertTrue(view.getElements().contains(new ElementView(softwareSystemB))); + } + + @Test + void addAllPeople_DoesNothing_WhenThereAreNoPeople() { + view.addAllPeople(); + assertEquals(0, view.getElements().size()); + } + + @Test + void addAllPeople_AddsAllPeople_WhenThereAreSomePeopleInTheModel() { + Person userA = model.addPerson("User A", "Description"); + Person userB = model.addPerson("User B", "Description"); + + view.addAllPeople(); + + assertEquals(2, view.getElements().size()); + assertTrue(view.getElements().contains(new ElementView(userA))); + assertTrue(view.getElements().contains(new ElementView(userB))); + } + + @Test + void addAllElements_DoesNothing_WhenThereAreNoSoftwareSystemsOrPeople() { + view.addAllElements(); + assertEquals(0, view.getElements().size()); + } + + @Test + void addAllElements_AddsAllSoftwareSystemsAndPeople_WhenThereAreSomeSoftwareSystemsAndPeopleInTheModel() { + SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System", "Description"); + Person person = model.addPerson("Person", "Description"); + + view.addAllElements(); + + assertEquals(2, view.getElements().size()); + assertTrue(view.getElements().contains(new ElementView(softwareSystem))); + assertTrue(view.getElements().contains(new ElementView(person))); + } + + @Test + void addNearestNeighbours_ThrowsAnException_WhenANullElementIsSpecified() { + try { + view.addNearestNeighbours(null); + fail(); + } catch (IllegalArgumentException iae) { + assertEquals("An element must be specified.", iae.getMessage()); + } + } + + @Test + void addNearestNeighbours_ThrowsAnException_WhenAnElementThatIsNotAPersonOrSoftwareSystemIsSpecified() { + SoftwareSystem softwareSystem = model.addSoftwareSystem("The System", "Description"); + Container container = softwareSystem.addContainer("Container", "Description", "Technology"); + try { + view.addNearestNeighbours(container); + fail(); + } catch (IllegalArgumentException iae) { + assertEquals("A person or software system must be specified.", iae.getMessage()); + } + } + + @Test + void addNearestNeighbours_DoesNothing_WhenThereAreNoNeighbours() { + SoftwareSystem softwareSystem = model.addSoftwareSystem("The System", "Description"); + view.addNearestNeighbours(softwareSystem); + + assertEquals(1, view.getElements().size()); + } + + @Test + 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"); + Person userA = model.addPerson("User A", "Description"); + Person userB = model.addPerson("User B", "Description"); + + // userA -> systemA -> system -> systemB -> userB + userA.uses(softwareSystemA, ""); + softwareSystemA.uses(softwareSystem, ""); + softwareSystem.uses(softwareSystemB, ""); + softwareSystemB.delivers(userB, ""); + + // userA -> systemA -> web application -> systemB -> userB + // web application -> database + Container webApplication = softwareSystem.addContainer("Web Application", "", ""); + Container database = softwareSystem.addContainer("Database", "", ""); + softwareSystemA.uses(webApplication, ""); + webApplication.uses(softwareSystemB, ""); + webApplication.uses(database, ""); + + // userA -> systemA -> controller -> service -> repository -> database + Component controller = webApplication.addComponent("Controller", ""); + Component service = webApplication.addComponent("Service", ""); + Component repository = webApplication.addComponent("Repository", ""); + softwareSystemA.uses(controller, ""); + controller.uses(service, ""); + service.uses(repository, ""); + repository.uses(database, ""); + + // userA -> systemA -> controller -> service -> systemB -> userB + service.uses(softwareSystemB, ""); + + view.addNearestNeighbours(softwareSystem); + + assertEquals(3, view.getElements().size()); + assertTrue(view.getElements().contains(new ElementView(softwareSystemA))); + assertTrue(view.getElements().contains(new ElementView(softwareSystem))); + assertTrue(view.getElements().contains(new ElementView(softwareSystemB))); + + view = views.createSystemLandscapeView("context", "Description"); + view.addNearestNeighbours(softwareSystemA); + + assertEquals(3, view.getElements().size()); + assertTrue(view.getElements().contains(new ElementView(userA))); + assertTrue(view.getElements().contains(new ElementView(softwareSystemA))); + assertTrue(view.getElements().contains(new ElementView(softwareSystem))); + } + + @Test + void addDefaultElements() { + CustomElement element = model.addCustomElement("Custom"); + Person user = model.addPerson("User"); + SoftwareSystem softwareSystem1 = model.addSoftwareSystem("Software System 1"); + SoftwareSystem softwareSystem2 = model.addSoftwareSystem("Software System 2"); + + view.addDefaultElements(); + + assertEquals(3, view.getElements().size()); + assertFalse(view.getElements().contains(new ElementView(element))); + assertTrue(view.getElements().contains(new ElementView(user))); + assertTrue(view.getElements().contains(new ElementView(softwareSystem1))); + assertTrue(view.getElements().contains(new ElementView(softwareSystem2))); + + element.uses(softwareSystem1, "Uses"); + view.addDefaultElements(); + + assertEquals(4, view.getElements().size()); + assertTrue(view.getElements().contains(new ElementView(element))); + assertTrue(view.getElements().contains(new ElementView(user))); + assertTrue(view.getElements().contains(new ElementView(softwareSystem1))); + assertTrue(view.getElements().contains(new ElementView(softwareSystem2))); + } + +} \ No newline at end of file diff --git a/structurizr-core/src/test/java/com/structurizr/view/TerminologyTests.java b/structurizr-core/src/test/java/com/structurizr/view/TerminologyTests.java new file mode 100644 index 000000000..316a23bb7 --- /dev/null +++ b/structurizr-core/src/test/java/com/structurizr/view/TerminologyTests.java @@ -0,0 +1,56 @@ +package com.structurizr.view; + +import com.structurizr.Workspace; +import com.structurizr.model.*; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class TerminologyTests { + + @Test + 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"); + Component component = container.addComponent("Component"); + DeploymentNode deploymentNode = workspace.getModel().addDeploymentNode("Deployment Node"); + InfrastructureNode infrastructureNode = deploymentNode.addInfrastructureNode("Infrastructure Node"); + SoftwareSystemInstance softwareSystemInstance = deploymentNode.add(softwareSystem); + 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)); + assertEquals("Component", terminology.findTerminology(component)); + assertEquals("Deployment Node", terminology.findTerminology(deploymentNode)); + assertEquals("Infrastructure Node", terminology.findTerminology(infrastructureNode)); + assertEquals("Software System", terminology.findTerminology(softwareSystemInstance)); + assertEquals("Container", terminology.findTerminology(containerInstance)); + assertEquals("Relationship", terminology.findTerminology(relationship)); + + terminology.setPerson("PERSON"); + terminology.setSoftwareSystem("SOFTWARE SYSTEM"); + terminology.setContainer("CONTAINER"); + terminology.setComponent("COMPONENT"); + terminology.setDeploymentNode("DEPLOYMENT NODE"); + terminology.setInfrastructureNode("INFRASTRUCTURE NODE"); + terminology.setRelationship("RELATIONSHIP"); + + assertEquals("PERSON", terminology.findTerminology(person)); + assertEquals("SOFTWARE SYSTEM", terminology.findTerminology(softwareSystem)); + assertEquals("CONTAINER", terminology.findTerminology(container)); + assertEquals("COMPONENT", terminology.findTerminology(component)); + assertEquals("DEPLOYMENT NODE", terminology.findTerminology(deploymentNode)); + assertEquals("INFRASTRUCTURE NODE", terminology.findTerminology(infrastructureNode)); + assertEquals("SOFTWARE SYSTEM", terminology.findTerminology(softwareSystemInstance)); + assertEquals("CONTAINER", terminology.findTerminology(containerInstance)); + assertEquals("RELATIONSHIP", terminology.findTerminology(relationship)); + } + +} \ No newline at end of file diff --git a/structurizr-core/src/test/java/com/structurizr/view/VertexTests.java b/structurizr-core/src/test/java/com/structurizr/view/VertexTests.java new file mode 100644 index 000000000..f9377d309 --- /dev/null +++ b/structurizr-core/src/test/java/com/structurizr/view/VertexTests.java @@ -0,0 +1,25 @@ +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.assertNotEquals; + +public class VertexTests { + + @Test + void equals() { + Vertex vertex1 = new Vertex(123, 456); + Vertex vertex2 = new Vertex(123, 456); + Vertex vertex3 = new Vertex(456, 123); + + assertNotEquals(vertex1, null); + assertNotEquals(vertex1, "hello world"); + + assertEquals(vertex1, vertex1); + assertEquals(vertex1, vertex2); + assertEquals(vertex2, vertex1); + assertNotEquals(vertex2, vertex3); + } + +} \ No newline at end of file diff --git a/structurizr-core/src/test/java/com/structurizr/view/ViewSetTests.java b/structurizr-core/src/test/java/com/structurizr/view/ViewSetTests.java new file mode 100644 index 000000000..4bec2222f --- /dev/null +++ b/structurizr-core/src/test/java/com/structurizr/view/ViewSetTests.java @@ -0,0 +1,1230 @@ +package com.structurizr.view; + +import com.structurizr.Workspace; +import com.structurizr.model.*; +import org.junit.jupiter.api.Test; + +import java.util.Collections; +import java.util.HashSet; + +import static org.junit.jupiter.api.Assertions.*; + +public class ViewSetTests { + + private Workspace createWorkspace() { + Workspace workspace = new Workspace("Name", "Description"); + Model model = workspace.getModel(); + + SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System", "Description"); + Person person = model.addPerson("Person", "Description"); + person.uses(softwareSystem, "Uses"); + Container container = softwareSystem.addContainer("Container", "Description", "Technology"); + Component component = container.addComponent("Component", "Description", "Technology"); + + DeploymentNode deploymentNode = model.addDeploymentNode("Deployment Node", "Description", "Technology"); + ContainerInstance containerInstance = deploymentNode.add(container); + + return workspace; + } + + @Test + void createCustomView_GeneratesAKey_WhenANullKeyIsSpecified() { + View view = new Workspace("", "").getViews().createCustomView(null, "Title", "Description"); + assertEquals("Custom-001", view.getKey()); + assertTrue(view.isGeneratedKey()); + } + + @Test + void createCustomView_GeneratesAKey_WhenAnEmptyKeyIsSpecified() { + View view = new Workspace("", "").getViews().createCustomView(" ", "Title", "Description"); + assertEquals("Custom-001", view.getKey()); + assertTrue(view.isGeneratedKey()); + } + + @Test + void createCustomView_ThrowsAnException_WhenADuplicateKeyIsSpecified() { + try { + Workspace workspace = new Workspace("Name", "Description"); + workspace.getViews().createCustomView("key", "Title", "Description"); + workspace.getViews().createCustomView("key", "Title", "Description"); + fail(); + } catch (IllegalArgumentException iae) { + assertEquals("A view with the key key already exists.", iae.getMessage()); + } + } + + @Test + void createCustomView_DoesNotThrowAnException_WhenUniqueKeysAreSpecified() { + Workspace workspace = new Workspace("Name", "Description"); + workspace.getViews().createCustomView("key1", "Title", "Description"); + workspace.getViews().createCustomView("key2", "Title", "Description"); + } + + @Test + void createCustomView() { + Workspace workspace = new Workspace("Name", "Description"); + CustomView customView = workspace.getViews().createCustomView("key", "Title", "Description"); + assertEquals("key", customView.getKey()); + assertEquals("Title", customView.getTitle()); + assertEquals("Description", customView.getDescription()); + + assertEquals(1, workspace.getViews().getCustomViews().size()); + } + + @Test + void createSystemLandscapeView_GeneratesAKey_WhenANullKeyIsSpecified() { + SystemLandscapeView view = new Workspace("", "").getViews().createSystemLandscapeView(null, "Description"); + assertEquals("SystemLandscape-001", view.getKey()); + assertTrue(view.isGeneratedKey()); + } + + @Test + void createSystemLandscapeView_GeneratesAKey_WhenAnEmptyKeyIsSpecified() { + SystemLandscapeView view = new Workspace("", "").getViews().createSystemLandscapeView(" ", "Description"); + assertEquals("SystemLandscape-001", view.getKey()); + assertTrue(view.isGeneratedKey()); + } + + @Test + void createSystemLandscapeView_ThrowsAnException_WhenADuplicateKeyIsSpecified() { + try { + Workspace workspace = new Workspace("Name", "Description"); + workspace.getViews().createSystemLandscapeView("key", "Description"); + workspace.getViews().createSystemLandscapeView("key", "Description"); + fail(); + } catch (IllegalArgumentException iae) { + assertEquals("A view with the key key already exists.", iae.getMessage()); + } + } + + @Test + void createSystemLandscapeView_DoesNotThrowAnException_WhenUniqueKeysAreSpecified() { + Workspace workspace = new Workspace("Name", "Description"); + workspace.getViews().createSystemLandscapeView("key1", "Description"); + workspace.getViews().createSystemLandscapeView("key2", "Description"); + } + + @Test + void createSystemLandscapeView() { + Workspace workspace = new Workspace("Name", "Description"); + SystemLandscapeView systemLandscapeView = workspace.getViews().createSystemLandscapeView("key", "Description"); + assertEquals("key", systemLandscapeView.getKey()); + assertEquals("Description", systemLandscapeView.getDescription()); + } + + @Test + void createSystemContextView_ThrowsAnException_WhenASoftwareSystemIsSpecified() { + try { + new Workspace("", "").getViews().createSystemContextView(null, null, "Description"); + fail(); + } catch (IllegalArgumentException iae) { + assertEquals("A software system must be specified.", iae.getMessage()); + } + } + + @Test + void createSystemContextView_GeneratesAKey_WhenANullKeyIsSpecified() { + Workspace workspace = new Workspace("Name", "Description"); + SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Software System", "Description"); + SystemContextView view = workspace.getViews().createSystemContextView(softwareSystem, null, "Description"); + assertEquals("SystemContext-001", view.getKey()); + assertTrue(view.isGeneratedKey()); + } + + @Test + void createSystemContextView_GeneratesAKey_WhenAnEmptyKeyIsSpecified() { + Workspace workspace = new Workspace("Name", "Description"); + SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Software System", "Description"); + SystemContextView view = workspace.getViews().createSystemContextView(softwareSystem, " ", "Description"); + assertEquals("SystemContext-001", view.getKey()); + assertTrue(view.isGeneratedKey()); + } + + @Test + void createSystemContextView_ThrowsAnException_WhenADuplicateKeyIsSpecified() { + try { + Workspace workspace = new Workspace("Name", "Description"); + SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Software System", "Description"); + workspace.getViews().createSystemContextView(softwareSystem, "key", "Description"); + workspace.getViews().createSystemContextView(softwareSystem, "key", "Description"); + fail(); + } catch (IllegalArgumentException iae) { + assertEquals("A view with the key key already exists.", iae.getMessage()); + } + } + + @Test + void createSystemContextView() { + Workspace workspace = new Workspace("Name", "Description"); + SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Software System", "Description"); + SystemContextView systemContextView = workspace.getViews().createSystemContextView(softwareSystem, "key", "Description"); + assertEquals("key", systemContextView.getKey()); + assertEquals("Description", systemContextView.getDescription()); + assertSame(softwareSystem, systemContextView.getSoftwareSystem()); + } + + @Test + void createContainerView_ThrowsAnException_WhenASoftwareSystemIsSpecified() { + try { + new Workspace("", "").getViews().createContainerView(null, null, "Description"); + fail(); + } catch (IllegalArgumentException iae) { + assertEquals("A software system must be specified.", iae.getMessage()); + } + } + + @Test + void createContainerView_GeneratesAKey_WhenANullKeyIsSpecified() { + Workspace workspace = new Workspace("Name", "Description"); + SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Software System", "Description"); + ContainerView view = workspace.getViews().createContainerView(softwareSystem, null, "Description"); + assertEquals("Container-001", view.getKey()); + assertTrue(view.isGeneratedKey()); + } + + @Test + void createContainerView_GeneratesAKey_WhenAnEmptyKeyIsSpecified() { + Workspace workspace = new Workspace("Name", "Description"); + SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Software System", "Description"); + ContainerView view = workspace.getViews().createContainerView(softwareSystem, " ", "Description"); + assertEquals("Container-001", view.getKey()); + assertTrue(view.isGeneratedKey()); + } + + @Test + void createContainerView_ThrowsAnException_WhenADuplicateKeyIsSpecified() { + try { + Workspace workspace = new Workspace("Name", "Description"); + SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Software System", "Description"); + workspace.getViews().createContainerView(softwareSystem, "key", "Description"); + workspace.getViews().createContainerView(softwareSystem, "key", "Description"); + fail(); + } catch (IllegalArgumentException iae) { + assertEquals("A view with the key key already exists.", iae.getMessage()); + } + } + + @Test + void createContainerView() { + Workspace workspace = new Workspace("Name", "Description"); + SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Software System", "Description"); + ContainerView containerView = workspace.getViews().createContainerView(softwareSystem, "key", "Description"); + assertEquals("key", containerView.getKey()); + assertEquals("Description", containerView.getDescription()); + assertSame(softwareSystem, containerView.getSoftwareSystem()); + } + + @Test + void createComponentView_ThrowsAnException_WhenASoftwareSystemIsSpecified() { + try { + new Workspace("", "").getViews().createComponentView(null, null, "Description"); + fail(); + } catch (IllegalArgumentException iae) { + assertEquals("A container must be specified.", iae.getMessage()); + } + } + + @Test + void createComponentView_GeneratesAKey_WhenANullKeyIsSpecified() { + Workspace workspace = new Workspace("Name", "Description"); + SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Software System", "Description"); + Container container = softwareSystem.addContainer("Container", "Description", "Technology"); + ComponentView view = workspace.getViews().createComponentView(container, null, "Description"); + assertEquals("Component-001", view.getKey()); + assertTrue(view.isGeneratedKey()); + } + + @Test + void createComponentView_GeneratesAKey_WhenAnEmptyKeyIsSpecified() { + Workspace workspace = new Workspace("Name", "Description"); + SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Software System", "Description"); + Container container = softwareSystem.addContainer("Container", "Description", "Technology"); + ComponentView view = workspace.getViews().createComponentView(container, " ", "Description"); + assertEquals("Component-001", view.getKey()); + assertTrue(view.isGeneratedKey()); + } + + @Test + void createComponentView_ThrowsAnException_WhenADuplicateKeyIsSpecified() { + try { + Workspace workspace = new Workspace("Name", "Description"); + SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Software System", "Description"); + Container container = softwareSystem.addContainer("Container", "Description", "Technology"); + workspace.getViews().createComponentView(container, "key", "Description"); + workspace.getViews().createComponentView(container, "key", "Description"); + fail(); + } catch (IllegalArgumentException iae) { + assertEquals("A view with the key key already exists.", iae.getMessage()); + } + } + + @Test + void createComponentView() { + Workspace workspace = new Workspace("Name", "Description"); + SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Software System", "Description"); + Container container = softwareSystem.addContainer("Container", "Description", "Technology"); + ComponentView componentView = workspace.getViews().createComponentView(container, "key", "Description"); + assertEquals("key", componentView.getKey()); + assertEquals("Description", componentView.getDescription()); + assertSame(softwareSystem, componentView.getSoftwareSystem()); + assertSame(container, componentView.getContainer()); + } + + @Test + void createDynamicView_GeneratesAKey_WhenANullKeyIsSpecified() { + Workspace workspace = new Workspace("Name", "Description"); + DynamicView view = workspace.getViews().createDynamicView(null); + assertEquals("Dynamic-001", view.getKey()); + assertTrue(view.isGeneratedKey()); + } + + @Test + void createDynamicView_GeneratesAKey_WhenAnEmptyKeyIsSpecified() { + Workspace workspace = new Workspace("Name", "Description"); + DynamicView view = workspace.getViews().createDynamicView(" ", "Description"); + assertEquals("Dynamic-001", view.getKey()); + assertTrue(view.isGeneratedKey()); + } + + @Test + void createDynamicView() { + Workspace workspace = new Workspace("Name", "Description"); + + DynamicView dynamicView = workspace.getViews().createDynamicView("key", "Description"); + assertEquals("key", dynamicView.getKey()); + assertEquals("Description", dynamicView.getDescription()); + assertNull(dynamicView.getSoftwareSystem()); + assertNull(dynamicView.getElement()); + } + + @Test + void createDynamicViewForASoftwareSystem_ThrowsAnException_WhenANullSoftwareSystemIsSpecified() { + try { + new Workspace("", "").getViews().createDynamicView((SoftwareSystem) null, "key", "Description"); + fail(); + } catch (IllegalArgumentException iae) { + assertEquals("A software system must be specified.", iae.getMessage()); + } + } + + @Test + void createDynamicViewForASoftwareSystem_GeneratesAKey_WhenANullKeyIsSpecified() { + Workspace workspace = new Workspace("Name", "Description"); + SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Software System", "Description"); + DynamicView view = workspace.getViews().createDynamicView(softwareSystem, null, "Description"); + assertEquals("Dynamic-001", view.getKey()); + assertTrue(view.isGeneratedKey()); + } + + @Test + void createDynamicViewForASoftwareSystem_GeneratesAKey_WhenAnEmptyKeyIsSpecified() { + Workspace workspace = new Workspace("Name", "Description"); + SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Software System", "Description"); + DynamicView view = workspace.getViews().createDynamicView(softwareSystem, " ", "Description"); + assertEquals("Dynamic-001", view.getKey()); + assertTrue(view.isGeneratedKey()); + } + + @Test + void createDynamicViewForASoftwareSystem_ThrowsAnException_WhenADuplicateKeyIsUsed() { + Workspace workspace = new Workspace("Name", "Description"); + SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Name", "Description"); + + workspace.getViews().createDeploymentView(softwareSystem, "dynamic", "Description"); + try { + workspace.getViews().createDeploymentView(softwareSystem, "dynamic", "Description"); + fail(); + } catch (IllegalArgumentException iae) { + assertEquals("A view with the key dynamic already exists.", iae.getMessage()); + } + } + + @Test + void createDynamicViewForSoftwareSystem() { + Workspace workspace = new Workspace("Name", "Description"); + SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Name", "Description"); + + DynamicView dynamicView = workspace.getViews().createDynamicView(softwareSystem, "key", "Description"); + assertEquals("key", dynamicView.getKey()); + assertEquals("Description", dynamicView.getDescription()); + assertSame(softwareSystem, dynamicView.getSoftwareSystem()); + assertSame(softwareSystem, dynamicView.getElement()); + } + + @Test + void createDynamicViewForAContainer_ThrowsAnException_WhenANullContainerIsSpecified() { + try { + new Workspace("", "").getViews().createDynamicView((Container) null, "key", "Description"); + fail(); + } catch (IllegalArgumentException iae) { + assertEquals("A container must be specified.", iae.getMessage()); + } + } + + @Test + void createDynamicViewForAContainer_GeneratesAKey_WhenANullKeyIsSpecified() { + Workspace workspace = new Workspace("Name", "Description"); + SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Software System", "Description"); + Container container = softwareSystem.addContainer("Container", "Description", "Technology"); + DynamicView view = workspace.getViews().createDynamicView(container, null, "Description"); + assertEquals("Dynamic-001", view.getKey()); + assertTrue(view.isGeneratedKey()); + } + + @Test + void createDynamicViewForAContainer_GeneratesAKey_WhenAnEmptyKeyIsSpecified() { + Workspace workspace = new Workspace("Name", "Description"); + SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Software System", "Description"); + Container container = softwareSystem.addContainer("Container", "Description", "Technology"); + DynamicView view = workspace.getViews().createDynamicView(container, " ", "Description"); + assertEquals("Dynamic-001", view.getKey()); + assertTrue(view.isGeneratedKey()); + } + + @Test + void createDynamicViewForContainer() { + Workspace workspace = new Workspace("Name", "Description"); + SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Name", "Description"); + Container container = softwareSystem.addContainer("Container", "Description", "Technology"); + + DynamicView dynamicView = workspace.getViews().createDynamicView(container, "key", "Description"); + assertEquals("key", dynamicView.getKey()); + assertEquals("Description", dynamicView.getDescription()); + assertSame(softwareSystem, dynamicView.getSoftwareSystem()); + assertSame(container, dynamicView.getElement()); + } + + @Test + void createDeploymentView_GeneratesAKey_WhenANullKeyIsSpecified() { + Workspace workspace = new Workspace("Name", "Description"); + DeploymentView view = workspace.getViews().createDeploymentView(null); + assertEquals("Deployment-001", view.getKey()); + assertTrue(view.isGeneratedKey()); + } + + @Test + void createDeploymentView_GeneratesAKey_WhenAnEmptyKeyIsSpecified() { + Workspace workspace = new Workspace("Name", "Description"); + DeploymentView view = workspace.getViews().createDeploymentView(" ", "Description"); + assertEquals("Deployment-001", view.getKey()); + assertTrue(view.isGeneratedKey()); + } + + @Test + void createDeploymentView_ThrowsAnException_WhenADuplicateKeyIsUsed() { + Workspace workspace = new Workspace("Name", "Description"); + SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Name", "Description"); + + workspace.getViews().createDeploymentView(softwareSystem, "deployment", "Description"); + try { + workspace.getViews().createDeploymentView(softwareSystem, "deployment", "Description"); + fail(); + } catch (IllegalArgumentException iae) { + assertEquals("A view with the key deployment already exists.", iae.getMessage()); + } + } + + @Test + void createDeploymentView() { + Workspace workspace = new Workspace("Name", "Description"); + + DeploymentView deploymentView = workspace.getViews().createDeploymentView("key", "Description"); + assertEquals("key", deploymentView.getKey()); + assertEquals("Description", deploymentView.getDescription()); + assertNull(deploymentView.getSoftwareSystem()); + } + + @Test + void createDeploymentViewForASoftwareSystem_ThrowsAnException_WhenANullSoftwareSystemIsSpecified() { + try { + new Workspace("", "").getViews().createDeploymentView((SoftwareSystem) null, "key", "Description"); + fail(); + } catch (IllegalArgumentException iae) { + assertEquals("A software system must be specified.", iae.getMessage()); + } + } + + @Test + void createDeploymentViewForASoftwareSystem_GeneratesAKey_WhenANullKeyIsSpecified() { + Workspace workspace = new Workspace("Name", "Description"); + SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Software System", "Description"); + DeploymentView view = workspace.getViews().createDeploymentView(softwareSystem, null, "Description"); + assertEquals("Deployment-001", view.getKey()); + assertTrue(view.isGeneratedKey()); + } + + @Test + void createDeploymentViewForASoftwareSystem_GeneratesAKey_WhenAnEmptyKeyIsSpecified() { + Workspace workspace = new Workspace("Name", "Description"); + SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Software System", "Description"); + DeploymentView view = workspace.getViews().createDeploymentView(softwareSystem, " ", "Description"); + assertEquals("Deployment-001", view.getKey()); + assertTrue(view.isGeneratedKey()); + } + + @Test + void createDeploymentViewForASoftwareSystem_ThrowsAnException_WhenADuplicateKeyIsUsed() { + Workspace workspace = new Workspace("Name", "Description"); + SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Name", "Description"); + + workspace.getViews().createDeploymentView(softwareSystem, "deployment", "Description"); + try { + workspace.getViews().createDeploymentView(softwareSystem, "deployment", "Description"); + fail(); + } catch (IllegalArgumentException iae) { + assertEquals("A view with the key deployment already exists.", iae.getMessage()); + } + } + + @Test + void createDeploymentViewForSoftwareSystem() { + Workspace workspace = new Workspace("Name", "Description"); + SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Name", "Description"); + + DeploymentView deploymentView = workspace.getViews().createDeploymentView(softwareSystem, "key", "Description"); + assertEquals("key", deploymentView.getKey()); + assertEquals("Description", deploymentView.getDescription()); + assertSame(softwareSystem, deploymentView.getSoftwareSystem()); + } + + @Test + void createFilteredView_ThrowsAnException_WhenANullViewIsSpecified() { + try { + Workspace workspace = new Workspace("Name", "Description"); + workspace.getViews().createFilteredView((SystemLandscapeView)null, "key", "Description", FilterMode.Include, "tag1", "tag2"); + fail(); + } catch (IllegalArgumentException iae) { + assertEquals("A view must be specified.", iae.getMessage()); + } + } + + @Test + void createFilteredView_GeneratesAKey_WhenANullKeyIsSpecified() { + Workspace workspace = new Workspace("Name", "Description"); + SystemLandscapeView view = workspace.getViews().createSystemLandscapeView("systemLandscape", "Description"); + FilteredView filteredView = workspace.getViews().createFilteredView(view, null, "Description", FilterMode.Include, "tag1", "tag2"); + assertEquals("Filtered-001", filteredView.getKey()); + assertTrue(filteredView.isGeneratedKey()); + } + + @Test + void createFilteredView_GeneratesAKey_WhenAnEmptyKeyIsSpecified() { + Workspace workspace = new Workspace("Name", "Description"); + SystemLandscapeView view = workspace.getViews().createSystemLandscapeView("systemLandscape", "Description"); + FilteredView filteredView = workspace.getViews().createFilteredView(view, " ", "Description", FilterMode.Include, "tag1", "tag2"); + assertEquals("Filtered-001", filteredView.getKey()); + assertTrue(filteredView.isGeneratedKey()); + } + + @Test + 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"); + try { + workspace.getViews().createFilteredView(view, "filtered", "Description", FilterMode.Include, "tag1", "tag2"); + fail(); + } catch (IllegalArgumentException iae) { + assertEquals("A view with the key filtered already exists.", iae.getMessage()); + } + } + + @Test + 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"); + + 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 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(); + SoftwareSystem softwareSystem1 = workspace1.getModel().getSoftwareSystemWithName("Software System"); + SystemContextView view1 = workspace1.getViews().createSystemContextView(softwareSystem1, "context", "Description"); + view1.setKey(null); // this simulates views created by previous versions of the client library + view1.addAllElements(); + view1.getElements().iterator().next().setX(100); + view1.setPaperSize(PaperSize.A3_Landscape); + + Workspace workspace2 = createWorkspace(); + SoftwareSystem softwareSystem2 = workspace2.getModel().getSoftwareSystemWithName("Software System"); + SystemContextView view2 = workspace2.getViews().createSystemContextView(softwareSystem2, "context", "Description"); + view2.setKey(null); // this simulates views created by previous versions of the client library + view2.addAllElements(); + + workspace2.getViews().copyLayoutInformationFrom(workspace1.getViews()); + assertEquals(100, view2.getElements().iterator().next().getX()); + assertEquals(PaperSize.A3_Landscape, view2.getPaperSize()); + } + + @Test + void copyLayoutInformationFrom_WhenAViewKeyHasBeenIntroduced() { + Workspace workspace1 = createWorkspace(); + SoftwareSystem softwareSystem1 = workspace1.getModel().getSoftwareSystemWithName("Software System"); + SystemContextView view1 = workspace1.getViews().createSystemContextView(softwareSystem1, "context", "Description"); + view1.setKey(null); // this simulates views created by previous versions of the client library + view1.addAllElements(); + view1.getElements().iterator().next().setX(100); + view1.setPaperSize(PaperSize.A3_Landscape); + + Workspace workspace2 = createWorkspace(); + SoftwareSystem softwareSystem2 = workspace2.getModel().getSoftwareSystemWithName("Software System"); + SystemContextView view2 = workspace2.getViews().createSystemContextView(softwareSystem2, "context", "Description"); + view2.addAllElements(); + + workspace2.getViews().copyLayoutInformationFrom(workspace1.getViews()); + assertEquals(100, view2.getElements().iterator().next().getX()); + assertEquals(PaperSize.A3_Landscape, view2.getPaperSize()); + } + + @Test + void copyLayoutInformationFrom_IgnoresThePaperSize_WhenThePaperSizeIsSet() { + Workspace workspace1 = createWorkspace(); + SoftwareSystem softwareSystem1 = workspace1.getModel().getSoftwareSystemWithName("Software System"); + SystemContextView view1 = workspace1.getViews().createSystemContextView(softwareSystem1, "context", "Description"); + view1.setPaperSize(PaperSize.A3_Landscape); + + Workspace workspace2 = createWorkspace(); + SoftwareSystem softwareSystem2 = workspace2.getModel().getSoftwareSystemWithName("Software System"); + SystemContextView view2 = workspace2.getViews().createSystemContextView(softwareSystem2, "context", "Description"); + view2.setPaperSize(PaperSize.A5_Portrait); + + workspace2.getViews().copyLayoutInformationFrom(workspace1.getViews()); + assertEquals(PaperSize.A5_Portrait, view2.getPaperSize()); + } + + @Test + void copyLayoutInformationFrom_CopiesThePaperSize_WhenThePaperSizeIsNotSet() { + Workspace workspace1 = createWorkspace(); + SoftwareSystem softwareSystem1 = workspace1.getModel().getSoftwareSystemWithName("Software System"); + SystemContextView view1 = workspace1.getViews().createSystemContextView(softwareSystem1, "context", "Description"); + view1.setPaperSize(PaperSize.A3_Landscape); + + Workspace workspace2 = createWorkspace(); + SoftwareSystem softwareSystem2 = workspace2.getModel().getSoftwareSystemWithName("Software System"); + SystemContextView view2 = workspace2.getViews().createSystemContextView(softwareSystem2, "context", "Description"); + + workspace2.getViews().copyLayoutInformationFrom(workspace1.getViews()); + assertEquals(PaperSize.A3_Landscape, view2.getPaperSize()); + } + + @Test + void copyLayoutInformationFrom_WhenTheSystemLandscapeViewKeysMatch() { + Workspace workspace1 = createWorkspace(); + SoftwareSystem softwareSystem1 = workspace1.getModel().getSoftwareSystemWithName("Software System"); + SystemLandscapeView view1 = workspace1.getViews().createSystemLandscapeView("landscape", "Description"); + view1.addAllElements(); + view1.getElements().iterator().next().setX(100); + view1.setPaperSize(PaperSize.A3_Landscape); + + Workspace workspace2 = createWorkspace(); + SoftwareSystem softwareSystem2 = workspace2.getModel().getSoftwareSystemWithName("Software System"); + SystemLandscapeView view2 = workspace2.getViews().createSystemLandscapeView("context", "Description"); + view2.addAllElements(); + + workspace2.getViews().copyLayoutInformationFrom(workspace1.getViews()); + assertEquals(100, view2.getElements().iterator().next().getX()); + assertEquals(PaperSize.A3_Landscape, view2.getPaperSize()); + } + + @Test + void copyLayoutInformationFrom_DoesNotDoAnythingIfThereIsNoSystemLandscapeViewToCopyInformationFrom() { + Workspace workspace1 = createWorkspace(); + + Workspace workspace2 = createWorkspace(); + SoftwareSystem softwareSystem2 = workspace2.getModel().getSoftwareSystemWithName("Software System"); + SystemLandscapeView view2 = workspace2.getViews().createSystemLandscapeView("landscape", "Description"); + view2.addAllElements(); + + workspace2.getViews().copyLayoutInformationFrom(workspace1.getViews()); + assertEquals(0, view2.getElements().iterator().next().getX()); // default + assertNull(view2.getPaperSize()); // default + } + + @Test + void copyLayoutInformationFrom_WhenTheSystemContextViewKeysMatch() { + Workspace workspace1 = createWorkspace(); + SoftwareSystem softwareSystem1 = workspace1.getModel().getSoftwareSystemWithName("Software System"); + SystemContextView view1 = workspace1.getViews().createSystemContextView(softwareSystem1, "context", "Description"); + view1.addAllElements(); + view1.getElements().iterator().next().setX(100); + view1.setPaperSize(PaperSize.A3_Landscape); + + Workspace workspace2 = createWorkspace(); + SoftwareSystem softwareSystem2 = workspace2.getModel().getSoftwareSystemWithName("Software System"); + SystemContextView view2 = workspace2.getViews().createSystemContextView(softwareSystem2, "context", "Description"); + view2.addAllElements(); + + workspace2.getViews().copyLayoutInformationFrom(workspace1.getViews()); + assertEquals(100, view2.getElements().iterator().next().getX()); + assertEquals(PaperSize.A3_Landscape, view2.getPaperSize()); + } + + @Test + void copyLayoutInformationFrom_DoesNotDoAnythingIfThereIsNoSystemContextViewToCopyInformationFrom() { + Workspace workspace1 = createWorkspace(); + + Workspace workspace2 = createWorkspace(); + SoftwareSystem softwareSystem2 = workspace2.getModel().getSoftwareSystemWithName("Software System"); + SystemContextView view2 = workspace2.getViews().createSystemContextView(softwareSystem2, "context", "Description"); + view2.addAllElements(); + + workspace2.getViews().copyLayoutInformationFrom(workspace1.getViews()); + assertEquals(0, view2.getElements().iterator().next().getX()); // default + assertNull(view2.getPaperSize()); // default + } + + @Test + void copyLayoutInformationFrom_WhenTheContainerViewKeysMatch() { + Workspace workspace1 = createWorkspace(); + SoftwareSystem softwareSystem1 = workspace1.getModel().getSoftwareSystemWithName("Software System"); + ContainerView view1 = workspace1.getViews().createContainerView(softwareSystem1, "containers", "Description"); + view1.addAllElements(); + view1.getElements().iterator().next().setX(100); + view1.setPaperSize(PaperSize.A3_Landscape); + + Workspace workspace2 = createWorkspace(); + SoftwareSystem softwareSystem2 = workspace2.getModel().getSoftwareSystemWithName("Software System"); + ContainerView view2 = workspace2.getViews().createContainerView(softwareSystem2, "containers", "Description"); + view2.addAllElements(); + + workspace2.getViews().copyLayoutInformationFrom(workspace1.getViews()); + assertEquals(100, view2.getElements().iterator().next().getX()); + assertEquals(PaperSize.A3_Landscape, view2.getPaperSize()); + } + + @Test + void copyLayoutInformationFrom_DoesNotDoAnythingIfThereIsNoContainerViewToCopyInformationFrom() { + Workspace workspace1 = createWorkspace(); + + Workspace workspace2 = createWorkspace(); + SoftwareSystem softwareSystem2 = workspace2.getModel().getSoftwareSystemWithName("Software System"); + ContainerView view2 = workspace2.getViews().createContainerView(softwareSystem2, "containers", "Description"); + view2.addAllElements(); + + workspace2.getViews().copyLayoutInformationFrom(workspace1.getViews()); + assertEquals(0, view2.getElements().iterator().next().getX()); // default + assertNull(view2.getPaperSize()); // default + } + + @Test + void copyLayoutInformationFrom_WhenTheComponentViewKeysMatch() { + Workspace workspace1 = createWorkspace(); + Container container1 = workspace1.getModel().getSoftwareSystemWithName("Software System").getContainerWithName("Container"); + ComponentView view1 = workspace1.getViews().createComponentView(container1, "containers", "Description"); + view1.addAllElements(); + view1.getElements().iterator().next().setX(100); + view1.setPaperSize(PaperSize.A3_Landscape); + + Workspace workspace2 = createWorkspace(); + Container container2 = workspace2.getModel().getSoftwareSystemWithName("Software System").getContainerWithName("Container"); + ComponentView view2 = workspace2.getViews().createComponentView(container2, "containers", "Description"); + view2.addAllElements(); + + workspace2.getViews().copyLayoutInformationFrom(workspace1.getViews()); + assertEquals(100, view2.getElements().iterator().next().getX()); + assertEquals(PaperSize.A3_Landscape, view2.getPaperSize()); + } + + @Test + void copyLayoutInformationFrom_DoesNotDoAnythingIfThereIsNoComponentViewToCopyInformationFrom() { + Workspace workspace1 = createWorkspace(); + + Workspace workspace2 = createWorkspace(); + Container container2 = workspace2.getModel().getSoftwareSystemWithName("Software System").getContainerWithName("Container"); + ComponentView view2 = workspace2.getViews().createComponentView(container2, "components", "Description"); + view2.addAllElements(); + + workspace2.getViews().copyLayoutInformationFrom(workspace1.getViews()); + assertEquals(0, view2.getElements().iterator().next().getX()); // default + assertNull(view2.getPaperSize()); // default + } + + @Test + void copyLayoutInformationFrom_WhenTheDynamicViewKeysMatch() { + Workspace workspace1 = createWorkspace(); + Person person1 = workspace1.getModel().getPersonWithName("Person"); + SoftwareSystem softwareSystem1 = workspace1.getModel().getSoftwareSystemWithName("Software System"); + DynamicView view1 = workspace1.getViews().createDynamicView("context", "Description"); + view1.add(person1, softwareSystem1); + view1.getElements().iterator().next().setX(100); + view1.setPaperSize(PaperSize.A3_Landscape); + + Workspace workspace2 = createWorkspace(); + Person person2 = workspace2.getModel().getPersonWithName("Person"); + SoftwareSystem softwareSystem2 = workspace2.getModel().getSoftwareSystemWithName("Software System"); + DynamicView view2 = workspace2.getViews().createDynamicView("context", "Description"); + view2.add(person2, softwareSystem2); + + workspace2.getViews().copyLayoutInformationFrom(workspace1.getViews()); + assertEquals(100, view2.getElements().iterator().next().getX()); + assertEquals(PaperSize.A3_Landscape, view2.getPaperSize()); + } + + @Test + void copyLayoutInformationFrom_DoesNotDoAnythingIfThereIsNoDynamicViewToCopyInformationFrom() { + Workspace workspace1 = createWorkspace(); + + Workspace workspace2 = createWorkspace(); + Person person2 = workspace2.getModel().getPersonWithName("Person"); + SoftwareSystem softwareSystem2 = workspace2.getModel().getSoftwareSystemWithName("Software System"); + DynamicView view2 = workspace2.getViews().createDynamicView("context", "Description"); + view2.add(person2, softwareSystem2); + + workspace2.getViews().copyLayoutInformationFrom(workspace1.getViews()); + assertEquals(0, view2.getElements().iterator().next().getX()); // default + assertNull(view2.getPaperSize()); // default + } + + @Test + void copyLayoutInformationFrom_WhenTheDeploymentViewKeysMatch() { + Workspace workspace1 = createWorkspace(); + DeploymentNode deploymentNode1 = workspace1.getModel().getDeploymentNodeWithName("Deployment Node"); + DeploymentView view1 = workspace1.getViews().createDeploymentView("key", "Description"); + view1.add(deploymentNode1); + view1.getElements().stream().filter(ev -> ev.getElement() instanceof ContainerInstance).findFirst().get().setX(100); + view1.getElements().stream().filter(ev -> ev.getElement() instanceof ContainerInstance).findFirst().get().setY(200); + view1.setPaperSize(PaperSize.A3_Landscape); + + Workspace workspace2 = createWorkspace(); + DeploymentNode deploymentNode2 = workspace2.getModel().getDeploymentNodeWithName("Deployment Node"); + DeploymentView view2 = workspace2.getViews().createDeploymentView("key", "Description"); + view2.add(deploymentNode2); + + workspace2.getViews().copyLayoutInformationFrom(workspace1.getViews()); + assertEquals(100, view2.getElements().stream().filter(ev -> ev.getElement() instanceof ContainerInstance).findFirst().get().getX()); + assertEquals(200, view2.getElements().stream().filter(ev -> ev.getElement() instanceof ContainerInstance).findFirst().get().getY()); + assertEquals(PaperSize.A3_Landscape, view2.getPaperSize()); + } + + @Test + void copyLayoutInformationFrom_DoesNotDoAnythingIfThereIsNoDeploymentViewToCopyInformationFrom() { + Workspace workspace1 = createWorkspace(); + + Workspace workspace2 = createWorkspace(); + DeploymentNode deploymentNode2 = workspace2.getModel().getDeploymentNodeWithName("Deployment Node"); + DeploymentView view2 = workspace2.getViews().createDeploymentView("key", "Description"); + view2.add(deploymentNode2); + + workspace2.getViews().copyLayoutInformationFrom(workspace1.getViews()); + assertEquals(0, view2.getElements().stream().filter(ev -> ev.getElement() instanceof ContainerInstance).findFirst().get().getX()); // default + assertEquals(0, view2.getElements().stream().filter(ev -> ev.getElement() instanceof ContainerInstance).findFirst().get().getY()); // default + assertNull(view2.getPaperSize()); // default + } + + private HashSet elementViewsFor(Element... elements) { + HashSet set = new HashSet<>(); + + for (Element element : elements) { + ElementView elementView = new ElementView(); + elementView.setId(element.getId()); + set.add(elementView); + } + + return set; + } + + private HashSet relationshipViewsFor(Relationship... relationships) { + HashSet set = new HashSet<>(); + + for (Relationship relationship : relationships) { + RelationshipView relationshipView = new RelationshipView(); + relationshipView.setId(relationship.getId()); + set.add(relationshipView); + } + + return set; + } + + @Test + void hydrate() { + Workspace workspace = new Workspace("Name", "Description"); + Model model = workspace.getModel(); + ViewSet views = workspace.getViews(); + Person person = model.addPerson("Person", "Description"); + SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System", "Description"); + Container container = softwareSystem.addContainer("Container", "Description", "Technology"); + Component component = container.addComponent("Component", "Description", "Technology"); + Relationship personUsesSoftwareSystemRelationship = person.uses(softwareSystem, "uses"); + DeploymentNode deploymentNode = model.addDeploymentNode("Deployment Node", "Description", "Technology"); + ContainerInstance containerInstance = deploymentNode.add(container); + + SystemLandscapeView systemLandscapeView = new SystemLandscapeView(); + systemLandscapeView.setKey("systemLandscape"); // this is used for the filtered view below + systemLandscapeView.setElements(elementViewsFor(person, softwareSystem)); + systemLandscapeView.setRelationships(relationshipViewsFor(personUsesSoftwareSystemRelationship)); + views.setSystemLandscapeViews(Collections.singleton(systemLandscapeView)); + + SystemContextView systemContextView = new SystemContextView(); + systemContextView.setKey("systemContext"); + systemContextView.setSoftwareSystemId(softwareSystem.getId()); + systemContextView.setElements(elementViewsFor(softwareSystem)); + views.setSystemContextViews(Collections.singleton(systemContextView)); + + ContainerView containerView = new ContainerView(); + containerView.setKey("containers"); + containerView.setSoftwareSystemId(softwareSystem.getId()); + containerView.setElements(elementViewsFor(container)); + views.setContainerViews(Collections.singleton(containerView)); + + ComponentView componentView = new ComponentView(); + componentView.setKey("components"); + componentView.setSoftwareSystemId(softwareSystem.getId()); + componentView.setContainerId(container.getId()); + componentView.setElements(elementViewsFor(component)); + views.setComponentViews(Collections.singleton(componentView)); + + DynamicView dynamicView = new DynamicView(); + dynamicView.setKey("dynamic"); + dynamicView.setElementId(softwareSystem.getId()); + dynamicView.setElements(elementViewsFor(component)); + views.setDynamicViews(Collections.singleton(dynamicView)); + + DeploymentView deploymentView = new DeploymentView(); + deploymentView.setKey("deployment"); + deploymentView.setSoftwareSystemId(softwareSystem.getId()); + deploymentView.setElements(elementViewsFor(deploymentNode, containerInstance)); + views.setDeploymentViews(Collections.singleton(deploymentView)); + + FilteredView filteredView = new FilteredView(); + filteredView.setKey("filtered"); + filteredView.setBaseViewKey(systemLandscapeView.getKey()); + views.setFilteredViews(Collections.singleton(filteredView)); + + workspace.getViews().hydrate(model); + + assertSame(model, systemLandscapeView.getModel()); + assertSame(views, systemLandscapeView.getViewSet()); + assertSame(person, systemLandscapeView.getElementView(person).getElement()); + assertSame(softwareSystem, systemLandscapeView.getElementView(softwareSystem).getElement()); + assertSame(personUsesSoftwareSystemRelationship, systemLandscapeView.getRelationshipView(personUsesSoftwareSystemRelationship).getRelationship()); + + assertSame(model, systemContextView.getModel()); + assertSame(views, systemContextView.getViewSet()); + assertSame(softwareSystem, systemContextView.getSoftwareSystem()); + assertSame(softwareSystem, systemContextView.getElementView(softwareSystem).getElement()); + + assertSame(model, containerView.getModel()); + assertSame(views, containerView.getViewSet()); + assertSame(softwareSystem, containerView.getSoftwareSystem()); + assertSame(container, containerView.getElementView(container).getElement()); + + assertSame(model, componentView.getModel()); + assertSame(views, componentView.getViewSet()); + assertSame(softwareSystem, componentView.getSoftwareSystem()); + assertSame(container, componentView.getContainer()); + assertSame(component, componentView.getElementView(component).getElement()); + + assertSame(model, dynamicView.getModel()); + assertSame(views, dynamicView.getViewSet()); + assertSame(softwareSystem, dynamicView.getSoftwareSystem()); + assertSame(softwareSystem, dynamicView.getElement()); + assertSame(component, dynamicView.getElementView(component).getElement()); + + assertSame(model, deploymentView.getModel()); + assertSame(views, deploymentView.getViewSet()); + assertSame(softwareSystem, deploymentView.getSoftwareSystem()); + assertSame(deploymentNode, deploymentView.getElementView(deploymentNode).getElement()); + assertSame(containerInstance, deploymentView.getElementView(containerInstance).getElement()); + + assertSame(systemLandscapeView, filteredView.getView()); + } + + @Test + void setEnterpriseContextViews_IsSupportedForOlderWorkspaces() { + ViewSet views = new Workspace("", "").getViews(); + SystemLandscapeView systemLandscapeView = views.createSystemLandscapeView("key", "Description"); + views.setEnterpriseContextViews(Collections.singleton(systemLandscapeView)); + assertEquals(1, views.getSystemLandscapeViews().size()); + assertSame(systemLandscapeView, views.getSystemLandscapeViews().iterator().next()); + } + + @Test + void createDefaultViews() { + Workspace workspace = new Workspace("Name", "Description"); + Model model = workspace.getModel(); + ViewSet views = workspace.getViews(); + + 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", "", ""); + + DeploymentNode dev = model.addDeploymentNode("Development", "Developer Laptop", "", ""); + DeploymentNode live = model.addDeploymentNode("Live", "Amazon Web Services", "", ""); + DeploymentNode liveEc2 = live.addDeploymentNode("EC2"); + + views.createDefaultViews(); + + assertEquals(1, views.getSystemLandscapeViews().size()); + assertEquals("SystemLandscape-001", views.getSystemLandscapeViews().iterator().next().getKey()); + + assertEquals(2, views.getSystemContextViews().size()); + 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("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("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()); + + live.addInfrastructureNode("Route 53"); + + views.clear(); + views.createDefaultViews(); + + assertEquals(1, views.getDeploymentViews().size()); + assertSame("Live", views.getDeploymentViews().stream().filter(v -> v.getKey().equals("Deployment-001")).findFirst().get().getEnvironment()); + + dev.add(ss1); + liveEc2.add(c1); + liveEc2.add(c2); + + views.clear(); + views.createDefaultViews(); + + assertEquals(3, views.getDeploymentViews().size()); + 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 + void copyLayoutInformationFrom_DoesNothing_WhenMergeFromRemoteIsSetToFalse() { + Workspace workspace1 = createWorkspace(); + SoftwareSystem softwareSystem1 = workspace1.getModel().getSoftwareSystemWithName("Software System"); + SystemLandscapeView view1 = workspace1.getViews().createSystemLandscapeView("landscape", "Description"); + view1.addAllElements(); + view1.getElements().iterator().next().setX(100); + view1.setPaperSize(PaperSize.A3_Landscape); + + Workspace workspace2 = createWorkspace(); + SoftwareSystem softwareSystem2 = workspace2.getModel().getSoftwareSystemWithName("Software System"); + SystemLandscapeView view2 = workspace2.getViews().createSystemLandscapeView("context", "Description"); + view2.addAllElements(); + view2.setMergeFromRemote(false); + + workspace2.getViews().copyLayoutInformationFrom(workspace1.getViews()); + assertEquals(0, view2.getElements().iterator().next().getX()); + assertNull(view2.getPaperSize()); + } + + @Test + void view_ordering() { + Workspace workspace = new Workspace("Name", "Description"); + SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Software System"); + Container container = softwareSystem.addContainer("Container"); + ViewSet views = workspace.getViews(); + + CustomView customView = views.createCustomView("custom1", "Title", "Description"); + SystemLandscapeView systemLandscapeView1 = views.createSystemLandscapeView("landscape1", "Description"); + FilteredView filteredView = views.createFilteredView(systemLandscapeView1, "filtered1", "Description", FilterMode.Include, "Tag 1"); + SystemContextView systemContextView = views.createSystemContextView(softwareSystem, "context1", "Description"); + ContainerView containerView = views.createContainerView(softwareSystem, "container1", "Description"); + ComponentView componentView = views.createComponentView(container, "component1", "Description"); + DynamicView dynamicView = views.createDynamicView("dynamic1", "Description"); + DeploymentView deploymentView = views.createDeploymentView("deployment1", "Description"); + SystemLandscapeView systemLandscapeView2 = views.createSystemLandscapeView("landscape2", "Description"); + + assertEquals(1, customView.getOrder()); + assertEquals(2, systemLandscapeView1.getOrder()); + assertEquals(3, filteredView.getOrder()); + assertEquals(4, systemContextView.getOrder()); + assertEquals(5, containerView.getOrder()); + assertEquals(6, componentView.getOrder()); + assertEquals(7, dynamicView.getOrder()); + assertEquals(8, deploymentView.getOrder()); + 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()); + } + + @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 diff --git a/structurizr-core/src/test/java/com/structurizr/view/ViewTests.java b/structurizr-core/src/test/java/com/structurizr/view/ViewTests.java new file mode 100644 index 000000000..068c17991 --- /dev/null +++ b/structurizr-core/src/test/java/com/structurizr/view/ViewTests.java @@ -0,0 +1,494 @@ +package com.structurizr.view; + +import com.structurizr.AbstractWorkspaceTestBase; +import com.structurizr.Workspace; +import com.structurizr.model.*; +import org.junit.jupiter.api.Test; + +import java.util.Arrays; +import java.util.Iterator; + +import static org.junit.jupiter.api.Assertions.*; + +public class ViewTests extends AbstractWorkspaceTestBase { + + @Test + void construction() { + SoftwareSystem softwareSystem = model.addSoftwareSystem("The System", "Description"); + StaticView view = new SystemContextView(softwareSystem, "key", "Description"); + assertEquals("key", view.getKey()); + assertEquals("Description", view.getDescription()); + assertNull(view.getAutomaticLayout()); + } + + @Test + void construction_WhenTheViewKeyContainsAForwardSlashCharacter() { + SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System"); + StaticView view = new SystemContextView(softwareSystem, "key/1", "Description"); + assertEquals("key_1", view.getKey()); + } + + @Test + void addAllSoftwareSystems_DoesNothing_WhenThereAreNoOtherSoftwareSystemsInTheModel() { + SoftwareSystem softwareSystem = model.addSoftwareSystem("The System", "Description"); + StaticView view = new SystemContextView(softwareSystem, "context", "Description"); + assertEquals(1, view.getElements().size()); + view.addAllSoftwareSystems(); + assertEquals(1, view.getElements().size()); + } + + @Test + void addAllSoftwareSystems_DoesAddAllSoftwareSystems_WhenThereAreSoftwareSystemsInTheModel() { + 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(); + + assertEquals(4, view.getElements().size()); + Iterator it = view.getElements().iterator(); + assertSame(softwareSystem, it.next().getElement()); + assertSame(softwareSystemA, it.next().getElement()); + assertSame(softwareSystemB, it.next().getElement()); + assertSame(softwareSystemC, it.next().getElement()); + } + + @Test + void addSoftwareSystem_ThrowsAnException_WhenGivenNull() { + SoftwareSystem softwareSystem = model.addSoftwareSystem("The System", "Description"); + StaticView view = new SystemContextView(softwareSystem, "context", "Description"); + + try { + view.add((SoftwareSystem) null); + fail(); + } catch (IllegalArgumentException iae) { + assertEquals("An element must be specified.", iae.getMessage()); + } + } + + @Test + void addSoftwareSystem_AddsTheSoftwareSystem_WhenTheSoftwareSystemIsInTheModel() { + SoftwareSystem softwareSystem = model.addSoftwareSystem("The System", "Description"); + SoftwareSystem softwareSystemA = model.addSoftwareSystem("System A", "Description"); + + StaticView view = new SystemContextView(softwareSystem, "context", "Description"); + view.add(softwareSystemA); + assertEquals(2, view.getElements().size()); + Iterator it = view.getElements().iterator(); + assertSame(softwareSystem, it.next().getElement()); + assertSame(softwareSystemA, it.next().getElement()); + } + + @Test + void addAllPeople_DoesNothing_WhenThereAreNoPeopleInTheModel() { + SoftwareSystem softwareSystem = model.addSoftwareSystem("The System", "Description"); + + StaticView view = new SystemContextView(softwareSystem, "context", "Description"); + assertEquals(1, view.getElements().size()); + + view.addAllPeople(); + assertEquals(1, view.getElements().size()); + } + + @Test + void addAllPeople_DoesAddAllPeople_WhenThereArePeopleInTheModel() { + 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(); + + assertEquals(4, view.getElements().size()); + Iterator it = view.getElements().iterator(); + assertSame(softwareSystem, it.next().getElement()); + assertSame(person1, it.next().getElement()); + assertSame(person2, it.next().getElement()); + assertSame(person3, it.next().getElement()); + } + + @Test + void addPerson_ThrowsAnException_WhenGivenNull() { + SoftwareSystem softwareSystem = model.addSoftwareSystem("The System", "Description"); + StaticView view = new SystemContextView(softwareSystem, "context", "Description"); + try { + view.add((Person) null); + fail(); + } catch (IllegalArgumentException iae) { + assertEquals("An element must be specified.", iae.getMessage()); + } + } + + @Test + void addPerson_AddsThePerson_WhenThPersonIsInTheModel() { + SoftwareSystem softwareSystem = model.addSoftwareSystem("The System", "Description"); + StaticView view = new SystemContextView(softwareSystem, "context", "Description"); + + Person person1 = model.addPerson("Person 1", "Description"); + view.add(person1); + + assertEquals(2, view.getElements().size()); + Iterator it = view.getElements().iterator(); + assertSame(softwareSystem, it.next().getElement()); + assertSame(person1, it.next().getElement()); + } + + @Test + void removeElementsWithNoRelationships_RemovesAllElements_WhenTheViewHasNoRelationshipsBetweenElements() { + SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System", "Description"); + Person person = model.addPerson("Person", "Description"); + + StaticView view = views.createSystemLandscapeView("context", "Description"); + view.addAllSoftwareSystems(); + view.addAllPeople(); + view.removeElementsWithNoRelationships(); + + assertEquals(0, view.getElements().size()); + } + + @Test + void removeElementsWithNoRelationships_RemovesOnlyThoseElementsWithoutRelationships_WhenTheViewContainsSomeUnlinkedElements() { + 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"); + person1.uses(softwareSystem, "uses"); + + view.addAllSoftwareSystems(); + view.addAllPeople(); + assertEquals(5, view.getElements().size()); + + view.removeElementsWithNoRelationships(); + assertEquals(3, view.getElements().size()); + } + + @Test + void copyLayoutInformationFrom() { + Workspace workspace1 = new Workspace("", ""); + Model model1 = workspace1.getModel(); + SoftwareSystem softwareSystem1A = model1.addSoftwareSystem("System A", "Description"); + SoftwareSystem softwareSystem1B = model1.addSoftwareSystem("System B", "Description"); + Person person1 = model1.addPerson("Person", "Description"); + Relationship personUsesSoftwareSystem1 = person1.uses(softwareSystem1A, "Uses"); + + // create a view with SystemA and Person (locations are set for both, relationship has vertices) + StaticView staticView1 = new SystemContextView(softwareSystem1A, "context", "Description"); + staticView1.setPaperSize(PaperSize.A3_Landscape); + staticView1.setDimensions(new Dimensions(123, 456)); + staticView1.add(softwareSystem1B); + staticView1.getElementView(softwareSystem1B).setX(123); + staticView1.getElementView(softwareSystem1B).setY(321); + staticView1.add(person1); + staticView1.getElementView(person1).setX(456); + staticView1.getElementView(person1).setY(654); + staticView1.getRelationshipView(personUsesSoftwareSystem1).setVertices(Arrays.asList(new Vertex(123, 456))); + staticView1.getRelationshipView(personUsesSoftwareSystem1).setPosition(70); + staticView1.getRelationshipView(personUsesSoftwareSystem1).setRouting(Routing.Orthogonal); + + // and create a dynamic view, as they are treated slightly differently + DynamicView dynamicView1 = new DynamicView(model1, "dynamic", "Description"); + dynamicView1.add(person1, "Overridden description", softwareSystem1A); + dynamicView1.getElementView(person1).setX(111); + dynamicView1.getElementView(person1).setY(222); + dynamicView1.getElementView(softwareSystem1A).setX(333); + dynamicView1.getElementView(softwareSystem1A).setY(444); + dynamicView1.getRelationshipView(personUsesSoftwareSystem1).setVertices(Arrays.asList(new Vertex(555, 666))); + dynamicView1.getRelationshipView(personUsesSoftwareSystem1).setPosition(30); + dynamicView1.getRelationshipView(personUsesSoftwareSystem1).setRouting(Routing.Direct); + + Workspace workspace2 = new Workspace("", ""); + Model model2 = workspace2.getModel(); + // creating these in the opposite order will cause them to get different internal IDs + SoftwareSystem softwareSystem2B = model2.addSoftwareSystem("System B", "Description"); + SoftwareSystem softwareSystem2A = model2.addSoftwareSystem("System A", "Description"); + Person person2 = model2.addPerson("Person", "Description"); + Relationship personUsesSoftwareSystem2 = person2.uses(softwareSystem2A, "Uses"); + + // create a view with SystemB and Person (locations are 0,0 for both) + StaticView staticView2 = new SystemContextView(softwareSystem2A, "context", "Description"); + staticView2.add(softwareSystem2B); + staticView2.add(person2); + assertEquals(0, staticView2.getElementView(softwareSystem2B).getX()); + assertEquals(0, staticView2.getElementView(softwareSystem2B).getY()); + assertEquals(0, staticView2.getElementView(softwareSystem2B).getX()); + assertEquals(0, staticView2.getElementView(softwareSystem2B).getY()); + assertEquals(0, staticView2.getElementView(person2).getX()); + assertEquals(0, staticView2.getElementView(person2).getY()); + assertTrue(staticView2.getRelationshipView(personUsesSoftwareSystem2).getVertices().isEmpty()); + + // and create a dynamic view (locations are 0,0) + DynamicView dynamicView2 = new DynamicView(model2, "dynamic", "Description"); + dynamicView2.add(person2, "Overridden description", softwareSystem2A); + + staticView2.copyLayoutInformationFrom(staticView1); + assertEquals(PaperSize.A3_Landscape, staticView2.getPaperSize()); + assertEquals(123, staticView2.getDimensions().getWidth()); + assertEquals(456, staticView2.getDimensions().getHeight()); + assertEquals(0, staticView2.getElementView(softwareSystem2A).getX()); + assertEquals(0, staticView2.getElementView(softwareSystem2A).getY()); + assertEquals(123, staticView2.getElementView(softwareSystem2B).getX()); + assertEquals(321, staticView2.getElementView(softwareSystem2B).getY()); + assertEquals(456, staticView2.getElementView(person2).getX()); + assertEquals(654, staticView2.getElementView(person2).getY()); + Vertex vertex1 = staticView2.getRelationshipView(personUsesSoftwareSystem2).getVertices().iterator().next(); + assertEquals(123, vertex1.getX()); + assertEquals(456, vertex1.getY()); + assertEquals(70, staticView2.getRelationshipView(personUsesSoftwareSystem2).getPosition().intValue()); + assertEquals(Routing.Orthogonal, staticView2.getRelationshipView(personUsesSoftwareSystem2).getRouting()); + + dynamicView2.copyLayoutInformationFrom(dynamicView1); + assertEquals(111, dynamicView2.getElementView(person2).getX()); + assertEquals(222, dynamicView2.getElementView(person2).getY()); + assertEquals(333, dynamicView2.getElementView(softwareSystem2A).getX()); + assertEquals(444, dynamicView2.getElementView(softwareSystem2A).getY()); + Vertex vertex2 = dynamicView2.getRelationshipView(personUsesSoftwareSystem2).getVertices().iterator().next(); + assertEquals(555, vertex2.getX()); + assertEquals(666, vertex2.getY()); + assertEquals(30, dynamicView2.getRelationshipView(personUsesSoftwareSystem2).getPosition().intValue()); + assertEquals(Routing.Direct, dynamicView2.getRelationshipView(personUsesSoftwareSystem2).getRouting()); + } + + @Test + void removeElementsThatAreUnreachableFrom_DoesNothing_WhenANullElementIsSpecified() { + SoftwareSystem softwareSystem = model.addSoftwareSystem("The System", "Description"); + StaticView view = new SystemContextView(softwareSystem, "context", "Description"); + view.removeElementsThatAreUnreachableFrom(null); + } + + @Test + void removeElementsThatAreUnreachableFrom_DoesNothing_WhenAllElementsCanBeReached() { + SoftwareSystem softwareSystem = model.addSoftwareSystem("The System", "Description"); + SoftwareSystem softwareSystemA = model.addSoftwareSystem("System A", ""); + SoftwareSystem softwareSystemB = model.addSoftwareSystem("System B", ""); + + softwareSystem.uses(softwareSystemA, "uses"); + softwareSystemA.uses(softwareSystemB, "uses"); + + StaticView view = new SystemContextView(softwareSystem, "context", "Description"); + view.addAllElements(); + assertEquals(3, view.getElements().size()); + + view.removeElementsThatAreUnreachableFrom(softwareSystem); + assertEquals(3, view.getElements().size()); + } + + @Test + void removeElementsThatAreUnreachableFrom_RemovesOrphanedElements_WhenThereAreSomeOrphanedElements() { + SoftwareSystem softwareSystem = model.addSoftwareSystem("The System", "Description"); + SoftwareSystem softwareSystemA = model.addSoftwareSystem("System A", ""); + SoftwareSystem softwareSystemB = model.addSoftwareSystem("System B", ""); + SoftwareSystem softwareSystemC = model.addSoftwareSystem("System C", ""); + + softwareSystem.uses(softwareSystemA, "uses"); + softwareSystemA.uses(softwareSystemB, "uses"); + + StaticView view = new SystemContextView(softwareSystem, "context", "Description"); + view.addAllElements(); + assertEquals(4, view.getElements().size()); + + view.removeElementsThatAreUnreachableFrom(softwareSystem); + assertEquals(3, view.getElements().size()); + assertFalse(view.getElements().contains(new ElementView(softwareSystemC))); + } + + @Test + void removeElementsThatAreUnreachableFrom_RemovesUnreachableElements_WhenThereAreSomeUnreachableElements() { + SoftwareSystem softwareSystem = model.addSoftwareSystem("The System", "Description"); + SoftwareSystem softwareSystemA = model.addSoftwareSystem("System A", ""); + SoftwareSystem softwareSystemB = model.addSoftwareSystem("System B", ""); + + softwareSystem.uses(softwareSystemA, "uses"); + softwareSystemA.uses(softwareSystemB, "uses"); + + StaticView view = new SystemContextView(softwareSystem, "context", "Description"); + view.addAllElements(); + assertEquals(3, view.getElements().size()); + + view.removeElementsThatAreUnreachableFrom(softwareSystemB); + assertEquals(2, view.getElements().size()); + assertFalse(view.getElements().contains(new ElementView(softwareSystemA))); + } + + @Test + 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", ""); + + user.uses(softwareSystem1, ""); + user.uses(softwareSystem2, ""); + softwareSystem1.delivers(user, ""); + + StaticView view = new SystemContextView(softwareSystem1, "context", "Description"); + view.addAllElements(); + assertEquals(3, view.getElements().size()); + + // this should remove software system 2 + view.removeElementsThatAreUnreachableFrom(softwareSystem1); + assertEquals(2, view.getElements().size()); + assertTrue(view.getElements().contains(new ElementView(softwareSystem1))); + assertTrue(view.getElements().contains(new ElementView(user))); + } + + @Test + 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"); + + softwareSystem1.uses(softwareSystem2, "Uses"); + softwareSystem2.uses(softwareSystem3, "Uses"); + softwareSystem3.uses(softwareSystem1, "Uses"); + + StaticView view = new SystemContextView(softwareSystem1, "context", "Description"); + view.addAllElements(); + + assertEquals(3, view.getRelationships().size()); + view.remove((Relationship) null); + } + + @Test + 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"); + + Relationship relationship12 = softwareSystem1.uses(softwareSystem2, "Uses"); + Relationship relationship23 = softwareSystem2.uses(softwareSystem3, "Uses"); + Relationship relationship31 = softwareSystem3.uses(softwareSystem1, "Uses"); + + StaticView view = new SystemContextView(softwareSystem1, "context", "Description"); + view.addAllElements(); + + assertEquals(3, view.getRelationships().size()); + view.remove(relationship31); + + assertEquals(2, view.getRelationships().size()); + assertTrue(view.getRelationships().contains(new RelationshipView(relationship12))); + assertTrue(view.getRelationships().contains(new RelationshipView(relationship23))); + } + + @Test + void setKey_ThrowsAnException_WhenANullKeyIsSpecified() { + assertThrows(IllegalArgumentException.class, () -> { + SoftwareSystem softwareSystem = model.addSoftwareSystem("Name", "Description"); + new SystemContextView(softwareSystem, null, "Description"); + }); + } + + @Test + void setKey_ThrowsAnException_WhenAnEmptyKeyIsSpecified() { + assertThrows(IllegalArgumentException.class, () -> { + SoftwareSystem softwareSystem = model.addSoftwareSystem("Name", "Description"); + new SystemContextView(softwareSystem, " ", "Description"); + }); + } + + @Test + void addElement_ThrowsAnException_WhenTheSpecifiedElementDoesNotExistInTheModel() { + try { + Workspace workspace = new Workspace("1", ""); + SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Software System"); + + SystemLandscapeView view = new Workspace("", "").getViews().createSystemLandscapeView("key", "Description"); + view.add(softwareSystem); + fail(); + } catch (IllegalArgumentException iae) { + assertEquals("The element named Software System does not exist in the model associated with this view.", iae.getMessage()); + } + } + + @Test + void enableAutomaticLayout_EnablesAutoLayoutWithSomeDefaultValues_WhenTrueIsSpecified() { + SystemLandscapeView view = new Workspace("", "").getViews().createSystemLandscapeView("key", "Description"); + view.enableAutomaticLayout(); + + assertNotNull(view.getAutomaticLayout()); + assertEquals(AutomaticLayout.RankDirection.TopBottom, view.getAutomaticLayout().getRankDirection()); + assertEquals(300, view.getAutomaticLayout().getRankSeparation()); + assertEquals(600, view.getAutomaticLayout().getNodeSeparation()); + assertEquals(200, view.getAutomaticLayout().getEdgeSeparation()); + assertFalse(view.getAutomaticLayout().isVertices()); + } + + @Test + void enableAutomaticLayout_DisablesAutoLayout_WhenFalseIsSpecified() { + SystemLandscapeView view = new Workspace("", "").getViews().createSystemLandscapeView("key", "Description"); + view.enableAutomaticLayout(); + assertNotNull(view.getAutomaticLayout()); + + view.disableAutomaticLayout(); + assertNull(view.getAutomaticLayout()); + } + + @Test + void enableAutomaticLayout() { + SystemLandscapeView view = new Workspace("", "").getViews().createSystemLandscapeView("key", "Description"); + view.enableAutomaticLayout(AutomaticLayout.RankDirection.LeftRight, 100, 200, 300, true); + + assertNotNull(view.getAutomaticLayout()); + assertEquals(AutomaticLayout.RankDirection.LeftRight, view.getAutomaticLayout().getRankDirection()); + assertEquals(100, view.getAutomaticLayout().getRankSeparation()); + assertEquals(200, view.getAutomaticLayout().getNodeSeparation()); + assertEquals(300, view.getAutomaticLayout().getEdgeSeparation()); + assertTrue(view.getAutomaticLayout().isVertices()); + } + + @Test + void addCustomElement_AddsTheCustomElementToTheView() { + Workspace workspace = new Workspace("", ""); + SystemLandscapeView view = workspace.getViews().createSystemLandscapeView("key", "Description"); + + CustomElement box1 = workspace.getModel().addCustomElement("Box 1"); + CustomElement box2 = workspace.getModel().addCustomElement("Box 2"); + box1.uses(box2, "Uses"); + + view.add(box1); + assertEquals(1, view.getElements().size()); + assertEquals(0, view.getRelationships().size()); + + view.add(box2); + assertEquals(2, view.getElements().size()); + assertEquals(1, view.getRelationships().size()); + } + + @Test + void addCustomElementWithoutRelationships_AddsTheCustomElementToTheView() { + Workspace workspace = new Workspace("", ""); + SystemLandscapeView view = workspace.getViews().createSystemLandscapeView("key", "Description"); + + CustomElement box1 = workspace.getModel().addCustomElement("Box 1"); + CustomElement box2 = workspace.getModel().addCustomElement("Box 2"); + box1.uses(box2, "Uses"); + + view.add(box1); + assertEquals(1, view.getElements().size()); + assertEquals(0, view.getRelationships().size()); + + view.add(box2, false); + assertEquals(2, view.getElements().size()); + assertEquals(0, view.getRelationships().size()); + } + + @Test + void removeCustomElement_RemovesTheCustomElementFromTheView() { + Workspace workspace = new Workspace("", ""); + SystemLandscapeView view = workspace.getViews().createSystemLandscapeView("key", "Description"); + + CustomElement box1 = workspace.getModel().addCustomElement("Box 1"); + + view.add(box1); + assertEquals(1, view.getElements().size()); + + view.remove(box1); + assertEquals(0, view.getElements().size()); + } + +} \ No newline at end of file diff --git a/structurizr-core/src/test/resources/image.png b/structurizr-core/src/test/resources/image.png new file mode 100644 index 000000000..9ecfbfd32 Binary files /dev/null and b/structurizr-core/src/test/resources/image.png differ diff --git a/structurizr-core/src/test/resources/image.svg b/structurizr-core/src/test/resources/image.svg new file mode 100644 index 000000000..fe686329a --- /dev/null +++ b/structurizr-core/src/test/resources/image.svg @@ -0,0 +1 @@ +BobBobAliceAlicehello \ No newline at end of file diff --git a/structurizr-core/test/integration/com/structurizr/api/StructurizrClientIntegrationTests.java b/structurizr-core/test/integration/com/structurizr/api/StructurizrClientIntegrationTests.java deleted file mode 100644 index 28e34a9a3..000000000 --- a/structurizr-core/test/integration/com/structurizr/api/StructurizrClientIntegrationTests.java +++ /dev/null @@ -1,62 +0,0 @@ -package com.structurizr.api; - -import com.structurizr.Workspace; -import com.structurizr.encryption.AesEncryptionStrategy; -import com.structurizr.model.Person; -import com.structurizr.model.SoftwareSystem; -import com.structurizr.view.SystemContextView; -import org.junit.Before; -import org.junit.Test; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; - -public class StructurizrClientIntegrationTests { - - private StructurizrClient structurizrClient; - - @Before - public void setUp() { - structurizrClient = new StructurizrClient("81ace434-94a1-486f-a786-37bbeaa44e08", "a8673e21-7b6f-4f52-be65-adb7248be86b"); - structurizrClient.setWorkspaceArchiveLocation(null); - structurizrClient.setMergeFromRemote(false); - } - - @Test - public void test_putAndGetWorkspace_WithoutEncryption() throws Exception { - Workspace workspace = new Workspace("Structurizr client library tests", "A test workspace for the Structurizr client library"); - SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Software System", "Description"); - Person person = workspace.getModel().addPerson("Person", "Description"); - person.uses(softwareSystem, "Uses"); - SystemContextView systemContextView = workspace.getViews().createSystemContextView(softwareSystem, "SystemContext", "Description"); - systemContextView.addAllElements(); - - structurizrClient.putWorkspace(20081, workspace); - - workspace = structurizrClient.getWorkspace(20081); - assertTrue(workspace.getModel().contains(softwareSystem)); - assertTrue(workspace.getModel().contains(person)); - assertEquals(1, workspace.getModel().getRelationships().size()); - assertEquals(1, workspace.getViews().getSystemContextViews().size()); - } - - @Test - public void test_putAndGetWorkspace_WithEncryption() throws Exception { - structurizrClient.setEncryptionStrategy(new AesEncryptionStrategy("password")); - Workspace workspace = new Workspace("Structurizr client library tests", "A test workspace for the Structurizr client library"); - SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Software System", "Description"); - Person person = workspace.getModel().addPerson("Person", "Description"); - person.uses(softwareSystem, "Uses"); - SystemContextView systemContextView = workspace.getViews().createSystemContextView(softwareSystem, "SystemContext", "Description"); - systemContextView.addAllElements(); - - structurizrClient.putWorkspace(20081, workspace); - - workspace = structurizrClient.getWorkspace(20081); - assertTrue(workspace.getModel().contains(softwareSystem)); - assertTrue(workspace.getModel().contains(person)); - assertEquals(1, workspace.getModel().getRelationships().size()); - assertEquals(1, workspace.getViews().getSystemContextViews().size()); - } - -} \ 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 deleted file mode 100644 index 69a5318e7..000000000 --- a/structurizr-core/test/unit/com/structurizr/WorkspaceTests.java +++ /dev/null @@ -1,107 +0,0 @@ -package com.structurizr; - -import com.structurizr.documentation.StructurizrDocumentationTemplate; -import com.structurizr.model.Component; -import com.structurizr.model.Container; -import com.structurizr.model.SoftwareSystem; -import org.junit.Test; - -import java.io.File; - -import static org.junit.Assert.*; - -public class WorkspaceTests { - - private Workspace workspace = new Workspace("Name", "Description"); - - @Test - public void test_setApi_DoesNotThrowAnException_WhenANullUrlIsSpecified() { - workspace.setApi(null); - } - - @Test - public void test_setApi_DoesNotThrowAnException_WhenAnEmptyUrlIsSpecified() { - workspace.setApi(""); - } - - @Test - public void test_setApi_ThrowsAnException_WhenAnInvalidUrlIsSpecified() { - try { - workspace.setApi("www.somedomain.com"); - fail(); - } catch (Exception e) { - assertEquals("www.somedomain.com is not a valid URL.", e.getMessage()); - } - } - - @Test - public void test_setApi_DoesNotThrowAnException_WhenAnValidUrlIsSpecified() { - workspace.setApi("https://www.somedomain.com"); - assertEquals("https://www.somedomain.com", workspace.getApi()); - } - - @Test - public void test_hasApi_ReturnsFalse_WhenANullApiIsSpecified() { - workspace.setApi(null); - assertFalse(workspace.hasApi()); - } - - @Test - public void test_hasApi_ReturnsFalse_WhenAnEmptyApiIsSpecified() { - workspace.setApi(" "); - assertFalse(workspace.hasApi()); - } - - @Test - public void test_hasApi_ReturnsTrue_WhenAUrlIsSpecified() { - workspace.setApi("https://www.somedomain.com"); - assertTrue(workspace.hasApi()); - } - - @Test - public void test_isEmpty_ReturnsTrue_WhenThereAreNoElementsViewsOrDocumentation() { - workspace = new Workspace("Name", "Description"); - assertTrue(workspace.isEmpty()); - } - - @Test - public 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() { - workspace = new Workspace("Name", "Description"); - workspace.getViews().createEnterpriseContextView("key", "Description"); - assertFalse(workspace.isEmpty()); - } - - @Test - public void test_isEmpty_ReturnsFalse_WhenThereIsDocumentation() throws Exception { - workspace = new Workspace("Name", "Description"); - StructurizrDocumentationTemplate template = new StructurizrDocumentationTemplate(workspace); - template.addImage(new File("../docs/images/structurizr-logo.png")); - assertFalse(workspace.isEmpty()); - } - - @Test - public 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", " "); - 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()); - } - -} diff --git a/structurizr-core/test/unit/com/structurizr/analysis/AbstractComponentFinderStrategyTests.java b/structurizr-core/test/unit/com/structurizr/analysis/AbstractComponentFinderStrategyTests.java deleted file mode 100644 index 3c466760e..000000000 --- a/structurizr-core/test/unit/com/structurizr/analysis/AbstractComponentFinderStrategyTests.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.structurizr.analysis; - -import org.junit.Test; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.fail; - -public class AbstractComponentFinderStrategyTests { - - private AbstractComponentFinderStrategy strategy = new TypeMatcherComponentFinderStrategy(); - - @Test - public void test_addSupportingTypesStrategy_ThrowsAnException_WhenANullSupportingTypesStrategyIsSpecified() { - try { - strategy.addSupportingTypesStrategy(null); - fail(); - } catch (IllegalArgumentException iae) { - assertEquals("A supporting types strategy must be provided.", iae.getMessage()); - } - } - -} diff --git a/structurizr-core/test/unit/com/structurizr/analysis/AnnotationTypeMatcherTests.java b/structurizr-core/test/unit/com/structurizr/analysis/AnnotationTypeMatcherTests.java deleted file mode 100644 index 6e2ed87bc..000000000 --- a/structurizr-core/test/unit/com/structurizr/analysis/AnnotationTypeMatcherTests.java +++ /dev/null @@ -1,38 +0,0 @@ -package com.structurizr.analysis; - -import com.structurizr.annotation.Component; -import org.junit.Test; - -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; - -public class AnnotationTypeMatcherTests { - - @Test(expected = IllegalArgumentException.class) - public void test_construction_ThrowsAnExceptionWhenANullAnnotationClassIsSupplied() - { - new AnnotationTypeMatcher(null, "", ""); - } - - @Test - public void test_matches_ReturnsFalse_WhenTheGivenTypeDoesNotHaveTheAnnotation() - { - AnnotationTypeMatcher matcher = new AnnotationTypeMatcher(Component.class, "", ""); - assertFalse(matcher.matches(MyService.class)); - } - - @Test - public void test_matches_ReturnsTrue_WhenTheGivenTypeDoesHaveTheAnnotation() - { - AnnotationTypeMatcher matcher = new AnnotationTypeMatcher(Component.class, "", ""); - assertTrue(matcher.matches(MyController.class)); - } - - @Component - private class MyController { - } - - private class MyService { - } - -} diff --git a/structurizr-core/test/unit/com/structurizr/analysis/ComponentFinderTests.java b/structurizr-core/test/unit/com/structurizr/analysis/ComponentFinderTests.java deleted file mode 100644 index 894c27717..000000000 --- a/structurizr-core/test/unit/com/structurizr/analysis/ComponentFinderTests.java +++ /dev/null @@ -1,44 +0,0 @@ -package com.structurizr.analysis; - -import com.structurizr.AbstractWorkspaceTestBase; -import com.structurizr.model.Container; -import org.junit.Test; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.fail; - -public class ComponentFinderTests extends AbstractWorkspaceTestBase { - - @Test - public void test_construction_ThrowsAnException_WhenANullContainerIsSpecified() { - try { - new ComponentFinder(null, null); - fail(); - } catch (IllegalArgumentException iae) { - assertEquals("A container must be specified.", iae.getMessage()); - } - } - - @Test - public void test_construction_ThrowsAnException_WhenANullPackageNameIsSpecified() { - try { - Container container = model.addSoftwareSystem("Software System", "").addContainer("Container", "", ""); - new ComponentFinder(container, null); - fail(); - } catch (IllegalArgumentException iae) { - assertEquals("A package name must be specified.", iae.getMessage()); - } - } - - @Test - public void test_construction_ThrowsAnException_WhenNoComponentFinderStrategiesAreSpecified() { - try { - Container container = model.addSoftwareSystem("Software System", "").addContainer("Container", "", ""); - new ComponentFinder(container, "com.mycompany.myapp"); - fail(); - } catch (IllegalArgumentException iae) { - assertEquals("One or more ComponentFinderStrategy objects must be specified.", iae.getMessage()); - } - } - -} \ No newline at end of file diff --git a/structurizr-core/test/unit/com/structurizr/analysis/DefaultTypeRepositoryTests.java b/structurizr-core/test/unit/com/structurizr/analysis/DefaultTypeRepositoryTests.java deleted file mode 100644 index 6ad72e9b2..000000000 --- a/structurizr-core/test/unit/com/structurizr/analysis/DefaultTypeRepositoryTests.java +++ /dev/null @@ -1,69 +0,0 @@ -package com.structurizr.analysis; - -import org.junit.Test; - -import java.util.HashSet; -import java.util.Set; -import java.util.regex.Pattern; -import java.util.stream.Collectors; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; - -public class DefaultTypeRepositoryTests { - - private DefaultTypeRepository typeRepository; - - @Test - public void test_getAllTypes_ReturnsAnEmptySet_WhenNoTypesWereFound() { - typeRepository = new DefaultTypeRepository("com.structurizr.analysis.foo", new HashSet<>()); - Set> types = typeRepository.getAllTypes(); - assertTrue(types.isEmpty()); - } - - @Test - public void test_getAllTypes_ReturnsANonEmptySet_WhenTypesWereFound() { - typeRepository = new DefaultTypeRepository("test.DefaultTypeRepository", new HashSet<>()); - Set types = typeRepository.getAllTypes().stream().map(Class::getCanonicalName).collect(Collectors.toSet()); - assertEquals(4, types.size()); - - assertTrue(types.contains("test.DefaultTypeRepository.SomeAbstractClass")); - assertTrue(types.contains("test.DefaultTypeRepository.SomeClass")); - assertTrue(types.contains("test.DefaultTypeRepository.SomeEnum")); - assertTrue(types.contains("test.DefaultTypeRepository.SomeInterface")); - } - - @Test - public void test_getAllTypes_ReturnsANonEmptySet_WhenTypesAreFoundAndExclusionsHaveBeenSpecified() { - Set exclusions = new HashSet<>(); - exclusions.add(Pattern.compile(".*Abstract.*")); - typeRepository = new DefaultTypeRepository("test.DefaultTypeRepository", exclusions); - Set types = typeRepository.getAllTypes().stream().map(Class::getCanonicalName).collect(Collectors.toSet()); - assertEquals(3, types.size()); - - assertTrue(types.contains("test.DefaultTypeRepository.SomeEnum")); - assertTrue(types.contains("test.DefaultTypeRepository.SomeClass")); - assertTrue(types.contains("test.DefaultTypeRepository.SomeInterface")); - } - - @Test - public void test_findReferencedTypes_ReturnsASetOnlyContainingJavaLangObject_WhenThereAreNoTypesReferenced() throws Exception { - typeRepository = new DefaultTypeRepository("test.DefaultTypeRepository", new HashSet<>()); - Set types = typeRepository.findReferencedTypes("test.DefaultTypeRepository.SomeInterface").stream().map(Class::getCanonicalName).collect(Collectors.toSet()); - assertEquals(1, types.size()); - - assertTrue(types.contains("java.lang.Object")); - } - - @Test - public void test_findReferencedTypes_ReturnsANonEmptySet_WhenThereAreTypesReferenced() throws Exception { - typeRepository = new DefaultTypeRepository("test.DefaultTypeRepository", new HashSet<>()); - Set types = typeRepository.findReferencedTypes("test.DefaultTypeRepository.SomeClass").stream().map(Class::getCanonicalName).collect(Collectors.toSet()); - assertEquals(3, types.size()); - - assertTrue(types.contains("test.DefaultTypeRepository.SomeEnum")); - assertTrue(types.contains("test.DefaultTypeRepository.SomeAbstractClass")); - assertTrue(types.contains("com.structurizr.annotation.Component")); - } - -} \ No newline at end of file diff --git a/structurizr-core/test/unit/com/structurizr/analysis/ExtendsClassTypeMatcherTests.java b/structurizr-core/test/unit/com/structurizr/analysis/ExtendsClassTypeMatcherTests.java deleted file mode 100644 index 49f81ff65..000000000 --- a/structurizr-core/test/unit/com/structurizr/analysis/ExtendsClassTypeMatcherTests.java +++ /dev/null @@ -1,55 +0,0 @@ -package com.structurizr.analysis; - -import org.junit.Test; - -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; - -public class ExtendsClassTypeMatcherTests { - - @Test(expected = IllegalArgumentException.class) - public void test_construction_ThrowsAnExceptionWhenAnInterfaceTypeIsSupplied() - { - new ExtendsClassTypeMatcher(MyRepository.class, "", ""); - } - - @Test(expected = IllegalArgumentException.class) - public void test_construction_ThrowsAnExceptionWhenAnEnumTypeIsSupplied() - { - new ExtendsClassTypeMatcher(MyEnum.class, "", ""); - } - - @Test - public void test_matches_ReturnsFalse_WhenTheGivenTypeDoesNotExtendTheClass() - { - ExtendsClassTypeMatcher matcher = new ExtendsClassTypeMatcher(AbstractComponent.class, "", ""); - assertFalse(matcher.matches(MyOtherClass.class)); - } - - @Test - public void test_matches_ReturnsTrue_WhenTheGivenTypeDoesExtendTheClass() - { - ExtendsClassTypeMatcher matcher = new ExtendsClassTypeMatcher(AbstractComponent.class, "", ""); - assertTrue(matcher.matches(MyController.class)); - assertTrue(matcher.matches(MyRepositoryImpl.class)); - } - - private abstract class AbstractComponent { - } - - private class MyController extends AbstractComponent { - } - - private interface MyRepository { - } - - private class MyRepositoryImpl extends AbstractComponent implements MyRepository { - } - - private class MyOtherClass { - } - - private enum MyEnum { - } - -} diff --git a/structurizr-core/test/unit/com/structurizr/analysis/ImplementsInterfaceTypeMatcherTests.java b/structurizr-core/test/unit/com/structurizr/analysis/ImplementsInterfaceTypeMatcherTests.java deleted file mode 100644 index 517fcb248..000000000 --- a/structurizr-core/test/unit/com/structurizr/analysis/ImplementsInterfaceTypeMatcherTests.java +++ /dev/null @@ -1,39 +0,0 @@ -package com.structurizr.analysis; - -import org.junit.Test; - -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; - -public class ImplementsInterfaceTypeMatcherTests { - - @Test(expected = IllegalArgumentException.class) - public void test_construction_ThrowsAnExceptionWhenANonInterfaceTypeIsSupplied() - { - new ImplementsInterfaceTypeMatcher(MyRepositoryImpl.class, "", ""); - } - - @Test - public void test_matches_ReturnsFalse_WhenTheGivenTypeDoesNotImplementTheInterface() - { - ImplementsInterfaceTypeMatcher matcher = new ImplementsInterfaceTypeMatcher(MyRepository.class, "", ""); - assertFalse(matcher.matches(MyController.class)); - } - - @Test - public void test_matches_ReturnsTrue_WhenTheGivenTypeDoesImplementTheInterface() - { - ImplementsInterfaceTypeMatcher matcher = new ImplementsInterfaceTypeMatcher(MyRepository.class, "", ""); - assertTrue(matcher.matches(MyRepositoryImpl.class)); - } - - private class MyController { - } - - private interface MyRepository { - } - - private class MyRepositoryImpl implements MyRepository { - } - -} diff --git a/structurizr-core/test/unit/com/structurizr/analysis/JavadocCommentFilterTests.java b/structurizr-core/test/unit/com/structurizr/analysis/JavadocCommentFilterTests.java deleted file mode 100644 index 01e774e1f..000000000 --- a/structurizr-core/test/unit/com/structurizr/analysis/JavadocCommentFilterTests.java +++ /dev/null @@ -1,61 +0,0 @@ -package com.structurizr.analysis; - -import org.junit.Test; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNull; - -public class JavadocCommentFilterTests { - - @Test(expected = IllegalArgumentException.class) - public void test_construction_ThrowsAnIllegalArgumentException_WhenZeroIsSpecified() { - new JavadocCommentFilter(0); - } - - @Test(expected = IllegalArgumentException.class) - public void test_construction_ThrowsAnIllegalArgumentException_WhenANegativeNumberIsSpecified() { - 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.")); - } - -} diff --git a/structurizr-core/test/unit/com/structurizr/analysis/NameSuffixTypeMatcherTests.java b/structurizr-core/test/unit/com/structurizr/analysis/NameSuffixTypeMatcherTests.java deleted file mode 100644 index 8942ea3f3..000000000 --- a/structurizr-core/test/unit/com/structurizr/analysis/NameSuffixTypeMatcherTests.java +++ /dev/null @@ -1,39 +0,0 @@ -package com.structurizr.analysis; - -import org.junit.Test; - -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; - -public class NameSuffixTypeMatcherTests { - - @Test(expected = IllegalArgumentException.class) - public void test_construction_ThrowsAnExceptionWhenANullSuffixIsSupplied() - { - new NameSuffixTypeMatcher(null, "", ""); - } - - @Test(expected = IllegalArgumentException.class) - public void test_construction_ThrowsAnExceptionWhenAnEmptyStringSuffixIsSupplied() - { - new NameSuffixTypeMatcher(" ", "", ""); - } - - @Test - public void test_matches_ReturnsFalse_WhenTheNameOfTheGivenTypeDoesNotHaveTheSuffix() - { - NameSuffixTypeMatcher matcher = new NameSuffixTypeMatcher("Component", "", ""); - assertFalse(matcher.matches(MyController.class)); - } - - @Test - public void Ttest_matches_ReturnsTrue_WhenTheNameOfTheGivenTypeDoesHaveTheSuffix() - { - NameSuffixTypeMatcher matcher = new NameSuffixTypeMatcher("Controller", "", ""); - assertTrue(matcher.matches(MyController.class)); - } - - private class MyController { - } - -} \ No newline at end of file diff --git a/structurizr-core/test/unit/com/structurizr/analysis/RegexTypeMatcherTests.java b/structurizr-core/test/unit/com/structurizr/analysis/RegexTypeMatcherTests.java deleted file mode 100644 index 25cca6946..000000000 --- a/structurizr-core/test/unit/com/structurizr/analysis/RegexTypeMatcherTests.java +++ /dev/null @@ -1,57 +0,0 @@ -package com.structurizr.analysis; - -import org.junit.Test; - -import java.util.regex.Pattern; - -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; - -public class RegexTypeMatcherTests { - - @Test(expected = IllegalArgumentException.class) - public void test_construction_ThrowsAnExceptionWhenANullRegexAsAStringIsSupplied() - { - new RegexTypeMatcher((String)null, "", ""); - } - - @Test(expected = IllegalArgumentException.class) - public void test_construction_ThrowsAnExceptionWhenANullRegexAsAPatternIsSupplied() - { - new RegexTypeMatcher((Pattern)null, "", ""); - } - - @Test - public void test_matches_ReturnsFalse_WhenTheNameOfTheGivenTypeDoesNotMatchTheRegex() - { - RegexTypeMatcher matcher = new RegexTypeMatcher(Pattern.compile("MyController"), "", ""); - assertFalse(matcher.matches(MyController.class)); - - matcher = new RegexTypeMatcher("MyController", "", ""); - assertFalse(matcher.matches(MyController.class)); - } - - @Test - public void test_matches_ReturnsTrue_WhenTheNameOfTheGivenTypeDoesMatchTheRegex() - { - String regex = ".*\\.analysis\\..*Controller"; - - RegexTypeMatcher matcher = new RegexTypeMatcher(Pattern.compile(regex), "", ""); - assertTrue(matcher.matches(MyController.class)); - - matcher = new RegexTypeMatcher(regex, "", ""); - assertTrue(matcher.matches(MyController.class)); - } - - @Test - public void test_matches_ReturnsFalse_WhenPassedANullType() { - String regex = ".*\\.analysis\\..*Controller"; - - RegexTypeMatcher matcher = new RegexTypeMatcher(Pattern.compile(regex), "", ""); - assertFalse(matcher.matches(null)); - } - - private class MyController { - } - -} \ No newline at end of file diff --git a/structurizr-core/test/unit/com/structurizr/analysis/SourceCodeComponentFinderStrategyTests.java b/structurizr-core/test/unit/com/structurizr/analysis/SourceCodeComponentFinderStrategyTests.java deleted file mode 100644 index b69f2ebe8..000000000 --- a/structurizr-core/test/unit/com/structurizr/analysis/SourceCodeComponentFinderStrategyTests.java +++ /dev/null @@ -1,74 +0,0 @@ -package com.structurizr.analysis; - -import com.structurizr.Workspace; -import com.structurizr.model.Component; -import com.structurizr.model.Container; -import com.structurizr.model.Model; -import com.structurizr.model.SoftwareSystem; -import org.junit.Before; -import org.junit.Test; - -import java.io.File; - -import static org.junit.Assert.assertEquals; - -public class SourceCodeComponentFinderStrategyTests { - - private Container webApplication; - private Component someComponent; - private File sourcePath = new File("test/unit"); - - @Before - public void setUp() { - Workspace workspace = new Workspace("Name", "Description"); - Model model = workspace.getModel(); - - SoftwareSystem softwareSystem = model.addSoftwareSystem("Name", "Description"); - webApplication = softwareSystem.addContainer("Name", "Description", "Technology"); - - someComponent = webApplication.addComponent( - "SomeComponent", - "test.SourceCodeComponentFinderStrategy.SomeComponent", - "", ""); - someComponent.addSupportingType("test.SourceCodeComponentFinderStrategy.SomeComponentImpl"); - } - - @Test - public void test_findComponents() throws Exception { - ComponentFinder componentFinder = new ComponentFinder( - webApplication, - "test.SourceCodeComponentFinderStrategy", - new SourceCodeComponentFinderStrategy(sourcePath) - ); - componentFinder.findComponents(); - - assertEquals("A component that does something.", someComponent.getDescription()); - assertEquals(20, someComponent.getSize()); - } - - @Test - public void test_findComponents_TruncatesComponentDescriptions_WhenComponentDescriptionsAreTooLong() throws Exception { - ComponentFinder componentFinder = new ComponentFinder( - webApplication, - "test.SourceCodeComponentFinderStrategy", - new SourceCodeComponentFinderStrategy(sourcePath, 10) - ); - componentFinder.findComponents(); - - assertEquals("A compo...", someComponent.getDescription()); - } - - @Test - public void test_findComponents_DoesNotSetTheComponentDescription_WhenTheComponentAlreadyHasADescription() throws Exception { - someComponent.setDescription("An existing description."); - ComponentFinder componentFinder = new ComponentFinder( - webApplication, - "test.SourceCodeComponentFinderStrategy", - new SourceCodeComponentFinderStrategy(sourcePath, 10) - ); - componentFinder.findComponents(); - - assertEquals("An existing description.", someComponent.getDescription()); - } - -} \ No newline at end of file diff --git a/structurizr-core/test/unit/com/structurizr/analysis/StructurizrAnnotationsComponentFinderStrategyTests.java b/structurizr-core/test/unit/com/structurizr/analysis/StructurizrAnnotationsComponentFinderStrategyTests.java deleted file mode 100644 index bbf61ca42..000000000 --- a/structurizr-core/test/unit/com/structurizr/analysis/StructurizrAnnotationsComponentFinderStrategyTests.java +++ /dev/null @@ -1,154 +0,0 @@ -package com.structurizr.analysis; - -import com.structurizr.Workspace; -import com.structurizr.model.*; -import org.junit.Before; -import org.junit.Test; - -import static org.junit.Assert.*; - -public class StructurizrAnnotationsComponentFinderStrategyTests { - - private SoftwareSystem external1, external2, softwareSystem; - private Person anonymousUser, authenticatedUser; - private Container webBrowser, apiClient; - private Container webApplication; - private Container database; - - @Before - public void setUp() throws Exception { - Workspace workspace = new Workspace("Name", ""); - Model model = workspace.getModel(); - - external1 = model.addSoftwareSystem("External 1", ""); - external2 = model.addSoftwareSystem("External 2", ""); - anonymousUser = model.addPerson("Anonymous User", ""); - authenticatedUser = model.addPerson("Authenticated User", ""); - softwareSystem = model.addSoftwareSystem("Software System", ""); - webBrowser = softwareSystem.addContainer("Web Browser", "", ""); - apiClient = softwareSystem.addContainer("API Client", "", ""); - webApplication = softwareSystem.addContainer("Name", "", ""); - database = softwareSystem.addContainer("Database", "", ""); - - // the default usage of the StructurizrAnnotationsComponentFinderStrategy - // just has the FirstImplementationOfInterfaceSupportingTypesStrategy included - ComponentFinder componentFinder = new ComponentFinder( - webApplication, - "test.StructurizrAnnotationsComponentFinderStrategy", - new StructurizrAnnotationsComponentFinderStrategy() - ); - componentFinder.findComponents(); - } - - @Test - public void test_Component() { - assertEquals(2, webApplication.getComponents().size()); - - Component controller = webApplication.getComponentWithName("Controller"); - assertNotNull(controller); - assertEquals("Controller", controller.getName()); - assertEquals("test.StructurizrAnnotationsComponentFinderStrategy.Controller", controller.getType()); - assertEquals("Does something.", controller.getDescription()); - assertEquals(1, controller.getCode().size()); - assertCodeElementInComponent(controller, "test.StructurizrAnnotationsComponentFinderStrategy.Controller", CodeElementRole.Primary); - - Component repository = webApplication.getComponentWithName("Repository"); - assertNotNull(repository); - assertEquals("Repository", repository.getName()); - assertEquals("test.StructurizrAnnotationsComponentFinderStrategy.Repository", repository.getType()); - assertEquals("Manages some data.", repository.getDescription()); - assertEquals(2, repository.getCode().size()); - assertCodeElementInComponent(repository, "test.StructurizrAnnotationsComponentFinderStrategy.Repository", CodeElementRole.Primary); - assertCodeElementInComponent(repository, "test.StructurizrAnnotationsComponentFinderStrategy.RepositoryImpl", CodeElementRole.Supporting); - } - - private void assertCodeElementInComponent(Component component, String type, CodeElementRole role) { - for (CodeElement codeElement : component.getCode()) { - if (codeElement.getType().equals(type)) { - if (codeElement.getRole() == role) { - return; - } - } - } - - fail(); - } - - @Test - public void test_UsedByPerson() { - Component controller = webApplication.getComponentWithName("Controller"); - - assertEquals(1, anonymousUser.getRelationships().size()); - Relationship relationship = anonymousUser.getRelationships().stream().filter(r -> r.getDestination() == controller).findFirst().get(); - assertEquals("Uses to do something", relationship.getDescription()); - assertEquals("HTTPS", relationship.getTechnology()); - - assertEquals(1, authenticatedUser.getRelationships().size()); - relationship = authenticatedUser.getRelationships().stream().filter(r -> r.getDestination() == controller).findFirst().get(); - assertEquals("Uses to do something too", relationship.getDescription()); - assertEquals("", relationship.getTechnology()); - } - - @Test - public void test_UsedBySoftwareSystem() { - Component controller = webApplication.getComponentWithName("Controller"); - - assertEquals(1, external1.getRelationships().size()); - Relationship relationship = external1.getRelationships().stream().filter(r -> r.getDestination() == controller).findFirst().get(); - assertEquals("Uses to do something", relationship.getDescription()); - assertEquals("HTTPS", relationship.getTechnology()); - - assertEquals(1, external2.getRelationships().size()); - relationship = external2.getRelationships().stream().filter(r -> r.getDestination() == controller).findFirst().get(); - assertEquals("Uses to do something too", relationship.getDescription()); - assertEquals("", relationship.getTechnology()); - } - - @Test - public void test_UsedByContainer() { - Component controller = webApplication.getComponentWithName("Controller"); - - assertEquals(1, webBrowser.getRelationships().size()); - Relationship relationship = webBrowser.getRelationships().stream().filter(r -> r.getDestination() == controller).findFirst().get(); - assertEquals("Makes calls to", relationship.getDescription()); - assertEquals("HTTPS", relationship.getTechnology()); - - assertEquals(1, apiClient.getRelationships().size()); - relationship = apiClient.getRelationships().stream().filter(r -> r.getDestination() == controller).findFirst().get(); - assertEquals("Makes API calls to", relationship.getDescription()); - assertEquals("HTTPS", relationship.getTechnology()); - } - - @Test - public void test_UsesComponent() - { - Component controller = webApplication.getComponentWithName("Controller"); - Component repository = webApplication.getComponentWithName("Repository"); - - Relationship relationship = controller.getRelationships().stream().filter(r -> r.getDestination() == repository).findFirst().get(); - assertEquals("Reads from and writes to", relationship.getDescription()); - assertEquals("Just a method call", relationship.getTechnology()); - } - - @Test - public void test_UsesContainer() - { - Component repository = webApplication.getComponentWithName("Repository"); - - assertEquals(1, repository.getRelationships().size()); - Relationship relationship = repository.getRelationships().stream().filter(r -> r.getDestination() == database).findFirst().get(); - assertEquals("Reads from and writes to", relationship.getDescription()); - assertEquals("JDBC", relationship.getTechnology()); - } - - @Test - public void test_UsesSoftwareSystem() - { - Component controller = webApplication.getComponentWithName("Controller"); - - Relationship relationship = controller.getRelationships().stream().filter(r -> r.getDestination() == external1).findFirst().get(); - assertEquals("Sends information to", relationship.getDescription()); - assertEquals("HTTPS", relationship.getTechnology()); - } - -} \ No newline at end of file diff --git a/structurizr-core/test/unit/com/structurizr/analysis/TypeMatcherComponentFinderStrategyTests.java b/structurizr-core/test/unit/com/structurizr/analysis/TypeMatcherComponentFinderStrategyTests.java deleted file mode 100644 index 5e4c5e2b1..000000000 --- a/structurizr-core/test/unit/com/structurizr/analysis/TypeMatcherComponentFinderStrategyTests.java +++ /dev/null @@ -1,59 +0,0 @@ -package com.structurizr.analysis; - -import com.structurizr.Workspace; -import com.structurizr.model.Component; -import com.structurizr.model.Container; -import com.structurizr.model.Model; -import com.structurizr.model.SoftwareSystem; -import org.junit.Before; -import org.junit.Test; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; - -public class TypeMatcherComponentFinderStrategyTests { - - private Container container; - - @Before - public void setUp() { - Workspace workspace = new Workspace("Name", "Description"); - Model model = workspace.getModel(); - - SoftwareSystem softwareSystem = model.addSoftwareSystem("Name", "Description"); - container = softwareSystem.addContainer("Name", "Description", "Technology"); - } - - @Test - public void test_basicUsage() throws Exception { - ComponentFinder componentFinder = new ComponentFinder( - container, - "test.TypeMatcherComponentFinderStrategy", - new TypeMatcherComponentFinderStrategy( - new NameSuffixTypeMatcher("Controller", "Controller description", "Controller technology"), - new NameSuffixTypeMatcher("Repository", "Repository description", "Repository technology") - ) - ); - componentFinder.findComponents(); - - assertEquals(2, container.getComponents().size()); - - Component myController = container.getComponentWithName("MyController"); - assertNotNull(myController); - assertEquals("MyController", myController.getName()); - assertEquals("test.TypeMatcherComponentFinderStrategy.MyController", myController.getType()); - assertEquals("Controller description", myController.getDescription()); - assertEquals("Controller technology", myController.getTechnology()); - - Component myRepository = container.getComponentWithName("MyRepository"); - assertNotNull(myRepository); - assertEquals("MyRepository", myRepository.getName()); - assertEquals("test.TypeMatcherComponentFinderStrategy.MyRepository", myRepository.getType()); - assertEquals("Repository description", myRepository.getDescription()); - assertEquals("Repository technology", myRepository.getTechnology()); - - assertEquals(1, myController.getRelationships().size()); - assertNotNull(myController.getRelationships().stream().filter(r -> r.getDestination() == myRepository).findFirst().get()); - } - -} \ No newline at end of file diff --git a/structurizr-core/test/unit/com/structurizr/analysis/TypeUtilsTests.java b/structurizr-core/test/unit/com/structurizr/analysis/TypeUtilsTests.java deleted file mode 100644 index 99c5f1b40..000000000 --- a/structurizr-core/test/unit/com/structurizr/analysis/TypeUtilsTests.java +++ /dev/null @@ -1,154 +0,0 @@ -package com.structurizr.analysis; - -import com.structurizr.annotation.Component; -import com.structurizr.annotation.UsedByPerson; -import org.junit.Test; -import test.TypeUtils.AnotherClass; -import test.TypeUtils.SomeInterface; - -import java.util.HashSet; -import java.util.Set; - -import static org.junit.Assert.*; - -public class TypeUtilsTests { - - @Test - public void test_getCategory_ReturnsNull_WhenTheSpecifiedTypeCouldNotBeFound() throws Exception { - assertNull(TypeUtils.getCategory("com.company.app.Class")); - } - - @Test - public void test_getCategory_ReturnsInterface_WhenTheSpecifiedTypeIsAnInterface() throws Exception { - TypeCategory typeCategory = TypeUtils.getCategory("test.TypeUtils.SomeInterface"); - assertSame(TypeCategory.INTERFACE, typeCategory); - } - - @Test - public void test_getCategory_ReturnsAbstractClass_WhenTheSpecifiedTypeIsAnAbstractClass() throws Exception { - TypeCategory typeCategory = TypeUtils.getCategory("test.TypeUtils.SomeAbstractClass"); - assertSame(TypeCategory.ABSTRACT_CLASS, typeCategory); - } - - @Test - public void test_getCategory_ReturnsAbstractClass_WhenTheSpecifiedTypeIsAClass() throws Exception { - TypeCategory typeCategory = TypeUtils.getCategory("test.TypeUtils.SomeClass"); - assertSame(TypeCategory.CLASS, typeCategory); - } - - @Test - public void test_getCategory_ReturnsEnum_WhenTheSpecifiedTypeIsAnEnum() throws Exception { - TypeCategory typeCategory = TypeUtils.getCategory("test.TypeUtils.SomeEnum"); - assertSame(TypeCategory.ENUM, typeCategory); - } - - @Test - public void test_getVisibility_ReturnsNull_WhenTheSpecifiedTypeCouldNotBeFound() throws Exception { - assertNull(TypeUtils.getVisibility("com.company.app.Class")); - } - - @Test - public void test_getVisibility_ReturnsPublic_WhenTheSpecifiedTypeIsPublic() throws Exception { - TypeVisibility typeCategory= TypeUtils.getVisibility("test.TypeUtils.SomeInterface"); - assertSame(TypeVisibility.PUBLIC, typeCategory); - } - - @Test - public void test_getVisibility_ReturnsPackage_WhenTheSpecifiedTypeIsPackageScoped() throws Exception { - TypeVisibility typeCategory= TypeUtils.getVisibility("test.TypeUtils.SomeClass"); - assertSame(TypeVisibility.PACKAGE, typeCategory); - } - - @Test - public void test_findTypesAnnotatedWith_ThrowsAnException_WhenANullAnnotationTypeIsSpecified() { - try { - TypeUtils.findTypesAnnotatedWith(null, new HashSet<>()); - fail(); - } catch (IllegalArgumentException iae) { - iae.printStackTrace(); - assertEquals("An annotation type must be specified.", iae.getMessage()); - } - } - - @Test - public void test_findTypesAnnotatedWith_ReturnsAnEmptySet_WhenNoTypesWithTheSpecifiedAnnotationAreFound() throws Exception { - Set> typesToSearch = new HashSet<>(); - typesToSearch.add(ClassLoader.getSystemClassLoader().loadClass("test.TypeUtils.SomeClass")); - Set> types = TypeUtils.findTypesAnnotatedWith(UsedByPerson.class, typesToSearch); - assertTrue(types.isEmpty()); - } - - @Test - public void test_findTypesAnnotatedWith_ReturnsANonEmptySet_WhenTypesWithTheSpecifiedAnnotationAreFound() throws Exception { - Set> typesToSearch = new HashSet<>(); - typesToSearch.add(ClassLoader.getSystemClassLoader().loadClass("test.TypeUtils.SomeClass")); - Set> types = TypeUtils.findTypesAnnotatedWith(Component.class, typesToSearch); - assertEquals(1, types.size()); - assertEquals("test.TypeUtils.SomeClass", types.iterator().next().getCanonicalName()); - } - - @Test - public void test_getFirstImplementationOfInterface_ThrowsAnException_WhenANullInterfaceIsSpecified() { - try { - TypeUtils.findFirstImplementationOfInterface(null, new HashSet<>()); - fail(); - } catch (IllegalArgumentException iae) { - assertEquals("An interface type must be provided.", iae.getMessage()); - } - } - - @Test - public void test_getFirstImplementationOfInterface_ThrowsAnException_WhenANonInterfaceIsSpecified() { - try { - TypeUtils.findFirstImplementationOfInterface(this.getClass(), new HashSet<>()); - fail(); - } catch (IllegalArgumentException iae) { - assertEquals("The interface type must represent an interface.", iae.getMessage()); - } - } - - @Test - public void test_getFirstImplementationOfInterface_ThrowsAnException_WhenANullSetOfTypesIsSpecified() { - try { - TypeUtils.findFirstImplementationOfInterface(SomeInterface.class, null); - fail(); - } catch (IllegalArgumentException iae) { - assertEquals("The set of types to search through must be provided.", iae.getMessage()); - } - } - - @Test - public void test_getFirstImplementationOfInterface_ReturnsNull_WhenAnImplementationCannotBeFound() { - Set> classes = new HashSet<>(); - classes.add(AnotherClass.class); - Class implementationClass = TypeUtils.findFirstImplementationOfInterface(SomeInterface.class, classes); - assertNull(implementationClass); - } - - @Test - public void test_getFirstImplementationOfInterface_ReturnsNull_WhenOnlyTheInterfaceIsFound() { - Set> classes = new HashSet<>(); - classes.add(SomeInterface.class); - Class implementationClass = TypeUtils.findFirstImplementationOfInterface(SomeInterface.class, classes); - assertNull(implementationClass); - } - - @Test - public void test_getFirstImplementationOfInterface_ReturnsNull_WhenOnlyAnAbstractImplementationIsFound() throws Exception { - Set> classes = new HashSet<>(); - classes.add(Class.forName("test.TypeUtils.SomeAbstractClass")); - Class implementationClass = TypeUtils.findFirstImplementationOfInterface(SomeInterface.class, classes); - assertNull(implementationClass); - } - - @Test - public void test_getFirstImplementationOfInterface_ReturnsAnImplementation_WhenAnConcreteImplementationIsFound() throws Exception { - Set> classes = new HashSet<>(); - classes.add(SomeInterface.class); - classes.add(Class.forName("test.TypeUtils.SomeAbstractClass")); - classes.add(Class.forName("test.TypeUtils.SomeClass")); - Class implementationClass = TypeUtils.findFirstImplementationOfInterface(SomeInterface.class, classes); - assertSame("test.TypeUtils.SomeClass", implementationClass.getCanonicalName()); - } - -} \ No newline at end of file diff --git a/structurizr-core/test/unit/com/structurizr/analysis/reflections/AbstractComponentFinderStrategyTests.java b/structurizr-core/test/unit/com/structurizr/analysis/reflections/AbstractComponentFinderStrategyTests.java deleted file mode 100644 index e19ac4adb..000000000 --- a/structurizr-core/test/unit/com/structurizr/analysis/reflections/AbstractComponentFinderStrategyTests.java +++ /dev/null @@ -1,284 +0,0 @@ -package com.structurizr.analysis.reflections; - -import com.structurizr.Workspace; -import com.structurizr.analysis.*; -import com.structurizr.model.*; -import org.junit.Before; -import org.junit.Test; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; - -public class AbstractComponentFinderStrategyTests { - - private Container webApplication; - - @Before - public void setUp() { - Workspace workspace = new Workspace("Name", "Description"); - Model model = workspace.getModel(); - - SoftwareSystem softwareSystem = model.addSoftwareSystem("Name", "Description"); - webApplication = softwareSystem.addContainer("Name", "Description", "Technology"); - } - - @Test - public void test_findComponents_DoesNotBreak_WhenThereIsACyclicDependency() throws Exception { - ComponentFinder componentFinder = new ComponentFinder( - webApplication, - "com.structurizr.analysis.reflections.cyclicDependency", - new TypeMatcherComponentFinderStrategy( - new NameSuffixTypeMatcher("Component", "", "") - ) - ); - componentFinder.findComponents(); - - assertEquals(2, webApplication.getComponents().size()); - - Component aComponent = webApplication.getComponentWithName("AComponent"); - assertNotNull(aComponent); - assertEquals("AComponent", aComponent.getName()); - assertEquals("com.structurizr.analysis.reflections.cyclicDependency.AComponent", aComponent.getType()); - - Component bComponent = webApplication.getComponentWithName("BComponent"); - assertNotNull(bComponent); - assertEquals("BComponent", bComponent.getName()); - assertEquals("com.structurizr.analysis.reflections.cyclicDependency.BComponent", bComponent.getType()); - - assertEquals(1, aComponent.getRelationships().size()); - assertNotNull(aComponent.getRelationships().stream().filter(r -> r.getDestination() == bComponent).findFirst().get()); - - assertEquals(1, bComponent.getRelationships().size()); - assertNotNull(bComponent.getRelationships().stream().filter(r -> r.getDestination() == aComponent).findFirst().get()); - } - - @Test - public void test_findComponents_CorrectlyFindsDependenciesFromSuperclass() throws Exception { - ComponentFinder componentFinder = new ComponentFinder( - webApplication, - "com.structurizr.analysis.reflections.dependenciesFromSuperClass", - new TypeMatcherComponentFinderStrategy( - new NameSuffixTypeMatcher("Component", "", "") - ) - ); - componentFinder.findComponents(); - - assertEquals(2, webApplication.getComponents().size()); - - Component someComponent = webApplication.getComponentWithName("SomeComponent"); - assertNotNull(someComponent); - assertEquals("SomeComponent", someComponent.getName()); - assertEquals("com.structurizr.analysis.reflections.dependenciesFromSuperClass.SomeComponent", someComponent.getType()); - - Component loggingComponent = webApplication.getComponentWithName("LoggingComponent"); - assertNotNull(loggingComponent); - assertEquals("LoggingComponent", loggingComponent.getName()); - assertEquals("com.structurizr.analysis.reflections.dependenciesFromSuperClass.LoggingComponent", loggingComponent.getType()); - - assertEquals(1, someComponent.getRelationships().size()); - assertNotNull(someComponent.getRelationships().stream().filter(r -> r.getDestination() == loggingComponent).findFirst().get()); - } - - @Test - public void test_findComponents_CorrectlyFindsNoDependenciesWhenTwoComponentsImplementTheSameInterface() throws Exception { - ComponentFinder componentFinder = new ComponentFinder( - webApplication, - "com.structurizr.analysis.reflections.featureinterface", - new TypeMatcherComponentFinderStrategy( - new NameSuffixTypeMatcher("Component", "", "") - ) - ); - componentFinder.findComponents(); - - assertEquals(2, webApplication.getComponents().size()); - - Component someComponent = webApplication.getComponentWithName("SomeComponent"); - assertNotNull(someComponent); - assertEquals("SomeComponent", someComponent.getName()); - assertEquals("com.structurizr.analysis.reflections.featureinterface.SomeComponent", someComponent.getType()); - - Component otherComponent = webApplication.getComponentWithName("OtherComponent"); - assertNotNull(otherComponent); - assertEquals("OtherComponent", otherComponent.getName()); - assertEquals("com.structurizr.analysis.reflections.featureinterface.OtherComponent", otherComponent.getType()); - - assertEquals(0, someComponent.getRelationships().size()); - assertEquals(0, otherComponent.getRelationships().size()); - } - - @Test - public void test_findComponents_CorrectlyFindsDependenciesBetweenComponentsFoundByDifferentComponentFinders_WhenPackage1IsScannedFirst() throws Exception { - ComponentFinder componentFinder1 = new ComponentFinder( - webApplication, - "com.structurizr.analysis.reflections.multipleComponentFinders.package1", - new TypeMatcherComponentFinderStrategy( - new NameSuffixTypeMatcher("Controller", "", "") - ) - ); - - ComponentFinder componentFinder2 = new ComponentFinder( - webApplication, - "com.structurizr.analysis.reflections.multipleComponentFinders.package2", - new TypeMatcherComponentFinderStrategy( - new NameSuffixTypeMatcher("Repository", "", "") - ) - ); - - componentFinder1.findComponents(); - componentFinder2.findComponents(); - - assertEquals(2, webApplication.getComponents().size()); - Component myController = webApplication.getComponentWithName("MyController"); - Component myRepository = webApplication.getComponentWithName("MyRepository"); - assertEquals(1, myController.getRelationships().size()); - assertNotNull(myController.getRelationships().stream().filter(r -> r.getDestination() == myRepository).findFirst().get()); - } - - @Test - public void test_findComponents_CorrectlyFindsDependenciesBetweenComponentsFoundByDifferentComponentFinders_WhenPackage2IsScannedFirst() throws Exception { - ComponentFinder componentFinder1 = new ComponentFinder( - webApplication, - "com.structurizr.analysis.reflections.multipleComponentFinders.package1", - new TypeMatcherComponentFinderStrategy( - new NameSuffixTypeMatcher("Controller", "", "") - ) - ); - - ComponentFinder componentFinder2 = new ComponentFinder( - webApplication, - "com.structurizr.analysis.reflections.multipleComponentFinders.package2", - new TypeMatcherComponentFinderStrategy( - new NameSuffixTypeMatcher("Repository", "", "") - ) - ); - - componentFinder2.findComponents(); - componentFinder1.findComponents(); - - assertEquals(2, webApplication.getComponents().size()); - Component myController = webApplication.getComponentWithName("MyController"); - Component myRepository = webApplication.getComponentWithName("MyRepository"); - assertEquals(1, myController.getRelationships().size()); - assertNotNull(myController.getRelationships().stream().filter(r -> r.getDestination() == myRepository).findFirst().get()); - } - - @Test - public void test_findComponents_CorrectlyFindsSupportingTypes_WhenTheDefaultStrategyIsUsed() throws Exception { - ComponentFinder componentFinder = new ComponentFinder( - webApplication, - "com.structurizr.analysis.reflections.supportingTypes.myapp", - new StructurizrAnnotationsComponentFinderStrategy() - ); - componentFinder.findComponents(); - - assertEquals(2, webApplication.getComponents().size()); - Component myController = webApplication.getComponentWithName("MyController"); - Component myRepository = webApplication.getComponentWithName("MyRepository"); - assertEquals(1, myController.getRelationships().size()); - assertNotNull(myController.getRelationships().stream().filter(r -> r.getDestination() == myRepository).findFirst().get()); - - // the default strategy for supporting types is to find the first implementation - // class if the component type is an interface - assertEquals(1, myController.getCode().size()); - assertCodeElementInComponent(myController, "com.structurizr.analysis.reflections.supportingTypes.myapp.web.MyController", CodeElementRole.Primary); - - assertEquals(2, myRepository.getCode().size()); - assertCodeElementInComponent(myRepository, "com.structurizr.analysis.reflections.supportingTypes.data.MyRepository", CodeElementRole.Primary); - assertCodeElementInComponent(myRepository, "com.structurizr.analysis.reflections.supportingTypes.data.MyRepositoryImpl", CodeElementRole.Supporting); - } - - @Test - public void test_findComponents_CorrectlyFindsSupportingTypes_WhenTheReferencedTypesInSamePackageSupportingTypesStrategyIsUsed() throws Exception { - ComponentFinder componentFinder = new ComponentFinder( - webApplication, - "com.structurizr.analysis.reflections.supportingTypes.myapp", - new StructurizrAnnotationsComponentFinderStrategy( - new FirstImplementationOfInterfaceSupportingTypesStrategy(), - new ReferencedTypesInSamePackageSupportingTypesStrategy() - ) - ); - componentFinder.findComponents(); - - assertEquals(2, webApplication.getComponents().size()); - Component myController = webApplication.getComponentWithName("MyController"); - Component myRepository = webApplication.getComponentWithName("MyRepository"); - assertEquals(1, myController.getRelationships().size()); - assertNotNull(myController.getRelationships().stream().filter(r -> r.getDestination() == myRepository).findFirst().get()); - - assertEquals(1, myController.getCode().size()); - assertEquals(3, myRepository.getCode().size()); - assertCodeElementInComponent(myRepository, "com.structurizr.analysis.reflections.supportingTypes.myapp.data.MyRepository", CodeElementRole.Primary); - assertCodeElementInComponent(myRepository, "com.structurizr.analysis.reflections.supportingTypes.myapp.data.MyRepositoryImpl", CodeElementRole.Supporting); - assertCodeElementInComponent(myRepository, "com.structurizr.analysis.reflections.supportingTypes.myapp.data.MyRepositoryRowMapper", CodeElementRole.Supporting); - } - - @Test - public void test_findComponents_CorrectlyFindsSupportingTypes_WhenTheReferencedTypesStrategyIsUsedAndIndirectlyReferencedTypesShouldBeIncluded() throws Exception { - ComponentFinder componentFinder = new ComponentFinder( - webApplication, - "com.structurizr.analysis.reflections.supportingTypes.myapp", - new StructurizrAnnotationsComponentFinderStrategy( - new FirstImplementationOfInterfaceSupportingTypesStrategy(), - new ReferencedTypesSupportingTypesStrategy() - ) - ); - componentFinder.findComponents(); - - assertEquals(2, webApplication.getComponents().size()); - Component myController = webApplication.getComponentWithName("MyController"); - Component myRepository = webApplication.getComponentWithName("MyRepository"); - assertEquals(1, myController.getRelationships().size()); - assertNotNull(myController.getRelationships().stream().filter(r -> r.getDestination() == myRepository).findFirst().get()); - - assertEquals(2, myController.getCode().size()); - assertCodeElementInComponent(myController, "com.structurizr.analysis.reflections.supportingTypes.myapp.MyController", CodeElementRole.Primary); - assertCodeElementInComponent(myController, "com.structurizr.analysis.reflections.supportingTypes.myapp.AbstractComponent", CodeElementRole.Supporting); - - assertEquals(5, myRepository.getCode().size()); - assertCodeElementInComponent(myController, "com.structurizr.analysis.reflections.supportingTypes.myapp.data.MyRepository", CodeElementRole.Primary); - assertCodeElementInComponent(myController, "com.structurizr.analysis.reflections.supportingTypes.myapp.AbstractComponent", CodeElementRole.Supporting); - assertCodeElementInComponent(myController, "com.structurizr.analysis.reflections.supportingTypes.myapp.data.MyRepositoryImpl", CodeElementRole.Supporting); - assertCodeElementInComponent(myController, "com.structurizr.analysis.reflections.supportingTypes.myapp.data.MyRepositoryRowMapper", CodeElementRole.Supporting); - assertCodeElementInComponent(myController, "com.structurizr.analysis.reflections.supportingTypes.myapp.util.RowMapperHelper", CodeElementRole.Supporting); - } - - @Test - public void test_findComponents_CorrectlyFindsSupportingTypes_WhenTheReferencedTypesStrategyIsUsedAndIndirectlyReferencedTypesShouldBeExcluded() throws Exception { - ComponentFinder componentFinder = new ComponentFinder( - webApplication, - "com.structurizr.analysis.reflections.supportingTypes.myapp", - new StructurizrAnnotationsComponentFinderStrategy( - new FirstImplementationOfInterfaceSupportingTypesStrategy(), - new ReferencedTypesSupportingTypesStrategy(false) - ) - ); - componentFinder.findComponents(); - - assertEquals(2, webApplication.getComponents().size()); - Component myController = webApplication.getComponentWithName("MyController"); - Component myRepository = webApplication.getComponentWithName("MyRepository"); - assertEquals(1, myController.getRelationships().size()); - assertNotNull(myController.getRelationships().stream().filter(r -> r.getDestination() == myRepository).findFirst().get()); - - assertEquals(2, myController.getCode().size()); - assertCodeElementInComponent(myController, "com.structurizr.analysis.reflections.supportingTypes.myapp.MyController", CodeElementRole.Primary); - assertCodeElementInComponent(myController, "com.structurizr.analysis.reflections.supportingTypes.myapp.AbstractComponent", CodeElementRole.Supporting); - - assertEquals(4, myRepository.getCode().size()); - assertCodeElementInComponent(myController, "com.structurizr.analysis.reflections.supportingTypes.myapp.data.MyRepository", CodeElementRole.Primary); - assertCodeElementInComponent(myController, "com.structurizr.analysis.reflections.supportingTypes.myapp.AbstractComponent", CodeElementRole.Supporting); - assertCodeElementInComponent(myController, "com.structurizr.analysis.reflections.supportingTypes.myapp.data.MyRepositoryImpl", CodeElementRole.Supporting); - assertCodeElementInComponent(myController, "com.structurizr.analysis.reflections.supportingTypes.myapp.data.MyRepositoryRowMapper", CodeElementRole.Supporting); - } - - private boolean assertCodeElementInComponent(Component component, String type, CodeElementRole role) { - for (CodeElement codeElement : component.getCode()) { - if (codeElement.getType().equals(type)) { - return codeElement.getRole() == role; - } - } - - return false; - } - -} diff --git a/structurizr-core/test/unit/com/structurizr/analysis/reflections/cyclicDependency/AComponent.java b/structurizr-core/test/unit/com/structurizr/analysis/reflections/cyclicDependency/AComponent.java deleted file mode 100644 index 2ffb6ddfc..000000000 --- a/structurizr-core/test/unit/com/structurizr/analysis/reflections/cyclicDependency/AComponent.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.structurizr.analysis.reflections.cyclicDependency; - -public class AComponent { - - private BComponent bComponent = new BComponent(); - -} diff --git a/structurizr-core/test/unit/com/structurizr/analysis/reflections/cyclicDependency/BComponent.java b/structurizr-core/test/unit/com/structurizr/analysis/reflections/cyclicDependency/BComponent.java deleted file mode 100644 index e5f2e4203..000000000 --- a/structurizr-core/test/unit/com/structurizr/analysis/reflections/cyclicDependency/BComponent.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.structurizr.analysis.reflections.cyclicDependency; - -public class BComponent { - - private AComponent aComponent = new AComponent(); - -} diff --git a/structurizr-core/test/unit/com/structurizr/analysis/reflections/dependenciesFromSuperClass/ComponentBase.java b/structurizr-core/test/unit/com/structurizr/analysis/reflections/dependenciesFromSuperClass/ComponentBase.java deleted file mode 100644 index 6ea109ea1..000000000 --- a/structurizr-core/test/unit/com/structurizr/analysis/reflections/dependenciesFromSuperClass/ComponentBase.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.structurizr.analysis.reflections.dependenciesFromSuperClass; - -public abstract class ComponentBase { - - private LoggingComponent loggingComponent = new LoggingComponent(); - -} diff --git a/structurizr-core/test/unit/com/structurizr/analysis/reflections/dependenciesFromSuperClass/LoggingComponent.java b/structurizr-core/test/unit/com/structurizr/analysis/reflections/dependenciesFromSuperClass/LoggingComponent.java deleted file mode 100644 index 73b0fe07f..000000000 --- a/structurizr-core/test/unit/com/structurizr/analysis/reflections/dependenciesFromSuperClass/LoggingComponent.java +++ /dev/null @@ -1,4 +0,0 @@ -package com.structurizr.analysis.reflections.dependenciesFromSuperClass; - -public class LoggingComponent { -} diff --git a/structurizr-core/test/unit/com/structurizr/analysis/reflections/dependenciesFromSuperClass/SomeComponent.java b/structurizr-core/test/unit/com/structurizr/analysis/reflections/dependenciesFromSuperClass/SomeComponent.java deleted file mode 100644 index 215a7507b..000000000 --- a/structurizr-core/test/unit/com/structurizr/analysis/reflections/dependenciesFromSuperClass/SomeComponent.java +++ /dev/null @@ -1,4 +0,0 @@ -package com.structurizr.analysis.reflections.dependenciesFromSuperClass; - -public class SomeComponent extends ComponentBase { -} diff --git a/structurizr-core/test/unit/com/structurizr/analysis/reflections/featureinterface/FeatureInterface.java b/structurizr-core/test/unit/com/structurizr/analysis/reflections/featureinterface/FeatureInterface.java deleted file mode 100644 index 7268297d7..000000000 --- a/structurizr-core/test/unit/com/structurizr/analysis/reflections/featureinterface/FeatureInterface.java +++ /dev/null @@ -1,4 +0,0 @@ -package com.structurizr.analysis.reflections.featureinterface; - -public interface FeatureInterface { -} diff --git a/structurizr-core/test/unit/com/structurizr/analysis/reflections/featureinterface/OtherComponent.java b/structurizr-core/test/unit/com/structurizr/analysis/reflections/featureinterface/OtherComponent.java deleted file mode 100644 index 74d366918..000000000 --- a/structurizr-core/test/unit/com/structurizr/analysis/reflections/featureinterface/OtherComponent.java +++ /dev/null @@ -1,4 +0,0 @@ -package com.structurizr.analysis.reflections.featureinterface; - -public class OtherComponent implements FeatureInterface { -} diff --git a/structurizr-core/test/unit/com/structurizr/analysis/reflections/featureinterface/SomeComponent.java b/structurizr-core/test/unit/com/structurizr/analysis/reflections/featureinterface/SomeComponent.java deleted file mode 100644 index 06b9a6ebf..000000000 --- a/structurizr-core/test/unit/com/structurizr/analysis/reflections/featureinterface/SomeComponent.java +++ /dev/null @@ -1,4 +0,0 @@ -package com.structurizr.analysis.reflections.featureinterface; - -public class SomeComponent implements FeatureInterface { -} diff --git a/structurizr-core/test/unit/com/structurizr/analysis/reflections/multipleComponentFinders/package1/MyController.java b/structurizr-core/test/unit/com/structurizr/analysis/reflections/multipleComponentFinders/package1/MyController.java deleted file mode 100644 index c5bf1591f..000000000 --- a/structurizr-core/test/unit/com/structurizr/analysis/reflections/multipleComponentFinders/package1/MyController.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.structurizr.analysis.reflections.multipleComponentFinders.package1; - -import com.structurizr.analysis.reflections.multipleComponentFinders.package2.MyRepository; - -public class MyController { - - private MyRepository myRepository; - -} diff --git a/structurizr-core/test/unit/com/structurizr/analysis/reflections/multipleComponentFinders/package2/MyRepository.java b/structurizr-core/test/unit/com/structurizr/analysis/reflections/multipleComponentFinders/package2/MyRepository.java deleted file mode 100644 index 0bd641e00..000000000 --- a/structurizr-core/test/unit/com/structurizr/analysis/reflections/multipleComponentFinders/package2/MyRepository.java +++ /dev/null @@ -1,4 +0,0 @@ -package com.structurizr.analysis.reflections.multipleComponentFinders.package2; - -public interface MyRepository { -} diff --git a/structurizr-core/test/unit/com/structurizr/analysis/reflections/supportingTypes/myapp/AbstractComponent.java b/structurizr-core/test/unit/com/structurizr/analysis/reflections/supportingTypes/myapp/AbstractComponent.java deleted file mode 100644 index fd4633e7e..000000000 --- a/structurizr-core/test/unit/com/structurizr/analysis/reflections/supportingTypes/myapp/AbstractComponent.java +++ /dev/null @@ -1,4 +0,0 @@ -package com.structurizr.analysis.reflections.supportingTypes.myapp; - -public abstract class AbstractComponent { -} diff --git a/structurizr-core/test/unit/com/structurizr/analysis/reflections/supportingTypes/myapp/data/MyRepository.java b/structurizr-core/test/unit/com/structurizr/analysis/reflections/supportingTypes/myapp/data/MyRepository.java deleted file mode 100644 index 5f0b0cf90..000000000 --- a/structurizr-core/test/unit/com/structurizr/analysis/reflections/supportingTypes/myapp/data/MyRepository.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.structurizr.analysis.reflections.supportingTypes.myapp.data; - -import com.structurizr.annotation.Component; - -@Component -public interface MyRepository { -} diff --git a/structurizr-core/test/unit/com/structurizr/analysis/reflections/supportingTypes/myapp/data/MyRepositoryImpl.java b/structurizr-core/test/unit/com/structurizr/analysis/reflections/supportingTypes/myapp/data/MyRepositoryImpl.java deleted file mode 100644 index f37daa89d..000000000 --- a/structurizr-core/test/unit/com/structurizr/analysis/reflections/supportingTypes/myapp/data/MyRepositoryImpl.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.structurizr.analysis.reflections.supportingTypes.myapp.data; - -import com.structurizr.analysis.reflections.supportingTypes.myapp.AbstractComponent; - -class MyRepositoryImpl extends AbstractComponent implements MyRepository { - - private MyRepositoryRowMapper rowMapper; - -} diff --git a/structurizr-core/test/unit/com/structurizr/analysis/reflections/supportingTypes/myapp/data/MyRepositoryRowMapper.java b/structurizr-core/test/unit/com/structurizr/analysis/reflections/supportingTypes/myapp/data/MyRepositoryRowMapper.java deleted file mode 100644 index 235e2e5a2..000000000 --- a/structurizr-core/test/unit/com/structurizr/analysis/reflections/supportingTypes/myapp/data/MyRepositoryRowMapper.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.structurizr.analysis.reflections.supportingTypes.myapp.data; - -import com.structurizr.analysis.reflections.supportingTypes.myapp.util.RowMapperHelper; - -class MyRepositoryRowMapper { - - private RowMapperHelper helper; - -} diff --git a/structurizr-core/test/unit/com/structurizr/analysis/reflections/supportingTypes/myapp/util/RowMapperHelper.java b/structurizr-core/test/unit/com/structurizr/analysis/reflections/supportingTypes/myapp/util/RowMapperHelper.java deleted file mode 100644 index 23ffbbc60..000000000 --- a/structurizr-core/test/unit/com/structurizr/analysis/reflections/supportingTypes/myapp/util/RowMapperHelper.java +++ /dev/null @@ -1,4 +0,0 @@ -package com.structurizr.analysis.reflections.supportingTypes.myapp.util; - -public class RowMapperHelper { -} diff --git a/structurizr-core/test/unit/com/structurizr/analysis/reflections/supportingTypes/myapp/web/MyController.java b/structurizr-core/test/unit/com/structurizr/analysis/reflections/supportingTypes/myapp/web/MyController.java deleted file mode 100644 index bfa706a20..000000000 --- a/structurizr-core/test/unit/com/structurizr/analysis/reflections/supportingTypes/myapp/web/MyController.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.structurizr.analysis.reflections.supportingTypes.myapp.web; - -import com.structurizr.analysis.reflections.supportingTypes.myapp.AbstractComponent; -import com.structurizr.analysis.reflections.supportingTypes.myapp.data.MyRepository; -import com.structurizr.annotation.Component; - -@Component -class MyController extends AbstractComponent { - - private MyRepository myRepository; - -} diff --git a/structurizr-core/test/unit/com/structurizr/api/ApiErrorTests.java b/structurizr-core/test/unit/com/structurizr/api/ApiErrorTests.java deleted file mode 100644 index dd23b4b21..000000000 --- a/structurizr-core/test/unit/com/structurizr/api/ApiErrorTests.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.structurizr.api; - -import org.junit.Test; - -import static org.junit.Assert.assertEquals; - -public class ApiErrorTests { - - @Test - public void test_parse_createsAnApiErrorObjectWithTheSpecifiedErrorMessage() throws Exception { - ApiError apiError = ApiError.parse("{\"message\": \"Hello\"}"); - assertEquals("Hello", apiError.getMessage()); - } - -} diff --git a/structurizr-core/test/unit/com/structurizr/api/HmacContentTests.java b/structurizr-core/test/unit/com/structurizr/api/HmacContentTests.java deleted file mode 100644 index 7397d0ed0..000000000 --- a/structurizr-core/test/unit/com/structurizr/api/HmacContentTests.java +++ /dev/null @@ -1,20 +0,0 @@ -package com.structurizr.api; - -import org.junit.Test; - -import static org.junit.Assert.assertEquals; - -public class HmacContentTests { - - - @Test - public void test_toString_WhenThereAreNoStrings() { - assertEquals("", new HmacContent().toString()); - } - - @Test - public void test_toString_WhenThereAreSomeStrings() { - assertEquals("String1\nString2\nString3\n", new HmacContent("String1", "String2", "String3").toString()); - } - -} diff --git a/structurizr-core/test/unit/com/structurizr/api/Md5DigestTests.java b/structurizr-core/test/unit/com/structurizr/api/Md5DigestTests.java deleted file mode 100644 index 6cf3d737f..000000000 --- a/structurizr-core/test/unit/com/structurizr/api/Md5DigestTests.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.structurizr.api; - -import org.junit.Test; - -import static org.junit.Assert.assertEquals; - -public class Md5DigestTests { - - private Md5Digest md5 = new Md5Digest(); - - @Test - public void test_generate_TreatsNullAsEmptyContent() throws Exception { - assertEquals(md5.generate(null), md5.generate("")); - } - - @Test - public void test_generate() throws Exception { - assertEquals("ed076287532e86365e841e92bfc50d8c", md5.generate("Hello World!")); - assertEquals("d41d8cd98f00b204e9800998ecf8427e", md5.generate("")); - } - -} \ No newline at end of file diff --git a/structurizr-core/test/unit/com/structurizr/api/StructurizrClientTests.java b/structurizr-core/test/unit/com/structurizr/api/StructurizrClientTests.java deleted file mode 100644 index c7da484e8..000000000 --- a/structurizr-core/test/unit/com/structurizr/api/StructurizrClientTests.java +++ /dev/null @@ -1,54 +0,0 @@ -package com.structurizr.api; - -import com.structurizr.Workspace; -import org.junit.Test; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.fail; - -public class StructurizrClientTests { - - private StructurizrClient structurizrClient; - - @Test - public void test_setUrl_RemovesTheTrailingSlash_WhenATrailingSlashIsAdded() { - structurizrClient = new StructurizrClient("https://api.structurizr.com/", "key", "secret"); - assertEquals("https://api.structurizr.com", structurizrClient.getUrl()); - - structurizrClient.setUrl("https://api.structurizr.com/"); - assertEquals("https://api.structurizr.com", structurizrClient.getUrl()); - } - - @Test - public void test_setUrl() { - structurizrClient = new StructurizrClient("https://api.structurizr.com", "key", "secret"); - assertEquals("https://api.structurizr.com", structurizrClient.getUrl()); - - structurizrClient.setUrl("https://api.structurizr.com"); - assertEquals("https://api.structurizr.com", structurizrClient.getUrl()); - } - - @Test(expected = StructurizrClientException.class) - public void test_putWorkspace_ThrowsAnException_WhenANullWorkspaceIsSpecified() throws Exception { - structurizrClient = new StructurizrClient("https://api.structurizr.com", "key", "secret"); - structurizrClient.putWorkspace(1234, null); - } - - @Test(expected = StructurizrClientException.class) - public void test_putWorkspace_ThrowsAnException_WhenTheWorkspaceIdIsNotSet() throws Exception { - structurizrClient = new StructurizrClient("https://api.structurizr.com", "key", "secret"); - Workspace workspace = new Workspace("Name", "Description"); - structurizrClient.putWorkspace(0, workspace); - } - - @Test - public void test_constructionWithAPropertiesFile_ThrowsAnException_WhenNoPropertiesAreFound() { - try { - structurizrClient = new StructurizrClient(); - fail(); - } catch (Exception e) { - assertEquals("Could not find a structurizr.properties file on the classpath.", e.getMessage()); - } - } - -} diff --git a/structurizr-core/test/unit/com/structurizr/documentation/Arc42DocumentationTemplateTests.java b/structurizr-core/test/unit/com/structurizr/documentation/Arc42DocumentationTemplateTests.java deleted file mode 100644 index 575a01ddd..000000000 --- a/structurizr-core/test/unit/com/structurizr/documentation/Arc42DocumentationTemplateTests.java +++ /dev/null @@ -1,128 +0,0 @@ -package com.structurizr.documentation; - -import com.structurizr.AbstractWorkspaceTestBase; -import com.structurizr.model.Element; -import com.structurizr.model.SoftwareSystem; -import org.junit.Before; -import org.junit.Test; - -import java.io.File; -import java.io.IOException; - -import static org.junit.Assert.*; - -public class Arc42DocumentationTemplateTests extends AbstractWorkspaceTestBase { - - private SoftwareSystem softwareSystem; - private Arc42DocumentationTemplate template; - - @Before - public void setUp() { - softwareSystem = workspace.getModel().addSoftwareSystem("Name", "Description"); - template = new Arc42DocumentationTemplate(workspace); - } - - @Test - public void test_construction_ThrowsAnException_WhenANullWorkspaceIsSpecified() { - try { - new Arc42DocumentationTemplate(null); - fail(); - } catch (Exception e) { - assertEquals("A workspace must be specified.", e.getMessage()); - } - } - - @Test - public void test_addAllSectionsWithContentAsStrings() { - Section section; - - section = template.addIntroductionAndGoalsSection(softwareSystem, Format.Markdown, "Section 1"); - assertSection(softwareSystem, "Introduction and Goals", Format.Markdown, "Section 1", 1, section); - - section = template.addConstraintsSection(softwareSystem, Format.Markdown, "Section 2"); - assertSection(softwareSystem, "Constraints", Format.Markdown, "Section 2", 2, section); - - section = template.addContextAndScopeSection(softwareSystem, Format.Markdown, "Section 3"); - assertSection(softwareSystem, "Context and Scope", Format.Markdown, "Section 3", 3, section); - - section = template.addSolutionStrategySection(softwareSystem, Format.Markdown, "Section 4"); - assertSection(softwareSystem, "Solution Strategy", Format.Markdown, "Section 4", 4, section); - - section = template.addBuildingBlockViewSection(softwareSystem, Format.Markdown, "Section 5"); - assertSection(softwareSystem, "Building Block View", Format.Markdown, "Section 5", 5, section); - - section = template.addRuntimeViewSection(softwareSystem, Format.Markdown, "Section 6"); - assertSection(softwareSystem, "Runtime View", Format.Markdown, "Section 6", 6, section); - - section = template.addDeploymentViewSection(softwareSystem, Format.Markdown, "Section 7"); - assertSection(softwareSystem, "Deployment View", Format.Markdown, "Section 7", 7, section); - - section = template.addCrosscuttingConceptsSection(softwareSystem, Format.Markdown, "Section 8"); - assertSection(softwareSystem, "Crosscutting Concepts", Format.Markdown, "Section 8", 8, section); - - section = template.addArchitecturalDecisionsSection(softwareSystem, Format.Markdown, "Section 9"); - assertSection(softwareSystem, "Architectural Decisions", Format.Markdown, "Section 9", 9, section); - - section = template.addQualityRequirementsSection(softwareSystem, Format.Markdown, "Section 10"); - assertSection(softwareSystem, "Quality Requirements", Format.Markdown, "Section 10", 10, section); - - section = template.addRisksAndTechnicalDebtSection(softwareSystem, Format.Markdown, "Section 11"); - assertSection(softwareSystem, "Risks and Technical Debt", Format.Markdown, "Section 11", 11, section); - - section = template.addGlossarySection(softwareSystem, Format.Markdown, "Section 12"); - assertSection(softwareSystem, "Glossary", Format.Markdown, "Section 12", 12, section); - } - - @Test - public void test_addAllSectionsWithContentFromFiles() throws IOException { - Section section; - File root = new File(".//test/unit/com/structurizr/documentation/arc42"); - - section = template.addIntroductionAndGoalsSection(softwareSystem, new File(root, "introduction-and-goals.md")); - assertSection(softwareSystem, "Introduction and Goals", Format.Markdown, "Section 1", 1, section); - - section = template.addConstraintsSection(softwareSystem, new File(root, "constraints.md")); - assertSection(softwareSystem, "Constraints", Format.Markdown, "Section 2", 2, section); - - section = template.addContextAndScopeSection(softwareSystem, new File(root, "context-and-scope.md")); - assertSection(softwareSystem, "Context and Scope", Format.Markdown, "Section 3", 3, section); - - section = template.addSolutionStrategySection(softwareSystem, new File(root, "solution-strategy.md")); - assertSection(softwareSystem, "Solution Strategy", Format.Markdown, "Section 4", 4, section); - - section = template.addBuildingBlockViewSection(softwareSystem, new File(root, "building-block-view.md")); - assertSection(softwareSystem, "Building Block View", Format.Markdown, "Section 5", 5, section); - - section = template.addRuntimeViewSection(softwareSystem, new File(root, "runtime-view.md")); - assertSection(softwareSystem, "Runtime View", Format.Markdown, "Section 6", 6, section); - - section = template.addDeploymentViewSection(softwareSystem, new File(root, "deployment-view.md")); - assertSection(softwareSystem, "Deployment View", Format.Markdown, "Section 7", 7, section); - - section = template.addCrosscuttingConceptsSection(softwareSystem, new File(root, "crosscutting-concepts.md")); - assertSection(softwareSystem, "Crosscutting Concepts", Format.Markdown, "Section 8", 8, section); - - section = template.addArchitecturalDecisionsSection(softwareSystem, new File(root, "architectural-decisions.md")); - assertSection(softwareSystem, "Architectural Decisions", Format.Markdown, "Section 9", 9, section); - - section = template.addQualityRequirementsSection(softwareSystem, new File(root, "quality-requirements.md")); - assertSection(softwareSystem, "Quality Requirements", Format.Markdown, "Section 10", 10, section); - - section = template.addRisksAndTechnicalDebtSection(softwareSystem, new File(root, "risks-and-technical-debt.md")); - assertSection(softwareSystem, "Risks and Technical Debt", Format.Markdown, "Section 11", 11, section); - - section = template.addGlossarySection(softwareSystem, new File(root, "glossary.md")); - assertSection(softwareSystem, "Glossary", Format.Markdown, "Section 12", 12, section); - } - - private void assertSection(Element element, String type, Format format, String content, int order, Section section) { - assertTrue(workspace.getDocumentation().getSections().contains(section)); - assertEquals(element, section.getElement()); - assertEquals(element.getId(), section.getElementId()); - assertEquals(type, section.getType()); - assertEquals(format, section.getFormat()); - assertEquals(content, section.getContent()); - assertEquals(order, section.getOrder()); - } - -} \ No newline at end of file diff --git a/structurizr-core/test/unit/com/structurizr/documentation/AutomaticDocumentTemplateTests.java b/structurizr-core/test/unit/com/structurizr/documentation/AutomaticDocumentTemplateTests.java deleted file mode 100644 index 51f09c578..000000000 --- a/structurizr-core/test/unit/com/structurizr/documentation/AutomaticDocumentTemplateTests.java +++ /dev/null @@ -1,84 +0,0 @@ -package com.structurizr.documentation; - -import com.structurizr.AbstractWorkspaceTestBase; -import com.structurizr.model.Element; -import com.structurizr.model.SoftwareSystem; -import org.junit.Before; -import org.junit.Test; - -import java.io.File; -import java.io.IOException; -import java.util.List; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; - -public class AutomaticDocumentTemplateTests extends AbstractWorkspaceTestBase { - - private SoftwareSystem softwareSystem; - private AutomaticDocumentationTemplate template; - - @Before - public void setUp() { - softwareSystem = workspace.getModel().addSoftwareSystem("Name", "Description"); - template = new AutomaticDocumentationTemplate(workspace); - } - - @Test - public void test_construction_ThrowsAnException_WhenANullWorkspaceIsSpecified() { - try { - new AutomaticDocumentationTemplate(null); - fail(); - } catch (IllegalArgumentException iae) { - assertEquals("A workspace must be specified.", iae.getMessage()); - } - } - - @Test - public void test_addSections_ThrowsAnException_WhenTheDirectoryDoesNotExist() throws Exception { - try { - new AutomaticDocumentationTemplate(workspace).addSections(new File(".//test/unit/com/structurizr/documentation/foo")); - fail(); - } catch (IllegalArgumentException iae) { - assertTrue(iae.getMessage().endsWith("foo does not exist.")); - } - } - - @Test - public void test_addSections_ThrowsAnException_WhenTheDirectoryIsNotADirectory() throws Exception { - try { - new AutomaticDocumentationTemplate(workspace).addSections(new File(".//test/unit/com/structurizr/documentation/automatic/01-section-1.md")); - fail(); - } catch (IllegalArgumentException iae) { - assertTrue(iae.getMessage().endsWith("01-section-1.md is not a directory.")); - } - } - - @Test - public void test_addSections() throws IOException { - Section section; - File root = new File(".//test/unit/com/structurizr/documentation/automatic"); - - List
sections = template.addSections(softwareSystem, root); - assertEquals(6, sections.size()); - - assertSection(softwareSystem, "Section 1", Format.Markdown, "## Section 1", 1, sections.get(0)); - assertSection(softwareSystem, "Section 2", Format.Markdown, "## Section 2", 2, sections.get(1)); - assertSection(softwareSystem, "Section 3", Format.Markdown, "## Section 3", 3, sections.get(2)); - assertSection(softwareSystem, "Section 4", Format.AsciiDoc, "== Section 4", 4, sections.get(3)); - assertSection(softwareSystem, "Section 5", Format.AsciiDoc, "== Section 5", 5, sections.get(4)); - assertSection(softwareSystem, "Section 6", Format.AsciiDoc, "== Section 6", 6, sections.get(5)); - } - - private void assertSection(Element element, String type, Format format, String content, int order, Section section) { - assertTrue(workspace.getDocumentation().getSections().contains(section)); - assertEquals(element, section.getElement()); - assertEquals(element.getId(), section.getElementId()); - assertEquals(type, section.getType()); - assertEquals(format, section.getFormat()); - assertEquals(content, section.getContent()); - assertEquals(order, section.getOrder()); - } - -} diff --git a/structurizr-core/test/unit/com/structurizr/documentation/DocumentationTemplateTests.java b/structurizr-core/test/unit/com/structurizr/documentation/DocumentationTemplateTests.java deleted file mode 100644 index 2c803af27..000000000 --- a/structurizr-core/test/unit/com/structurizr/documentation/DocumentationTemplateTests.java +++ /dev/null @@ -1,249 +0,0 @@ -package com.structurizr.documentation; - -import com.structurizr.AbstractWorkspaceTestBase; -import com.structurizr.Workspace; -import com.structurizr.model.SoftwareSystem; -import org.junit.Before; -import org.junit.Test; - -import java.io.File; -import java.io.IOException; -import java.util.Collection; - -import static org.junit.Assert.*; - -public class DocumentationTemplateTests extends AbstractWorkspaceTestBase { - - private SoftwareSystem softwareSystem; - private StructurizrDocumentationTemplate template; - private Documentation documentation; - - @Before - public void setUp() { - Workspace workspace = new Workspace("Name", "Description"); - softwareSystem = workspace.getModel().addSoftwareSystem("Name", "Description"); - - template = new StructurizrDocumentationTemplate(workspace); - documentation = workspace.getDocumentation(); - } - - @Test - public void test_addSection_ThrowsAnException_WhenThatSectionAlreadyExists() { - template.addContextSection(softwareSystem, Format.Markdown, "Some Markdown content"); - assertEquals(1, documentation.getSections().size()); - - try { - template.addContextSection(softwareSystem, Format.Markdown, "Some Markdown content"); - fail(); - } catch (IllegalArgumentException iae) { - // this is the expected exception - assertEquals("A section of type Context for Name already exists.", iae.getMessage()); - assertEquals(1, documentation.getSections().size()); - } - } - - @Test - public void test_addImages_DoesNothing_WhenThereAreNoImageFilesInTheSpecifiedDirectory() throws IOException { - template.addImages(new File(".//test/unit/com/structurizr/documentation/noimages")); - assertTrue(documentation.getImages().isEmpty()); - } - - @Test - public void test_addImages_ThrowsAnException_WhenTheSpecifiedDirectoryIsNull() throws IOException { - try { - template.addImages(null); - fail(); - } catch (IllegalArgumentException iae) { - assertEquals("Directory path must not be null.", iae.getMessage()); - } - } - - @Test - public void test_addImages_ThrowsAnException_WhenTheSpecifiedDirectoryIsNotADirectory() throws IOException { - try { - template.addImages(new File(".//test/unit/com/structurizr/documentation/structurizr/context.md")); - fail(); - } catch (IllegalArgumentException iae) { - assertTrue(iae.getMessage().endsWith("context.md is not a directory.")); - } - } - - @Test - public void test_addImages_ThrowsAnException_WhenTheSpecifiedDirectoryDoesNotExist() throws IOException { - try { - template.addImages(new File(".//test/unit/com/structurizr/documentation/blah")); - fail(); - } catch (IllegalArgumentException iae) { - assertTrue(iae.getMessage().endsWith("blah does not exist.")); - } - } - - @Test - public void test_addImages_AddsAllImagesFromTheSpecifiedDirectory_WhenThereAreImageFilesInTheSpecifiedDirectory() throws IOException { - assertTrue(documentation.getImages().isEmpty()); - Collection images = template.addImages(new File(".//test/unit/com/structurizr/documentation/images")); - assertEquals(4, documentation.getImages().size()); - assertEquals(4, images.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_addImages_AddsAllImagesFromTheSpecifiedDirectory_WhenThereAreImageFilesInTheSpecifiedDirectoryAndSubDirectories() throws IOException { - assertTrue(documentation.getImages().isEmpty()); - template.addImages(new File(".//test/unit/com/structurizr/documentation")); - assertEquals(8, 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 pngInSubDirectory = documentation.getImages().stream().filter(i -> i.getName().equals("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("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("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("image.gif")).findFirst().get(); - assertEquals("image/gif", gifInSubDirectory.getType()); - assertEquals("R0lGODlhIAAaAPcAAAAAAAACCwAFHAAGFAAGIwAIFgAKHAAKJgAMKgAOMwAPPAARHwAUOQ0UHQIVMgMVJQMVLAoVKwwVJBEVHgAYOAQYJwkYORAYJQIZKwoZJQMaMgoaKwobMxQcKQsjRwMnVxcoPBsoOAAtWx8vQAAwXwIzaQ9HehZLhEVMWRRNjB5Ng0VNYUtQWkFRXhZShBVTjRRVkxlVjhtVlBtWmQpXoxFXmxZYmRFZpBhZjxpZlRValRlamAdcrgxcqg1cpCFdmw1esRReqhleqx9eoyhenhNgrAxitBlirxNktCZknw5luxllsyJlrBJmuhhmuCNnsCVnpBRptRRpvBxpryNprhVqwhtrvBxrtSJrtRxtwCVuuyFytCZ0xid0uit0wCV4xi97xDh9xDGAyzuAy0KBy0SCw0iDw0aG0EuGyjuI2EuM1EiN2EOO2EaP1USR26SipZ+kqKSmsqamrGGq76SquG+w9Gy0/Im05nK183m1+Xa2+YS27YW28YS3+Iq38Iu37HO59Iq57HS6/oq683277IS79IW77ZG77YS8+3u9/Iu++Y7A9X7B/4PB8oLC/ozC/PX3///39f///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACwAAAAAIAAaAEcI/wAlxUEhYQMGCAghaMDw4KCDCBAwVEgIYYNCDRxWxIGU0AFCDAhOMFnSREkVIEB69PDh48YNlTyMKGly5QSBCw4cYDgI4QECGFaaINkCqFCgQkiNDkKEaNAeRWaCwCDQQE6AAhY1IKTg4AABAgcOCPhKloCAs2QdGAggZ+dChg8yQNyw4cEDCRIiRMhQAYOGvxYrPNhwAAKHnQnjHnhxRQqSK4mQGkpaaJBlp4jOBJFxQILWhoMlIJBxJCgSMHWYIkK6FFEiRoLYBNGSojOLASNAa+XwFrHHiBhy5oQQwe6GEANaIOSQUIMDBSVO5MgBQ8aMGTJkxMg+w4YNGDhckP9g0BMxwsEHYlxBIsWIGDuMVDNVNAhpIjc9rMw4EGFi4gw/GVEFEl7o8cgdhkxGmWSIoCEEDAg8MBxihMEghRJSXCEIInwkZUggRxUyGSJhIAHDARlA4NFgGKQoQhJUXHGFFUgEYeONQQghRBBLWPEEER9AUFUAA1j0gIq/AafiRzwlttMDAgxAR4pHPpkQQ31RtJNwHvWXgXFKfrTABg4gABYCaKYZFpoJJFAmBAQgoFVxCGnlnAMlDIEFe1b02ScWgGJhRRZ9RjEFFCLE1dFHCLywpxRN8MBFGmyw0UYbamS6hhtfCOhECgRIYF5PGSx2RRRR9DCHI5FR5scfgUy/pgcWS8hAgEFNKqZDFFIA0QUehfyRIGWWjUjGZhH6RxwEANYgxYBa5LHHIcOKmBQhjRSCRg0/3GrBik/+FJQRv1ZGGYhMDbJIIWUUYUNnCBwmGHpASSGFqo/04SGIhazmiCB77nDARFrBJQEBIi2BBBI0YBFGGRBHTAYZY2jRwxJMqCBABysIAAKTwYlgwgsvwADDDtmZXF12271wgggeGMBCJG+gcJGdGGyQwc4ZNIAXzxlIMEEDO9OFAhySBAQAOw==", gifInSubDirectory.getContent()); - } - - @Test - public void test_addImage_AddsTheSpecifiedImage_WhenTheSpecifiedFileExists() throws IOException { - assertTrue(documentation.getImages().isEmpty()); - template.addImage(new File(".//test/unit/com/structurizr/documentation/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_addImage_ThrowsAnException_WhenTheSpecifiedFileIsNull() throws IOException { - try { - template.addImage(null); - fail(); - } catch (IllegalArgumentException iae) { - assertEquals("A file must be specified.", iae.getMessage()); - } - } - - @Test - public void test_addImage_ThrowsAnException_WhenTheSpecifiedFileIsNotAFile() throws IOException { - try { - template.addImage(new File(".//test/unit/com/structurizr/documentation/")); - fail(); - } catch (IllegalArgumentException iae) { - assertTrue(iae.getMessage().endsWith("documentation is not a file.")); - } - } - - @Test - public void test_addImage_ThrowsAnException_WhenTheSpecifiedFileDoesNotExist() throws IOException { - try { - template.addImage(new File(".//test/unit/com/structurizr/documentation/some-other-image.png")); - fail(); - } catch (IllegalArgumentException iae) { - assertTrue(iae.getMessage().endsWith("some-other-image.png does not exist.")); - } - } - - @Test - public void test_addImage_ThrowsAnException_WhenTheSpecifiedFileIsNotAnImage() throws IOException { - try { - template.addImage(new File(".//test/unit/com/structurizr/documentation/structurizr/context.md")); - fail(); - } catch (IllegalArgumentException iae) { - assertTrue(iae.getMessage().endsWith("context.md is not a supported image file.")); - } - } - - @Test - public void test_readFiles_ThrowsAnException_WhenPassedAFileThatDoesNotExist() throws IOException { - try { - template.addContextSection(softwareSystem, new File("./no-such-file.txt")); - fail(); - } catch (IllegalArgumentException iae) { - assertTrue(iae.getMessage().endsWith("no-such-file.txt does not exist.")); - } - } - - @Test - public void test_readFiles_ThrowsAnException_WhenPassedANullFile() throws IOException { - try { - template.addContextSection(softwareSystem, (File)null); - fail(); - } catch (IllegalArgumentException iae) { - assertEquals("One or more files must be specified.", iae.getMessage()); - } - } - - @Test - public void test_readFiles_ThrowsAnException_WhenPassedAnEmptyCollection() throws IOException { - try { - template.addContextSection(softwareSystem, new File[]{}); - fail(); - } catch (IllegalArgumentException iae) { - assertEquals("One or more files must be specified.", iae.getMessage()); - } - } - - @Test - public void test_readFiles_AddsAllFiles_WhenPassedADirectory() throws IOException { - Section section = template.addContextSection(softwareSystem, new File(".//test/unit/com/structurizr/documentation/markdown")); - assertEquals("File 1" + System.lineSeparator() + - "File 2", section.getContent()); - } - - @Test - public void test_addSection_AddsASectionWithAGroupOf1_WhenAGroupLessThan1IsSpecified() { - Section section = template.addSection(softwareSystem, "Custom Section", 0, Format.Markdown, "Custom content"); - assertEquals(1, section.getGroup()); - } - - @Test - public void test_addSection_AddsASectionWithAGroupOf5_WhenAGroupMoreThan5IsSpecified() { - Section section = template.addSection(softwareSystem, "Custom Section", 6, Format.Markdown, "Custom content"); - assertEquals(5, section.getGroup()); - } - -} \ No newline at end of file diff --git a/structurizr-core/test/unit/com/structurizr/documentation/EncryptedJsonWriterTests.java b/structurizr-core/test/unit/com/structurizr/documentation/EncryptedJsonWriterTests.java deleted file mode 100644 index f988f72fd..000000000 --- a/structurizr-core/test/unit/com/structurizr/documentation/EncryptedJsonWriterTests.java +++ /dev/null @@ -1,40 +0,0 @@ -package com.structurizr.documentation; - -import com.structurizr.Workspace; -import com.structurizr.encryption.AesEncryptionStrategy; -import com.structurizr.encryption.EncryptedJsonWriter; -import com.structurizr.encryption.EncryptedWorkspace; -import org.junit.Test; - -import java.io.StringWriter; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.fail; - -public class EncryptedJsonWriterTests { - - @Test - public void test_write_ThrowsAnIllegalArgumentException_WhenANullEncryptedWorkspaceIsSpecified() throws Exception { - try { - EncryptedJsonWriter writer = new EncryptedJsonWriter(true); - writer.write(null, new StringWriter()); - fail(); - } catch (IllegalArgumentException e) { - assertEquals("EncryptedWorkspace cannot be null.", e.getMessage()); - } - } - - @Test - public void test_write_ThrowsAnIllegalArgumentException_WhenANullWriterIsSpecified() throws Exception { - try { - EncryptedJsonWriter writer = new EncryptedJsonWriter(true); - Workspace workspace = new Workspace("Name", "Description"); - EncryptedWorkspace encryptedWorkspace = new EncryptedWorkspace(workspace, new AesEncryptionStrategy("password")); - writer.write(encryptedWorkspace, null); - fail(); - } catch (IllegalArgumentException e) { - assertEquals("Writer cannot be null.", e.getMessage()); - } - } - -} diff --git a/structurizr-core/test/unit/com/structurizr/documentation/FormatFinderTests.java b/structurizr-core/test/unit/com/structurizr/documentation/FormatFinderTests.java deleted file mode 100644 index c4d1a7df6..000000000 --- a/structurizr-core/test/unit/com/structurizr/documentation/FormatFinderTests.java +++ /dev/null @@ -1,36 +0,0 @@ -package com.structurizr.documentation; - -import org.junit.Test; - -import java.io.File; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.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() { - 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-core/test/unit/com/structurizr/documentation/StructurizrDocumentationTemplateTests.java b/structurizr-core/test/unit/com/structurizr/documentation/StructurizrDocumentationTemplateTests.java deleted file mode 100644 index f43f5e8f8..000000000 --- a/structurizr-core/test/unit/com/structurizr/documentation/StructurizrDocumentationTemplateTests.java +++ /dev/null @@ -1,171 +0,0 @@ -package com.structurizr.documentation; - -import com.structurizr.AbstractWorkspaceTestBase; -import com.structurizr.model.Component; -import com.structurizr.model.Container; -import com.structurizr.model.Element; -import com.structurizr.model.SoftwareSystem; -import org.junit.Before; -import org.junit.Test; - -import java.io.File; -import java.io.IOException; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; - -public class StructurizrDocumentationTemplateTests extends AbstractWorkspaceTestBase { - - private SoftwareSystem softwareSystem; - private Container containerA; - private Container containerB; - private Component componentA1; - private Component componentA2; - private StructurizrDocumentationTemplate template; - - @Before - public void setUp() { - softwareSystem = workspace.getModel().addSoftwareSystem("Name", "Description"); - containerA = softwareSystem.addContainer("Container A", "Description", "Technology"); - containerB = softwareSystem.addContainer("Container B", "Description", "Technology"); - componentA1 = containerA.addComponent("Component A1", "Description", "Technology"); - componentA2 = containerA.addComponent("Component A2", "Description", "Technology"); - template = new StructurizrDocumentationTemplate(workspace); - } - - @Test - public void test_construction_ThrowsAnException_WhenANullWorkspaceIsSpecified() { - try { - new StructurizrDocumentationTemplate(null); - fail(); - } catch (Exception e) { - assertEquals("A workspace must be specified.", e.getMessage()); - } - } - - @Test - public void test_addAllSectionsWithContentAsStrings() { - Section section; - - section = template.addContextSection(softwareSystem, Format.Markdown, "Context section"); - assertSection(softwareSystem, "Context", 1, Format.Markdown, "Context section", 1, section); - - section = template.addFunctionalOverviewSection(softwareSystem, Format.Markdown, "Functional overview section"); - assertSection(softwareSystem, "Functional Overview", 2, Format.Markdown, "Functional overview section", 2, section); - - section = template.addQualityAttributesSection(softwareSystem, Format.Markdown, "Quality attributes section"); - assertSection(softwareSystem, "Quality Attributes", 2, Format.Markdown, "Quality attributes section", 3, section); - - section = template.addConstraintsSection(softwareSystem, Format.Markdown, "Constraints section"); - assertSection(softwareSystem, "Constraints", 2, Format.Markdown, "Constraints section", 4, section); - - section = template.addPrinciplesSection(softwareSystem, Format.Markdown, "Principles section"); - assertSection(softwareSystem, "Principles", 2, Format.Markdown, "Principles section", 5, section); - - section = template.addSoftwareArchitectureSection(softwareSystem, Format.Markdown, "Software architecture section"); - assertSection(softwareSystem, "Software Architecture", 3, Format.Markdown, "Software architecture section", 6, section); - - section = template.addContainersSection(softwareSystem, Format.Markdown, "Containers section"); - assertSection(softwareSystem, "Containers", 3, Format.Markdown, "Containers section", 7, section); - - section = template.addComponentsSection(containerA, Format.Markdown, "Components section for container A"); - assertSection(containerA, "Components", 3, Format.Markdown, "Components section for container A", 8, section); - - section = template.addComponentsSection(containerB, Format.Markdown, "Components section for container B"); - assertSection(containerB, "Components", 3, Format.Markdown, "Components section for container B", 9, section); - - section = template.addCodeSection(componentA1, Format.Markdown, "Code section for component A1"); - assertSection(componentA1, "Code", 3, Format.Markdown, "Code section for component A1", 10, section); - - section = template.addCodeSection(componentA2, Format.Markdown, "Code section for component A2"); - assertSection(componentA2, "Code", 3, Format.Markdown, "Code section for component A2", 11, section); - - section = template.addDataSection(softwareSystem, Format.Markdown, "Data section"); - assertSection(softwareSystem, "Data", 3, Format.Markdown, "Data section", 12, section); - - section = template.addInfrastructureArchitectureSection(softwareSystem, Format.Markdown, "Infrastructure architecture section"); - assertSection(softwareSystem, "Infrastructure Architecture", 4, Format.Markdown, "Infrastructure architecture section", 13, section); - - section = template.addDeploymentSection(softwareSystem, Format.Markdown, "Deployment section"); - assertSection(softwareSystem, "Deployment", 4, Format.Markdown, "Deployment section", 14, section); - - section = template.addDevelopmentEnvironmentSection(softwareSystem, Format.Markdown, "Development environment section"); - assertSection(softwareSystem, "Development Environment", 4, Format.Markdown, "Development environment section", 15, section); - - section = template.addOperationAndSupportSection(softwareSystem, Format.Markdown, "Operation and support section"); - assertSection(softwareSystem, "Operation and Support", 4, Format.Markdown, "Operation and support section", 16, section); - - section = template.addDecisionLogSection(softwareSystem, Format.Markdown, "Decision log section"); - assertSection(softwareSystem, "Decision Log", 5, Format.Markdown, "Decision log section", 17, section); - } - - @Test - public void test_addAllSectionsWithContentFromFiles() throws IOException { - Section section; - File root = new File(".//test/unit/com/structurizr/documentation/structurizr"); - - section = template.addContextSection(softwareSystem, new File(root, "context.md")); - assertSection(softwareSystem, "Context", 1, Format.Markdown, "Context section", 1, section); - - section = template.addFunctionalOverviewSection(softwareSystem, new File(root, "functional-overview.md")); - assertSection(softwareSystem, "Functional Overview", 2, Format.Markdown, "Functional overview section", 2, section); - - section = template.addQualityAttributesSection(softwareSystem, new File(root, "quality-attributes.md")); - assertSection(softwareSystem, "Quality Attributes", 2, Format.Markdown, "Quality attributes section", 3, section); - - section = template.addConstraintsSection(softwareSystem, new File(root, "constraints.md")); - assertSection(softwareSystem, "Constraints", 2, Format.Markdown, "Constraints section", 4, section); - - section = template.addPrinciplesSection(softwareSystem, new File(root, "principles.md")); - assertSection(softwareSystem, "Principles", 2, Format.Markdown, "Principles section", 5, section); - - section = template.addSoftwareArchitectureSection(softwareSystem, new File(root, "software-architecture.md")); - assertSection(softwareSystem, "Software Architecture", 3, Format.Markdown, "Software architecture section", 6, section); - - section = template.addContainersSection(softwareSystem, new File(root, "containers.md")); - assertSection(softwareSystem, "Containers", 3, Format.Markdown, "Containers section", 7, section); - - section = template.addComponentsSection(containerA, new File(root, "components-for-containerA.md")); - assertSection(containerA, "Components", 3, Format.Markdown, "Components section for container A", 8, section); - - section = template.addComponentsSection(containerB, new File(root, "components-for-containerB.md")); - assertSection(containerB, "Components", 3, Format.Markdown, "Components section for container B", 9, section); - - section = template.addCodeSection(componentA1, new File(root, "code-for-componentA1.md")); - assertSection(componentA1, "Code", 3, Format.Markdown, "Code section for component A1", 10, section); - - section = template.addCodeSection(componentA2, new File(root, "code-for-componentA2.md")); - assertSection(componentA2, "Code", 3, Format.Markdown, "Code section for component A2", 11, section); - - section = template.addDataSection(softwareSystem, new File(root, "data.md")); - assertSection(softwareSystem, "Data", 3, Format.Markdown, "Data section", 12, section); - - section = template.addInfrastructureArchitectureSection(softwareSystem, new File(root, "infrastructure-architecture.md")); - assertSection(softwareSystem, "Infrastructure Architecture", 4, Format.Markdown, "Infrastructure architecture section", 13, section); - - section = template.addDeploymentSection(softwareSystem, new File(root, "deployment.md")); - assertSection(softwareSystem, "Deployment", 4, Format.Markdown, "Deployment section", 14, section); - - section = template.addDevelopmentEnvironmentSection(softwareSystem, new File(root, "development-environment.md")); - assertSection(softwareSystem, "Development Environment", 4, Format.Markdown, "Development environment section", 15, section); - - section = template.addOperationAndSupportSection(softwareSystem, new File(root, "operation-and-support.md")); - assertSection(softwareSystem, "Operation and Support", 4, Format.Markdown, "Operation and support section", 16, section); - - section = template.addDecisionLogSection(softwareSystem, new File(root, "decision-log.md")); - assertSection(softwareSystem, "Decision Log", 5, Format.Markdown, "Decision log section", 17, section); - } - - private void assertSection(Element element, String type, int group, Format format, String content, int order, Section section) { - assertTrue(workspace.getDocumentation().getSections().contains(section)); - assertEquals(element, section.getElement()); - assertEquals(element.getId(), section.getElementId()); - assertEquals(type, section.getType()); - assertEquals(group, section.getGroup()); - assertEquals(format, section.getFormat()); - assertEquals(content, section.getContent()); - assertEquals(order, section.getOrder()); - } - -} \ No newline at end of file diff --git a/structurizr-core/test/unit/com/structurizr/documentation/ViewpointsAndPerspectivesDocumentationTemplateTests.java b/structurizr-core/test/unit/com/structurizr/documentation/ViewpointsAndPerspectivesDocumentationTemplateTests.java deleted file mode 100644 index db1dd9797..000000000 --- a/structurizr-core/test/unit/com/structurizr/documentation/ViewpointsAndPerspectivesDocumentationTemplateTests.java +++ /dev/null @@ -1,99 +0,0 @@ -package com.structurizr.documentation; - -import com.structurizr.AbstractWorkspaceTestBase; -import com.structurizr.Workspace; -import com.structurizr.model.Element; -import com.structurizr.model.SoftwareSystem; -import org.junit.Before; -import org.junit.Test; - -import java.io.File; -import java.io.IOException; - -import static org.junit.Assert.*; - -public class ViewpointsAndPerspectivesDocumentationTemplateTests extends AbstractWorkspaceTestBase { - - private SoftwareSystem softwareSystem; - private ViewpointsAndPerspectivesDocumentationTemplate template; - - @Before - public void setUp() { - softwareSystem = workspace.getModel().addSoftwareSystem("Name", "Description"); - template = new ViewpointsAndPerspectivesDocumentationTemplate(workspace); - } - - @Test - public void test_construction_ThrowsAnException_WhenANullWorkspaceIsSpecified() { - try { - new ViewpointsAndPerspectivesDocumentationTemplate(null); - fail(); - } catch (Exception e) { - assertEquals("A workspace must be specified.", e.getMessage()); - } - } - - @Test - public void test_addAllSectionsWithContentAsStrings() { - Section section; - - section = template.addIntroductionSection(softwareSystem, Format.Markdown, "Section 1"); - assertSection(softwareSystem, "Introduction", Format.Markdown, "Section 1", 1, section); - - section = template.addGlossarySection(softwareSystem, Format.Markdown, "Section 2"); - assertSection(softwareSystem, "Glossary", Format.Markdown, "Section 2", 2, section); - - section = template.addSystemStakeholdersAndRequirementsSection(softwareSystem, Format.Markdown, "Section 3"); - assertSection(softwareSystem, "System Stakeholders and Requirements", Format.Markdown, "Section 3", 3, section); - - section = template.addArchitecturalForcesSection(softwareSystem, Format.Markdown, "Section 4"); - assertSection(softwareSystem, "Architectural Forces", Format.Markdown, "Section 4", 4, section); - - section = template.addArchitecturalViewsSection(softwareSystem, Format.Markdown, "Section 5"); - assertSection(softwareSystem, "Architectural Views", Format.Markdown, "Section 5", 5, section); - - section = template.addSystemQualitiesSection(softwareSystem, Format.Markdown, "Section 6"); - assertSection(softwareSystem, "System Qualities", Format.Markdown, "Section 6", 6, section); - - section = template.addAppendicesSection(softwareSystem, Format.Markdown, "Section 7"); - assertSection(softwareSystem, "Appendices", Format.Markdown, "Section 7", 7, section); - } - - @Test - public void test_addAllSectionsWithContentFromFiles() throws IOException { - Section section; - File root = new File(".//test/unit/com/structurizr/documentation/viewpointsandperspectives"); - - section = template.addIntroductionSection(softwareSystem, new File(root, "01-introduction.md")); - assertSection(softwareSystem, "Introduction", Format.Markdown, "Section 1", 1, section); - - section = template.addGlossarySection(softwareSystem, new File(root, "02-glossary.md")); - assertSection(softwareSystem, "Glossary", Format.Markdown, "Section 2", 2, section); - - section = template.addSystemStakeholdersAndRequirementsSection(softwareSystem, new File(root, "03-system-stakeholders-and-requirements.md")); - assertSection(softwareSystem, "System Stakeholders and Requirements", Format.Markdown, "Section 3", 3, section); - - section = template.addArchitecturalForcesSection(softwareSystem, new File(root, "04-architectural-forces.md")); - assertSection(softwareSystem, "Architectural Forces", Format.Markdown, "Section 4", 4, section); - - section = template.addArchitecturalViewsSection(softwareSystem, new File(root, "05-architectural-views.md")); - assertSection(softwareSystem, "Architectural Views", Format.Markdown, "Section 5", 5, section); - - section = template.addSystemQualitiesSection(softwareSystem, new File(root, "06-system-qualities.md")); - assertSection(softwareSystem, "System Qualities", Format.Markdown, "Section 6", 6, section); - - section = template.addAppendicesSection(softwareSystem, new File(root, "07-appendices.adoc")); - assertSection(softwareSystem, "Appendices", Format.AsciiDoc, "Section 7", 7, section); - } - - private void assertSection(Element element, String type, Format format, String content, int order, Section section) { - assertTrue(workspace.getDocumentation().getSections().contains(section)); - assertEquals(element, section.getElement()); - assertEquals(element.getId(), section.getElementId()); - assertEquals(type, section.getType()); - assertEquals(format, section.getFormat()); - assertEquals(content, section.getContent()); - assertEquals(order, section.getOrder()); - } - -} \ No newline at end of file diff --git a/structurizr-core/test/unit/com/structurizr/documentation/arc42/architectural-decisions.md b/structurizr-core/test/unit/com/structurizr/documentation/arc42/architectural-decisions.md deleted file mode 100644 index 5373ebb11..000000000 --- a/structurizr-core/test/unit/com/structurizr/documentation/arc42/architectural-decisions.md +++ /dev/null @@ -1 +0,0 @@ -Section 9 \ No newline at end of file diff --git a/structurizr-core/test/unit/com/structurizr/documentation/arc42/building-block-view.md b/structurizr-core/test/unit/com/structurizr/documentation/arc42/building-block-view.md deleted file mode 100644 index ad5da1b79..000000000 --- a/structurizr-core/test/unit/com/structurizr/documentation/arc42/building-block-view.md +++ /dev/null @@ -1 +0,0 @@ -Section 5 \ No newline at end of file diff --git a/structurizr-core/test/unit/com/structurizr/documentation/arc42/constraints.md b/structurizr-core/test/unit/com/structurizr/documentation/arc42/constraints.md deleted file mode 100644 index e21243ae1..000000000 --- a/structurizr-core/test/unit/com/structurizr/documentation/arc42/constraints.md +++ /dev/null @@ -1 +0,0 @@ -Section 2 \ No newline at end of file diff --git a/structurizr-core/test/unit/com/structurizr/documentation/arc42/context-and-scope.md b/structurizr-core/test/unit/com/structurizr/documentation/arc42/context-and-scope.md deleted file mode 100644 index bfda5c524..000000000 --- a/structurizr-core/test/unit/com/structurizr/documentation/arc42/context-and-scope.md +++ /dev/null @@ -1 +0,0 @@ -Section 3 \ No newline at end of file diff --git a/structurizr-core/test/unit/com/structurizr/documentation/arc42/crosscutting-concepts.md b/structurizr-core/test/unit/com/structurizr/documentation/arc42/crosscutting-concepts.md deleted file mode 100644 index f2cb8a961..000000000 --- a/structurizr-core/test/unit/com/structurizr/documentation/arc42/crosscutting-concepts.md +++ /dev/null @@ -1 +0,0 @@ -Section 8 \ No newline at end of file diff --git a/structurizr-core/test/unit/com/structurizr/documentation/arc42/deployment-view.md b/structurizr-core/test/unit/com/structurizr/documentation/arc42/deployment-view.md deleted file mode 100644 index df62d0d99..000000000 --- a/structurizr-core/test/unit/com/structurizr/documentation/arc42/deployment-view.md +++ /dev/null @@ -1 +0,0 @@ -Section 7 \ No newline at end of file diff --git a/structurizr-core/test/unit/com/structurizr/documentation/arc42/glossary.md b/structurizr-core/test/unit/com/structurizr/documentation/arc42/glossary.md deleted file mode 100644 index 4f640e7c6..000000000 --- a/structurizr-core/test/unit/com/structurizr/documentation/arc42/glossary.md +++ /dev/null @@ -1 +0,0 @@ -Section 12 \ No newline at end of file diff --git a/structurizr-core/test/unit/com/structurizr/documentation/arc42/introduction-and-goals.md b/structurizr-core/test/unit/com/structurizr/documentation/arc42/introduction-and-goals.md deleted file mode 100644 index 08d597adb..000000000 --- a/structurizr-core/test/unit/com/structurizr/documentation/arc42/introduction-and-goals.md +++ /dev/null @@ -1 +0,0 @@ -Section 1 \ No newline at end of file diff --git a/structurizr-core/test/unit/com/structurizr/documentation/arc42/quality-requirements.md b/structurizr-core/test/unit/com/structurizr/documentation/arc42/quality-requirements.md deleted file mode 100644 index 822a2f582..000000000 --- a/structurizr-core/test/unit/com/structurizr/documentation/arc42/quality-requirements.md +++ /dev/null @@ -1 +0,0 @@ -Section 10 \ No newline at end of file diff --git a/structurizr-core/test/unit/com/structurizr/documentation/arc42/risks-and-technical-debt.md b/structurizr-core/test/unit/com/structurizr/documentation/arc42/risks-and-technical-debt.md deleted file mode 100644 index 84f6ede49..000000000 --- a/structurizr-core/test/unit/com/structurizr/documentation/arc42/risks-and-technical-debt.md +++ /dev/null @@ -1 +0,0 @@ -Section 11 \ No newline at end of file diff --git a/structurizr-core/test/unit/com/structurizr/documentation/arc42/runtime-view.md b/structurizr-core/test/unit/com/structurizr/documentation/arc42/runtime-view.md deleted file mode 100644 index 185ac75f2..000000000 --- a/structurizr-core/test/unit/com/structurizr/documentation/arc42/runtime-view.md +++ /dev/null @@ -1 +0,0 @@ -Section 6 \ No newline at end of file diff --git a/structurizr-core/test/unit/com/structurizr/documentation/arc42/solution-strategy.md b/structurizr-core/test/unit/com/structurizr/documentation/arc42/solution-strategy.md deleted file mode 100644 index 276e6c9c2..000000000 --- a/structurizr-core/test/unit/com/structurizr/documentation/arc42/solution-strategy.md +++ /dev/null @@ -1 +0,0 @@ -Section 4 \ No newline at end of file diff --git a/structurizr-core/test/unit/com/structurizr/documentation/markdown/1.md b/structurizr-core/test/unit/com/structurizr/documentation/markdown/1.md deleted file mode 100644 index 49351eb5b..000000000 --- a/structurizr-core/test/unit/com/structurizr/documentation/markdown/1.md +++ /dev/null @@ -1 +0,0 @@ -File 1 \ No newline at end of file diff --git a/structurizr-core/test/unit/com/structurizr/documentation/markdown/subdirectory/2.md b/structurizr-core/test/unit/com/structurizr/documentation/markdown/subdirectory/2.md deleted file mode 100644 index 9fbb45ed0..000000000 --- a/structurizr-core/test/unit/com/structurizr/documentation/markdown/subdirectory/2.md +++ /dev/null @@ -1 +0,0 @@ -File 2 \ No newline at end of file diff --git a/structurizr-core/test/unit/com/structurizr/documentation/structurizr/code-for-componentA1.md b/structurizr-core/test/unit/com/structurizr/documentation/structurizr/code-for-componentA1.md deleted file mode 100644 index 21243abd8..000000000 --- a/structurizr-core/test/unit/com/structurizr/documentation/structurizr/code-for-componentA1.md +++ /dev/null @@ -1 +0,0 @@ -Code section for component A1 \ No newline at end of file diff --git a/structurizr-core/test/unit/com/structurizr/documentation/structurizr/code-for-componentA2.md b/structurizr-core/test/unit/com/structurizr/documentation/structurizr/code-for-componentA2.md deleted file mode 100644 index 24b5bae70..000000000 --- a/structurizr-core/test/unit/com/structurizr/documentation/structurizr/code-for-componentA2.md +++ /dev/null @@ -1 +0,0 @@ -Code section for component A2 \ No newline at end of file diff --git a/structurizr-core/test/unit/com/structurizr/documentation/structurizr/components-for-containerA.md b/structurizr-core/test/unit/com/structurizr/documentation/structurizr/components-for-containerA.md deleted file mode 100644 index 6fa1b4642..000000000 --- a/structurizr-core/test/unit/com/structurizr/documentation/structurizr/components-for-containerA.md +++ /dev/null @@ -1 +0,0 @@ -Components section for container A \ No newline at end of file diff --git a/structurizr-core/test/unit/com/structurizr/documentation/structurizr/components-for-containerB.md b/structurizr-core/test/unit/com/structurizr/documentation/structurizr/components-for-containerB.md deleted file mode 100644 index 2a3018378..000000000 --- a/structurizr-core/test/unit/com/structurizr/documentation/structurizr/components-for-containerB.md +++ /dev/null @@ -1 +0,0 @@ -Components section for container B \ No newline at end of file diff --git a/structurizr-core/test/unit/com/structurizr/documentation/structurizr/constraints.md b/structurizr-core/test/unit/com/structurizr/documentation/structurizr/constraints.md deleted file mode 100644 index d74f7cb80..000000000 --- a/structurizr-core/test/unit/com/structurizr/documentation/structurizr/constraints.md +++ /dev/null @@ -1 +0,0 @@ -Constraints section \ No newline at end of file diff --git a/structurizr-core/test/unit/com/structurizr/documentation/structurizr/containers.md b/structurizr-core/test/unit/com/structurizr/documentation/structurizr/containers.md deleted file mode 100644 index 6650fca08..000000000 --- a/structurizr-core/test/unit/com/structurizr/documentation/structurizr/containers.md +++ /dev/null @@ -1 +0,0 @@ -Containers section \ No newline at end of file diff --git a/structurizr-core/test/unit/com/structurizr/documentation/structurizr/context.md b/structurizr-core/test/unit/com/structurizr/documentation/structurizr/context.md deleted file mode 100644 index a0a45438a..000000000 --- a/structurizr-core/test/unit/com/structurizr/documentation/structurizr/context.md +++ /dev/null @@ -1 +0,0 @@ -Context section \ No newline at end of file diff --git a/structurizr-core/test/unit/com/structurizr/documentation/structurizr/data.md b/structurizr-core/test/unit/com/structurizr/documentation/structurizr/data.md deleted file mode 100644 index 414410448..000000000 --- a/structurizr-core/test/unit/com/structurizr/documentation/structurizr/data.md +++ /dev/null @@ -1 +0,0 @@ -Data section \ No newline at end of file diff --git a/structurizr-core/test/unit/com/structurizr/documentation/structurizr/decision-log.md b/structurizr-core/test/unit/com/structurizr/documentation/structurizr/decision-log.md deleted file mode 100644 index 8164d68ab..000000000 --- a/structurizr-core/test/unit/com/structurizr/documentation/structurizr/decision-log.md +++ /dev/null @@ -1 +0,0 @@ -Decision log section \ No newline at end of file diff --git a/structurizr-core/test/unit/com/structurizr/documentation/structurizr/deployment.md b/structurizr-core/test/unit/com/structurizr/documentation/structurizr/deployment.md deleted file mode 100644 index 7a132359f..000000000 --- a/structurizr-core/test/unit/com/structurizr/documentation/structurizr/deployment.md +++ /dev/null @@ -1 +0,0 @@ -Deployment section \ No newline at end of file diff --git a/structurizr-core/test/unit/com/structurizr/documentation/structurizr/development-environment.md b/structurizr-core/test/unit/com/structurizr/documentation/structurizr/development-environment.md deleted file mode 100644 index c5bfe01c5..000000000 --- a/structurizr-core/test/unit/com/structurizr/documentation/structurizr/development-environment.md +++ /dev/null @@ -1 +0,0 @@ -Development environment section \ No newline at end of file diff --git a/structurizr-core/test/unit/com/structurizr/documentation/structurizr/functional-overview.md b/structurizr-core/test/unit/com/structurizr/documentation/structurizr/functional-overview.md deleted file mode 100644 index 206fe2125..000000000 --- a/structurizr-core/test/unit/com/structurizr/documentation/structurizr/functional-overview.md +++ /dev/null @@ -1 +0,0 @@ -Functional overview section \ No newline at end of file diff --git a/structurizr-core/test/unit/com/structurizr/documentation/structurizr/infrastructure-architecture.md b/structurizr-core/test/unit/com/structurizr/documentation/structurizr/infrastructure-architecture.md deleted file mode 100644 index b4b913086..000000000 --- a/structurizr-core/test/unit/com/structurizr/documentation/structurizr/infrastructure-architecture.md +++ /dev/null @@ -1 +0,0 @@ -Infrastructure architecture section \ No newline at end of file diff --git a/structurizr-core/test/unit/com/structurizr/documentation/structurizr/operation-and-support.md b/structurizr-core/test/unit/com/structurizr/documentation/structurizr/operation-and-support.md deleted file mode 100644 index 16cd2b445..000000000 --- a/structurizr-core/test/unit/com/structurizr/documentation/structurizr/operation-and-support.md +++ /dev/null @@ -1 +0,0 @@ -Operation and support section \ No newline at end of file diff --git a/structurizr-core/test/unit/com/structurizr/documentation/structurizr/principles.md b/structurizr-core/test/unit/com/structurizr/documentation/structurizr/principles.md deleted file mode 100644 index 28b7bee70..000000000 --- a/structurizr-core/test/unit/com/structurizr/documentation/structurizr/principles.md +++ /dev/null @@ -1 +0,0 @@ -Principles section \ No newline at end of file diff --git a/structurizr-core/test/unit/com/structurizr/documentation/structurizr/quality-attributes.md b/structurizr-core/test/unit/com/structurizr/documentation/structurizr/quality-attributes.md deleted file mode 100644 index 53e4c95aa..000000000 --- a/structurizr-core/test/unit/com/structurizr/documentation/structurizr/quality-attributes.md +++ /dev/null @@ -1 +0,0 @@ -Quality attributes section \ No newline at end of file diff --git a/structurizr-core/test/unit/com/structurizr/documentation/structurizr/software-architecture.md b/structurizr-core/test/unit/com/structurizr/documentation/structurizr/software-architecture.md deleted file mode 100644 index 412625abf..000000000 --- a/structurizr-core/test/unit/com/structurizr/documentation/structurizr/software-architecture.md +++ /dev/null @@ -1 +0,0 @@ -Software architecture section \ No newline at end of file diff --git a/structurizr-core/test/unit/com/structurizr/documentation/viewpointsandperspectives/01-introduction.md b/structurizr-core/test/unit/com/structurizr/documentation/viewpointsandperspectives/01-introduction.md deleted file mode 100644 index 08d597adb..000000000 --- a/structurizr-core/test/unit/com/structurizr/documentation/viewpointsandperspectives/01-introduction.md +++ /dev/null @@ -1 +0,0 @@ -Section 1 \ No newline at end of file diff --git a/structurizr-core/test/unit/com/structurizr/documentation/viewpointsandperspectives/02-glossary.md b/structurizr-core/test/unit/com/structurizr/documentation/viewpointsandperspectives/02-glossary.md deleted file mode 100644 index e21243ae1..000000000 --- a/structurizr-core/test/unit/com/structurizr/documentation/viewpointsandperspectives/02-glossary.md +++ /dev/null @@ -1 +0,0 @@ -Section 2 \ No newline at end of file diff --git a/structurizr-core/test/unit/com/structurizr/documentation/viewpointsandperspectives/03-system-stakeholders-and-requirements.md b/structurizr-core/test/unit/com/structurizr/documentation/viewpointsandperspectives/03-system-stakeholders-and-requirements.md deleted file mode 100644 index bfda5c524..000000000 --- a/structurizr-core/test/unit/com/structurizr/documentation/viewpointsandperspectives/03-system-stakeholders-and-requirements.md +++ /dev/null @@ -1 +0,0 @@ -Section 3 \ No newline at end of file diff --git a/structurizr-core/test/unit/com/structurizr/documentation/viewpointsandperspectives/04-architectural-forces.md b/structurizr-core/test/unit/com/structurizr/documentation/viewpointsandperspectives/04-architectural-forces.md deleted file mode 100644 index 276e6c9c2..000000000 --- a/structurizr-core/test/unit/com/structurizr/documentation/viewpointsandperspectives/04-architectural-forces.md +++ /dev/null @@ -1 +0,0 @@ -Section 4 \ No newline at end of file diff --git a/structurizr-core/test/unit/com/structurizr/documentation/viewpointsandperspectives/05-architectural-views.md b/structurizr-core/test/unit/com/structurizr/documentation/viewpointsandperspectives/05-architectural-views.md deleted file mode 100644 index ad5da1b79..000000000 --- a/structurizr-core/test/unit/com/structurizr/documentation/viewpointsandperspectives/05-architectural-views.md +++ /dev/null @@ -1 +0,0 @@ -Section 5 \ No newline at end of file diff --git a/structurizr-core/test/unit/com/structurizr/documentation/viewpointsandperspectives/06-system-qualities.md b/structurizr-core/test/unit/com/structurizr/documentation/viewpointsandperspectives/06-system-qualities.md deleted file mode 100644 index 185ac75f2..000000000 --- a/structurizr-core/test/unit/com/structurizr/documentation/viewpointsandperspectives/06-system-qualities.md +++ /dev/null @@ -1 +0,0 @@ -Section 6 \ No newline at end of file diff --git a/structurizr-core/test/unit/com/structurizr/documentation/viewpointsandperspectives/07-appendices.adoc b/structurizr-core/test/unit/com/structurizr/documentation/viewpointsandperspectives/07-appendices.adoc deleted file mode 100644 index df62d0d99..000000000 --- a/structurizr-core/test/unit/com/structurizr/documentation/viewpointsandperspectives/07-appendices.adoc +++ /dev/null @@ -1 +0,0 @@ -Section 7 \ No newline at end of file diff --git a/structurizr-core/test/unit/com/structurizr/encryption/AesEncryptionStrategyTests.java b/structurizr-core/test/unit/com/structurizr/encryption/AesEncryptionStrategyTests.java deleted file mode 100644 index 8c8f4a8ec..000000000 --- a/structurizr-core/test/unit/com/structurizr/encryption/AesEncryptionStrategyTests.java +++ /dev/null @@ -1,96 +0,0 @@ -package com.structurizr.encryption; - -import org.junit.Test; - -import javax.crypto.BadPaddingException; -import java.security.InvalidKeyException; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.fail; - -public class AesEncryptionStrategyTests { - - @Test - public void test_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 - public void test_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 - public void test_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"); - assertEquals("Hello world", strategy.decrypt(ciphertext)); - } - - @Test - public void test_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 - } catch (Exception e) { - fail(); - } - } - - @Test(expected = BadPaddingException.class) - public void test_decrypt_doesNotDecryptTheCiphertext_WhenTheIncorrectIterationCountIsUsed() throws Exception { - 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); - } - - @Test(expected = BadPaddingException.class) - public void test_decrypt_doesNotDecryptTheCiphertext_WhenTheIncorrectSaltIsUsed() throws Exception { - 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); - } - - @Test(expected = BadPaddingException.class) - public void test_decrypt_doesNotDecryptTheCiphertext_WhenTheIncorrectIvIsUsed() throws Exception { - 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); - } - - @Test(expected = BadPaddingException.class) - public void test_decrypt_doesNotDecryptTheCiphertext_WhenTheIncorrectPassphraseIsUsed() 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(), "The Wrong Password"); - strategy.decrypt(ciphertext); - } - -} diff --git a/structurizr-core/test/unit/com/structurizr/encryption/EncryptedWorkspaceTests.java b/structurizr-core/test/unit/com/structurizr/encryption/EncryptedWorkspaceTests.java deleted file mode 100644 index 1ae8cd443..000000000 --- a/structurizr-core/test/unit/com/structurizr/encryption/EncryptedWorkspaceTests.java +++ /dev/null @@ -1,101 +0,0 @@ -package com.structurizr.encryption; - -import com.structurizr.Workspace; -import com.structurizr.io.json.JsonWriter; -import org.junit.Before; -import org.junit.Test; - -import java.io.StringWriter; - -import static junit.framework.TestCase.assertSame; -import static org.junit.Assert.assertEquals; - -public class EncryptedWorkspaceTests { - - private EncryptedWorkspace encryptedWorkspace; - private Workspace workspace; - private EncryptionStrategy encryptionStrategy; - - @Before - public void setUp() throws Exception { - workspace = new Workspace("Name", "Description"); - workspace.setVersion("1.2.3"); - workspace.setThumbnail("thumbnail data"); - workspace.setId(1234); - - encryptionStrategy = new MockEncryptionStrategy(); - encryptedWorkspace = new EncryptedWorkspace(workspace, encryptionStrategy); - } - - @Test - public void test_construction_WhenTwoParametersAreSpecified() throws Exception { - encryptedWorkspace = new EncryptedWorkspace(workspace, encryptionStrategy); - - assertEquals("Name", encryptedWorkspace.getName()); - assertEquals("Description", encryptedWorkspace.getDescription()); - assertEquals("1.2.3", encryptedWorkspace.getVersion()); - assertEquals("thumbnail data", encryptedWorkspace.getThumbnail()); - assertEquals(1234, encryptedWorkspace.getId()); - - assertSame(workspace, encryptedWorkspace.getWorkspace()); - assertSame(encryptionStrategy, encryptedWorkspace.getEncryptionStrategy()); - - JsonWriter jsonWriter = new JsonWriter(false); - StringWriter stringWriter = new StringWriter(); - jsonWriter.write(workspace, stringWriter); - - assertEquals(stringWriter.toString(), encryptedWorkspace.getPlaintext()); - assertEquals(encryptionStrategy.encrypt(stringWriter.toString()), encryptedWorkspace.getCiphertext()); - } - - @Test - public void test_construction_WhenThreeParametersAreSpecified() throws Exception { - JsonWriter jsonWriter = new JsonWriter(false); - StringWriter stringWriter = new StringWriter(); - jsonWriter.write(workspace, stringWriter); - - encryptedWorkspace = new EncryptedWorkspace(workspace, stringWriter.toString(), encryptionStrategy); - - assertEquals("Name", encryptedWorkspace.getName()); - assertEquals("Description", encryptedWorkspace.getDescription()); - assertEquals("1.2.3", encryptedWorkspace.getVersion()); - assertEquals("thumbnail data", encryptedWorkspace.getThumbnail()); - assertEquals(1234, encryptedWorkspace.getId()); - - assertSame(workspace, encryptedWorkspace.getWorkspace()); - assertSame(encryptionStrategy, encryptedWorkspace.getEncryptionStrategy()); - - assertEquals(stringWriter.toString(), encryptedWorkspace.getPlaintext()); - assertEquals(encryptionStrategy.encrypt(stringWriter.toString()), encryptedWorkspace.getCiphertext()); - } - - @Test - public void test_getPlaintext_ReturnsTheDecryptedVersionOfTheCiphertext() throws Exception { - String cipherText = encryptedWorkspace.getCiphertext(); - - encryptedWorkspace = new EncryptedWorkspace(); - encryptedWorkspace.setEncryptionStrategy(encryptionStrategy); - encryptedWorkspace.setCiphertext(cipherText); - - assertEquals(new StringBuilder(cipherText).reverse().toString(), encryptedWorkspace.getPlaintext()); - } - - @Test - public void test_getWorkspace_ReturnsTheWorkspace_WhenACipherextIsSpecified() throws Exception { - JsonWriter jsonWriter = new JsonWriter(false); - StringWriter stringWriter = new StringWriter(); - jsonWriter.write(workspace, stringWriter); - String expected = stringWriter.toString(); - - encryptedWorkspace = new EncryptedWorkspace(); - encryptedWorkspace.setEncryptionStrategy(encryptionStrategy); - encryptedWorkspace.setCiphertext(encryptionStrategy.encrypt(expected)); - - workspace = encryptedWorkspace.getWorkspace(); - assertEquals("Name", workspace.getName()); - stringWriter = new StringWriter(); - jsonWriter.write(workspace, stringWriter); - assertEquals(expected, stringWriter.toString()); - } - -} diff --git a/structurizr-core/test/unit/com/structurizr/io/json/JsonTests.java b/structurizr-core/test/unit/com/structurizr/io/json/JsonTests.java deleted file mode 100644 index 491f7d537..000000000 --- a/structurizr-core/test/unit/com/structurizr/io/json/JsonTests.java +++ /dev/null @@ -1,74 +0,0 @@ -package com.structurizr.io.json; - -import com.structurizr.Workspace; -import com.structurizr.model.*; -import com.structurizr.view.*; -import org.junit.Test; - -import java.io.StringReader; -import java.io.StringWriter; - -public class JsonTests { - - @Test - public void test_write_and_read() throws Exception { - Workspace workspace1 = createWorkspace(); - - // output the model as JSON - JsonWriter jsonWriter = new JsonWriter(true); - StringWriter stringWriter = new StringWriter(); - jsonWriter.write(workspace1, stringWriter); - - JsonReader jsonReader = new JsonReader(); - StringReader stringReader = new StringReader(stringWriter.toString()); - Workspace workspace2 = jsonReader.read(stringReader); - } - - protected Workspace createWorkspace() { - Workspace workspace = new Workspace("My workspace", "Description"); - Model model = workspace.getModel(); - - SoftwareSystem mySoftwareSystem = model.addSoftwareSystem(Location.Internal, "My Software System", "Description"); - mySoftwareSystem.addTags("Internal"); - - Person person = model.addPerson(Location.External, "A User", "Description"); - person.addTags("External"); - person.uses(mySoftwareSystem, "Uses"); - - Container webApplication = mySoftwareSystem.addContainer("Web Application", "Description", "Apache Tomcat"); - Container database = mySoftwareSystem.addContainer("Database", "Description", "MySQL"); - person.uses(webApplication, "Uses"); - Relationship webApplicationToDatabase = webApplication.uses(database, "Reads from and writes to"); - webApplicationToDatabase.addTags("JDBC"); - - Component componentA = webApplication.addComponent("ComponentA", "Description", "Technology A"); - Component componentB = webApplication.addComponent("com.somecompany.system.ComponentB", "com.somecompany.system.ComponentBImpl", "Description", "Technology B"); - person.uses(componentA, "Uses"); - componentA.uses(componentB, "Uses"); - componentB.uses(database, "Reads from and writes to"); - - ViewSet views = workspace.getViews(); - SystemContextView systemContextView = views.createSystemContextView(mySoftwareSystem, "context", "Description"); - systemContextView.addAllSoftwareSystems(); - systemContextView.addAllPeople(); - - ContainerView containerView = views.createContainerView(mySoftwareSystem, "containers", "Description"); - containerView.addAllSoftwareSystems(); - containerView.addAllPeople(); - containerView.addAllContainers(); - - ComponentView componentView = views.createComponentView(webApplication, "components", "Description"); - componentView.addAllSoftwareSystems(); - componentView.addAllPeople(); - componentView.addAllContainers(); - componentView.addAllComponents(); - - views.getConfiguration().getStyles().add(new ElementStyle(Tags.ELEMENT, 600, 450, "#dddddd", "#000000", 30)); - views.getConfiguration().getStyles().add(new ElementStyle("Internal", null, null, "#041F37", "#ffffff", null)); - views.getConfiguration().getStyles().add(new RelationshipStyle(Tags.RELATIONSHIP, 4, "#dddddd", true, Routing.Direct, 25, 300, null)); - views.getConfiguration().getStyles().add(new RelationshipStyle("JDBC", 4, "#ff0000", true, Routing.Direct, 25, 300, null)); - - return workspace; - } - -} diff --git a/structurizr-core/test/unit/com/structurizr/io/json/JsonWriterTests.java b/structurizr-core/test/unit/com/structurizr/io/json/JsonWriterTests.java deleted file mode 100644 index 48049ebbc..000000000 --- a/structurizr-core/test/unit/com/structurizr/io/json/JsonWriterTests.java +++ /dev/null @@ -1,36 +0,0 @@ -package com.structurizr.io.json; - -import com.structurizr.Workspace; -import org.junit.Test; - -import java.io.StringWriter; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.fail; - -public class JsonWriterTests { - - @Test - public void test_write_ThrowsAnIllegalArgumentException_WhenANullWorkspaceIsSpecified() throws Exception { - try { - JsonWriter writer = new JsonWriter(true); - writer.write(null, new StringWriter()); - fail(); - } catch (IllegalArgumentException e) { - assertEquals("Workspace cannot be null.", e.getMessage()); - } - } - - @Test - public void test_write_ThrowsAnIllegalArgumentException_WhenANullWriterIsSpecified() throws Exception { - try { - JsonWriter writer = new JsonWriter(true); - Workspace workspace = new Workspace("Name", "Description"); - writer.write(workspace, null); - fail(); - } catch (IllegalArgumentException e) { - assertEquals("Writer cannot be null.", e.getMessage()); - } - } - -} diff --git a/structurizr-core/test/unit/com/structurizr/io/plantuml/PlantUMLWriterTests.java b/structurizr-core/test/unit/com/structurizr/io/plantuml/PlantUMLWriterTests.java deleted file mode 100644 index 91ad122cc..000000000 --- a/structurizr-core/test/unit/com/structurizr/io/plantuml/PlantUMLWriterTests.java +++ /dev/null @@ -1,500 +0,0 @@ -package com.structurizr.io.plantuml; - -import com.structurizr.Workspace; -import com.structurizr.model.*; -import com.structurizr.view.*; -import org.junit.Before; -import org.junit.Test; - -import java.io.StringWriter; - -import static org.junit.Assert.assertEquals; - -public class PlantUMLWriterTests { - private static final String DATA_STORE_TAG = "DataStore"; - private static final String SOME_TAG = "Some"; - - private PlantUMLWriter plantUMLWriter; - private Workspace workspace; - private StringWriter stringWriter; - - @Before - public void setUp() { - plantUMLWriter = new PlantUMLWriter(); - - // Public plantuml.com/plantuml server limits dimensions to 2000, but local servers can be configured - // differently. Setting limit here to 1999 to be provably different to plantuml.com AND still usable at the - // public server AND bigger than A6 so that we can ensure smaller paper sizes are respected correctly. - plantUMLWriter.setSizeLimit(1999); - workspace = new Workspace("Name", "Description"); - stringWriter = new StringWriter(); - } - - @Test - public void test_writeWorkspace_DoesNotThrowAnExceptionWhenPassedNullParameters() throws Exception { - plantUMLWriter.write((Workspace) null, null); - plantUMLWriter.write(workspace, null); - plantUMLWriter.write((Workspace) null, stringWriter); - } - - @Test - public void test_writeView_DoesNotThrowAnExceptionWhenPassedNullParameters() throws Exception { - populateWorkspace(); - - plantUMLWriter.write((View) null, null); - plantUMLWriter.write(workspace.getViews().getEnterpriseContextViews().stream().findFirst().get(), null); - plantUMLWriter.write((View) null, stringWriter); - } - - @Test - public void test_writeWorkspace() throws Exception { - populateWorkspace(); - - plantUMLWriter.write(workspace, stringWriter); - assertEquals( - ENTERPRISE_CONTEXT_VIEW + - SYSTEM_CONTEXT_VIEW + - CONTAINER_VIEW + - COMPONENT_VIEW + - DYNAMIC_VIEW + - DEPLOYMENT_VIEW, stringWriter.toString()); - } - - @Test - public void test_writeEnterpriseContextView() throws Exception { - populateWorkspace(); - - EnterpriseContextView enterpriseContextView = workspace.getViews().getEnterpriseContextViews() - .stream().findFirst().get(); - plantUMLWriter.write(enterpriseContextView, stringWriter); - - assertEquals(ENTERPRISE_CONTEXT_VIEW, stringWriter.toString()); - - } - - @Test - public void test_writeSystemContextView() throws Exception { - populateWorkspace(); - - SystemContextView systemContextView = workspace.getViews().getSystemContextViews() - .stream().findFirst().get(); - plantUMLWriter.write(systemContextView, stringWriter); - - assertEquals(SYSTEM_CONTEXT_VIEW, stringWriter.toString()); - } - - @Test - public void test_writeContainerView() throws Exception { - populateWorkspace(); - - ContainerView containerView = workspace.getViews().getContainerViews() - .stream().findFirst().get(); - plantUMLWriter.write(containerView, stringWriter); - - assertEquals(CONTAINER_VIEW, stringWriter.toString()); - } - - @Test - public void test_writeComponentsView() throws Exception { - populateWorkspace(); - - ComponentView componentView = workspace.getViews().getComponentViews() - .stream().findFirst().get(); - plantUMLWriter.write(componentView, stringWriter); - - assertEquals(COMPONENT_VIEW, stringWriter.toString()); - } - - @Test - public void test_writeDynamicView() throws Exception { - populateWorkspace(); - - DynamicView dynamicView = workspace.getViews().getDynamicViews() - .stream().findFirst().get(); - plantUMLWriter.write(dynamicView, stringWriter); - - assertEquals(DYNAMIC_VIEW, stringWriter.toString()); - } - - @Test - public void test_writeDeploymentView() throws Exception { - populateWorkspace(); - - DeploymentView deploymentView = workspace.getViews().getDeploymentViews() - .stream().findFirst().get(); - plantUMLWriter.write(deploymentView, stringWriter); - - assertEquals(DEPLOYMENT_VIEW, stringWriter.toString()); - } - - private void populateWorkspace() { - Model model = workspace.getModel(); - model.setEnterprise(new Enterprise("Some Enterprise")); - - Person user = model.addPerson(Location.Internal, "User", - "A detailed description of the user to be displayed on the diagrams"); - SoftwareSystem softwareSystem = model.addSoftwareSystem(Location.Internal, "Software System", ""); - user.uses(softwareSystem, "Uses"); - - SoftwareSystem emailSystem = model.addSoftwareSystem(Location.External, "E-mail System", "An SMTP relay configured to send emails to the users."); - softwareSystem.uses(emailSystem, "Sends e-mail using"); - emailSystem.delivers(user, "Delivers e-mails to"); - - Container webApplication = softwareSystem.addContainer("Web Application", "", ""); - Container database = softwareSystem.addContainer("Database", "A relational database management system, likely PostgreSQL or MySQL but anything with JDBC drivers would be suitable.", ""); - database.addTags(DATA_STORE_TAG); - user.uses(webApplication, "Uses", "HTTP"); - webApplication.uses(database, "Reads from and writes to", "JDBC"); - webApplication.uses(emailSystem, "Sends e-mail using"); - - Component controller = webApplication.addComponent("SomeController", "", "Spring MVC Controller"); - controller.addTags(SOME_TAG); - Component emailComponent = webApplication.addComponent("EmailComponent", ""); - Component repository = webApplication.addComponent("SomeRepository", "", "Spring Data"); - repository.addTags(SOME_TAG); - user.uses(controller, "Uses", "HTTP"); - controller.uses(repository, "Uses"); - controller.uses(emailComponent, "Sends e-mail using"); - repository.uses(database, "Reads from and writes to", "JDBC"); - emailComponent.uses(emailSystem, "Sends e-mails using", "SMTP"); - - DeploymentNode webServer = model.addDeploymentNode("Web Server", "A server hosted at AWS EC2.", "Ubuntu 12.04 LTS"); - webServer.addDeploymentNode("Apache Tomcat", "The live web server", "Apache Tomcat 8.x") - .add(webApplication); - DeploymentNode databaseServer = model.addDeploymentNode("Database Server", "A server hosted at AWS EC2.", "Ubuntu 12.04 LTS"); - databaseServer.addDeploymentNode("MySQL", "The live database server", "MySQL 5.5.x") - .add(database); - - EnterpriseContextView - enterpriseContextView = workspace.getViews().createEnterpriseContextView("enterpriseContext", ""); - enterpriseContextView.addAllElements(); - - SystemContextView - systemContextView = workspace.getViews().createSystemContextView(softwareSystem, "systemContext", ""); - systemContextView.addAllElements(); - - ContainerView containerView = workspace.getViews().createContainerView(softwareSystem, "containers", ""); - containerView.setPaperSize(PaperSize.A2_Landscape); - containerView.addAllElements(); - - ComponentView componentView = workspace.getViews().createComponentView(webApplication, "components", ""); - componentView.setPaperSize(PaperSize.A6_Portrait); - componentView.addAllElements(); - - DynamicView dynamicView = workspace.getViews().createDynamicView(webApplication, "dynamic", ""); - dynamicView.add(user, "Requests /something", controller); - dynamicView.add(controller, repository); - dynamicView.add(repository, "select * from something", database); - - DeploymentView deploymentView = workspace.getViews().createDeploymentView(softwareSystem, "deployment", ""); - deploymentView.addAllDeploymentNodes(); - - Styles styles = workspace.getViews().getConfiguration().getStyles(); - styles.addElementStyle(DATA_STORE_TAG).shape(Shape.Cylinder); - } - - @Test - public void test_toPlantUML_ReturnsAnEmptyArray_WhenPassedANullWorkspace() throws Exception { - assertEquals(0, plantUMLWriter.toPlantUML(null).length); - } - - @Test - public void test_toPlantUML_ReturnsAnEmptyArray_WhenTheWorkspaceContainsNoDiagrams() throws Exception { - assertEquals(0, plantUMLWriter.toPlantUML(new Workspace("", "")).length); - } - - @Test - public void test_toPlantUML_ReturnsAnArrayOfDiagramsWhenThereAreDiagrams() throws Exception { - populateWorkspace(); - String diagrams[] = plantUMLWriter.toPlantUML(workspace); - assertEquals(6, diagrams.length); - assertEquals(ENTERPRISE_CONTEXT_VIEW, diagrams[0]); - assertEquals(SYSTEM_CONTEXT_VIEW, diagrams[1]); - assertEquals(CONTAINER_VIEW, diagrams[2]); - assertEquals(COMPONENT_VIEW, diagrams[3]); - assertEquals(DYNAMIC_VIEW, diagrams[4]); - assertEquals(DEPLOYMENT_VIEW, diagrams[5]); - } - - @Test - public void test_writeView_IncludesSkinParams_WhenSkinParamsAreAdded() throws Exception { - workspace = new Workspace("", ""); - workspace.getModel().addSoftwareSystem("My software system", "").setLocation(Location.Internal); - workspace.getViews().createEnterpriseContextView("key", "").addAllElements(); - plantUMLWriter.addSkinParam("handwritten", "true"); - - plantUMLWriter.write(workspace, stringWriter); - - assertEquals("@startuml" + System.lineSeparator() + - "scale max 1999x1999" + System.lineSeparator() + - "title Enterprise Context" + System.lineSeparator() + - "" + System.lineSeparator() + - "skinparam {" + System.lineSeparator() + - " shadowing false" + System.lineSeparator() + - " arrowColor #707070" + System.lineSeparator() + - " actorBorderColor #707070" + System.lineSeparator() + - " componentBorderColor #707070" + System.lineSeparator() + - " rectangleBorderColor #707070" + System.lineSeparator() + - " noteBackgroundColor #ffffff" + System.lineSeparator() + - " noteBorderColor #707070" + System.lineSeparator() + - " handwritten true" + System.lineSeparator() + - "}" + System.lineSeparator() + - "package \"Enterprise\" {" + System.lineSeparator() + - " rectangle \"My software system\" <> as 1 #dddddd" + System.lineSeparator() + - "}" + System.lineSeparator() + - "@enduml" + System.lineSeparator(), stringWriter.toString()); - } - - @Test - public void test_writeView_IncludesStyling_WhenStylesAreAdded() throws Exception { - workspace = new Workspace("", ""); - SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("My software system", ""); - Person user = workspace.getModel().addPerson("A user", ""); - user.uses(softwareSystem, "Uses"); - workspace.getViews().createSystemContextView(softwareSystem, "key", "").addAllElements(); - workspace.getViews().getConfiguration().getStyles().addElementStyle(Tags.SOFTWARE_SYSTEM).background("#ff0000"); - workspace.getViews().getConfiguration().getStyles().addElementStyle(Tags.PERSON).background("#00ff00"); - workspace.getViews().getConfiguration().getStyles().addRelationshipStyle(Tags.RELATIONSHIP).color("#0000ff"); - - plantUMLWriter.write(workspace, stringWriter); - - assertEquals("@startuml" + System.lineSeparator() + - "scale max 1999x1999" + System.lineSeparator() + - "title My software system - System Context" + System.lineSeparator() + - "" + System.lineSeparator() + - "skinparam {" + System.lineSeparator() + - " shadowing false" + System.lineSeparator() + - " arrowColor #707070" + System.lineSeparator() + - " actorBorderColor #707070" + System.lineSeparator() + - " componentBorderColor #707070" + System.lineSeparator() + - " rectangleBorderColor #707070" + System.lineSeparator() + - " noteBackgroundColor #ffffff" + System.lineSeparator() + - " noteBorderColor #707070" + System.lineSeparator() + - "}" + System.lineSeparator() + - "rectangle \"A user\" <> as 2 #00ff00" + System.lineSeparator() + - "rectangle \"My software system\" <> as 1 #ff0000" + System.lineSeparator() + - "2 .[#0000ff].> 1 : Uses" + System.lineSeparator() + - "@enduml" + System.lineSeparator(), stringWriter.toString()); - } - - private static final String ENTERPRISE_CONTEXT_VIEW = "@startuml" + System.lineSeparator() + - "scale max 1999x1999" + System.lineSeparator() + - "title Enterprise Context for Some Enterprise" + System.lineSeparator() + - "" + System.lineSeparator() + - "skinparam {" + System.lineSeparator() + - " shadowing false" + System.lineSeparator() + - " arrowColor #707070" + System.lineSeparator() + - " actorBorderColor #707070" + System.lineSeparator() + - " componentBorderColor #707070" + System.lineSeparator() + - " rectangleBorderColor #707070" + System.lineSeparator() + - " noteBackgroundColor #ffffff" + System.lineSeparator() + - " noteBorderColor #707070" + System.lineSeparator() + - "}" + System.lineSeparator() + - "rectangle 4 <> #dddddd [" + System.lineSeparator() + - " E-mail System" + System.lineSeparator() + - " --" + System.lineSeparator() + - " An SMTP relay configured to" + System.lineSeparator() + - " send emails to the users." + System.lineSeparator() + - "]" + System.lineSeparator() + - "package \"Some Enterprise\" {" + System.lineSeparator() + - " rectangle 1 <> #dddddd [" + System.lineSeparator() + - " User" + System.lineSeparator() + - " --" + System.lineSeparator() + - " A detailed description of the" + System.lineSeparator() + - " user to be displayed on the" + System.lineSeparator() + - " diagrams" + System.lineSeparator() + - " ]" + System.lineSeparator() + - " rectangle \"Software System\" <> as 2 #dddddd" + System.lineSeparator() + - "}" + System.lineSeparator() + - "4 .[#707070].> 1 : Delivers e-mails to" + System.lineSeparator() + - "2 .[#707070].> 4 : Sends e-mail using" + System.lineSeparator() + - "1 .[#707070].> 2 : Uses" + System.lineSeparator() + - "@enduml" + System.lineSeparator(); - - private static final String SYSTEM_CONTEXT_VIEW = "@startuml" + System.lineSeparator() + - "scale max 1999x1999" + System.lineSeparator() + - "title Software System - System Context" + System.lineSeparator() + - "" + System.lineSeparator() + - "skinparam {" + System.lineSeparator() + - " shadowing false" + System.lineSeparator() + - " arrowColor #707070" + System.lineSeparator() + - " actorBorderColor #707070" + System.lineSeparator() + - " componentBorderColor #707070" + System.lineSeparator() + - " rectangleBorderColor #707070" + System.lineSeparator() + - " noteBackgroundColor #ffffff" + System.lineSeparator() + - " noteBorderColor #707070" + System.lineSeparator() + - "}" + System.lineSeparator() + - "rectangle 4 <> #dddddd [" + System.lineSeparator() + - " E-mail System" + System.lineSeparator() + - " --" + System.lineSeparator() + - " An SMTP relay configured to" + System.lineSeparator() + - " send emails to the users." + System.lineSeparator() + - "]" + System.lineSeparator() + - "rectangle \"Software System\" <> as 2 #dddddd" + System.lineSeparator() + - "rectangle 1 <> #dddddd [" + System.lineSeparator() + - " User" + System.lineSeparator() + - " --" + System.lineSeparator() + - " A detailed description of the" + System.lineSeparator() + - " user to be displayed on the" + System.lineSeparator() + - " diagrams" + System.lineSeparator() + - "]" + System.lineSeparator() + - "4 .[#707070].> 1 : Delivers e-mails to" + System.lineSeparator() + - "2 .[#707070].> 4 : Sends e-mail using" + System.lineSeparator() + - "1 .[#707070].> 2 : Uses" + System.lineSeparator() + - "@enduml" + System.lineSeparator(); - - private static final String CONTAINER_VIEW = "@startuml" + System.lineSeparator() + - "scale max 1999x1413" + System.lineSeparator() + - "title Software System - Containers" + System.lineSeparator() + - "" + System.lineSeparator() + - "skinparam {" + System.lineSeparator() + - " shadowing false" + System.lineSeparator() + - " arrowColor #707070" + System.lineSeparator() + - " actorBorderColor #707070" + System.lineSeparator() + - " componentBorderColor #707070" + System.lineSeparator() + - " rectangleBorderColor #707070" + System.lineSeparator() + - " noteBackgroundColor #ffffff" + System.lineSeparator() + - " noteBorderColor #707070" + System.lineSeparator() + - "}" + System.lineSeparator() + - "rectangle 4 <> #dddddd [" + System.lineSeparator() + - " E-mail System" + System.lineSeparator() + - " --" + System.lineSeparator() + - " An SMTP relay configured to" + System.lineSeparator() + - " send emails to the users." + System.lineSeparator() + - "]" + System.lineSeparator() + - "rectangle 1 <> #dddddd [" + System.lineSeparator() + - " User" + System.lineSeparator() + - " --" + System.lineSeparator() + - " A detailed description of the" + System.lineSeparator() + - " user to be displayed on the" + System.lineSeparator() + - " diagrams" + System.lineSeparator() + - "]" + System.lineSeparator() + - "package \"Software System\" <> {" + System.lineSeparator() + - " database 8 <> #dddddd [" + System.lineSeparator() + - " Database" + System.lineSeparator() + - " --" + System.lineSeparator() + - " A relational database" + System.lineSeparator() + - " management system, likely" + System.lineSeparator() + - " PostgreSQL or MySQL but" + System.lineSeparator() + - " anything with JDBC drivers" + System.lineSeparator() + - " would be suitable." + System.lineSeparator() + - " ]" + System.lineSeparator() + - " rectangle \"Web Application\" <> as 7 #dddddd" + System.lineSeparator() + - "}" + System.lineSeparator() + - "4 .[#707070].> 1 : Delivers e-mails to" + System.lineSeparator() + - "1 .[#707070].> 7 : <>\\nUses" + System.lineSeparator() + - "7 .[#707070].> 8 : <>\\nReads from and writes to" + System.lineSeparator() + - "7 .[#707070].> 4 : Sends e-mail using" + System.lineSeparator() + - "@enduml" + System.lineSeparator(); - - private static final String COMPONENT_VIEW = "@startuml" + System.lineSeparator() + - "scale max 1240x1748" + System.lineSeparator() + - "title Software System - Web Application - Components" + System.lineSeparator() + - "" + System.lineSeparator() + - "skinparam {" + System.lineSeparator() + - " shadowing false" + System.lineSeparator() + - " arrowColor #707070" + System.lineSeparator() + - " actorBorderColor #707070" + System.lineSeparator() + - " componentBorderColor #707070" + System.lineSeparator() + - " rectangleBorderColor #707070" + System.lineSeparator() + - " noteBackgroundColor #ffffff" + System.lineSeparator() + - " noteBorderColor #707070" + System.lineSeparator() + - "}" + System.lineSeparator() + - "database 8 <> #dddddd [" + System.lineSeparator() + - " Database" + System.lineSeparator() + - " --" + System.lineSeparator() + - " A relational database" + System.lineSeparator() + - " management system, likely" + System.lineSeparator() + - " PostgreSQL or MySQL but" + System.lineSeparator() + - " anything with JDBC drivers" + System.lineSeparator() + - " would be suitable." + System.lineSeparator() + - "]" + System.lineSeparator() + - "rectangle 4 <> #dddddd [" + System.lineSeparator() + - " E-mail System" + System.lineSeparator() + - " --" + System.lineSeparator() + - " An SMTP relay configured to" + System.lineSeparator() + - " send emails to the users." + System.lineSeparator() + - "]" + System.lineSeparator() + - "rectangle 1 <> #dddddd [" + System.lineSeparator() + - " User" + System.lineSeparator() + - " --" + System.lineSeparator() + - " A detailed description of the" + System.lineSeparator() + - " user to be displayed on the" + System.lineSeparator() + - " diagrams" + System.lineSeparator() + - "]" + System.lineSeparator() + - "package \"Web Application\" <> {" + System.lineSeparator() + - " component \"EmailComponent\" <> as 13 #dddddd" + System.lineSeparator() + - " component \"SomeController\" <> as 12 #dddddd" + System.lineSeparator() + - " component \"SomeRepository\" <> as 14 #dddddd" + System.lineSeparator() + - "}" + System.lineSeparator() + - "4 .[#707070].> 1 : Delivers e-mails to" + System.lineSeparator() + - "13 .[#707070].> 4 : <>\\nSends e-mails using" + System.lineSeparator() + - "12 .[#707070].> 13 : Sends e-mail using" + System.lineSeparator() + - "12 .[#707070].> 14 : Uses" + System.lineSeparator() + - "14 .[#707070].> 8 : <>\\nReads from and writes to" + System.lineSeparator() + - "1 .[#707070].> 12 : <>\\nUses" + System.lineSeparator() + - "@enduml" + System.lineSeparator(); - - private static final String DYNAMIC_VIEW = "@startuml" + System.lineSeparator() + - "scale max 1999x1999" + System.lineSeparator() + - "title Web Application - Dynamic" + System.lineSeparator() + - "" + System.lineSeparator() + - "skinparam {" + System.lineSeparator() + - " shadowing false" + System.lineSeparator() + - " arrowColor #707070" + System.lineSeparator() + - " actorBorderColor #707070" + System.lineSeparator() + - " componentBorderColor #707070" + System.lineSeparator() + - " rectangleBorderColor #707070" + System.lineSeparator() + - " noteBackgroundColor #ffffff" + System.lineSeparator() + - " noteBorderColor #707070" + System.lineSeparator() + - "}" + System.lineSeparator() + - "database 8 <> #dddddd [" + System.lineSeparator() + - " Database" + System.lineSeparator() + - " --" + System.lineSeparator() + - " A relational database" + System.lineSeparator() + - " management system, likely" + System.lineSeparator() + - " PostgreSQL or MySQL but" + System.lineSeparator() + - " anything with JDBC drivers" + System.lineSeparator() + - " would be suitable." + System.lineSeparator() + - "]" + System.lineSeparator() + - "component \"SomeController\" <> as 12 #dddddd" + System.lineSeparator() + - "component \"SomeRepository\" <> as 14 #dddddd" + System.lineSeparator() + - "rectangle 1 <> #dddddd [" + System.lineSeparator() + - " User" + System.lineSeparator() + - " --" + System.lineSeparator() + - " A detailed description of the" + System.lineSeparator() + - " user to be displayed on the" + System.lineSeparator() + - " diagrams" + System.lineSeparator() + - "]" + System.lineSeparator() + - "1 -[#707070]> 12 : 1. Requests /something" + System.lineSeparator() + - "12 -[#707070]> 14 : 2. Uses" + System.lineSeparator() + - "14 -[#707070]> 8 : 3. select * from something" + System.lineSeparator() + - "@enduml" + System.lineSeparator(); - - private static final String DEPLOYMENT_VIEW = "@startuml" + System.lineSeparator() + - "scale max 1999x1999" + System.lineSeparator() + - "title Software System - Deployment" + System.lineSeparator() + - "" + System.lineSeparator() + - "skinparam {" + System.lineSeparator() + - " shadowing false" + System.lineSeparator() + - " arrowColor #707070" + System.lineSeparator() + - " actorBorderColor #707070" + System.lineSeparator() + - " componentBorderColor #707070" + System.lineSeparator() + - " rectangleBorderColor #707070" + System.lineSeparator() + - " noteBackgroundColor #ffffff" + System.lineSeparator() + - " noteBorderColor #707070" + System.lineSeparator() + - "}" + System.lineSeparator() + - "node \"Database Server\" <> as 23 {" + System.lineSeparator() + - " node \"MySQL\" <> as 24 {" + System.lineSeparator() + - " database \"Database\" <> as 25 #dddddd" + System.lineSeparator() + - " }" + System.lineSeparator() + - "}" + System.lineSeparator() + - "node \"Web Server\" <> as 20 {" + System.lineSeparator() + - " node \"Apache Tomcat\" <> as 21 {" + System.lineSeparator() + - " rectangle \"Web Application\" <> as 22 #dddddd" + System.lineSeparator() + - " }" + System.lineSeparator() + - "}" + System.lineSeparator() + - "22 .[#707070].> 25 : <>\\nReads from and writes to" + System.lineSeparator() + - "@enduml" + System.lineSeparator(); - -} diff --git a/structurizr-core/test/unit/com/structurizr/io/websequencediagrams/WebSequenceDiagramsWriterTests.java b/structurizr-core/test/unit/com/structurizr/io/websequencediagrams/WebSequenceDiagramsWriterTests.java deleted file mode 100644 index 369c09078..000000000 --- a/structurizr-core/test/unit/com/structurizr/io/websequencediagrams/WebSequenceDiagramsWriterTests.java +++ /dev/null @@ -1,59 +0,0 @@ -package com.structurizr.io.websequencediagrams; - -import com.structurizr.Workspace; -import com.structurizr.model.InteractionStyle; -import com.structurizr.model.Model; -import com.structurizr.model.Relationship; -import com.structurizr.model.SoftwareSystem; -import com.structurizr.view.DynamicView; -import org.junit.Before; -import org.junit.Test; - -import java.io.StringWriter; - -import static org.junit.Assert.assertEquals; - -public class WebSequenceDiagramsWriterTests { - - private WebSequenceDiagramsWriter webSequenceDiagramsWriter; - private Workspace workspace; - private StringWriter stringWriter; - - @Before - public void setUp() { - webSequenceDiagramsWriter = new WebSequenceDiagramsWriter(); - workspace = new Workspace("Name", "Description"); - stringWriter = new StringWriter(); - } - - @Test - public void test_write_DoesNotThrowAnExceptionWhenPassedNullParameters() throws Exception { - webSequenceDiagramsWriter.write(null, null); - webSequenceDiagramsWriter.write(workspace, null); - webSequenceDiagramsWriter.write(null, stringWriter); - } - - @Test - public void test_write_createsAWebSequenceDiagram() throws Exception { - Model model = workspace.getModel(); - SoftwareSystem a = model.addSoftwareSystem("System A", ""); - SoftwareSystem b = model.addSoftwareSystem("System B", ""); - SoftwareSystem c = model.addSoftwareSystem("System C", ""); - - a.uses(b, ""); - Relationship bc = b.uses(c, ""); - bc.setInteractionStyle(InteractionStyle.Asynchronous); - - DynamicView view = workspace.getViews().createDynamicView("Some Key", "A description of the diagram"); - view.add(a, "Does something using", b); - view.add(b, "Does something then using", c); - - webSequenceDiagramsWriter.write(workspace, stringWriter); - assertEquals("title Dynamic - Some Key" + System.lineSeparator() + - System.lineSeparator() + - "System A->System B: Does something using" + System.lineSeparator() + - "System B->>System C: Does something then using" + System.lineSeparator() + - System.lineSeparator(), stringWriter.toString()); - } - -} 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 79f4a7017..000000000 --- a/structurizr-core/test/unit/com/structurizr/model/CodeElementTests.java +++ /dev/null @@ -1,113 +0,0 @@ -package com.structurizr.model; - -import org.junit.Test; - -import static org.junit.Assert.*; - -public class CodeElementTests { - - @Test - public 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() { - CodeElement codeElement = new CodeElement("SomeComponent"); - assertEquals("SomeComponent", codeElement.getName()); - assertEquals("SomeComponent", codeElement.getType()); - } - - @Test - public void test_languageProperty() { - CodeElement codeElement = new CodeElement("com.structurizr.component.SomeComponent"); - assertEquals("Java", codeElement.getLanguage()); - - codeElement.setLanguage("Scala"); - assertEquals("Scala", codeElement.getLanguage()); - } - - @Test - public void test_categoryProperty() { - CodeElement codeElement = new CodeElement("com.structurizr.component.SomeComponent"); - assertNull(codeElement.getCategory()); - - codeElement.setCategory("class"); - assertEquals("class", codeElement.getCategory()); - } - - @Test - public void test_visibilityProperty() { - CodeElement codeElement = new CodeElement("com.structurizr.component.SomeComponent"); - assertNull(codeElement.getVisibility()); - - codeElement.setVisibility("package"); - assertEquals("package", codeElement.getVisibility()); - } - - @Test - public 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 - public 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() { - 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(expected = IllegalArgumentException.class) - public void test_construction_ThrowsAnIllegalArgumentException_WhenAnEmptyFullyQualifiedNameIsSpecified() { - new CodeElement(" "); - } - - @Test - public void test_equals_ReturnsFalse_WhenComparedToNull() { - CodeElement codeElement = new CodeElement("com.structurizr.component.SomeComponent"); - assertFalse(codeElement.equals(null)); - } - - @Test - public void test_equals_ReturnsFalse_WhenComparedToDifferentTypeOfObject() { - CodeElement codeElement = new CodeElement("com.structurizr.component.SomeComponent"); - assertFalse(codeElement.equals("hello")); - } - - @Test - public 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)); - } - - @Test - public 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)); - } - -} diff --git a/structurizr-core/test/unit/com/structurizr/model/ComponentTests.java b/structurizr-core/test/unit/com/structurizr/model/ComponentTests.java deleted file mode 100644 index aa09c0270..000000000 --- a/structurizr-core/test/unit/com/structurizr/model/ComponentTests.java +++ /dev/null @@ -1,152 +0,0 @@ -package com.structurizr.model; - -import com.structurizr.AbstractWorkspaceTestBase; -import org.junit.Test; - -import java.util.Set; - -import static junit.framework.TestCase.assertNull; -import static org.junit.Assert.*; - -public class ComponentTests extends AbstractWorkspaceTestBase { - - private SoftwareSystem softwareSystem = model.addSoftwareSystem(Location.External, "System", "Description"); - private Container container = softwareSystem.addContainer("Container", "Description", "Some technology"); - - @Test - public void test_getName_ReturnsTheGivenName_WhenANameIsGiven() { - Component component = new Component(); - component.setName("Some name"); - assertEquals("Some name", component.getName()); - } - - @Test - public void test_getCanonicalName() { - Component component = container.addComponent("Component", "Description"); - assertEquals("/System/Container/Component", component.getCanonicalName()); - } - - @Test - public void test_getCanonicalName_WhenNameContainsASlashCharacter() { - Component component = container.addComponent("Name1/Name2", "Description"); - assertEquals("/System/Container/Name1Name2", component.getCanonicalName()); - } - - @Test - public void test_getParent_ReturnsTheParentContainer() { - Component component = container.addComponent("Component", "Description"); - assertEquals(container, component.getParent()); - } - - @Test - public void test_getContainer_ReturnsTheParentContainer() { - Component component = container.addComponent("Name", "Description"); - assertEquals(container, component.getContainer()); - } - - @Test - public void test_removeTags_DoesNotRemoveRequiredTags() { - Component component = new Component(); - assertTrue(component.getTags().contains(Tags.ELEMENT)); - assertTrue(component.getTags().contains(Tags.COMPONENT)); - - component.removeTag(Tags.COMPONENT); - component.removeTag(Tags.ELEMENT); - - assertTrue(component.getTags().contains(Tags.ELEMENT)); - assertTrue(component.getTags().contains(Tags.COMPONENT)); - } - - @Test - public void test_getPackage_ReturnsNull_WhenNoTypeHasBeenSet() { - Component component = new Component(); - assertNull(component.getType()); - assertNull(component.getPackage()); - } - - @Test - public void test_getPackage_ReturnsThePackageName_WhenATypeHasBeenSet() { - Component component = new Component(); - component.setType(ComponentTests.class.getCanonicalName()); - assertEquals("com.structurizr.model", component.getPackage()); - } - - @Test - public void test_getPackage_ReturnsThePackageName_WhenATypeHasNotBeenSet() { - Component component = new Component(); - assertNull(component.getPackage()); - } - - @Test - public void test_technologyProperty() { - Component component = new Component(); - assertNull(component.getTechnology()); - - component.setTechnology("Spring Bean"); - assertEquals("Spring Bean", component.getTechnology()); - } - - @Test - public void test_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 - public void test_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 - public void test_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 - public void test_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 - public void test_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()); - } - -} diff --git a/structurizr-core/test/unit/com/structurizr/model/ContainerInstanceTests.java b/structurizr-core/test/unit/com/structurizr/model/ContainerInstanceTests.java deleted file mode 100644 index dd52d160a..000000000 --- a/structurizr-core/test/unit/com/structurizr/model/ContainerInstanceTests.java +++ /dev/null @@ -1,116 +0,0 @@ -package com.structurizr.model; - -import com.structurizr.AbstractWorkspaceTestBase; -import org.junit.Test; - -import static org.junit.Assert.*; - -public class ContainerInstanceTests extends AbstractWorkspaceTestBase { - - private SoftwareSystem softwareSystem = model.addSoftwareSystem(Location.External, "System", "Description"); - private Container database = softwareSystem.addContainer("Database Schema", "Stores data", "MySQL"); - - @Test - public void test_construction() { - ContainerInstance containerInstance = model.addContainerInstance(database); - - assertSame(database, containerInstance.getContainer()); - assertEquals(database.getId(), containerInstance.getContainerId()); - assertEquals(1, containerInstance.getInstanceId()); - } - - @Test - public void test_getContainerId() { - ContainerInstance containerInstance = model.addContainerInstance(database); - - assertEquals(database.getId(), containerInstance.getContainerId()); - containerInstance.setContainer(null); - containerInstance.setContainerId("1234"); - assertEquals("1234", containerInstance.getContainerId()); - } - - @Test - public void test_getName() { - ContainerInstance containerInstance = model.addContainerInstance(database); - - assertNull(containerInstance.getName()); - - containerInstance.setName("foo"); - assertNull(containerInstance.getName()); - } - - @Test - public void test_getCanonicalName() { - ContainerInstance containerInstance = model.addContainerInstance(database); - - assertEquals("/System/Database Schema[1]", containerInstance.getCanonicalName()); - } - - @Test - public void test_getParent_ReturnsTheParentSoftwareSystem() { - ContainerInstance containerInstance = model.addContainerInstance(database); - - assertEquals(softwareSystem, containerInstance.getParent()); - } - - @Test - public void test_getRequiredTags() { - ContainerInstance containerInstance = model.addContainerInstance(database); - - assertEquals(1, containerInstance.getRequiredTags().size()); - assertTrue(containerInstance.getTags().contains(Tags.CONTAINER_INSTANCE)); - } - - @Test - public void test_getTags() { - ContainerInstance containerInstance = model.addContainerInstance(database); - - assertEquals("Element,Container,Container Instance", containerInstance.getTags()); - - database.addTags("Database"); - containerInstance.addTags("Primary Instance"); - assertEquals("Element,Container,Database,Container Instance,Primary Instance", containerInstance.getTags()); - } - - @Test - public void test_removeTags_DoesNotRemoveRequiredTags() { - ContainerInstance containerInstance = model.addContainerInstance(database); - - assertTrue(containerInstance.getTags().contains(Tags.ELEMENT)); - assertTrue(containerInstance.getTags().contains(Tags.CONTAINER)); - assertTrue(containerInstance.getTags().contains(Tags.CONTAINER_INSTANCE)); - - containerInstance.removeTag(Tags.CONTAINER_INSTANCE); - containerInstance.removeTag(Tags.CONTAINER); - containerInstance.removeTag(Tags.ELEMENT); - - assertTrue(containerInstance.getTags().contains(Tags.ELEMENT)); - assertTrue(containerInstance.getTags().contains(Tags.CONTAINER)); - assertTrue(containerInstance.getTags().contains(Tags.CONTAINER_INSTANCE)); - } - - @Test - public void test_uses_ThrowsAnException_WhenADestinationIsNotSpecified() { - ContainerInstance containerInstance = model.addContainerInstance(database); - - try { - containerInstance.uses(null, "", ""); - } catch (IllegalArgumentException iae) { - assertEquals("The destination of a relationship must be specified.", iae.getMessage()); - } - } - - @Test - public void test_uses_AddsARelationship_WhenADestinationIsSpecified() { - Container database = softwareSystem.addContainer("Database", "", ""); - ContainerInstance primaryDatabase = model.addContainerInstance(database); - ContainerInstance secondaryDatabase = model.addContainerInstance(database); - - Relationship relationship = primaryDatabase.uses(secondaryDatabase, "Replicates data to", "Some technology"); - assertSame(primaryDatabase, relationship.getSource()); - assertSame(secondaryDatabase, relationship.getDestination()); - assertEquals("Replicates data to", relationship.getDescription()); - assertEquals("Some technology", relationship.getTechnology()); - } - -} diff --git a/structurizr-core/test/unit/com/structurizr/model/ContainerTests.java b/structurizr-core/test/unit/com/structurizr/model/ContainerTests.java deleted file mode 100644 index 871f94b33..000000000 --- a/structurizr-core/test/unit/com/structurizr/model/ContainerTests.java +++ /dev/null @@ -1,57 +0,0 @@ -package com.structurizr.model; - -import com.structurizr.AbstractWorkspaceTestBase; -import org.junit.Test; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; - -public class ContainerTests extends AbstractWorkspaceTestBase { - - private SoftwareSystem softwareSystem = model.addSoftwareSystem(Location.External, "System", "Description"); - private Container container = softwareSystem.addContainer("Container", "Description", "Some technology"); - - @Test - public void test_getCanonicalName() { - assertEquals("/System/Container", container.getCanonicalName()); - } - - @Test - public void test_getCanonicalName_WhenNameContainsASlashCharacter() { - container.setName("Name1/Name2"); - assertEquals("/System/Name1Name2", container.getCanonicalName()); - } - - @Test - public void test_getParent_ReturnsTheParentSoftwareSystem() { - assertEquals(softwareSystem, container.getParent()); - } - - @Test - public void test_getSoftwareSystem_ReturnsTheParentSoftwareSystem() { - assertEquals(softwareSystem, container.getSoftwareSystem()); - } - - @Test - public void test_removeTags_DoesNotRemoveRequiredTags() { - assertTrue(container.getTags().contains(Tags.ELEMENT)); - assertTrue(container.getTags().contains(Tags.CONTAINER)); - - container.removeTag(Tags.CONTAINER); - container.removeTag(Tags.ELEMENT); - - assertTrue(container.getTags().contains(Tags.ELEMENT)); - assertTrue(container.getTags().contains(Tags.CONTAINER)); - } - - @Test(expected = IllegalArgumentException.class) - public void test_addComponent_ThrowsAnException_WhenANullNameIsSpecified() { - container.addComponent(null, ""); - } - - @Test(expected = IllegalArgumentException.class) - public void test_addComponent_ThrowsAnException_WhenAnEmptyNameIsSpecified() { - container.addComponent(" ", ""); - } - -} diff --git a/structurizr-core/test/unit/com/structurizr/model/DeploymentNodeTests.java b/structurizr-core/test/unit/com/structurizr/model/DeploymentNodeTests.java deleted file mode 100644 index 9ada70bbc..000000000 --- a/structurizr-core/test/unit/com/structurizr/model/DeploymentNodeTests.java +++ /dev/null @@ -1,168 +0,0 @@ -package com.structurizr.model; - -import com.structurizr.AbstractWorkspaceTestBase; -import com.structurizr.util.MapUtils; -import org.junit.Test; - -import static org.junit.Assert.*; - -public class DeploymentNodeTests extends AbstractWorkspaceTestBase { - - @Test - public void test_getCanonicalName_WhenTheDeploymentNodeHasNoParent() { - DeploymentNode deploymentNode = new DeploymentNode(); - deploymentNode.setName("Ubuntu Server"); - - assertEquals("/Ubuntu Server", deploymentNode.getCanonicalName()); - } - - @Test - public void test_getCanonicalName_WhenTheDeploymentNodeHasAParent() { - DeploymentNode parent = new DeploymentNode(); - parent.setName("Ubuntu Server"); - - DeploymentNode child = new DeploymentNode(); - child.setName("Apache Tomcat"); - child.setParent(parent); - - assertEquals("/Ubuntu Server/Apache Tomcat", child.getCanonicalName()); - } - - @Test - public void test_getParent_ReturnsTheParentDeploymentNode() { - DeploymentNode parent = new DeploymentNode(); - assertNull(parent.getParent()); - - DeploymentNode child = new DeploymentNode(); - child.setParent(parent); - assertSame(parent, child.getParent()); - } - - @Test - public void test_getRequiredTags() { - DeploymentNode deploymentNode = new DeploymentNode(); - assertEquals(0, deploymentNode.getRequiredTags().size()); - } - - @Test - public void test_getTags() { - DeploymentNode deploymentNode = new DeploymentNode(); - assertEquals("", deploymentNode.getTags()); - } - - @Test - public void test_add_ThrowsAnException_WhenAContainerIsNotSpecified() { - try { - DeploymentNode deploymentNode = new DeploymentNode(); - deploymentNode.add(null); - fail(); - } catch (IllegalArgumentException iae) { - assertEquals("A container must be specified.", iae.getMessage()); - } - } - - @Test - public void test_add_AddsAContainerInstance_WhenAContainerIsSpecified() { - SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System", ""); - Container container = softwareSystem.addContainer("Container", "", ""); - DeploymentNode deploymentNode = model.addDeploymentNode("Deployment Node", "", ""); - ContainerInstance containerInstance = deploymentNode.add(container); - - assertNotNull(containerInstance); - assertSame(container, containerInstance.getContainer()); - assertTrue(deploymentNode.getContainerInstances().contains(containerInstance)); - } - - @Test - public void test_addDeploymentNode_ThrowsAnException_WhenANameIsNotSpecified() { - try { - DeploymentNode parent = model.addDeploymentNode("Parent", "", ""); - parent.addDeploymentNode(null, "", ""); - fail(); - } catch (IllegalArgumentException iae) { - assertEquals("A name must be specified.", iae.getMessage()); - } - } - - @Test - public void test_addDeploymentNode_AddsAChildDeploymentNode_WhenANameIsSpecified() { - DeploymentNode parent = model.addDeploymentNode("Parent", "", ""); - - DeploymentNode child = parent.addDeploymentNode("Child 1", "Description", "Technology"); - assertNotNull(child); - assertEquals("Child 1", child.getName()); - assertEquals("Description", child.getDescription()); - assertEquals("Technology", child.getTechnology()); - assertEquals(1, child.getInstances()); - assertTrue(child.getProperties().isEmpty()); - assertTrue(parent.getChildren().contains(child)); - - child = parent.addDeploymentNode("Child 2", "Description", "Technology", 4); - assertNotNull(child); - assertEquals("Child 2", child.getName()); - assertEquals("Description", child.getDescription()); - assertEquals("Technology", child.getTechnology()); - assertEquals(4, child.getInstances()); - assertTrue(child.getProperties().isEmpty()); - assertTrue(parent.getChildren().contains(child)); - - child = parent.addDeploymentNode("Child 3", "Description", "Technology", 4, MapUtils.create("name=value")); - assertNotNull(child); - assertEquals("Child 3", child.getName()); - assertEquals("Description", child.getDescription()); - assertEquals("Technology", child.getTechnology()); - assertEquals(4, child.getInstances()); - assertEquals(1, child.getProperties().size()); - assertEquals("value", child.getProperties().get("name")); - assertTrue(parent.getChildren().contains(child)); - } - - @Test - public void test_uses_ThrowsAnException_WhenANullDestinationIsSpecified() { - try { - DeploymentNode deploymentNode = model.addDeploymentNode("Deployment Node", "", ""); - deploymentNode.uses(null, "", ""); - fail(); - } catch (IllegalArgumentException iae) { - assertEquals("The destination must be specified.", iae.getMessage()); - } - } - - @Test - public 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"); - - assertNotNull(relationship); - assertSame(primaryNode, relationship.getSource()); - assertSame(secondaryNode, relationship.getDestination()); - assertEquals("Replicates data to", relationship.getDescription()); - assertEquals("Some technology", relationship.getTechnology()); - } - - @Test - public void test_getDeploymentNodeWithName_ThrowsAnException_WhenANameIsNotSpecified() { - try { - DeploymentNode deploymentNode = new DeploymentNode(); - deploymentNode.getDeploymentNodeWithName(null); - fail(); - } catch (IllegalArgumentException iae) { - assertEquals("A name must be specified.", iae.getMessage()); - } - } - - @Test - public void test_getDeploymentNodeWithName_ReturnsNull_WhenThereIsNoDeploymentWithTheSpecifiedName() { - DeploymentNode deploymentNode = new DeploymentNode(); - assertNull(deploymentNode.getDeploymentNodeWithName("foo")); - } - - @Test - public void test_getDeploymentNodeWithName_ReturnsTheNamedDeploymentNode_WhenThereIsADeploymentWithTheSpecifiedName() { - DeploymentNode parent = model.addDeploymentNode("parent", "", ""); - DeploymentNode child = parent.addDeploymentNode("child", "", ""); - assertSame(child, parent.getDeploymentNodeWithName("child")); - } - -} \ No newline at end of file diff --git a/structurizr-core/test/unit/com/structurizr/model/ElementTests.java b/structurizr-core/test/unit/com/structurizr/model/ElementTests.java deleted file mode 100644 index 133a96a2e..000000000 --- a/structurizr-core/test/unit/com/structurizr/model/ElementTests.java +++ /dev/null @@ -1,387 +0,0 @@ -package com.structurizr.model; - -import com.structurizr.AbstractWorkspaceTestBase; -import com.structurizr.Workspace; -import org.junit.Test; - -import java.util.HashMap; -import java.util.Map; - -import static org.junit.Assert.*; - -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 - public void test_setName_ThrowsAnException_WhenANullValueIsSpecified() { - Element element = model.addSoftwareSystem("Name", "Description"); - try { - element.setName(null); - fail(); - } catch (Exception e) { - assertEquals("The name of an element must not be null or empty.", e.getMessage()); - } - } - - @Test - public void test_setName_ThrowsAnException_WhenAnEmptyValueIsSpecified() { - Element element = model.addSoftwareSystem("Name", "Description"); - try { - element.setName(" "); - fail(); - } catch (Exception e) { - assertEquals("The name of an element must not be null or empty.", e.getMessage()); - } - } - - @Test - public void test_getTags_WhenThereAreNoTags() { - Element element = model.addSoftwareSystem("Name", "Description"); - assertEquals("Element,Software System", element.getTags()); - } - - @Test - public 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() { - Element element = model.addSoftwareSystem("Name", "Description"); - element.setTags(null); - assertEquals("Element,Software System", element.getTags()); - } - - @Test - public void test_addTags_DoesNotDoAnything_WhenPassedNull() { - Element element = model.addSoftwareSystem("Name", "Description"); - element.addTags((String)null); - assertEquals("Element,Software System", element.getTags()); - - element.addTags(null, null, null); - assertEquals("Element,Software System", element.getTags()); - } - - @Test - public 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() { - 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_hasEfferentRelationshipWith_ReturnsFalse_WhenANullElementIsSpecified() { - SoftwareSystem softwareSystem1 = model.addSoftwareSystem("System 1", ""); - assertFalse(softwareSystem1.hasEfferentRelationshipWith(null)); - } - - @Test - public void test_hasEfferentRelationshipWith_ReturnsFalse_WhenTheSameElementIsSpecifiedAndNoCyclicRelationshipExists() { - SoftwareSystem softwareSystem1 = model.addSoftwareSystem("System 1", ""); - assertFalse(softwareSystem1.hasEfferentRelationshipWith(softwareSystem1)); - } - - @Test - public 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() { - SoftwareSystem softwareSystem1 = model.addSoftwareSystem("System 1", ""); - SoftwareSystem softwareSystem2 = model.addSoftwareSystem("System 2", ""); - softwareSystem1.uses(softwareSystem2, "uses"); - assertTrue(softwareSystem1.hasEfferentRelationshipWith(softwareSystem2)); - } - - @Test - public void test_getEfferentRelationshipWith_ReturnsNull_WhenANullElementIsSpecified() { - SoftwareSystem softwareSystem1 = model.addSoftwareSystem("System 1", ""); - assertNull(softwareSystem1.getEfferentRelationshipWith(null)); - } - - @Test - public void test_getEfferentRelationshipWith_ReturnsNull_WhenTheSameElementIsSpecifiedAndNoCyclicRelationshipExists() { - SoftwareSystem softwareSystem1 = model.addSoftwareSystem("System 1", ""); - assertNull(softwareSystem1.getEfferentRelationshipWith(softwareSystem1)); - } - - @Test - public void test_getEfferentRelationshipWith_ReturnsCyclicRelationship_WhenTheSameElementIsSpecifiedAndACyclicRelationshipExists() { - SoftwareSystem softwareSystem1 = model.addSoftwareSystem("System 1", ""); - softwareSystem1.uses(softwareSystem1, "uses"); - - Relationship relationship = softwareSystem1.getEfferentRelationshipWith(softwareSystem1); - assertSame(softwareSystem1, relationship.getSource()); - assertEquals("uses", relationship.getDescription()); - assertSame(softwareSystem1, relationship.getDestination()); - } - - @Test - public void test_getEfferentRelationshipWith_ReturnsTheRelationship_WhenThereIsARelationship() { - SoftwareSystem softwareSystem1 = model.addSoftwareSystem("System 1", ""); - SoftwareSystem softwareSystem2 = model.addSoftwareSystem("System 2", ""); - softwareSystem1.uses(softwareSystem2, "uses"); - - Relationship relationship = softwareSystem1.getEfferentRelationshipWith(softwareSystem2); - assertSame(softwareSystem1, relationship.getSource()); - assertEquals("uses", relationship.getDescription()); - assertSame(softwareSystem2, relationship.getDestination()); - } - - @Test - public void test_hasAfferentRelationships_ReturnsFalse_WhenThereAreNoIncomingRelationships() { - SoftwareSystem softwareSystem1 = model.addSoftwareSystem("System 1", ""); - SoftwareSystem softwareSystem2 = model.addSoftwareSystem("System 2", ""); - softwareSystem1.uses(softwareSystem2, "Uses"); - - assertFalse(softwareSystem1.hasAfferentRelationships()); - } - - @Test - public void test_hasAfferentRelationships_ReturnsTrue_WhenThereAreIncomingRelationships() { - SoftwareSystem softwareSystem1 = model.addSoftwareSystem("System 1", ""); - SoftwareSystem softwareSystem2 = model.addSoftwareSystem("System 2", ""); - softwareSystem1.uses(softwareSystem2, "Uses"); - - assertTrue(softwareSystem2.hasAfferentRelationships()); - } - - @Test - public void test_addRelationship_DoesNothing_WhenTheSameRelationshipWithASoftwareSystemIsAddedMoreThanOnce() { - SoftwareSystem a = model.addSoftwareSystem("A", ""); - SoftwareSystem b = model.addSoftwareSystem("B", ""); - - Relationship relationship = a.uses(b, "Uses"); - assertNotNull(relationship); - assertEquals(1, model.getRelationships().size()); - - assertNull(a.uses(b, "Uses")); - assertEquals(1, model.getRelationships().size()); - - assertNull(a.uses(b, "Uses", "Technology")); - assertEquals(1, model.getRelationships().size()); - - assertNull(a.uses(b, "Uses", "Technology", InteractionStyle.Synchronous)); - assertEquals(1, model.getRelationships().size()); - } - - @Test - public void test_addRelationship_DoesNothing_WhenTheSameRelationshipWithAContainerIsAddedMoreThanOnce() { - SoftwareSystem a = model.addSoftwareSystem("A", ""); - SoftwareSystem b = model.addSoftwareSystem("B", ""); - Container bb = b.addContainer("BB", "", ""); - - Relationship relationship = a.uses(bb, "Uses"); - assertNotNull(relationship); - assertEquals(1, model.getRelationships().size()); - - assertNull(a.uses(bb, "Uses")); - assertEquals(1, model.getRelationships().size()); - - assertNull(a.uses(bb, "Uses", "Technology")); - assertEquals(1, model.getRelationships().size()); - - assertNull(a.uses(bb, "Uses", "Technology", InteractionStyle.Synchronous)); - assertEquals(1, model.getRelationships().size()); - } - - @Test - public void test_addRelationship_DoesNothing_WhenTheSameRelationshipWithAComponentIsAddedMoreThanOnce() { - SoftwareSystem a = model.addSoftwareSystem("A", ""); - SoftwareSystem b = model.addSoftwareSystem("B", ""); - Container bb = b.addContainer("BB", "", ""); - Component bbb = bb.addComponent("BBB", "", ""); - - Relationship relationship = a.uses(bbb, "Uses"); - assertNotNull(relationship); - assertEquals(1, model.getRelationships().size()); - - assertNull(a.uses(bbb, "Uses")); - assertEquals(1, model.getRelationships().size()); - - assertNull(a.uses(bbb, "Uses", "Technology")); - assertEquals(1, model.getRelationships().size()); - - assertNull(a.uses(bbb, "Uses", "Technology", InteractionStyle.Synchronous)); - assertEquals(1, model.getRelationships().size()); - } - - @Test - public void test_addRelationship_DoesNothing_WhenTheSameRelationshipWithAPersonIsAddedMoreThanOnce() { - SoftwareSystem a = model.addSoftwareSystem("A", ""); - Person b = model.addPerson("B", ""); - - Relationship relationship = a.delivers(b, "Sends e-mail to"); - assertNotNull(relationship); - assertEquals(1, model.getRelationships().size()); - - assertNull(a.delivers(b, "Sends e-mail to")); - assertEquals(1, model.getRelationships().size()); - - assertNull(a.delivers(b, "Sends e-mail to", "Technology")); - assertEquals(1, model.getRelationships().size()); - - assertNull(a.delivers(b, "Sends e-mail to", "Technology", InteractionStyle.Synchronous)); - assertEquals(1, model.getRelationships().size()); - } - - @Test - public void test_equals_ReturnsFalse_WhenTestedAgainstNull() { - SoftwareSystem softwareSystem = model.addSoftwareSystem("Name", "Description"); - assertFalse(softwareSystem.equals(null)); - } - - @Test - public void test_equals_ReturnsFalse_WhenTheTwoObjectsAreDifferentTypes() { - SoftwareSystem softwareSystem = model.addSoftwareSystem("Name", "Description"); - assertFalse(softwareSystem.equals("hello world")); - } - - @Test - public void test_equals_ReturnsTrue_WhenTestedAgainstItself() { - SoftwareSystem softwareSystem = model.addSoftwareSystem("Name", "Description"); - assertTrue(softwareSystem.equals(softwareSystem)); - } - - @Test - public void test_equals_ReturnsFalse_WhenTheTwoObjectsAreDifferent() { - SoftwareSystem softwareSystemA = model.addSoftwareSystem("A", "Description"); - SoftwareSystem softwareSystemB = model.addSoftwareSystem("B", "Description"); - assertFalse(softwareSystemA.equals(softwareSystemB)); - } - - @Test - public void test_equals_ReturnsFalse_WhenTheTwoElementsHaveTheSameCanonicalNameButAreDifferentTypes() { - SoftwareSystem softwareSystem = model.addSoftwareSystem("Name", "Description"); - Person person = model.addPerson("Name", "Description"); - assertFalse(softwareSystem.equals(person)); - } - - @Test - public void test_equals_ReturnsTrue_WhenTheAnObjectWithTheSameCanonicalNameIsPassed() { - SoftwareSystem softwareSystem1 = new Workspace("", "").getModel().addSoftwareSystem("System 1", ""); - SoftwareSystem softwareSystem2 = new Workspace("", "").getModel().addSoftwareSystem("System 1", ""); - assertTrue(softwareSystem1.equals(softwareSystem2)); - assertTrue(softwareSystem2.equals(softwareSystem1)); - } - - @Test - public 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 - public void test_setUrl_DoesNothing_WhenANullUrlIsSpecified() { - Element element = model.addSoftwareSystem("Name", "Description"); - element.setUrl(null); - assertNull(element.getUrl()); - } - - @Test - public void test_setUrl_DoesNothing_WhenAnEmptyUrlIsSpecified() { - Element element = model.addSoftwareSystem("Name", "Description"); - element.setUrl(" "); - assertNull(element.getUrl()); - } - - @Test - public void test_getProperties_ReturnsAnEmptyList_WhenNoPropertiesHaveBeenAdded() { - Element element = model.addSoftwareSystem("Name", "Description"); - assertEquals(0, element.getProperties().size()); - } - - @Test - public void test_addProperty_ThrowsAnException_WhenTheNameIsNull() { - try { - Element element = model.addSoftwareSystem("Name", "Description"); - element.addProperty(null, "value"); - fail(); - } catch (IllegalArgumentException e) { - assertEquals("A property name must be specified.", e.getMessage()); - } - } - - @Test - public void test_addProperty_ThrowsAnException_WhenTheNameIsEmpty() { - try { - Element element = model.addSoftwareSystem("Name", "Description"); - element.addProperty(" ", "value"); - fail(); - } catch (IllegalArgumentException e) { - assertEquals("A property name must be specified.", e.getMessage()); - } - } - - @Test - public void test_addProperty_ThrowsAnException_WhenTheValueIsNull() { - try { - Element element = model.addSoftwareSystem("Name", "Description"); - element.addProperty("name", null); - fail(); - } catch (IllegalArgumentException e) { - assertEquals("A property value must be specified.", e.getMessage()); - } - } - - @Test - public void test_addProperty_ThrowsAnException_WhenTheValueIsEmpty() { - try { - Element element = model.addSoftwareSystem("Name", "Description"); - element.addProperty("name", " "); - fail(); - } catch (IllegalArgumentException e) { - assertEquals("A property value must be specified.", e.getMessage()); - } - } - - @Test - public 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() { - Element element = model.addSoftwareSystem("Name", "Description"); - element.setProperties(null); - assertEquals(0, element.getProperties().size()); - } - - @Test - public void test_setProperties_SetsTheProperties_WhenANonEmptyMapIsSpecified() { - Element element = model.addSoftwareSystem("Name", "Description"); - Map properties = new HashMap<>(); - properties.put("name", "value"); - element.setProperties(properties); - assertEquals(1, element.getProperties().size()); - assertEquals("value", element.getProperties().get("name")); - } - -} \ No newline at end of file diff --git a/structurizr-core/test/unit/com/structurizr/model/ModelTests.java b/structurizr-core/test/unit/com/structurizr/model/ModelTests.java deleted file mode 100644 index f69409792..000000000 --- a/structurizr-core/test/unit/com/structurizr/model/ModelTests.java +++ /dev/null @@ -1,618 +0,0 @@ -package com.structurizr.model; - -import com.structurizr.AbstractWorkspaceTestBase; -import org.junit.Test; - -import java.util.Set; - -import static org.junit.Assert.*; - -public class ModelTests extends AbstractWorkspaceTestBase { - - @Test(expected = IllegalArgumentException.class) - public void test_addSoftwareSystem_ThrowsAnException_WhenANullNameIsSpecified() { - model.addSoftwareSystem(null, ""); - } - - @Test(expected = IllegalArgumentException.class) - public void test_addSoftwareSystem_ThrowsAnException_WhenAnEmptyNameIsSpecified() { - model.addSoftwareSystem(" ", ""); - } - - @Test(expected = IllegalArgumentException.class) - public void test_addPerson_ThrowsAnException_WhenANullNameIsSpecified() { - model.addPerson(null, ""); - } - - @Test(expected = IllegalArgumentException.class) - public void test_addPerson_ThrowsAnException_WhenAnEmptyNameIsSpecified() { - model.addPerson(" ", ""); - } - - @Test - public void test_addSoftwareSystem_AddsTheSoftwareSystem_WhenASoftwareSystemDoesNotExistWithTheSameName() { - assertTrue(model.getSoftwareSystems().isEmpty()); - SoftwareSystem softwareSystem = model.addSoftwareSystem(Location.External, "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()); - assertSame(softwareSystem, model.getSoftwareSystems().iterator().next()); - } - - @Test - public void test_addSoftwareSystem_ThrowsAnException_WhenASoftwareSystemExistsWithTheSameName() { - SoftwareSystem softwareSystem = model.addSoftwareSystem(Location.External, "System A", "Some description"); - assertEquals(1, model.getSoftwareSystems().size()); - - try { - model.addSoftwareSystem(Location.External, "System A", "Description"); - fail(); - } catch (Exception e) { - assertEquals("A software system named 'System A' already exists.", e.getMessage()); - } - } - - @Test - public void test_addSoftwareSystemWithoutSpecifyingLocation_AddsTheSoftwareSystem_WhenASoftwareSystemDoesNotExistWithTheSameName() { - assertTrue(model.getSoftwareSystems().isEmpty()); - 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()); - assertSame(softwareSystem, model.getSoftwareSystems().iterator().next()); - } - - @Test - public 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()); - - assertEquals(Location.Internal, person.getLocation()); - assertEquals("Some internal user", person.getName()); - assertEquals("Some description", person.getDescription()); - assertEquals("1", person.getId()); - assertSame(person, model.getPeople().iterator().next()); - } - - @Test - public void test_addPerson_ThrowsAnException_WhenAPersonExistsWithTheSameName() { - Person person = model.addPerson(Location.Internal, "Admin User", "Description"); - assertEquals(1, model.getPeople().size()); - - try { - model.addPerson(Location.External, "Admin User", "Description"); - fail(); - } catch (Exception e) { - assertEquals("A person named 'Admin User' already exists.", e.getMessage()); - } - } - - @Test - public void test_addPerson_AddsThePersonWithoutSpecifyingTheLocation_WhenAPersonDoesNotExistWithTheSameName() { - assertTrue(model.getPeople().isEmpty()); - 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()); - assertSame(person, model.getPeople().iterator().next()); - } - - @Test - public void test_getElement_ReturnsNull_WhenAnElementWithTheSpecifiedIdDoesNotExist() { - assertNull(model.getElement("100")); - } - - @Test - public 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() { - Model newModel = new Model(); - SoftwareSystem softwareSystem = newModel.addSoftwareSystem(Location.Unspecified, "Name", "Description"); - assertFalse(model.contains(softwareSystem)); - } - - @Test - public void test_contains_ReturnsTrue_WhenTheSpecifiedElementIsInTheModel() { - SoftwareSystem softwareSystem = model.addSoftwareSystem(Location.Unspecified, "Name", "Description"); - assertTrue(model.contains(softwareSystem)); - } - - @Test - public void test_getSoftwareSystemWithName_ReturnsNull_WhenASoftwareSystemWithTheSpecifiedNameDoesNotExist() { - assertNull(model.getSoftwareSystemWithName("System X")); - } - - @Test - public 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_ReturnsNull_WhenASoftwareSystemWithTheSpecifiedIdDoesNotExist() { - assertNull(model.getSoftwareSystemWithId("100")); - } - - @Test - public 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() { - assertNull(model.getPersonWithName("Admin User")); - } - - @Test - public void test_getPersonWithName_ReturnsAPerson_WhenAPersonWithTheSpecifiedNameExists() { - Person person = model.addPerson(Location.External, "Admin User", "Description"); - assertSame(person, model.getPersonWithName("Admin User")); - } - - @Test - public void test_addRelationship_AddsARelationshipWithTheSpecifiedDescription() { - SoftwareSystem a = model.addSoftwareSystem("A", ""); - SoftwareSystem b = model.addSoftwareSystem("B", ""); - Relationship relationship = model.addRelationship(a, b, "Uses"); - - assertSame(a, relationship.getSource()); - assertSame(b, relationship.getDestination()); - assertEquals("Uses", relationship.getDescription()); - assertNull(relationship.getTechnology()); - assertEquals(InteractionStyle.Synchronous, relationship.getInteractionStyle()); - - assertTrue(model.getRelationships().contains(relationship)); - } - - @Test - public void test_addRelationship_AddsARelationshipWithTheSpecifiedDescriptionAndTechnology() { - SoftwareSystem a = model.addSoftwareSystem("A", ""); - SoftwareSystem b = model.addSoftwareSystem("B", ""); - Relationship relationship = model.addRelationship(a, b, "Uses", "HTTPS"); - - assertSame(a, relationship.getSource()); - assertSame(b, relationship.getDestination()); - assertEquals("Uses", relationship.getDescription()); - assertEquals("HTTPS", relationship.getTechnology()); - assertEquals(InteractionStyle.Synchronous, relationship.getInteractionStyle()); - - assertTrue(model.getRelationships().contains(relationship)); - } - - @Test - public void test_addRelationship_AddsARelationshipWithTheSpecifiedDescriptionAndTechnologyAndInteractionStyle() { - SoftwareSystem a = model.addSoftwareSystem("A", ""); - SoftwareSystem b = model.addSoftwareSystem("B", ""); - Relationship relationship = model.addRelationship(a, b, "Uses", "HTTPS", InteractionStyle.Asynchronous); - - assertSame(a, relationship.getSource()); - assertSame(b, relationship.getDestination()); - assertEquals("Uses", relationship.getDescription()); - assertEquals("HTTPS", relationship.getTechnology()); - assertEquals(InteractionStyle.Asynchronous, relationship.getInteractionStyle()); - - assertTrue(model.getRelationships().contains(relationship)); - } - - @Test - public void test_addImplicitRelationships_WhenSourceAndDestinationAreComponentsInDifferentSoftwareSystems() { - SoftwareSystem a = model.addSoftwareSystem("A", ""); - Container aa = a.addContainer("AA", "", ""); - Component aaa = aa.addComponent("AAA", "", ""); - - SoftwareSystem b = model.addSoftwareSystem("B", ""); - Container bb = b.addContainer("BB", "", ""); - Component bbb = bb.addComponent("BBB", "", ""); - - aaa.uses(bbb, "Uses"); - assertEquals(1, model.getRelationships().size()); - assertTrue(aaa.hasEfferentRelationshipWith(bbb)); - - // AAA->BBB implies AAA->BB AAA->B AA->BBB AA->BB AA->B A->BBB A->BB A->B - Set implicitRelationships = model.addImplicitRelationships(); - assertEquals(9, model.getRelationships().size()); - assertEquals(8, implicitRelationships.size()); - assertTrue(aaa.hasEfferentRelationshipWith(bb)); - assertTrue(aa.hasEfferentRelationshipWith(bbb)); - assertTrue(aa.hasEfferentRelationshipWith(bb)); - assertTrue(aaa.hasEfferentRelationshipWith(b)); - assertTrue(a.hasEfferentRelationshipWith(bbb)); - assertTrue(aa.hasEfferentRelationshipWith(b)); - assertTrue(a.hasEfferentRelationshipWith(bb)); - assertTrue(a.hasEfferentRelationshipWith(b)); - } - - @Test - public void test_addImplicitRelationships_WhenSourceIsAComponentAndDestinationIsAContainerInADifferentSoftwareSystem() { - SoftwareSystem a = model.addSoftwareSystem("A", ""); - Container aa = a.addContainer("AA", "", ""); - Component aaa = aa.addComponent("AAA", "", ""); - - SoftwareSystem b = model.addSoftwareSystem("B", ""); - Container bb = b.addContainer("BB", "", ""); - - aaa.uses(bb, "Uses"); - assertEquals(1, model.getRelationships().size()); - assertTrue(aaa.hasEfferentRelationshipWith(bb)); - - // AAA->BB implies AAA->B AA->BB AA->B A->BB A->B - Set implicitRelationships = model.addImplicitRelationships(); - assertEquals(6, model.getRelationships().size()); - assertEquals(5, implicitRelationships.size()); - assertTrue(aa.hasEfferentRelationshipWith(bb)); - assertTrue(aaa.hasEfferentRelationshipWith(b)); - assertTrue(aa.hasEfferentRelationshipWith(b)); - assertTrue(a.hasEfferentRelationshipWith(bb)); - assertTrue(a.hasEfferentRelationshipWith(b)); - } - - @Test - public void test_addImplicitRelationships_WhenSourceIsAComponentAndDestinationIsADifferentSoftwareSystem() { - SoftwareSystem a = model.addSoftwareSystem("A", ""); - Container aa = a.addContainer("AA", "", ""); - Component aaa = aa.addComponent("AAA", "", ""); - - SoftwareSystem b = model.addSoftwareSystem("B", ""); - - aaa.uses(b, "Uses"); - assertEquals(1, model.getRelationships().size()); - assertTrue(aaa.hasEfferentRelationshipWith(b)); - - // AAA->B implies AA->B A->B - Set implicitRelationships = model.addImplicitRelationships(); - assertEquals(3, model.getRelationships().size()); - assertEquals(2, implicitRelationships.size()); - assertTrue(aa.hasEfferentRelationshipWith(b)); - assertTrue(a.hasEfferentRelationshipWith(b)); - } - - @Test - public void test_addImplicitRelationships_WhenSourceIsAContainerAndDestinationIsAComponentInADifferentSoftwareSystem() { - SoftwareSystem a = model.addSoftwareSystem("A", ""); - Container aa = a.addContainer("AA", "", ""); - - SoftwareSystem b = model.addSoftwareSystem("B", ""); - Container bb = b.addContainer("BB", "", ""); - Component bbb = bb.addComponent("BBB", "", ""); - - aa.uses(bbb, "Uses"); - assertEquals(1, model.getRelationships().size()); - assertTrue(aa.hasEfferentRelationshipWith(bbb)); - - // AA->BBB implies AA->BB AA->B A->BBB A->BB A->B - Set implicitRelationships = model.addImplicitRelationships(); - assertEquals(6, model.getRelationships().size()); - assertEquals(5, implicitRelationships.size()); - assertTrue(aa.hasEfferentRelationshipWith(bb)); - assertTrue(aa.hasEfferentRelationshipWith(b)); - assertTrue(a.hasEfferentRelationshipWith(bbb)); - assertTrue(a.hasEfferentRelationshipWith(bb)); - assertTrue(a.hasEfferentRelationshipWith(b)); - } - - @Test - public void test_addImplicitRelationships_WhenSourceAndDestinationAreContainersInDifferentSoftwareSystems() { - SoftwareSystem a = model.addSoftwareSystem("A", ""); - Container aa = a.addContainer("AA", "", ""); - - SoftwareSystem b = model.addSoftwareSystem("B", ""); - Container bb = b.addContainer("BB", "", ""); - - aa.uses(bb, "Uses"); - assertEquals(1, model.getRelationships().size()); - assertTrue(aa.hasEfferentRelationshipWith(bb)); - - // AA->BB implies AA->B A->BB A->B - Set implicitRelationships = model.addImplicitRelationships(); - assertEquals(4, model.getRelationships().size()); - assertEquals(3, implicitRelationships.size()); - assertTrue(aa.hasEfferentRelationshipWith(b)); - assertTrue(a.hasEfferentRelationshipWith(bb)); - assertTrue(a.hasEfferentRelationshipWith(b)); - } - - @Test - public void test_addImplicitRelationships_WhenSourceIsAContainerAndDestinationIsADifferentSoftwareSystem() { - SoftwareSystem a = model.addSoftwareSystem("A", ""); - Container aa = a.addContainer("AA", "", ""); - - SoftwareSystem b = model.addSoftwareSystem("B", ""); - - aa.uses(b, "Uses"); - assertEquals(1, model.getRelationships().size()); - assertTrue(aa.hasEfferentRelationshipWith(b)); - - // AA->B implies A->B - Set implicitRelationships = model.addImplicitRelationships(); - assertEquals(2, model.getRelationships().size()); - assertEquals(1, implicitRelationships.size()); - assertTrue(a.hasEfferentRelationshipWith(b)); - } - - @Test - public void test_addImplicitRelationships_WhenSourceIsASoftwareSystemAndDestinationIsAComponentInADifferentSoftwareSystem() { - SoftwareSystem a = model.addSoftwareSystem("A", ""); - - SoftwareSystem b = model.addSoftwareSystem("B", ""); - Container bb = b.addContainer("BB", "", ""); - Component bbb = bb.addComponent("BBB", "", ""); - - a.uses(bbb, "Uses"); - assertEquals(1, model.getRelationships().size()); - assertTrue(a.hasEfferentRelationshipWith(bbb)); - - // A->BBB implies A->BB A->B - Set implicitRelationships = model.addImplicitRelationships(); - assertEquals(3, model.getRelationships().size()); - assertEquals(2, implicitRelationships.size()); - assertTrue(a.hasEfferentRelationshipWith(bb)); - assertTrue(a.hasEfferentRelationshipWith(b)); - } - - @Test - public void test_addImplicitRelationships_WhenSourceIsASoftwareSystemAndDestinationIsAContainerInADifferentSoftwareSystem() { - SoftwareSystem a = model.addSoftwareSystem("A", ""); - - SoftwareSystem b = model.addSoftwareSystem("B", ""); - Container bb = b.addContainer("BB", "", ""); - - a.uses(bb, "Uses"); - assertEquals(1, model.getRelationships().size()); - assertTrue(a.hasEfferentRelationshipWith(bb)); - - // A->BB implies A->B - Set implicitRelationships = model.addImplicitRelationships(); - assertEquals(2, model.getRelationships().size()); - assertEquals(1, implicitRelationships.size()); - assertTrue(a.hasEfferentRelationshipWith(b)); - } - - @Test - public void test_addImplicitRelationships_WhenSourceAndDestinationAreDifferentSoftwareSystems() { - SoftwareSystem a = model.addSoftwareSystem("A", ""); - - SoftwareSystem b = model.addSoftwareSystem("B", ""); - - a.uses(b, "Uses"); - assertEquals(1, model.getRelationships().size()); - assertTrue(a.hasEfferentRelationshipWith(b)); - - Set implicitRelationships = model.addImplicitRelationships(); - assertEquals(1, model.getRelationships().size()); - assertEquals(0, implicitRelationships.size()); - } - - @Test - public void test_addImplicitRelationships_WhenSourceAndDestinationAreComponentsInTheSameContainer() { - SoftwareSystem a = model.addSoftwareSystem("A", ""); - Container aa = a.addContainer("AA", "", ""); - Component aaa1 = aa.addComponent("AAA1", "", ""); - Component aaa2 = aa.addComponent("AAA2", "", ""); - - aaa1.uses(aaa2, "Uses"); - assertEquals(1, model.getRelationships().size()); - assertTrue(aaa1.hasEfferentRelationshipWith(aaa2)); - - Set implicitRelationships = model.addImplicitRelationships(); - assertEquals(1, model.getRelationships().size()); - assertEquals(0, implicitRelationships.size()); - } - - @Test - public void test_addImplicitRelationships_WhenSourceAndDestinationAreContainersInTheSameContainer() { - SoftwareSystem a = model.addSoftwareSystem("A", ""); - Container aa1 = a.addContainer("AA1", "", ""); - Container aa2 = a.addContainer("AA2", "", ""); - - aa1.uses(aa2, "Uses"); - assertEquals(1, model.getRelationships().size()); - assertTrue(aa1.hasEfferentRelationshipWith(aa2)); - - Set implicitRelationships = model.addImplicitRelationships(); - assertEquals(1, model.getRelationships().size()); - assertEquals(0, implicitRelationships.size()); - } - - @Test - public void test_addImplicitRelationships_WhenSourceAndDestinationAreComponentsInTheDifferentContainersInTheSameSoftwareSystem() { - SoftwareSystem a = model.addSoftwareSystem("A", ""); - Container aa1 = a.addContainer("AA1", "", ""); - Container aa2 = a.addContainer("AA2", "", ""); - Component aaa1 = aa1.addComponent("AAA1", "", ""); - Component aaa2 = aa2.addComponent("AAA2", "", ""); - - aaa1.uses(aaa2, "Uses"); - assertEquals(1, model.getRelationships().size()); - assertTrue(aaa1.hasEfferentRelationshipWith(aaa2)); - - // AAA1->AAA2 implies AAA1->AA2 AA1->AAA2 AA1->AA2 - Set implicitRelationships = model.addImplicitRelationships(); - assertEquals(4, model.getRelationships().size()); - assertEquals(3, implicitRelationships.size()); - assertTrue(aaa1.hasEfferentRelationshipWith(aa2)); - assertTrue(aa1.hasEfferentRelationshipWith(aaa2)); - assertTrue(aa1.hasEfferentRelationshipWith(aa2)); - } - - @Test - public void test_addContainerInstance_ThrowsAnException_WhenANullContainerIsSpecified() { - try { - model.addContainerInstance(null); - fail(); - } catch (Exception e) { - assertEquals("A container must be specified.", e.getMessage()); - } - } - - @Test - public void test_addContainerInstance_AddsAContainerInstance_WhenAContainerIsSpecified() { - SoftwareSystem softwareSystem1 = model.addSoftwareSystem("Software System 1", "Description"); - Container container1 = softwareSystem1.addContainer("Container 1", "Description", "Technology"); - - SoftwareSystem softwareSystem2 = model.addSoftwareSystem("Software System 2", "Description"); - Container container2 = softwareSystem2.addContainer("Container 2", "Description", "Technology"); - - SoftwareSystem softwareSystem3 = model.addSoftwareSystem("Software System 3", "Description"); - Container container3 = softwareSystem3.addContainer("Container 3", "Description", "Technology"); - - container1.uses(container2, "Uses 1", "Technology 1", InteractionStyle.Synchronous); - container2.uses(container3, "Uses 2", "Technology 2", InteractionStyle.Asynchronous); - - ContainerInstance containerInstance1 = model.addContainerInstance(container1); - ContainerInstance containerInstance2 = model.addContainerInstance(container2); - ContainerInstance containerInstance3 = model.addContainerInstance(container3); - - assertSame(container2, containerInstance2.getContainer()); - assertEquals(container2.getId(), containerInstance2.getContainerId()); - assertSame(softwareSystem2, containerInstance2.getParent()); - assertEquals("/Software System 2/Container 2[1]", containerInstance2.getCanonicalName()); - assertEquals("Element,Container,Container Instance", containerInstance2.getTags()); - - assertEquals(1, containerInstance1.getRelationships().size()); - Relationship relationship = containerInstance1.getRelationships().iterator().next(); - assertSame(containerInstance1, relationship.getSource()); - assertSame(containerInstance2, relationship.getDestination()); - assertEquals("Uses 1", relationship.getDescription()); - assertEquals("Technology 1", relationship.getTechnology()); - assertEquals(InteractionStyle.Synchronous, relationship.getInteractionStyle()); - - assertEquals(1, containerInstance2.getRelationships().size()); - relationship = containerInstance2.getRelationships().iterator().next(); - assertSame(containerInstance2, relationship.getSource()); - assertSame(containerInstance3, relationship.getDestination()); - assertEquals("Uses 2", relationship.getDescription()); - assertEquals("Technology 2", relationship.getTechnology()); - assertEquals(InteractionStyle.Asynchronous, relationship.getInteractionStyle()); - } - - @Test - public void test_getElement_ThrowsAnException_WhenANullIdIsSpecified() { - try { - model.getElement(null); - } catch (IllegalArgumentException iae) { - assertEquals("An ID must be specified.", iae.getMessage()); - } - } - - @Test - public void test_getElement_ThrowsAnException_WhenAnEmptyIdIsSpecified() { - try { - model.getElement(" "); - } catch (IllegalArgumentException iae) { - assertEquals("An ID must be specified.", iae.getMessage()); - } - } - - @Test - public void test_getElementWithCanonicalName_ThrowsAnException_WhenANullCanonicalNameIsSpecified() { - try { - model.getElementWithCanonicalName(null); - } catch (IllegalArgumentException iae) { - assertEquals("A canonical name must be specified.", iae.getMessage()); - } - } - - @Test - public void test_getElementWithCanonicalName_ThrowsAnException_WhenAnEmptyCanonicalNameIsSpecified() { - try { - model.getElementWithCanonicalName(" "); - } catch (IllegalArgumentException iae) { - assertEquals("A canonical name must be specified.", iae.getMessage()); - } - } - - @Test - public void test_getElementWithCanonicalName_ReturnsNull_WhenAnElementWithTheSpecifiedCanonicalNameDoesNotExist() { - assertNull(model.getElementWithCanonicalName("Software System")); - } - - @Test - public void test_getElementWithCanonicalName_ReturnsTheElement_WhenAnElementWithTheSpecifiedCanonicalNameExists() { - SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System", "Description"); - - assertSame(model.getElementWithCanonicalName("/Software System"), softwareSystem); - assertSame(model.getElementWithCanonicalName("Software System"), softwareSystem); - - Container container = softwareSystem.addContainer("Web Application", "Description", "Technology"); - assertSame(container, model.getElementWithCanonicalName("/Software System/Web Application")); - assertSame(container, model.getElementWithCanonicalName("Software System/Web Application")); - } - - @Test - public void test_addImplicitRelationships_SetsTheDescriptionOfThePropagatedRelationship_WhenThereIsOnlyOnePossibleDescription() { - Person user = model.addPerson("Person", "Description"); - SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System", "Description"); - Container webApplication = softwareSystem.addContainer("Web Application", "Description", "Technology"); - - user.uses(webApplication, "Uses", ""); - model.addImplicitRelationships(); - - assertEquals(2, user.getRelationships().size()); - - Relationship relationship = user.getRelationships().stream().filter(r -> r.getDestination() == softwareSystem).findFirst().get(); - assertEquals("Uses", relationship.getDescription()); - } - - @Test - public void test_addImplicitRelationships_DoeNotSetTheDescriptionOfThePropagatedRelationship_WhenThereIsMoreThanOnePossibleDescription() { - Person user = model.addPerson("Person", "Description"); - SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System", "Description"); - Container webApplication = softwareSystem.addContainer("Web Application", "Description", "Technology"); - - user.uses(webApplication, "Does something", ""); - user.uses(webApplication, "Does something else", ""); - model.addImplicitRelationships(); - - assertEquals(3, user.getRelationships().size()); - - Relationship relationship = user.getRelationships().stream().filter(r -> r.getDestination() == softwareSystem).findFirst().get(); - assertEquals("", relationship.getDescription()); - } - - @Test - public void test_addImplicitRelationships_SetsTheTechnologyOfThePropagatedRelationship_WhenThereIsOnlyOnePossibleTechnology() { - Person user = model.addPerson("Person", "Description"); - SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System", "Description"); - Container webApplication = softwareSystem.addContainer("Web Application", "Description", "Technology"); - - user.uses(webApplication, "Uses", "HTTPS"); - model.addImplicitRelationships(); - - assertEquals(2, user.getRelationships().size()); - - Relationship relationship = user.getRelationships().stream().filter(r -> r.getDestination() == softwareSystem).findFirst().get(); - assertEquals("HTTPS", relationship.getTechnology()); - } - - @Test - public void test_addImplicitRelationships_DoeNotSetTheTechnologyOfThePropagatedRelationship_WhenThereIsMoreThanOnePossibleTechnology() { - Person user = model.addPerson("Person", "Description"); - SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System", "Description"); - Container webApplication = softwareSystem.addContainer("Web Application", "Description", "Technology"); - - user.uses(webApplication, "Does something", "Some technology"); - user.uses(webApplication, "Does something else", "Some other technology"); - model.addImplicitRelationships(); - - assertEquals(3, user.getRelationships().size()); - - Relationship relationship = user.getRelationships().stream().filter(r -> r.getDestination() == softwareSystem).findFirst().get(); - assertEquals("", relationship.getTechnology()); - } - -} \ No newline at end of file diff --git a/structurizr-core/test/unit/com/structurizr/model/PersonTests.java b/structurizr-core/test/unit/com/structurizr/model/PersonTests.java deleted file mode 100644 index 8b89ec2f6..000000000 --- a/structurizr-core/test/unit/com/structurizr/model/PersonTests.java +++ /dev/null @@ -1,88 +0,0 @@ -package com.structurizr.model; - -import com.structurizr.AbstractWorkspaceTestBase; -import org.junit.Test; - -import static org.junit.Assert.*; - -public class PersonTests extends AbstractWorkspaceTestBase { - - private Person person = model.addPerson("Person", "Description"); - - @Test - public void test_getCanonicalName() { - assertEquals("/Person", person.getCanonicalName()); - } - - @Test - public void test_getCanonicalName_WhenNameContainsASlashCharacter() { - person.setName("Name1/Name2"); - assertEquals("/Name1Name2", person.getCanonicalName()); - } - - @Test - public void test_getParent_ReturnsNull() { - assertNull(person.getParent()); - } - - @Test - public void test_removeTags_DoesNotRemoveRequiredTags() { - assertTrue(person.getTags().contains(Tags.ELEMENT)); - assertTrue(person.getTags().contains(Tags.PERSON)); - - person.removeTag(Tags.PERSON); - person.removeTag(Tags.ELEMENT); - - assertTrue(person.getTags().contains(Tags.ELEMENT)); - assertTrue(person.getTags().contains(Tags.PERSON)); - } - - @Test - public void test_interactsWith_AddsARelationshipWhenTheDescriptionIsSpecified() { - Person person1 = model.addPerson("Person 1", "Description"); - Person person2 = model.addPerson("Person 2", "Description"); - - person1.interactsWith(person2, "Sends an e-mail to"); - assertEquals(1, person1.getRelationships().size()); - - Relationship relationship = person1.getRelationships().iterator().next(); - assertSame(person1, relationship.getSource()); - assertSame(person2, relationship.getDestination()); - assertEquals("Sends an e-mail to", relationship.getDescription()); - assertNull(relationship.getTechnology()); - assertEquals(InteractionStyle.Synchronous, relationship.getInteractionStyle()); - } - - @Test - public void test_interactsWith_AddsARelationshipWhenTheDescriptionAndTechnologyAreSpecified() { - Person person1 = model.addPerson("Person 1", "Description"); - Person person2 = model.addPerson("Person 2", "Description"); - - person1.interactsWith(person2, "Sends a message to", "E-mail"); - assertEquals(1, person1.getRelationships().size()); - - Relationship relationship = person1.getRelationships().iterator().next(); - assertSame(person1, relationship.getSource()); - assertSame(person2, relationship.getDestination()); - assertEquals("Sends a message to", relationship.getDescription()); - assertEquals("E-mail", relationship.getTechnology()); - assertEquals(InteractionStyle.Synchronous, relationship.getInteractionStyle()); - } - - @Test - public void test_interactsWith_AddsARelationshipWhenTheDescriptionAndTechnologyAndInteractionStyleAreSpecified() { - Person person1 = model.addPerson("Person 1", "Description"); - Person person2 = model.addPerson("Person 2", "Description"); - - person1.interactsWith(person2, "Sends a message to", "E-mail", InteractionStyle.Asynchronous); - assertEquals(1, person1.getRelationships().size()); - - Relationship relationship = person1.getRelationships().iterator().next(); - assertSame(person1, relationship.getSource()); - assertSame(person2, relationship.getDestination()); - assertEquals("Sends a message to", relationship.getDescription()); - assertEquals("E-mail", relationship.getTechnology()); - assertEquals(InteractionStyle.Asynchronous, relationship.getInteractionStyle()); - } - -} \ No newline at end of file diff --git a/structurizr-core/test/unit/com/structurizr/model/RelationshipTests.java b/structurizr-core/test/unit/com/structurizr/model/RelationshipTests.java deleted file mode 100644 index afdee41a9..000000000 --- a/structurizr-core/test/unit/com/structurizr/model/RelationshipTests.java +++ /dev/null @@ -1,80 +0,0 @@ -package com.structurizr.model; - -import com.structurizr.AbstractWorkspaceTestBase; -import org.junit.Before; -import org.junit.Test; - -import static org.junit.Assert.*; - -public class RelationshipTests extends AbstractWorkspaceTestBase { - - private SoftwareSystem softwareSystem1, softwareSystem2; - - @Before - public void setUp() { - softwareSystem1 = model.addSoftwareSystem(Location.Internal, "Name1", "Description"); - softwareSystem2 = model.addSoftwareSystem(Location.Internal, "Name2", "Description"); - } - - @Test - public void test_getDescription_NeverReturnsNull() { - Relationship relationship = softwareSystem1.uses(softwareSystem2, "Uses"); - relationship.setDescription(null); - assertEquals("", relationship.getDescription()); - } - - @Test - public void test_getTags_WhenThereAreNoTags() { - Relationship relationship = softwareSystem1.uses(softwareSystem2, "uses"); - assertEquals("Relationship,Synchronous", relationship.getTags()); - } - - @Test - public void test_getTags_ReturnsTheListOfTags_WhenThereAreSomeTags() { - Relationship relationship = softwareSystem1.uses(softwareSystem2, "uses"); - relationship.addTags("tag1", "tag2", "tag3"); - assertEquals("Relationship,Synchronous,tag1,tag2,tag3", relationship.getTags()); - } - - @Test - public void test_setTags_DoesNotDoAnything_WhenPassedNull() { - Relationship relationship = softwareSystem1.uses(softwareSystem2, "uses"); - relationship.setTags(null); - assertEquals("Relationship,Synchronous", relationship.getTags()); - } - - @Test - public void test_addTags_DoesNotDoAnything_WhenPassedNull() { - Relationship relationship = softwareSystem1.uses(softwareSystem2, "uses"); - relationship.addTags((String)null); - assertEquals("Relationship,Synchronous", relationship.getTags()); - - relationship.addTags(null, null, null); - assertEquals("Relationship,Synchronous", relationship.getTags()); - } - - @Test - public void test_addTags_AddsTags_WhenPassedSomeTags() { - Relationship relationship = softwareSystem1.uses(softwareSystem2, "uses"); - relationship.addTags(null, "tag1", null, "tag2"); - assertEquals("Relationship,Synchronous,tag1,tag2", relationship.getTags()); - } - - @Test - public void test_getInteractionStyle_ReturnsSynchronous_WhenNotExplicitlySet() { - Relationship relationship = softwareSystem1.uses(softwareSystem2, "uses"); - assertEquals(InteractionStyle.Synchronous, relationship.getInteractionStyle()); - } - - @Test - public void test_getTags_IncludesTheInteractionStyleWhenSpecified() { - Relationship relationship = softwareSystem1.uses(softwareSystem2, "uses"); - assertTrue(relationship.getTags().contains(Tags.SYNCHRONOUS)); - assertFalse(relationship.getTags().contains(Tags.ASYNCHRONOUS)); - - relationship.setInteractionStyle(InteractionStyle.Asynchronous); - assertFalse(relationship.getTags().contains(Tags.SYNCHRONOUS)); - assertTrue(relationship.getTags().contains(Tags.ASYNCHRONOUS)); - } - -} \ No newline at end of file diff --git a/structurizr-core/test/unit/com/structurizr/model/SoftwareSystemTests.java b/structurizr-core/test/unit/com/structurizr/model/SoftwareSystemTests.java deleted file mode 100644 index 26aaa933d..000000000 --- a/structurizr-core/test/unit/com/structurizr/model/SoftwareSystemTests.java +++ /dev/null @@ -1,194 +0,0 @@ -package com.structurizr.model; - -import com.structurizr.AbstractWorkspaceTestBase; -import org.junit.Test; - -import java.util.Iterator; - -import static org.junit.Assert.*; - -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(expected = IllegalArgumentException.class) - public void test_addContainer_ThrowsAnException_WhenAnEmptyNameIsSpecified() { - softwareSystem.addContainer(" ", "", ""); - } - - @Test - public void test_addContainer_AddsAContainer_WhenAContainerWithTheSameNameDoesNotExist() { - Container container = softwareSystem.addContainer("Web Application", "Description", "Spring MVC"); - assertEquals("Web Application", container.getName()); - assertEquals("Description", container.getDescription()); - assertEquals("Spring MVC", container.getTechnology()); - assertEquals("2", container.getId()); - assertEquals(1, softwareSystem.getContainers().size()); - assertSame(container, softwareSystem.getContainers().iterator().next()); - } - - @Test - public void test_addContainer_ThrowsAnException_WhenAContainerWithTheSameNameAlreadyExists() { - Container container = softwareSystem.addContainer("Web Application", "Description", "Spring MVC"); - assertEquals(1, softwareSystem.getContainers().size()); - - try { - softwareSystem.addContainer("Web Application", "Description", "Spring MVC"); - fail(); - } catch (Exception e) { - assertEquals("A container named 'Web Application' already exists for this software system.", e.getMessage()); - } - } - - @Test - public void test_getContainerWithName_ReturnsNull_WhenAContainerWithTheSpecifiedNameDoesNotExist() { - assertNull(softwareSystem.getContainerWithName("Web Application")); - } - - @Test - public 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() { - assertNull(softwareSystem.getContainerWithId("100")); - } - - @Test - public 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() { - 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"); - - assertEquals(1, systemA.getRelationships().size()); - assertEquals(0, systemB.getRelationships().size()); - Relationship relationship = systemA.getRelationships().iterator().next(); - assertEquals(systemA, relationship.getSource()); - assertEquals(systemB, relationship.getDestination()); - assertEquals("Gets some data from", relationship.getDescription()); - } - - @Test - public 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"); - systemA.uses(systemB, "Subscribes to updates using the Streaming API"); - - Iterator it = systemA.getRelationships().iterator(); - assertEquals(2, systemA.getRelationships().size()); - Relationship relationship = it.next(); - assertEquals(systemA, relationship.getSource()); - assertEquals(systemB, relationship.getDestination()); - assertEquals("Gets data using the REST API", relationship.getDescription()); - - relationship = it.next(); - assertEquals(systemA, relationship.getSource()); - assertEquals(systemB, relationship.getDestination()); - assertEquals("Subscribes to updates using the Streaming API", relationship.getDescription()); - } - - @Test - public 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"); - systemA.uses(systemB, "Gets data using the REST API"); - - assertEquals(1, systemA.getRelationships().size()); - } - - @Test - public 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"); - - assertEquals(1, system.getRelationships().size()); - assertEquals(0, person.getRelationships().size()); - Relationship relationship = system.getRelationships().iterator().next(); - assertEquals(system, relationship.getSource()); - assertEquals(person, relationship.getDestination()); - assertEquals("E-mails results to", relationship.getDescription()); - } - - @Test - public 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"); - system.delivers(person, "Text messages results to"); - - Iterator it = system.getRelationships().iterator(); - assertEquals(2, system.getRelationships().size()); - assertEquals(0, person.getRelationships().size()); - Relationship relationship = it.next(); - assertEquals(system, relationship.getSource()); - assertEquals(person, relationship.getDestination()); - assertEquals("E-mails results to", relationship.getDescription()); - - relationship = it.next(); - assertEquals(system, relationship.getSource()); - assertEquals(person, relationship.getDestination()); - assertEquals("Text messages results to", relationship.getDescription()); - } - - @Test - public 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"); - system.delivers(person, "E-mails results to"); - - assertEquals(1, system.getRelationships().size()); - } - - @Test - public void test_getTags_IncludesSoftwareSystemByDefault() { - SoftwareSystem system = model.addSoftwareSystem(Location.Internal, "System", "Description"); - assertEquals("Element,Software System", system.getTags()); - } - - @Test - public void test_getCanonicalName() { - SoftwareSystem system = model.addSoftwareSystem(Location.Internal, "System", "Description"); - assertEquals("/System", system.getCanonicalName()); - } - - @Test - public void test_getCanonicalName_WhenNameContainsASlashCharacter() { - SoftwareSystem softwareSystem = model.addSoftwareSystem("Name1/Name2", "Description"); - assertEquals("/Name1Name2", softwareSystem.getCanonicalName()); - } - - @Test - public void test_getParent_ReturnsNull() { - assertNull(softwareSystem.getParent()); - } - - @Test - public void test_removeTags_DoesNotRemoveRequiredTags() { - assertTrue(softwareSystem.getTags().contains(Tags.ELEMENT)); - assertTrue(softwareSystem.getTags().contains(Tags.SOFTWARE_SYSTEM)); - - softwareSystem.removeTag(Tags.SOFTWARE_SYSTEM); - softwareSystem.removeTag(Tags.ELEMENT); - - assertTrue(softwareSystem.getTags().contains(Tags.ELEMENT)); - assertTrue(softwareSystem.getTags().contains(Tags.SOFTWARE_SYSTEM)); - } - -} diff --git a/structurizr-core/test/unit/com/structurizr/util/ImageUtilsTests.java b/structurizr-core/test/unit/com/structurizr/util/ImageUtilsTests.java deleted file mode 100644 index f82a00b75..000000000 --- a/structurizr-core/test/unit/com/structurizr/util/ImageUtilsTests.java +++ /dev/null @@ -1,158 +0,0 @@ -package com.structurizr.util; - -import org.junit.Test; - -import java.io.File; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; - -public class ImageUtilsTests { - - @Test - public void test_getContentType_ThrowsAnException_WhenANullFileIsSpecified() throws Exception { - try { - ImageUtils.getContentType(null); - fail(); - } catch (IllegalArgumentException iae) { - assertEquals("A file must be specified.", iae.getMessage()); - } - } - - @Test - public void test_getContentType_ThrowsAnException_WhenAFileIsSpecifiedButItIsNotAFile() throws Exception { - try { - ImageUtils.getContentType(new File("../structurizr-core")); - fail(); - } catch (IllegalArgumentException iae) { - iae.printStackTrace(); - assertTrue(iae.getMessage().endsWith("structurizr-core is not a file.")); - } - } - - @Test - public void test_getContentType_ThrowsAnException_WhenAFileIsSpecifiedButItIsNotAnImage() throws Exception { - try { - ImageUtils.getContentType(new File("../build.gradle")); - fail(); - } catch (IllegalArgumentException iae) { - iae.printStackTrace(); - assertTrue(iae.getMessage().endsWith("build.gradle is not a supported image file.")); - } - } - - @Test - public void test_getContentType_ThrowsAnException_WhenAFileIsSpecifiedButItDoesNotExist() throws Exception { - try { - ImageUtils.getContentType(new File("./foo.xml")); - fail(); - } catch (IllegalArgumentException iae) { - assertTrue(iae.getMessage().endsWith("foo.xml does not exist.")); - } - } - - @Test - public 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 { - try { - ImageUtils.getImageAsBase64(null); - fail(); - } catch (IllegalArgumentException iae) { - assertEquals("A file must be specified.", iae.getMessage()); - } - } - - @Test - public void test_getImageAsBase64_ThrowsAnException_WhenAFileIsSpecifiedButItIsNotAFile() throws Exception { - try { - ImageUtils.getImageAsBase64(new File("../structurizr-core")); - fail(); - } catch (IllegalArgumentException iae) { - iae.printStackTrace(); - assertTrue(iae.getMessage().endsWith("structurizr-core is not a file.")); - } - } - - @Test - public void test_getImageAsBase64_ThrowsAnException_WhenAFileIsSpecifiedButItIsNotAnImage() throws Exception { - try { - ImageUtils.getImageAsBase64(new File("../build.gradle")); - fail(); - } catch (IllegalArgumentException iae) { - iae.printStackTrace(); - assertTrue(iae.getMessage().endsWith("build.gradle is not a supported image file.")); - } - } - - @Test - public void test_getImageAsBase64_ThrowsAnException_WhenAFileIsSpecifiedButItDoesNotExist() throws Exception { - try { - ImageUtils.getImageAsBase64(new File("./foo.xml")); - fail(); - } catch (IllegalArgumentException iae) { - assertTrue(iae.getMessage().endsWith("foo.xml does not exist.")); - } - } - - @Test - public 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 { - try { - ImageUtils.getImageAsDataUri(null); - fail(); - } catch (IllegalArgumentException iae) { - assertEquals("A file must be specified.", iae.getMessage()); - } - } - - @Test - public void test_getImageAsDataUri_ThrowsAnException_WhenAFileIsSpecifiedButItIsNotAFile() throws Exception { - try { - ImageUtils.getImageAsDataUri(new File("../structurizr-core")); - fail(); - } catch (IllegalArgumentException iae) { - iae.printStackTrace(); - assertTrue(iae.getMessage().endsWith("structurizr-core is not a file.")); - } - } - - @Test - public void test_getImageAsDataUri_ThrowsAnException_WhenAFileIsSpecifiedButItIsNotAnImage() throws Exception { - try { - ImageUtils.getImageAsDataUri(new File("../build.gradle")); - fail(); - } catch (IllegalArgumentException iae) { - iae.printStackTrace(); - assertTrue(iae.getMessage().endsWith("build.gradle is not a supported image file.")); - } - } - - @Test - public void test_getImageAsDataUri_ThrowsAnException_WhenAFileIsSpecifiedButItDoesNotExist() throws Exception { - try { - ImageUtils.getImageAsDataUri(new File("./foo.xml")); - fail(); - } catch (IllegalArgumentException iae) { - assertTrue(iae.getMessage().endsWith("foo.xml does not exist.")); - } - } - - @Test - public 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 - } - -} diff --git a/structurizr-core/test/unit/com/structurizr/util/UrlTests.java b/structurizr-core/test/unit/com/structurizr/util/UrlTests.java deleted file mode 100644 index 6669463ba..000000000 --- a/structurizr-core/test/unit/com/structurizr/util/UrlTests.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.structurizr.util; - -import org.junit.Test; - -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; - -public class UrlTests { - - @Test - public void test_isUrl_ReturnsFalse_WhenPassedNull() { - assertFalse(Url.isUrl(null)); - } - - @Test - public void test_isUrl_ReturnsFalse_WhenPassedAnEmptyString() { - assertFalse(Url.isUrl("")); - assertFalse(Url.isUrl(" ")); - } - - @Test - public void test_isUrl_ReturnsFalse_WhenPassedAnInvalidUrl() { - assertFalse(Url.isUrl("www.google.com")); - } - - @Test - public void test_isUrl_ReturnsTrue_WhenPassedAValidUrl() { - assertTrue(Url.isUrl("https://www.google.com")); - } - -} diff --git a/structurizr-core/test/unit/com/structurizr/util/WorkspaceUtilsTests.java b/structurizr-core/test/unit/com/structurizr/util/WorkspaceUtilsTests.java deleted file mode 100644 index e17677b47..000000000 --- a/structurizr-core/test/unit/com/structurizr/util/WorkspaceUtilsTests.java +++ /dev/null @@ -1,63 +0,0 @@ -package com.structurizr.util; - -import com.structurizr.Workspace; -import org.junit.Test; - -import java.io.File; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.fail; - -public class WorkspaceUtilsTests { - - @Test - public void test_loadWorkspaceFromJson_ThrowsAnException_WhenANullFileIsSpecified() { - try { - WorkspaceUtils.loadWorkspaceFromJson(null); - fail(); - } catch (Exception e) { - assertEquals("The path to a JSON file must be specified.", e.getMessage()); - } - } - - @Test - public void test_loadWorkspaceFromJson_ThrowsAnException_WhenTheFileDoesNotExist() { - try { - WorkspaceUtils.loadWorkspaceFromJson(new File("test/unit/com/structurizr/util/other-workspace.json")); - fail(); - } catch (Exception e) { - assertEquals("The specified JSON file does not exist.", e.getMessage()); - } - } - - @Test - public void test_saveWorkspaceToJson_ThrowsAnException_WhenANullWorkspaceIsSpecified() { - try { - WorkspaceUtils.saveWorkspaceToJson(null, null); - fail(); - } catch (Exception e) { - assertEquals("A workspace must be provided.", e.getMessage()); - } - } - - @Test - public void test_saveWorkspaceToJson_ThrowsAnException_WhenANullFileIsSpecified() { - try { - WorkspaceUtils.saveWorkspaceToJson(new Workspace("Name", "Description"), null); - fail(); - } catch (Exception e) { - assertEquals("The path to a JSON file must be specified.", e.getMessage()); - } - } - - @Test - public 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); - - workspace = WorkspaceUtils.loadWorkspaceFromJson(file); - assertEquals("Name", workspace.getName()); - } - -} diff --git a/structurizr-core/test/unit/com/structurizr/util/structurizr-logo.png b/structurizr-core/test/unit/com/structurizr/util/structurizr-logo.png deleted file mode 100644 index 9324ae8dc..000000000 Binary files a/structurizr-core/test/unit/com/structurizr/util/structurizr-logo.png and /dev/null differ 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 efc760b82..000000000 --- a/structurizr-core/test/unit/com/structurizr/view/ColorPairTests.java +++ /dev/null @@ -1,97 +0,0 @@ -package com.structurizr.view; - -import org.junit.Test; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.fail; - -public class ColorPairTests { - - @Test - public void test_construction() { - ColorPair colorPair = new ColorPair("#ffffff", "#000000"); - assertEquals("#ffffff", colorPair.getBackground()); - assertEquals("#000000", colorPair.getForeground()); - } - - @Test - public void test_setBackground_WithAValidHtmlColorCode() { - ColorPair colorPair = new ColorPair(); - colorPair.setBackground("#ffffff"); - assertEquals("#ffffff", colorPair.getBackground()); - } - - @Test - public void test_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 - public void test_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 - public void test_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 - public void test_setForeground_WithAValidHtmlColorCode() { - ColorPair colorPair = new ColorPair(); - colorPair.setForeground("#000000"); - assertEquals("#000000", colorPair.getForeground()); - } - - @Test - public void test_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 - public void test_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 - public void test_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()); - } - } - -} diff --git a/structurizr-core/test/unit/com/structurizr/view/ColorTests.java b/structurizr-core/test/unit/com/structurizr/view/ColorTests.java deleted file mode 100644 index 074f8170d..000000000 --- a/structurizr-core/test/unit/com/structurizr/view/ColorTests.java +++ /dev/null @@ -1,34 +0,0 @@ -package com.structurizr.view; - -import org.junit.Test; - -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; - -public class ColorTests { - - @Test - public void test_isHexColorCode_ReturnsFalse_WhenPassedNull() { - assertFalse(Color.isHexColorCode(null)); - } - - @Test - public void test_isHexColorCode_ReturnsFalse_WhenPassedAnEmptyString() { - assertFalse(Color.isHexColorCode("")); - } - - @Test - public void test_isHexColorCode_ReturnsFalse_WhenPassedAnInvalidString() { - assertFalse(Color.isHexColorCode("ffffff")); - assertFalse(Color.isHexColorCode("#fffff")); - assertFalse(Color.isHexColorCode("#gggggg")); - } - - @Test - public void test_isHexColorCode_ReturnsTrue_WhenPassedAnValidString() { - assertTrue(Color.isHexColorCode("#abcdef")); - assertTrue(Color.isHexColorCode("#ABCDEF")); - assertTrue(Color.isHexColorCode("#123456")); - } - -} \ No newline at end of file diff --git a/structurizr-core/test/unit/com/structurizr/view/ComponentViewTests.java b/structurizr-core/test/unit/com/structurizr/view/ComponentViewTests.java deleted file mode 100644 index 20fe04e85..000000000 --- a/structurizr-core/test/unit/com/structurizr/view/ComponentViewTests.java +++ /dev/null @@ -1,585 +0,0 @@ -package com.structurizr.view; - -import com.structurizr.AbstractWorkspaceTestBase; -import com.structurizr.model.*; -import org.junit.Before; -import org.junit.Test; - -import java.util.Set; -import java.util.stream.Collectors; - -import static org.junit.Assert.*; - -public class ComponentViewTests extends AbstractWorkspaceTestBase { - - private SoftwareSystem softwareSystem; - private Container webApplication; - private ComponentView view; - - @Before - public void setUp() { - softwareSystem = model.addSoftwareSystem(Location.Internal, "The System", "Description"); - webApplication = softwareSystem.addContainer("Web Application", "Does something", "Apache Tomcat"); - view = new ComponentView(webApplication, "Key", "Some description"); - } - - @Test - public void test_construction() { - assertEquals("The System - Web Application - Components", view.getName()); - assertEquals("Some description", view.getDescription()); - assertEquals(0, view.getElements().size()); - assertSame(softwareSystem, view.getSoftwareSystem()); - assertEquals(softwareSystem.getId(), view.getSoftwareSystemId()); - assertEquals(webApplication.getId(), view.getContainerId()); - assertSame(model, view.getModel()); - } - - @Test - public void test_addAllSoftwareSystems_DoesNothing_WhenThereAreNoOtherSoftwareSystems() { - assertEquals(0, view.getElements().size()); - view.addAllSoftwareSystems(); - assertEquals(0, view.getElements().size()); - } - - @Test - public void test_addAllSoftwareSystems_AddsAllSoftwareSystems_WhenThereAreSomeSoftwareSystemsInTheModel() { - SoftwareSystem softwareSystemA = model.addSoftwareSystem(Location.External, "System A", "Description"); - SoftwareSystem softwareSystemB = model.addSoftwareSystem(Location.External, "System B", "Description"); - - view.addAllSoftwareSystems(); - - assertEquals(2, view.getElements().size()); - assertTrue(view.getElements().contains(new ElementView(softwareSystemA))); - assertTrue(view.getElements().contains(new ElementView(softwareSystemB))); - } - - @Test - public void test_addAllPeople_DoesNothing_WhenThereAreNoPeople() { - assertEquals(0, view.getElements().size()); - view.addAllPeople(); - assertEquals(0, view.getElements().size()); - } - - @Test - public void test_addAllPeople_AddsAllPeople_WhenThereAreSomePeopleInTheModel() { - Person userA = model.addPerson(Location.External, "User A", "Description"); - Person userB = model.addPerson(Location.External, "User B", "Description"); - - view.addAllPeople(); - - assertEquals(2, view.getElements().size()); - assertTrue(view.getElements().contains(new ElementView(userA))); - assertTrue(view.getElements().contains(new ElementView(userB))); - } - - @Test - public void test_addAllElements_DoesNothing_WhenThereAreNoSoftwareSystemsOrPeople() { - assertEquals(0, view.getElements().size()); - view.addAllElements(); - assertEquals(0, view.getElements().size()); - } - - @Test - public 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"); - Person userB = model.addPerson(Location.External, "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"); - - view.addAllElements(); - - assertEquals(7, view.getElements().size()); - assertTrue(view.getElements().contains(new ElementView(softwareSystemA))); - assertTrue(view.getElements().contains(new ElementView(softwareSystemB))); - assertTrue(view.getElements().contains(new ElementView(userA))); - assertTrue(view.getElements().contains(new ElementView(userB))); - assertTrue(view.getElements().contains(new ElementView(database))); - assertTrue(view.getElements().contains(new ElementView(componentA))); - assertTrue(view.getElements().contains(new ElementView(componentB))); - } - - @Test - public void test_addAllContainers_DoesNothing_WhenThereAreNoContainers() { - assertEquals(0, view.getElements().size()); - view.addAllContainers(); - assertEquals(0, view.getElements().size()); - } - - @Test - public void test_addAllContainers_AddsAllContainers_WhenThereAreSomeContainers() { - Container database = softwareSystem.addContainer("Database", "Stores something", "MySQL"); - Container fileSystem = softwareSystem.addContainer("File System", "Stores something else", ""); - - view.addAllContainers(); - - assertEquals(2, view.getElements().size()); - assertTrue(view.getElements().contains(new ElementView(database))); - assertTrue(view.getElements().contains(new ElementView(fileSystem))); - } - - @Test - public void test_addAllComponents_DoesNothing_WhenThereAreNoComponents() { - assertEquals(0, view.getElements().size()); - view.addAllComponents(); - assertEquals(0, view.getElements().size()); - } - - @Test - public void test_addAllComponents_AddsAllComponents_WhenThereAreSomeComponents() { - Component componentA = webApplication.addComponent("Component A", "Does something", "Java"); - Component componentB = webApplication.addComponent("Component B", "Does something", "Java"); - - view.addAllComponents(); - - assertEquals(2, view.getElements().size()); - assertTrue(view.getElements().contains(new ElementView(componentA))); - assertTrue(view.getElements().contains(new ElementView(componentB))); - } - - @Test - public void test_add_DoesNothing_WhenANullContainerIsSpecified() { - assertEquals(0, view.getElements().size()); - view.add((Container) null); - assertEquals(0, view.getElements().size()); - } - - @Test - public void test_add_AddsTheContainer_WhenTheContainerIsNoInTheViewAlready() { - Container database = softwareSystem.addContainer("Database", "Stores something", "MySQL"); - - assertEquals(0, view.getElements().size()); - view.add(database); - assertEquals(1, view.getElements().size()); - assertTrue(view.getElements().contains(new ElementView(database))); - } - - @Test - public void test_add_DoesNothing_WhenTheSpecifiedContainerIsAlreadyInTheView() { - Container database = softwareSystem.addContainer("Database", "Stores something", "MySQL"); - view.add(database); - assertEquals(1, view.getElements().size()); - assertTrue(view.getElements().contains(new ElementView(database))); - - view.add(database); - assertEquals(1, view.getElements().size()); - } - - @Test - public void test_remove_DoesNothing_WhenANullContainerIsPassed() { - assertEquals(0, view.getElements().size()); - view.remove((Container) null); - assertEquals(0, view.getElements().size()); - } - - @Test - public void test_remove_RemovesTheContainer_WhenTheContainerIsInTheView() { - Container database = softwareSystem.addContainer("Database", "Stores something", "MySQL"); - view.add(database); - assertEquals(1, view.getElements().size()); - assertTrue(view.getElements().contains(new ElementView(database))); - - view.remove(database); - assertEquals(0, view.getElements().size()); - } - - @Test - public void test_remove_DoesNothing_WhenTheContainerIsNotInTheView() { - Container database = softwareSystem.addContainer("Database", "Stores something", "MySQL"); - Container fileSystem = softwareSystem.addContainer("File System", "Stores something else", ""); - - view.add(database); - assertEquals(1, view.getElements().size()); - assertTrue(view.getElements().contains(new ElementView(database))); - - view.remove(fileSystem); - assertEquals(1, view.getElements().size()); - assertTrue(view.getElements().contains(new ElementView(database))); - } - - @Test - public 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() { - Component componentA = webApplication.addComponent("Component A", "Does something", "Java"); - - assertEquals(0, view.getElements().size()); - view.add(componentA); - assertEquals(1, view.getElements().size()); - assertTrue(view.getElements().contains(new ElementView(componentA))); - } - - @Test - public void test_add_DoesNothing_WhenTheSpecifiedComponentIsAlreadyInTheView() { - Component componentA = webApplication.addComponent("Component A", "Does something", "Java"); - view.add(componentA); - assertEquals(1, view.getElements().size()); - assertTrue(view.getElements().contains(new ElementView(componentA))); - - view.add(componentA); - assertEquals(1, view.getElements().size()); - } - - @Test - public void test_add_ThrowsAnException_WhenTheSpecifiedComponentIsInADifferentContainer() { - try { - SoftwareSystem softwareSystemA = model.addSoftwareSystem("System A", "Description"); - - final Container containerA1 = softwareSystemA.addContainer("Container A1", "Description", "Tec"); - - final Container containerA2 = softwareSystemA.addContainer("Container A2", "Description", "Tec"); - final Component componentA2_1 = containerA2.addComponent("Component A2-1", "Description"); - - view = new ComponentView(containerA1, "components", "Description"); - view.add(componentA2_1); - } catch (Exception e) { - assertEquals("Only components belonging to Container A1 can be added to this view.", e.getMessage()); - } - } - - @Test - public void test_add_DoesNothing_WhenTheContainerOfTheViewIsAdded() { - assertEquals("the container itself is not added to the view", 0, view.getElements().stream().map(e -> e.getElement()).filter(e -> e.equals(webApplication)).count()); - view.add(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()); - } - - @Test - public 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()); - 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()); - } - - @Test - public void test_remove_DoesNothing_WhenANullComponentIsPassed() { - assertEquals(0, view.getElements().size()); - view.remove((Component) null); - assertEquals(0, view.getElements().size()); - } - - @Test - public void test_remove_RemovesTheComponent_WhenTheComponentIsInTheView() { - Component componentA = webApplication.addComponent("Component A", "Does something", "Java"); - view.add(componentA); - assertEquals(1, view.getElements().size()); - assertTrue(view.getElements().contains(new ElementView(componentA))); - - view.remove(componentA); - assertEquals(0, view.getElements().size()); - } - - @Test - public 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"); - - view.add(componentA); - view.add(componentB); - assertEquals(2, view.getElements().size()); - assertEquals(1, view.getRelationships().size()); - - view.remove(componentB); - assertEquals(1, view.getElements().size()); - assertEquals(0, view.getRelationships().size()); - } - - @Test - public void test_remove_DoesNothing_WhenTheComponentIsNotInTheView() { - Component componentA = webApplication.addComponent("Component A", "Does something", "Java"); - Component componentB = webApplication.addComponent("Component B", "Does something", "Java"); - - view.add(componentA); - assertEquals(1, view.getElements().size()); - assertTrue(view.getElements().contains(new ElementView(componentA))); - - view.remove(componentB); - assertEquals(1, view.getElements().size()); - assertTrue(view.getElements().contains(new ElementView(componentA))); - } - - @Test - public void test_addNearestNeightbours_DoesNothing_WhenANullElementIsSpecified() { - view.addNearestNeighbours(null); - - assertEquals(0, view.getElements().size()); - } - - @Test - public void test_addNearestNeighbours_DoesNothing_WhenThereAreNoNeighbours() { - view.addNearestNeighbours(softwareSystem); - - assertEquals(1, view.getElements().size()); - } - - @Test - public 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"); - Person userB = model.addPerson("User B", "Description"); - - // userA -> systemA -> system -> systemB -> userB - userA.uses(softwareSystemA, ""); - softwareSystemA.uses(softwareSystem, ""); - softwareSystem.uses(softwareSystemB, ""); - softwareSystemB.delivers(userB, ""); - - // userA -> systemA -> web application -> systemB -> userB - // web application -> database - Container database = softwareSystem.addContainer("Database", "", ""); - softwareSystemA.uses(webApplication, ""); - webApplication.uses(softwareSystemB, ""); - webApplication.uses(database, ""); - - // userA -> systemA -> controller -> service -> repository -> database - Component controller = webApplication.addComponent("Controller", ""); - Component service = webApplication.addComponent("Service", ""); - Component repository = webApplication.addComponent("Repository", ""); - softwareSystemA.uses(controller, ""); - controller.uses(service, ""); - service.uses(repository, ""); - repository.uses(database, ""); - - // userA -> systemA -> controller -> service -> systemB -> userB - service.uses(softwareSystemB, ""); - - view.addNearestNeighbours(softwareSystem); - - assertEquals(3, view.getElements().size()); - assertTrue(view.getElements().contains(new ElementView(softwareSystemA))); - assertTrue(view.getElements().contains(new ElementView(softwareSystem))); - assertTrue(view.getElements().contains(new ElementView(softwareSystemB))); - - view = new ComponentView(webApplication, "components", "Description"); - view.addNearestNeighbours(softwareSystemA); - - assertEquals(5, view.getElements().size()); - assertTrue(view.getElements().contains(new ElementView(userA))); - assertTrue(view.getElements().contains(new ElementView(softwareSystemA))); - assertTrue(view.getElements().contains(new ElementView(softwareSystem))); - assertTrue(view.getElements().contains(new ElementView(webApplication))); - assertTrue(view.getElements().contains(new ElementView(controller))); - - view = new ComponentView(webApplication, "components", "Description"); - view.addNearestNeighbours(webApplication); - - assertEquals(4, view.getElements().size()); - assertTrue(view.getElements().contains(new ElementView(softwareSystemA))); - assertTrue(view.getElements().contains(new ElementView(webApplication))); - assertTrue(view.getElements().contains(new ElementView(database))); - assertTrue(view.getElements().contains(new ElementView(softwareSystemB))); - - view = new ComponentView(webApplication, "components", "Description"); - view.addNearestNeighbours(controller); - - assertEquals(3, view.getElements().size()); - assertTrue(view.getElements().contains(new ElementView(softwareSystemA))); - assertTrue(view.getElements().contains(new ElementView(controller))); - assertTrue(view.getElements().contains(new ElementView(service))); - - view = new ComponentView(webApplication, "components", "Description"); - view.addNearestNeighbours(service); - - assertEquals(4, view.getElements().size()); - assertTrue(view.getElements().contains(new ElementView(controller))); - assertTrue(view.getElements().contains(new ElementView(service))); - assertTrue(view.getElements().contains(new ElementView(repository))); - assertTrue(view.getElements().contains(new ElementView(softwareSystemB))); - } - - @Test - public void test_addExternalDependencies_AddsOrphanedElements_WhenThereAreNoDirectRelationshipsWithAComponent() { - SoftwareSystem source = model.addSoftwareSystem("Source", ""); - SoftwareSystem destination = model.addSoftwareSystem("Destination", ""); - - SoftwareSystem a = model.addSoftwareSystem("A", ""); - Container aa = a.addContainer("AA", "", ""); - Component aaa = aa.addComponent("AAA", "", ""); - - source.uses(aa, ""); - aa.uses(destination, ""); - - view = new ComponentView(aa, "components", "Description"); - view.addAllComponents(); - view.addExternalDependencies(); - - // check that the view includes the desired elements - Set elementsInView = view.getElements().stream().map(ElementView::getElement).collect(Collectors.toSet()); - assertTrue(elementsInView.contains(aaa)); - - // but there are no relationships (because component AAA isn't directly related to anything) - assertEquals(0, view.getRelationships().size()); - } - - @Test - public void test_addExternalDependencies_AddsTheContainer_WhenAComponentHasARelationshipToAContainerInTheSameSoftwareSystem() { - SoftwareSystem softwareSystemA = model.addSoftwareSystem("Software System A", ""); - Container containerA = softwareSystemA.addContainer("Container A", "", ""); - Component componentA = containerA.addComponent("Component A", "", ""); - Container containerB = softwareSystemA.addContainer("Container B", "", ""); - - componentA.uses(containerB, "uses"); - - view = new ComponentView(containerA, "key", "description"); - view.addAllComponents(); - view.addExternalDependencies(); - assertEquals(2, view.getElements().size()); - assertTrue(view.getElements().contains(new ElementView(componentA))); - assertTrue(view.getElements().contains(new ElementView(containerB))); - } - - @Test - public void test_addExternalDependencies_AddsTheContainer_WhenAComponentHasARelationshipFromAContainerInTheSameSoftwareSystem() { - SoftwareSystem softwareSystemA = model.addSoftwareSystem("Software System A", ""); - Container containerA = softwareSystemA.addContainer("Container A", "", ""); - Component componentA = containerA.addComponent("Component A", "", ""); - Container containerB = softwareSystemA.addContainer("Container B", "", ""); - - containerB.uses(componentA, "uses"); - - view = new ComponentView(containerA, "key", "description"); - view.addAllComponents(); - view.addExternalDependencies(); - assertEquals(2, view.getElements().size()); - assertTrue(view.getElements().contains(new ElementView(componentA))); - assertTrue(view.getElements().contains(new ElementView(containerB))); - } - - @Test - public void test_addExternalDependencies_AddsTheParentContainer_WhenAComponentHasARelationshipToAComponentInADifferentContainerInTheSameSoftwareSystem() { - SoftwareSystem softwareSystemA = model.addSoftwareSystem("Software System A", ""); - Container containerA = softwareSystemA.addContainer("Container A", "", ""); - Component componentA = containerA.addComponent("Component A", "", ""); - Container containerB = softwareSystemA.addContainer("Container B", "", ""); - Component componentB = containerB.addComponent("Component B", "", ""); - - componentA.uses(componentB, "uses"); - - view = new ComponentView(containerA, "key", "description"); - view.addAllComponents(); - view.addExternalDependencies(); - assertEquals(2, view.getElements().size()); - assertTrue(view.getElements().contains(new ElementView(componentA))); - assertTrue(view.getElements().contains(new ElementView(containerB))); - } - - @Test - public void test_addExternalDependencies_AddsTheParentContainer_WhenAComponentHasARelationshipFromAComponentInADifferentContainerInTheSameSoftwareSystem() { - SoftwareSystem softwareSystemA = model.addSoftwareSystem("Software System A", ""); - Container containerA = softwareSystemA.addContainer("Container A", "", ""); - Component componentA = containerA.addComponent("Component A", "", ""); - Container containerB = softwareSystemA.addContainer("Container B", "", ""); - Component componentB = containerB.addComponent("Component B", "", ""); - - componentB.uses(componentA, "uses"); - - view = new ComponentView(containerA, "key", "description"); - view.addAllComponents(); - view.addExternalDependencies(); - assertEquals(2, view.getElements().size()); - assertTrue(view.getElements().contains(new ElementView(componentA))); - assertTrue(view.getElements().contains(new ElementView(containerB))); - } - - @Test - public void test_addExternalDependencies_AddsTheParentSoftwareSystem_WhenAComponentHasARelationshipToAContainerInAnotherSoftwareSystem() { - SoftwareSystem softwareSystemA = model.addSoftwareSystem("Software System A", ""); - Container containerA = softwareSystemA.addContainer("Container A", "", ""); - Component componentA = containerA.addComponent("Component A", "", ""); - SoftwareSystem softwareSystemB = model.addSoftwareSystem("Software System B", ""); - Container containerB = softwareSystemB.addContainer("Container B", "", ""); - - componentA.uses(containerB, "uses"); - - view = new ComponentView(containerA, "key", "description"); - view.addAllComponents(); - view.addExternalDependencies(); - assertEquals(2, view.getElements().size()); - assertTrue(view.getElements().contains(new ElementView(componentA))); - assertTrue(view.getElements().contains(new ElementView(softwareSystemB))); - } - - @Test - public void test_addExternalDependencies_AddsTheParentSoftwareSystem_WhenAComponentHasARelationshipFromAContainerInAnotherSoftwareSystem() { - SoftwareSystem softwareSystemA = model.addSoftwareSystem("Software System A", ""); - Container containerA = softwareSystemA.addContainer("Container A", "", ""); - Component componentA = containerA.addComponent("Component A", "", ""); - SoftwareSystem softwareSystemB = model.addSoftwareSystem("Software System B", ""); - Container containerB = softwareSystemB.addContainer("Container B", "", ""); - - containerB.uses(componentA, "uses"); - - view = new ComponentView(containerA, "key", "description"); - view.addAllComponents(); - view.addExternalDependencies(); - assertEquals(2, view.getElements().size()); - assertTrue(view.getElements().contains(new ElementView(componentA))); - assertTrue(view.getElements().contains(new ElementView(softwareSystemB))); - } - - @Test - public void test_addExternalDependencies_AddsTheParentSoftwareSystem_WhenAComponentHasARelationshipToAComponentInAnotherSoftwareSystem() { - SoftwareSystem softwareSystemA = model.addSoftwareSystem("Software System A", ""); - Container containerA = softwareSystemA.addContainer("Container A", "", ""); - Component componentA = containerA.addComponent("Component A", "", ""); - SoftwareSystem softwareSystemB = model.addSoftwareSystem("Software System B", ""); - Container containerB = softwareSystemB.addContainer("Container B", "", ""); - Component componentB = containerB.addComponent("Component B", "", ""); - - componentA.uses(componentB, "uses"); - - view = new ComponentView(containerA, "key", "description"); - view.addAllComponents(); - view.addExternalDependencies(); - assertEquals(2, view.getElements().size()); - assertTrue(view.getElements().contains(new ElementView(componentA))); - assertTrue(view.getElements().contains(new ElementView(softwareSystemB))); - } - - @Test - public void test_addExternalDependencies_AddsTheParentSoftwareSystem_WhenAComponentHasARelationshipFromAComponentInAnotherSoftwareSystem() { - SoftwareSystem softwareSystemA = model.addSoftwareSystem("Software System A", ""); - Container containerA = softwareSystemA.addContainer("Container A", "", ""); - Component componentA = containerA.addComponent("Component A", "", ""); - SoftwareSystem softwareSystemB = model.addSoftwareSystem("Software System B", ""); - Container containerB = softwareSystemB.addContainer("Container B", "", ""); - Component componentB = containerB.addComponent("Component B", "", ""); - - componentB.uses(componentA, "uses"); - - view = new ComponentView(containerA, "key", "description"); - view.addAllComponents(); - view.addExternalDependencies(); - assertEquals(2, view.getElements().size()); - assertTrue(view.getElements().contains(new ElementView(componentA))); - assertTrue(view.getElements().contains(new ElementView(softwareSystemB))); - } - - @Test - public void test_add_ThrowsAnExceptionWhenAddingAContainerThatBelongsToAnotherSoftwareSystem() { - try { - Container containerInADifferentSoftwareSystem = model.addSoftwareSystem("Other software system", "").addContainer("Other container", "", ""); - - ComponentView componentView = views.createComponentView(webApplication, "components", ""); - componentView.add(webApplication); - componentView.add(containerInADifferentSoftwareSystem); - fail(); - } catch (Exception e) { - assertEquals("Only containers belonging to The System can be added to this view.", e.getMessage()); - } - } - -} \ No newline at end of file diff --git a/structurizr-core/test/unit/com/structurizr/view/ConfigurationTests.java b/structurizr-core/test/unit/com/structurizr/view/ConfigurationTests.java deleted file mode 100644 index a58cd7f4f..000000000 --- a/structurizr-core/test/unit/com/structurizr/view/ConfigurationTests.java +++ /dev/null @@ -1,36 +0,0 @@ -package com.structurizr.view; - -import com.structurizr.AbstractWorkspaceTestBase; -import org.junit.Test; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNull; - -public class ConfigurationTests extends AbstractWorkspaceTestBase { - - @Test - public void test_defaultView_DoesNothing_WhenPassedNull() { - Configuration configuration = new Configuration(); - configuration.setDefaultView((View)null); - assertNull(configuration.getDefaultView()); - } - - @Test - public void test_defaultView() { - EnterpriseContextView view = views.createEnterpriseContextView("key", "Description"); - Configuration configuration = new Configuration(); - configuration.setDefaultView(view); - assertEquals("key", configuration.getDefaultView()); - } - - @Test - public void test_copyConfigurationFrom() { - Configuration source = new Configuration(); - source.setLastSavedView("someKey"); - - Configuration destination = new Configuration(); - destination.copyConfigurationFrom(source); - assertEquals("someKey", destination.getLastSavedView()); - } - -} diff --git a/structurizr-core/test/unit/com/structurizr/view/ContainerViewTests.java b/structurizr-core/test/unit/com/structurizr/view/ContainerViewTests.java deleted file mode 100644 index dcdde8496..000000000 --- a/structurizr-core/test/unit/com/structurizr/view/ContainerViewTests.java +++ /dev/null @@ -1,247 +0,0 @@ -package com.structurizr.view; - -import com.structurizr.AbstractWorkspaceTestBase; -import com.structurizr.model.*; -import org.junit.Before; -import org.junit.Test; - -import static org.junit.Assert.*; - -public class ContainerViewTests extends AbstractWorkspaceTestBase { - - private SoftwareSystem softwareSystem; - private ContainerView view; - - @Before - public void setUp() { - softwareSystem = model.addSoftwareSystem(Location.Internal, "The System", "Description"); - view = new ContainerView(softwareSystem, "containers", "Description"); - } - - @Test - public void test_construction() { - assertEquals("The System - Containers", view.getName()); - assertEquals("Description", view.getDescription()); - assertEquals(0, view.getElements().size()); - assertSame(softwareSystem, view.getSoftwareSystem()); - assertEquals(softwareSystem.getId(), view.getSoftwareSystemId()); - assertSame(model, view.getModel()); - } - - @Test - public void test_addAllSoftwareSystems_DoesNothing_WhenThereAreNoOtherSoftwareSystems() { - assertEquals(0, view.getElements().size()); - view.addAllSoftwareSystems(); - assertEquals(0, view.getElements().size()); - } - - @Test - public void test_addAllSoftwareSystems_AddsAllSoftwareSystems_WhenThereAreSomeSoftwareSystemsInTheModel() { - SoftwareSystem softwareSystemA = model.addSoftwareSystem(Location.External, "System A", "Description"); - SoftwareSystem softwareSystemB = model.addSoftwareSystem(Location.External, "System B", "Description"); - - view.addAllSoftwareSystems(); - - assertEquals(2, view.getElements().size()); - assertTrue(view.getElements().contains(new ElementView(softwareSystemA))); - assertTrue(view.getElements().contains(new ElementView(softwareSystemB))); - } - - @Test - public void test_addAllPeople_DoesNothing_WhenThereAreNoPeople() { - assertEquals(0, view.getElements().size()); - view.addAllPeople(); - assertEquals(0, view.getElements().size()); - } - - @Test - public void test_addAllPeople_AddsAllPeople_WhenThereAreSomePeopleInTheModel() { - Person userA = model.addPerson(Location.External, "User A", "Description"); - Person userB = model.addPerson(Location.External, "User B", "Description"); - - view.addAllPeople(); - - assertEquals(2, view.getElements().size()); - assertTrue(view.getElements().contains(new ElementView(userA))); - assertTrue(view.getElements().contains(new ElementView(userB))); - } - - @Test - public void test_addAllElements_DoesNothing_WhenThereAreNoSoftwareSystemsOrPeople() { - assertEquals(0, view.getElements().size()); - view.addAllElements(); - assertEquals(0, view.getElements().size()); - } - - @Test - public 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"); - Person userB = model.addPerson(Location.External, "User B", "Description"); - Container webApplication = softwareSystem.addContainer("Web Application", "Does something", "Apache Tomcat"); - Container database = softwareSystem.addContainer("Database", "Does something", "MySQL"); - - view.addAllElements(); - - assertEquals(6, view.getElements().size()); - assertTrue(view.getElements().contains(new ElementView(softwareSystemA))); - assertTrue(view.getElements().contains(new ElementView(softwareSystemB))); - assertTrue(view.getElements().contains(new ElementView(userA))); - assertTrue(view.getElements().contains(new ElementView(userB))); - assertTrue(view.getElements().contains(new ElementView(webApplication))); - assertTrue(view.getElements().contains(new ElementView(database))); - } - - @Test - public void test_addAllContainers_DoesNothing_WhenThereAreNoContainers() { - assertEquals(0, view.getElements().size()); - view.addAllContainers(); - assertEquals(0, view.getElements().size()); - } - - @Test - public void test_addAllContainers_AddsAllContainers_WhenThereAreSomeContainers() { - Container webApplication = softwareSystem.addContainer("Web Application", "Does something", "Apache Tomcat"); - Container database = softwareSystem.addContainer("Database", "Does something", "MySQL"); - - view.addAllContainers(); - - assertEquals(2, view.getElements().size()); - assertTrue(view.getElements().contains(new ElementView(webApplication))); - assertTrue(view.getElements().contains(new ElementView(database))); - } - - @Test - public void test_addNearestNeightbours_DoesNothing_WhenANullElementIsSpecified() { - view.addNearestNeighbours(null); - - assertEquals(0, view.getElements().size()); - } - - @Test - public void test_addNearestNeighbours_DoesNothing_WhenThereAreNoNeighbours() { - view.addNearestNeighbours(softwareSystem); - - assertEquals(1, view.getElements().size()); - } - - @Test - public 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"); - Person userB = model.addPerson("User B", "Description"); - - // userA -> systemA -> system -> systemB -> userB - userA.uses(softwareSystemA, ""); - softwareSystemA.uses(softwareSystem, ""); - softwareSystem.uses(softwareSystemB, ""); - softwareSystemB.delivers(userB, ""); - - // userA -> systemA -> web application -> systemB -> userB - // web application -> database - Container webApplication = softwareSystem.addContainer("Web Application", "", ""); - Container database = softwareSystem.addContainer("Database", "", ""); - softwareSystemA.uses(webApplication, ""); - webApplication.uses(softwareSystemB, ""); - webApplication.uses(database, ""); - - // userA -> systemA -> controller -> service -> repository -> database - Component controller = webApplication.addComponent("Controller", ""); - Component service = webApplication.addComponent("Service", ""); - Component repository = webApplication.addComponent("Repository", ""); - softwareSystemA.uses(controller, ""); - controller.uses(service, ""); - service.uses(repository, ""); - repository.uses(database, ""); - - // userA -> systemA -> controller -> service -> systemB -> userB - service.uses(softwareSystemB, ""); - - view.addNearestNeighbours(softwareSystem); - - assertEquals(3, view.getElements().size()); - assertTrue(view.getElements().contains(new ElementView(softwareSystemA))); - assertTrue(view.getElements().contains(new ElementView(softwareSystem))); - assertTrue(view.getElements().contains(new ElementView(softwareSystemB))); - - view = new ContainerView(softwareSystem, "containers", "Description"); - view.addNearestNeighbours(softwareSystemA); - - assertEquals(4, view.getElements().size()); - assertTrue(view.getElements().contains(new ElementView(userA))); - assertTrue(view.getElements().contains(new ElementView(softwareSystemA))); - assertTrue(view.getElements().contains(new ElementView(softwareSystem))); - assertTrue(view.getElements().contains(new ElementView(webApplication))); - - view = new ContainerView(softwareSystem, "containers", "Description"); - view.addNearestNeighbours(webApplication); - - assertEquals(4, view.getElements().size()); - assertTrue(view.getElements().contains(new ElementView(softwareSystemA))); - assertTrue(view.getElements().contains(new ElementView(webApplication))); - assertTrue(view.getElements().contains(new ElementView(database))); - assertTrue(view.getElements().contains(new ElementView(softwareSystemB))); - } - - @Test - public void test_remove_RemovesContainer() { - Container webApplication = softwareSystem.addContainer("Web Application", "", ""); - Container database = softwareSystem.addContainer("Database", "", ""); - - view.addAllContainers(); - assertEquals(2, view.getElements().size()); - - view.remove(webApplication); - assertEquals(1, view.getElements().size()); - assertTrue(view.getElements().contains(new ElementView(database))); - } - - @Test - public void test_remove_ElementsWithTag() { - final String TAG = "myTag"; - Container webApplication = softwareSystem.addContainer("Web Application", "", ""); - Container database = softwareSystem.addContainer("Database", "", ""); - database.addTags(TAG); - - view.addAllContainers(); - assertEquals(2, view.getElements().size()); - - view.removeElementsWithTag(TAG); - assertEquals(1, view.getElements().size()); - assertEquals(webApplication, view.getElements().iterator().next().getElement()); - } - - @Test - public void test_remove_RelationshipWithTag() { - final String TAG = "myTag"; - Container webApplication = softwareSystem.addContainer("Web Application", "", ""); - Container database = softwareSystem.addContainer("Database", "", ""); - webApplication.uses(database, "").addTags(TAG); - - view.addAllContainers(); - assertEquals(2, view.getElements().size()); - assertEquals(1, view.getRelationships().size()); - - view.removeRelationshipsWithTag(TAG); - assertEquals(2, view.getElements().size()); - assertEquals(0, view.getRelationships().size()); - } - - @Test - public void test_add_ThrowsAnExceptionWhenAddingAContainerThatBelongsToAnotherSoftwareSystem() { - try { - Container container = softwareSystem.addContainer("Container", "", ""); - Container containerInADifferentSoftwareSystem = model.addSoftwareSystem("Other software system", "").addContainer("Other container", "", ""); - - ContainerView containerView = views.createContainerView(softwareSystem, "containers", ""); - containerView.add(container); - containerView.add(containerInADifferentSoftwareSystem); - fail(); - } catch (Exception e) { - assertEquals("Only containers belonging to The System can be added to this view.", e.getMessage()); - } - } - -} diff --git a/structurizr-core/test/unit/com/structurizr/view/DynamicViewTests.java b/structurizr-core/test/unit/com/structurizr/view/DynamicViewTests.java deleted file mode 100644 index 932091ccf..000000000 --- a/structurizr-core/test/unit/com/structurizr/view/DynamicViewTests.java +++ /dev/null @@ -1,223 +0,0 @@ -package com.structurizr.view; - -import com.structurizr.AbstractWorkspaceTestBase; -import com.structurizr.Workspace; -import com.structurizr.model.*; -import org.junit.Before; -import org.junit.Test; - -import static org.junit.Assert.*; - -public class DynamicViewTests extends AbstractWorkspaceTestBase { - - private Person person; - private SoftwareSystem softwareSystemA; - private Container containerA1; - private Container containerA2; - private Container containerA3; - private Component componentA1; - private Component componentA2; - - private SoftwareSystem softwareSystemB; - private Container containerB1; - private Component componentB1; - - private Relationship relationship; - - @Before - public void setup() { - person = model.addPerson("Person", ""); - softwareSystemA = model.addSoftwareSystem("Software System A", ""); - containerA1 = softwareSystemA.addContainer("Container A1", "", ""); - componentA1 = containerA1.addComponent("Component A1", ""); - containerA2 = softwareSystemA.addContainer("Container A2", "", ""); - componentA2 = containerA2.addComponent("Component A2", ""); - containerA3 = softwareSystemA.addContainer("Container A3", "", ""); - relationship = containerA1.uses(containerA2, "uses"); - - softwareSystemB = model.addSoftwareSystem("Software System B", ""); - containerB1 = softwareSystemB.addContainer("Container B1", "", ""); - } - - @Test - public void test_add_ThrowsAnException_WhenTheScopeOfTheDynamicViewIsASoftwareSystemButAContainerInAnotherSoftwareSystemIsAdded() { - try { - DynamicView dynamicView = workspace.getViews().createDynamicView(softwareSystemA, "key", "Description"); - dynamicView.add(containerB1, containerA1); - fail(); - } catch (Exception e) { - assertEquals("Only containers that reside inside Software System A can be added to this view.", e.getMessage()); - } - } - - @Test - public void test_add_ThrowsAnException_WhenTheScopeOfTheDynamicViewIsASoftwareSystemButAComponentIsAdded() { - try { - DynamicView dynamicView = workspace.getViews().createDynamicView(softwareSystemA, "key", "Description"); - dynamicView.add(componentA1, containerA1); - fail(); - } catch (Exception e) { - assertEquals("Components can't be added to a dynamic view when the scope is a software system.", e.getMessage()); - } - } - - @Test - public void test_add_ThrowsAnException_WhenTheScopeOfTheDynamicViewIsASoftwareSystemAndTheSameSoftwareSystemIsAdded() { - try { - DynamicView dynamicView = workspace.getViews().createDynamicView(softwareSystemA, "key", "Description"); - dynamicView.add(softwareSystemA, containerA1); - fail(); - } catch (Exception e) { - assertEquals("Software System A is already the scope of this view and cannot be added to it.", e.getMessage()); - } - } - - @Test - public void test_add_ThrowsAnException_WhenTheScopeOfTheDynamicViewIsAContainerAndTheSameContainerIsAdded() { - try { - DynamicView dynamicView = workspace.getViews().createDynamicView(containerA1, "key", "Description"); - dynamicView.add(containerA1, containerA2); - fail(); - } catch (Exception e) { - assertEquals("Container A1 is already the scope of this view and cannot be added to it.", e.getMessage()); - } - } - - @Test - public void test_add_ThrowsAnException_WhenTheScopeOfTheDynamicViewIsAContainerAndTheParentSoftwareSystemIsAdded() { - try { - DynamicView dynamicView = workspace.getViews().createDynamicView(containerA1, "key", "Description"); - dynamicView.add(softwareSystemA, containerA2); - fail(); - } catch (Exception e) { - assertEquals("Software System A is already the scope of this view and cannot be added to it.", e.getMessage()); - } - } - - @Test - public void test_add_ThrowsAnException_WhenTheScopeOfTheDynamicViewIsAContainerAndAContainerInAnotherSoftwareSystemIsAdded() { - try { - DynamicView dynamicView = workspace.getViews().createDynamicView(containerA1, "key", "Description"); - dynamicView.add(containerB1, containerA2); - fail(); - } catch (Exception e) { - assertEquals("Only containers that reside inside Software System A can be added to this view.", e.getMessage()); - } - } - - @Test - public void test_add_ThrowsAnException_WhenTheScopeOfTheDynamicViewIsAContainerAndAComponentInAnotherContainerIsAdded() { - try { - DynamicView dynamicView = workspace.getViews().createDynamicView(containerA1, "key", "Description"); - dynamicView.add(componentA2, containerA2); - fail(); - } catch (Exception e) { - assertEquals("Only components that reside inside Container A1 can be added to this view.", e.getMessage()); - } - } - - @Test - public void test_add_AddsTheSourceAndDestinationElements_WhenARelationshipBetweenThemExists() { - final DynamicView dynamicView = workspace.getViews().createDynamicView(softwareSystemA, "key", "Description"); - dynamicView.add(containerA1, containerA2); - assertEquals(2, dynamicView.getElements().size()); - } - - @Test(expected = IllegalArgumentException.class) - public void test_add_ThrowsAnException_WhenThereIsNoRelationshipBetweenTheSourceAndDestinationElements() { - final DynamicView dynamicView = workspace.getViews().createDynamicView(softwareSystemA, "key", "Description"); - dynamicView.add(containerA1, containerA3); - } - - @Test - public void test_addRelationshipDirectly() { - final DynamicView dynamicView = workspace.getViews().createDynamicView(softwareSystemA, "key", "Description"); - dynamicView.add(relationship); - assertEquals(2, dynamicView.getElements().size()); - } - - @Test - public void test_add_AddsTheSourceAndDestinationElements_WhenARelationshipBetweenThemExistsAndTheDestinationIsAnExternalSoftwareSystem() { - DynamicView dynamicView = workspace.getViews().createDynamicView(softwareSystemA, "key", "Description"); - containerA2.uses(softwareSystemB, "", ""); - dynamicView.add(containerA2, softwareSystemB); - assertEquals(2, dynamicView.getElements().size()); - } - - @Test - public void test_normalSequence() { - workspace = new Workspace("Name", "Description"); - model = workspace.getModel(); - - SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System", "Description"); - Container container1 = softwareSystem.addContainer("Container 1", "Description", "Technology"); - Container container2 = softwareSystem.addContainer("Container 2", "Description", "Technology"); - Container container3 = softwareSystem.addContainer("Container 3", "Description", "Technology"); - - container1.uses(container2, "Uses"); - container1.uses(container3, "Uses"); - - DynamicView view = workspace.getViews().createDynamicView(softwareSystem, "key", "Description"); - - view.add(container1, container2); - view.add(container1, container3); - - assertSame(container2, view.getRelationships().stream().filter(r -> r.getOrder().equals("1")).findFirst().get().getRelationship().getDestination()); - assertSame(container3, view.getRelationships().stream().filter(r -> r.getOrder().equals("2")).findFirst().get().getRelationship().getDestination()); - } - - @Test - public void test_parallelSequence() { - workspace = new Workspace("Name", "Description"); - model = workspace.getModel(); - SoftwareSystem softwareSystem = model.addSoftwareSystem("Name", "Description"); - Person user = model.addPerson("User", "Description"); - Container microservice1 = softwareSystem.addContainer("Microservice 1", "", ""); - Container database1 = softwareSystem.addContainer("Database 1", "", ""); - Container microservice2 = softwareSystem.addContainer("Microservice 2", "", ""); - Container database2 = softwareSystem.addContainer("Database 2", "", ""); - Container microservice3 = softwareSystem.addContainer("Microservice 3", "", ""); - Container database3 = softwareSystem.addContainer("Database 3", "", ""); - Container messageBus = softwareSystem.addContainer("Message Bus", "", ""); - - user.uses(microservice1, "Updates using"); - microservice1.delivers(user, "Sends updates to"); - - microservice1.uses(database1, "Stores data in"); - microservice1.uses(messageBus, "Sends messages to"); - microservice1.uses(messageBus, "Sends messages to"); - - messageBus.uses(microservice2, "Sends messages to"); - messageBus.uses(microservice3, "Sends messages to"); - - microservice2.uses(database2, "Stores data in"); - microservice3.uses(database3, "Stores data in"); - - DynamicView view = workspace.getViews().createDynamicView(softwareSystem, "key", "Description"); - - view.add(user, "1", microservice1); - view.add(microservice1, "2", database1); - view.add(microservice1, "3", messageBus); - - view.startParallelSequence(); - view.add(messageBus, "4", microservice2); - view.add(microservice2, "5", database2); - view.endParallelSequence(); - - view.startParallelSequence(); - view.add(messageBus, "4", microservice3); - view.add(microservice3, "5", database3); - view.endParallelSequence(); - - view.add(microservice1, "5", database1); - - System.out.println(view.toString()); - - assertEquals(1, view.getRelationships().stream().filter(r -> r.getOrder().equals("1")).count()); - assertEquals(1, view.getRelationships().stream().filter(r -> r.getOrder().equals("2")).count()); - assertEquals(1, view.getRelationships().stream().filter(r -> r.getOrder().equals("3")).count()); - assertEquals(3, view.getRelationships().stream().filter(r -> r.getOrder().equals("4")).count()); - assertEquals(2, view.getRelationships().stream().filter(r -> r.getOrder().equals("5")).count()); - } - -} diff --git a/structurizr-core/test/unit/com/structurizr/view/ElementStyleTests.java b/structurizr-core/test/unit/com/structurizr/view/ElementStyleTests.java deleted file mode 100644 index f08c19f13..000000000 --- a/structurizr-core/test/unit/com/structurizr/view/ElementStyleTests.java +++ /dev/null @@ -1,128 +0,0 @@ -package com.structurizr.view; - -import org.junit.Test; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNull; - -public class ElementStyleTests { - - @Test - public void test_setOpacity() { - ElementStyle style = new ElementStyle(); - assertNull(style.getOpacity()); - - style.setOpacity(-1); - assertEquals(0, style.getOpacity().intValue()); - - style.setOpacity(0); - assertEquals(0, style.getOpacity().intValue()); - - style.setOpacity(50); - assertEquals(50, style.getOpacity().intValue()); - - style.setOpacity(100); - assertEquals(100, style.getOpacity().intValue()); - - style.setOpacity(101); - assertEquals(100, style.getOpacity().intValue()); - } - - @Test - public void test_opacity() { - ElementStyle style = new ElementStyle(); - assertNull(style.getOpacity()); - - style.opacity(-1); - assertEquals(0, style.getOpacity().intValue()); - - style.opacity(0); - assertEquals(0, style.getOpacity().intValue()); - - style.opacity(50); - assertEquals(50, style.getOpacity().intValue()); - - style.opacity(100); - assertEquals(100, style.getOpacity().intValue()); - - style.opacity(101); - assertEquals(100, style.getOpacity().intValue()); - } - - @Test - public void test_setColor_SetsTheColorProperty_WhenAValidHexColorCodeIsSpecified() { - ElementStyle style = new ElementStyle(); - style.setColor("#ffffff"); - assertEquals("#ffffff", style.getColor()); - - style.setColor("#FFFFFF"); - assertEquals("#FFFFFF", style.getColor()); - - style.setColor("#123456"); - assertEquals("#123456", style.getColor()); - } - - @Test - public void test_color_SetsTheColorProperty_WhenAValidHexColorCodeIsSpecified() { - ElementStyle style = new ElementStyle(); - style.color("#ffffff"); - assertEquals("#ffffff", style.getColor()); - - style.color("#FFFFFF"); - assertEquals("#FFFFFF", style.getColor()); - - style.color("#123456"); - assertEquals("#123456", style.getColor()); - } - - @Test(expected = IllegalArgumentException.class) - public void test_setColor_ThrowsAnException_WhenAnInvalidHexColorCodeIsSpecified() { - 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 - public void test_setBackground_SetsTheBackgroundProperty_WhenAValidHexColorCodeIsSpecified() { - ElementStyle style = new ElementStyle(); - style.setBackground("#ffffff"); - assertEquals("#ffffff", style.getBackground()); - - style.setBackground("#FFFFFF"); - assertEquals("#FFFFFF", style.getBackground()); - - style.setBackground("#123456"); - assertEquals("#123456", style.getBackground()); - } - - @Test - public void test_background_SetsTheBackgroundProperty_WhenAValidHexColorCodeIsSpecified() { - ElementStyle style = new ElementStyle(); - style.background("#ffffff"); - assertEquals("#ffffff", style.getBackground()); - - style.background("#FFFFFF"); - assertEquals("#FFFFFF", style.getBackground()); - - style.background("#123456"); - assertEquals("#123456", style.getBackground()); - } - - @Test(expected = IllegalArgumentException.class) - public void test_setBackground_ThrowsAnException_WhenAnInvalidHexColorCodeIsSpecified() { - ElementStyle style = new ElementStyle(); - style.setBackground("white"); - } - - @Test(expected = IllegalArgumentException.class) - public void test_background_ThrowsAnException_WhenAnInvalidHexColorCodeIsSpecified() { - ElementStyle style = new ElementStyle(); - style.background("white"); - } - -} diff --git a/structurizr-core/test/unit/com/structurizr/view/ElementViewTests.java b/structurizr-core/test/unit/com/structurizr/view/ElementViewTests.java deleted file mode 100644 index a1f1fcd63..000000000 --- a/structurizr-core/test/unit/com/structurizr/view/ElementViewTests.java +++ /dev/null @@ -1,35 +0,0 @@ -package com.structurizr.view; - -import com.structurizr.AbstractWorkspaceTestBase; -import com.structurizr.model.Element; -import com.structurizr.model.Location; -import org.junit.Test; - -import static org.junit.Assert.assertEquals; - -public class ElementViewTests extends AbstractWorkspaceTestBase { - - @Test - public 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() { - Element element = model.addSoftwareSystem(Location.External, "SystemA", ""); - ElementView elementView1 = new ElementView(element); - assertEquals(0, elementView1.getX()); - assertEquals(0, elementView1.getY()); - - ElementView elementView2 = new ElementView(element); - elementView2.setX(123); - elementView2.setY(456); - - elementView1.copyLayoutInformationFrom(elementView2); - assertEquals(123, elementView1.getX()); - assertEquals(456, elementView1.getY()); - } - -} diff --git a/structurizr-core/test/unit/com/structurizr/view/EnterpriseContextViewTests.java b/structurizr-core/test/unit/com/structurizr/view/EnterpriseContextViewTests.java deleted file mode 100644 index 6e5e62b6e..000000000 --- a/structurizr-core/test/unit/com/structurizr/view/EnterpriseContextViewTests.java +++ /dev/null @@ -1,104 +0,0 @@ -package com.structurizr.view; - -import com.structurizr.AbstractWorkspaceTestBase; -import com.structurizr.model.Enterprise; -import com.structurizr.model.Location; -import com.structurizr.model.Person; -import com.structurizr.model.SoftwareSystem; -import org.junit.Before; -import org.junit.Test; - -import static org.junit.Assert.*; - -public class EnterpriseContextViewTests extends AbstractWorkspaceTestBase { - - private EnterpriseContextView view; - - @Before - public void setUp() { - view = new EnterpriseContextView(model, "context", "Description"); - } - - @Test - public void test_construction() { - assertEquals("Enterprise Context", view.getName()); - assertEquals(0, view.getElements().size()); - assertSame(model, view.getModel()); - } - - @Test - public void test_getName_WhenNoEnterpriseIsSpecified() { - assertEquals("Enterprise Context", view.getName()); - } - - @Test - public void test_getName_WhenAnEnterpriseIsSpecified() { - model.setEnterprise(new Enterprise("Widgets Limited")); - assertEquals("Enterprise Context for Widgets Limited", view.getName()); - } - - @Test(expected = IllegalArgumentException.class) - public void test_getName_WhenAnEmptyEnterpriseNameIsSpecified() { - model.setEnterprise(new Enterprise("")); - } - - @Test(expected = IllegalArgumentException.class) - public void test_getName_WhenANullEnterpriseNameIsSpecified() { - model.setEnterprise(new Enterprise(null)); - } - - @Test - public void test_addAllSoftwareSystems_DoesNothing_WhenThereAreNoOtherSoftwareSystems() { - view.addAllSoftwareSystems(); - assertEquals(0, view.getElements().size()); - } - - @Test - public void test_addAllSoftwareSystems_AddsAllSoftwareSystems_WhenThereAreSomeSoftwareSystemsInTheModel() { - SoftwareSystem softwareSystemA = model.addSoftwareSystem(Location.External, "System A", "Description"); - SoftwareSystem softwareSystemB = model.addSoftwareSystem(Location.External, "System B", "Description"); - - view.addAllSoftwareSystems(); - - assertEquals(2, view.getElements().size()); - assertTrue(view.getElements().contains(new ElementView(softwareSystemA))); - assertTrue(view.getElements().contains(new ElementView(softwareSystemB))); - } - - @Test - public void test_addAllPeople_DoesNothing_WhenThereAreNoPeople() { - view.addAllPeople(); - assertEquals(0, view.getElements().size()); - } - - @Test - public void test_addAllPeople_AddsAllPeople_WhenThereAreSomePeopleInTheModel() { - Person userA = model.addPerson("User A", "Description"); - Person userB = model.addPerson("User B", "Description"); - - view.addAllPeople(); - - assertEquals(2, view.getElements().size()); - assertTrue(view.getElements().contains(new ElementView(userA))); - assertTrue(view.getElements().contains(new ElementView(userB))); - } - - @Test - public void test_addAllElements_DoesNothing_WhenThereAreNoSoftwareSystemsOrPeople() { - view.addAllElements(); - assertEquals(0, view.getElements().size()); - } - - @Test - public void test_addAllElements_AddsAllSoftwareSystemsAndPeople_WhenThereAreSomeSoftwareSystemsAndPeopleInTheModel() { - SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System", "Description"); - Person person = model.addPerson("Person", "Description"); - - view.addAllElements(); - - assertEquals(2, view.getElements().size()); - assertTrue(view.getElements().contains(new ElementView(softwareSystem))); - assertTrue(view.getElements().contains(new ElementView(person))); - } - -} \ No newline at end of file diff --git a/structurizr-core/test/unit/com/structurizr/view/FontTests.java b/structurizr-core/test/unit/com/structurizr/view/FontTests.java deleted file mode 100644 index 435c759df..000000000 --- a/structurizr-core/test/unit/com/structurizr/view/FontTests.java +++ /dev/null @@ -1,54 +0,0 @@ -package com.structurizr.view; - -import org.junit.Before; -import org.junit.Test; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNull; - -public class FontTests { - - private Font font; - - @Before - public void setUp() { - this.font = new Font(); - } - - @Test - public void construction_WithANameOnly() { - this.font = new Font("Times New Roman"); - assertEquals("Times New Roman", font.getName()); - } - - @Test - public 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() { - 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 - public void test_setUrl_DoesNothing_WhenANullUrlIsSpecified() { - font.setUrl(null); - assertNull(font.getUrl()); - } - - @Test - public void test_setUrl_DoesNothing_WhenAnEmptyUrlIsSpecified() { - font.setUrl(" "); - assertNull(font.getUrl()); - } - -} diff --git a/structurizr-core/test/unit/com/structurizr/view/RelationshipStyleTests.java b/structurizr-core/test/unit/com/structurizr/view/RelationshipStyleTests.java deleted file mode 100644 index 0091f19e7..000000000 --- a/structurizr-core/test/unit/com/structurizr/view/RelationshipStyleTests.java +++ /dev/null @@ -1,129 +0,0 @@ -package com.structurizr.view; - -import org.junit.Test; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNull; - -public class RelationshipStyleTests { - - private RelationshipStyle relationshipStyle = new RelationshipStyle("tag"); - - @Test - public void test_setPosition_SetsPositionToNull_WhenNullIsSpecified() { - relationshipStyle.setPosition(null); - assertNull(relationshipStyle.getPosition()); - } - - @Test - public void test_setPosition_SetsPositionToZero_WhenANegativeNumberIsSpecified() { - relationshipStyle.setPosition(-1); - assertEquals(new Integer(0), relationshipStyle.getPosition()); - } - - @Test - public void test_setPosition_SetsPositionToOneHundred_WhenANumberGreaterThanOneHundredIsSpecified() { - relationshipStyle.setPosition(101); - assertEquals(new Integer(100), relationshipStyle.getPosition()); - } - - @Test - public void test_setPosition_SetsPosition_WhenANumberBetweenZeroAndOneHundredIsSpecified() { - relationshipStyle.setPosition(0); - assertEquals(new Integer(0), relationshipStyle.getPosition()); - - relationshipStyle.setPosition(1); - assertEquals(new Integer(1), relationshipStyle.getPosition()); - - relationshipStyle.setPosition(50); - assertEquals(new Integer(50), relationshipStyle.getPosition()); - - - relationshipStyle.setPosition(99); - assertEquals(new Integer(99), relationshipStyle.getPosition()); - - relationshipStyle.setPosition(100); - assertEquals(new Integer(100), relationshipStyle.getPosition()); - } - - @Test - public void test_setOpacity() { - RelationshipStyle style = new RelationshipStyle(); - assertNull(style.getOpacity()); - - style.setOpacity(-1); - assertEquals(0, style.getOpacity().intValue()); - - style.setOpacity(0); - assertEquals(0, style.getOpacity().intValue()); - - style.setOpacity(50); - assertEquals(50, style.getOpacity().intValue()); - - style.setOpacity(100); - assertEquals(100, style.getOpacity().intValue()); - - style.setOpacity(101); - assertEquals(100, style.getOpacity().intValue()); - } - - @Test - public void test_opacity() { - RelationshipStyle style = new RelationshipStyle(); - assertNull(style.getOpacity()); - - style.opacity(-1); - assertEquals(0, style.getOpacity().intValue()); - - style.opacity(0); - assertEquals(0, style.getOpacity().intValue()); - - style.opacity(50); - assertEquals(50, style.getOpacity().intValue()); - - style.opacity(100); - assertEquals(100, style.getOpacity().intValue()); - - style.opacity(101); - assertEquals(100, style.getOpacity().intValue()); - } - - @Test - public void test_setColor_SetsTheColorProperty_WhenAValidHexColorCodeIsSpecified() { - ElementStyle style = new ElementStyle(); - style.setColor("#ffffff"); - assertEquals("#ffffff", style.getColor()); - - style.setColor("#FFFFFF"); - assertEquals("#FFFFFF", style.getColor()); - - style.setColor("#123456"); - assertEquals("#123456", style.getColor()); - } - - @Test - public void test_color_SetsTheColorProperty_WhenAValidHexColorCodeIsSpecified() { - ElementStyle style = new ElementStyle(); - style.color("#ffffff"); - assertEquals("#ffffff", style.getColor()); - - style.color("#FFFFFF"); - assertEquals("#FFFFFF", style.getColor()); - - style.color("#123456"); - assertEquals("#123456", style.getColor()); - } - - @Test(expected = IllegalArgumentException.class) - public void test_setColor_ThrowsAnException_WhenAnInvalidHexColorCodeIsSpecified() { - ElementStyle style = new ElementStyle(); - style.setColor("white"); - } - - @Test(expected = IllegalArgumentException.class) - public void test_color_ThrowsAnException_WhenAnInvalidHexColorCodeIsSpecified() { - ElementStyle style = new ElementStyle(); - style.color("white"); - } - -} diff --git a/structurizr-core/test/unit/com/structurizr/view/SequenceCounterTests.java b/structurizr-core/test/unit/com/structurizr/view/SequenceCounterTests.java deleted file mode 100644 index 375fe88ac..000000000 --- a/structurizr-core/test/unit/com/structurizr/view/SequenceCounterTests.java +++ /dev/null @@ -1,53 +0,0 @@ -package com.structurizr.view; - -import org.junit.Test; - -import static org.junit.Assert.assertEquals; - -public class SequenceCounterTests { - - @Test - public void test_increment_IncrementsTheCounter_WhenThereIsNoParent() { - SequenceCounter counter = new SequenceCounter(); - assertEquals("0", counter.toString()); - - counter.increment(); - assertEquals("1", counter.toString()); - - counter.increment(); - assertEquals("2", counter.toString()); - } - - @Test - public void test_counter_WhenThereIsOneParent() { - SequenceCounter parent = new SequenceCounter(); - parent.increment(); - assertEquals("1", parent.toString()); - - SequenceCounter child = new SequenceCounter(parent); - child.increment(); - assertEquals("1.1", child.toString()); - - child.increment(); - assertEquals("1.2", child.toString()); - } - - @Test - public void test_counter_WhenThereAreTwoParents() { - SequenceCounter parent = new SequenceCounter(); - parent.increment(); - assertEquals("1", parent.toString()); - - SequenceCounter child = new SequenceCounter(parent); - child.increment(); - assertEquals("1.1", child.toString()); - - SequenceCounter grandchild = new SequenceCounter(child); - grandchild.increment(); - assertEquals("1.1.1", grandchild.toString()); - - grandchild.increment(); - assertEquals("1.1.2", grandchild.toString()); - } - -} diff --git a/structurizr-core/test/unit/com/structurizr/view/SequenceNumberTests.java b/structurizr-core/test/unit/com/structurizr/view/SequenceNumberTests.java deleted file mode 100644 index f582ff33a..000000000 --- a/structurizr-core/test/unit/com/structurizr/view/SequenceNumberTests.java +++ /dev/null @@ -1,45 +0,0 @@ -package com.structurizr.view; - -import org.junit.Test; - -import static org.junit.Assert.assertEquals; - -public class SequenceNumberTests { - - @Test - public void test_increment() { - SequenceNumber sequenceNumber = new SequenceNumber(); - assertEquals("1", sequenceNumber.getNext()); - assertEquals("2", sequenceNumber.getNext()); - } - - @Test - public void test_childSequence() { - SequenceNumber sequenceNumber = new SequenceNumber(); - assertEquals("1", sequenceNumber.getNext()); - - sequenceNumber.startChildSequence(); - assertEquals("1.1", sequenceNumber.getNext()); - assertEquals("1.2", sequenceNumber.getNext()); - - sequenceNumber.endChildSequence(); - assertEquals("2", sequenceNumber.getNext()); - } - - @Test - public void test_parallelSequences() { - SequenceNumber sequenceNumber = new SequenceNumber(); - assertEquals("1", sequenceNumber.getNext()); - - sequenceNumber.startParallelSequence(); - assertEquals("2", sequenceNumber.getNext()); - sequenceNumber.endParallelSequence(); - - sequenceNumber.startParallelSequence(); - assertEquals("2", sequenceNumber.getNext()); - sequenceNumber.endParallelSequence(); - - assertEquals("2", sequenceNumber.getNext()); - } - -} diff --git a/structurizr-core/test/unit/com/structurizr/view/StyleTests.java b/structurizr-core/test/unit/com/structurizr/view/StyleTests.java deleted file mode 100644 index 6bfc22314..000000000 --- a/structurizr-core/test/unit/com/structurizr/view/StyleTests.java +++ /dev/null @@ -1,73 +0,0 @@ -package com.structurizr.view; - -import com.structurizr.AbstractWorkspaceTestBase; -import com.structurizr.model.Relationship; -import com.structurizr.model.SoftwareSystem; -import com.structurizr.model.Tags; -import org.junit.Test; - -import static org.junit.Assert.assertEquals; - -public class StyleTests extends AbstractWorkspaceTestBase { - - private Styles styles = new Styles(); - - @Test - public void test_findElementStyle_ReturnsTheDefaultStyle_WhenPassedNull() { - ElementStyle style = styles.findElementStyle(null); - assertEquals("#dddddd", style.getBackground()); - assertEquals("#000000", style.getColor()); - assertEquals(Shape.Box, style.getShape()); - } - - @Test - public void test_findElementStyle_ReturnsTheDefaultStyle_WhenNoStylesAreDefined() { - SoftwareSystem element = model.addSoftwareSystem("Name", "Description"); - ElementStyle style = styles.findElementStyle(element); - assertEquals("#dddddd", style.getBackground()); - assertEquals("#000000", style.getColor()); - assertEquals(Shape.Box, style.getShape()); - } - - @Test - public void test_findElementStyle_ReturnsTheCorrectStyle_WhenStylesAreDefined() { - SoftwareSystem element = model.addSoftwareSystem("Name", "Description"); - element.addTags("Some Tag"); - - styles.addElementStyle(Tags.SOFTWARE_SYSTEM).background("#ff0000").color("#ffffff"); - styles.addElementStyle("Some Tag").color("#0000ff").shape(Shape.RoundedBox); - - ElementStyle style = styles.findElementStyle(element); - assertEquals("#ff0000", style.getBackground()); - assertEquals("#0000ff", style.getColor()); - assertEquals(Shape.RoundedBox, style.getShape()); - } - - @Test - public void test_findRelationshipStyle_ReturnsTheDefaultStyle_WhenPassedNull() { - RelationshipStyle style = styles.findRelationshipStyle(null); - assertEquals("#707070", style.getColor()); - } - - @Test - public void test_findRelationshipStyle_ReturnsTheDefaultStyle_WhenNoStylesAreDefined() { - SoftwareSystem element = model.addSoftwareSystem("Name", "Description"); - Relationship relationship = element.uses(element, "Uses"); - RelationshipStyle style = styles.findRelationshipStyle(relationship); - assertEquals("#707070", style.getColor()); - } - - @Test - public void test_findRelationshipStyle_ReturnsTheCorrectStyle_WhenStylesAreDefined() { - SoftwareSystem element = model.addSoftwareSystem("Name", "Description"); - Relationship relationship = element.uses(element, "Uses"); - relationship.addTags("Some Tag"); - - styles.addRelationshipStyle(Tags.RELATIONSHIP).color("#ff0000"); - styles.addRelationshipStyle("Some Tag").color("#0000ff"); - - RelationshipStyle style = styles.findRelationshipStyle(relationship); - assertEquals("#0000ff", style.getColor()); - } - -} \ No newline at end of file diff --git a/structurizr-core/test/unit/com/structurizr/view/SystemContextViewTests.java b/structurizr-core/test/unit/com/structurizr/view/SystemContextViewTests.java deleted file mode 100644 index 71f2bb5f4..000000000 --- a/structurizr-core/test/unit/com/structurizr/view/SystemContextViewTests.java +++ /dev/null @@ -1,158 +0,0 @@ -package com.structurizr.view; - -import com.structurizr.AbstractWorkspaceTestBase; -import com.structurizr.model.*; -import org.junit.Before; -import org.junit.Test; - -import static org.junit.Assert.*; - -public class SystemContextViewTests extends AbstractWorkspaceTestBase { - - private SoftwareSystem softwareSystem; - private SystemContextView view; - - @Before - public void setUp() { - softwareSystem = model.addSoftwareSystem(Location.Internal, "The System", "Description"); - view = new SystemContextView(softwareSystem, "context", "Description"); - } - - @Test - public void test_construction() { - assertEquals("The System - System Context", view.getName()); - assertEquals(1, view.getElements().size()); - assertSame(view.getElements().iterator().next().getElement(), softwareSystem); - assertSame(softwareSystem, view.getSoftwareSystem()); - assertEquals(softwareSystem.getId(), view.getSoftwareSystemId()); - assertSame(model, view.getModel()); - } - - @Test - public void test_addAllSoftwareSystems_DoesNothing_WhenThereAreNoOtherSoftwareSystems() { - assertEquals(1, view.getElements().size()); - view.addAllSoftwareSystems(); - assertEquals(1, view.getElements().size()); - } - - @Test - public void test_addAllSoftwareSystems_AddsAllSoftwareSystems_WhenThereAreSomeSoftwareSystemsInTheModel() { - SoftwareSystem softwareSystemA = model.addSoftwareSystem(Location.External, "System A", "Description"); - SoftwareSystem softwareSystemB = model.addSoftwareSystem(Location.External, "System B", "Description"); - - view.addAllSoftwareSystems(); - - assertEquals(3, view.getElements().size()); - assertTrue(view.getElements().contains(new ElementView(softwareSystem))); - assertTrue(view.getElements().contains(new ElementView(softwareSystemA))); - assertTrue(view.getElements().contains(new ElementView(softwareSystemB))); - } - - @Test - public void test_addAllPeople_DoesNothing_WhenThereAreNoPeople() { - assertEquals(1, view.getElements().size()); - view.addAllPeople(); - assertEquals(1, view.getElements().size()); - } - - @Test - public void test_addAllPeople_AddsAllPeople_WhenThereAreSomePeopleInTheModel() { - Person userA = model.addPerson(Location.External, "User A", "Description"); - Person userB = model.addPerson(Location.External, "User B", "Description"); - - view.addAllPeople(); - - assertEquals(3, view.getElements().size()); - assertTrue(view.getElements().contains(new ElementView(softwareSystem))); - assertTrue(view.getElements().contains(new ElementView(userA))); - assertTrue(view.getElements().contains(new ElementView(userB))); - } - - @Test - public void test_addAllElements_DoesNothing_WhenThereAreNoSoftwareSystemsOrPeople() { - assertEquals(1, view.getElements().size()); - view.addAllElements(); - assertEquals(1, view.getElements().size()); - } - - @Test - public 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"); - Person userB = model.addPerson(Location.External, "User B", "Description"); - - view.addAllElements(); - - assertEquals(5, view.getElements().size()); - assertTrue(view.getElements().contains(new ElementView(softwareSystem))); - assertTrue(view.getElements().contains(new ElementView(softwareSystemA))); - assertTrue(view.getElements().contains(new ElementView(softwareSystemB))); - assertTrue(view.getElements().contains(new ElementView(userA))); - assertTrue(view.getElements().contains(new ElementView(userB))); - } - - @Test - public void test_addNearestNeightbours_DoesNothing_WhenANullElementIsSpecified() { - view.addNearestNeighbours(null); - - assertEquals(1, view.getElements().size()); - } - - @Test - public void test_addNearestNeighbours_DoesNothing_WhenThereAreNoNeighbours() { - view.addNearestNeighbours(softwareSystem); - - assertEquals(1, view.getElements().size()); - } - - @Test - public 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"); - Person userB = model.addPerson("User B", "Description"); - - // userA -> systemA -> system -> systemB -> userB - userA.uses(softwareSystemA, ""); - softwareSystemA.uses(softwareSystem, ""); - softwareSystem.uses(softwareSystemB, ""); - softwareSystemB.delivers(userB, ""); - - // userA -> systemA -> web application -> systemB -> userB - // web application -> database - Container webApplication = softwareSystem.addContainer("Web Application", "", ""); - Container database = softwareSystem.addContainer("Database", "", ""); - softwareSystemA.uses(webApplication, ""); - webApplication.uses(softwareSystemB, ""); - webApplication.uses(database, ""); - - // userA -> systemA -> controller -> service -> repository -> database - Component controller = webApplication.addComponent("Controller", ""); - Component service = webApplication.addComponent("Service", ""); - Component repository = webApplication.addComponent("Repository", ""); - softwareSystemA.uses(controller, ""); - controller.uses(service, ""); - service.uses(repository, ""); - repository.uses(database, ""); - - // userA -> systemA -> controller -> service -> systemB -> userB - service.uses(softwareSystemB, ""); - - view.addNearestNeighbours(softwareSystem); - - assertEquals(3, view.getElements().size()); - assertTrue(view.getElements().contains(new ElementView(softwareSystemA))); - assertTrue(view.getElements().contains(new ElementView(softwareSystem))); - assertTrue(view.getElements().contains(new ElementView(softwareSystemB))); - - view = new SystemContextView(softwareSystem, "context", "Description"); - view.addNearestNeighbours(softwareSystemA); - - assertEquals(3, view.getElements().size()); - assertTrue(view.getElements().contains(new ElementView(userA))); - assertTrue(view.getElements().contains(new ElementView(softwareSystemA))); - assertTrue(view.getElements().contains(new ElementView(softwareSystem))); - } - -} \ 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 deleted file mode 100644 index 0cdf7d594..000000000 --- a/structurizr-core/test/unit/com/structurizr/view/ViewSetTests.java +++ /dev/null @@ -1,341 +0,0 @@ -package com.structurizr.view; - -import com.structurizr.Workspace; -import com.structurizr.model.*; -import org.junit.Test; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.fail; - -public class ViewSetTests { - - private Workspace createWorkspace() { - Workspace workspace = new Workspace("Name", "Description"); - Model model = workspace.getModel(); - - SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System", "Description"); - Person person = model.addPerson("Person", "Description"); - person.uses(softwareSystem, "Uses"); - Container container = softwareSystem.addContainer("Container", "Description", "Technology"); - Component component = container.addComponent("Component", "Description", "Technology"); - - return workspace; - } - - @Test - public void test_copyLayoutInformationFrom_WhenAViewKeyIsNotSetButTheViewTitlesMatch() { - Workspace workspace1 = createWorkspace(); - SoftwareSystem softwareSystem1 = workspace1.getModel().getSoftwareSystemWithName("Software System"); - SystemContextView view1 = workspace1.getViews().createSystemContextView(softwareSystem1, "context", "Description"); - view1.setKey(null); // this simulates views created by previous versions of the client library - view1.addAllElements(); - view1.getElements().iterator().next().setX(100); - view1.setPaperSize(PaperSize.A3_Landscape); - - Workspace workspace2 = createWorkspace(); - SoftwareSystem softwareSystem2 = workspace2.getModel().getSoftwareSystemWithName("Software System"); - SystemContextView view2 = workspace2.getViews().createSystemContextView(softwareSystem2, "context", "Description"); - view2.setKey(null); // this simulates views created by previous versions of the client library - view2.addAllElements(); - - workspace2.getViews().copyLayoutInformationFrom(workspace1.getViews()); - assertEquals(100, view2.getElements().iterator().next().getX()); - assertEquals(PaperSize.A3_Landscape, view2.getPaperSize()); - } - - @Test - public void test_copyLayoutInformationFrom_WhenAViewKeyHasBeenIntroduced() { - Workspace workspace1 = createWorkspace(); - SoftwareSystem softwareSystem1 = workspace1.getModel().getSoftwareSystemWithName("Software System"); - SystemContextView view1 = workspace1.getViews().createSystemContextView(softwareSystem1, "context", "Description"); - view1.setKey(null); // this simulates views created by previous versions of the client library - view1.addAllElements(); - view1.getElements().iterator().next().setX(100); - view1.setPaperSize(PaperSize.A3_Landscape); - - Workspace workspace2 = createWorkspace(); - SoftwareSystem softwareSystem2 = workspace2.getModel().getSoftwareSystemWithName("Software System"); - SystemContextView view2 = workspace2.getViews().createSystemContextView(softwareSystem2, "context", "Description"); - view2.addAllElements(); - - workspace2.getViews().copyLayoutInformationFrom(workspace1.getViews()); - assertEquals(100, view2.getElements().iterator().next().getX()); - assertEquals(PaperSize.A3_Landscape, view2.getPaperSize()); - } - - @Test - public void test_copyLayoutInformationFrom_IgnoresThePaperSize_WhenThePaperSizeIsSet() { - Workspace workspace1 = createWorkspace(); - SoftwareSystem softwareSystem1 = workspace1.getModel().getSoftwareSystemWithName("Software System"); - SystemContextView view1 = workspace1.getViews().createSystemContextView(softwareSystem1, "context", "Description"); - view1.setPaperSize(PaperSize.A3_Landscape); - - Workspace workspace2 = createWorkspace(); - SoftwareSystem softwareSystem2 = workspace2.getModel().getSoftwareSystemWithName("Software System"); - SystemContextView view2 = workspace2.getViews().createSystemContextView(softwareSystem2, "context", "Description"); - view2.setPaperSize(PaperSize.A5_Portrait); - - workspace2.getViews().copyLayoutInformationFrom(workspace1.getViews()); - assertEquals(PaperSize.A5_Portrait, view2.getPaperSize()); - } - - @Test - public void test_copyLayoutInformationFrom_CopiesThePaperSize_WhenThePaperSizeIsNotSet() { - Workspace workspace1 = createWorkspace(); - SoftwareSystem softwareSystem1 = workspace1.getModel().getSoftwareSystemWithName("Software System"); - SystemContextView view1 = workspace1.getViews().createSystemContextView(softwareSystem1, "context", "Description"); - view1.setPaperSize(PaperSize.A3_Landscape); - - Workspace workspace2 = createWorkspace(); - SoftwareSystem softwareSystem2 = workspace2.getModel().getSoftwareSystemWithName("Software System"); - SystemContextView view2 = workspace2.getViews().createSystemContextView(softwareSystem2, "context", "Description"); - - workspace2.getViews().copyLayoutInformationFrom(workspace1.getViews()); - assertEquals(PaperSize.A3_Landscape, view2.getPaperSize()); - } - - @Test - public void test_copyLayoutInformationFrom_WhenTheSystemContextViewKeysMatch() { - Workspace workspace1 = createWorkspace(); - SoftwareSystem softwareSystem1 = workspace1.getModel().getSoftwareSystemWithName("Software System"); - SystemContextView view1 = workspace1.getViews().createSystemContextView(softwareSystem1, "context", "Description"); - view1.addAllElements(); - view1.getElements().iterator().next().setX(100); - view1.setPaperSize(PaperSize.A3_Landscape); - - Workspace workspace2 = createWorkspace(); - SoftwareSystem softwareSystem2 = workspace2.getModel().getSoftwareSystemWithName("Software System"); - SystemContextView view2 = workspace2.getViews().createSystemContextView(softwareSystem2, "context", "Description"); - view2.addAllElements(); - - workspace2.getViews().copyLayoutInformationFrom(workspace1.getViews()); - assertEquals(100, view2.getElements().iterator().next().getX()); - assertEquals(PaperSize.A3_Landscape, view2.getPaperSize()); - } - - @Test - public void test_copyLayoutInformationFrom_DoesNotDoAnythingIfThereIsNoSystemContextViewToCopyInformationFrom() { - Workspace workspace1 = createWorkspace(); - - Workspace workspace2 = createWorkspace(); - SoftwareSystem softwareSystem2 = workspace2.getModel().getSoftwareSystemWithName("Software System"); - SystemContextView view2 = workspace2.getViews().createSystemContextView(softwareSystem2, "context", "Description"); - view2.addAllElements(); - - workspace2.getViews().copyLayoutInformationFrom(workspace1.getViews()); - assertEquals(0, view2.getElements().iterator().next().getX()); // default - assertNull(view2.getPaperSize()); // default - } - - @Test - public void test_copyLayoutInformationFrom_WhenTheContainerViewKeysMatch() { - Workspace workspace1 = createWorkspace(); - SoftwareSystem softwareSystem1 = workspace1.getModel().getSoftwareSystemWithName("Software System"); - ContainerView view1 = workspace1.getViews().createContainerView(softwareSystem1, "containers", "Description"); - view1.addAllElements(); - view1.getElements().iterator().next().setX(100); - view1.setPaperSize(PaperSize.A3_Landscape); - - Workspace workspace2 = createWorkspace(); - SoftwareSystem softwareSystem2 = workspace2.getModel().getSoftwareSystemWithName("Software System"); - ContainerView view2 = workspace2.getViews().createContainerView(softwareSystem2, "containers", "Description"); - view2.addAllElements(); - - workspace2.getViews().copyLayoutInformationFrom(workspace1.getViews()); - assertEquals(100, view2.getElements().iterator().next().getX()); - assertEquals(PaperSize.A3_Landscape, view2.getPaperSize()); - } - - @Test - public void test_copyLayoutInformationFrom_DoesNotDoAnythingIfThereIsNoContainerViewToCopyInformationFrom() { - Workspace workspace1 = createWorkspace(); - - Workspace workspace2 = createWorkspace(); - SoftwareSystem softwareSystem2 = workspace2.getModel().getSoftwareSystemWithName("Software System"); - ContainerView view2 = workspace2.getViews().createContainerView(softwareSystem2, "containers", "Description"); - view2.addAllElements(); - - workspace2.getViews().copyLayoutInformationFrom(workspace1.getViews()); - assertEquals(0, view2.getElements().iterator().next().getX()); // default - assertNull(view2.getPaperSize()); // default - } - - @Test - public void test_copyLayoutInformationFrom_WhenTheComponentViewKeysMatch() { - Workspace workspace1 = createWorkspace(); - Container container1 = workspace1.getModel().getSoftwareSystemWithName("Software System").getContainerWithName("Container"); - ComponentView view1 = workspace1.getViews().createComponentView(container1, "containers", "Description"); - view1.addAllElements(); - view1.getElements().iterator().next().setX(100); - view1.setPaperSize(PaperSize.A3_Landscape); - - Workspace workspace2 = createWorkspace(); - Container container2 = workspace2.getModel().getSoftwareSystemWithName("Software System").getContainerWithName("Container"); - ComponentView view2 = workspace2.getViews().createComponentView(container2, "containers", "Description"); - view2.addAllElements(); - - workspace2.getViews().copyLayoutInformationFrom(workspace1.getViews()); - assertEquals(100, view2.getElements().iterator().next().getX()); - assertEquals(PaperSize.A3_Landscape, view2.getPaperSize()); - } - - @Test - public void test_copyLayoutInformationFrom_DoesNotDoAnythingIfThereIsNoComponentViewToCopyInformationFrom() { - Workspace workspace1 = createWorkspace(); - - Workspace workspace2 = createWorkspace(); - Container container2 = workspace2.getModel().getSoftwareSystemWithName("Software System").getContainerWithName("Container"); - ComponentView view2 = workspace2.getViews().createComponentView(container2, "components", "Description"); - view2.addAllElements(); - - workspace2.getViews().copyLayoutInformationFrom(workspace1.getViews()); - assertEquals(0, view2.getElements().iterator().next().getX()); // default - assertNull(view2.getPaperSize()); // default - } - - @Test - public void test_copyLayoutInformationFrom_WhenTheDynamicViewKeysMatch() { - Workspace workspace1 = createWorkspace(); - Person person1 = workspace1.getModel().getPersonWithName("Person"); - SoftwareSystem softwareSystem1 = workspace1.getModel().getSoftwareSystemWithName("Software System"); - DynamicView view1 = workspace1.getViews().createDynamicView("context", "Description"); - view1.add(person1, softwareSystem1); - view1.getElements().iterator().next().setX(100); - view1.setPaperSize(PaperSize.A3_Landscape); - - Workspace workspace2 = createWorkspace(); - Person person2 = workspace2.getModel().getPersonWithName("Person"); - SoftwareSystem softwareSystem2 = workspace2.getModel().getSoftwareSystemWithName("Software System"); - DynamicView view2 = workspace2.getViews().createDynamicView("context", "Description"); - view2.add(person2, softwareSystem2); - - workspace2.getViews().copyLayoutInformationFrom(workspace1.getViews()); - assertEquals(100, view2.getElements().iterator().next().getX()); - assertEquals(PaperSize.A3_Landscape, view2.getPaperSize()); - } - - @Test - public void test_copyLayoutInformationFrom_DoesNotDoAnythingIfThereIsNoDynamicViewToCopyInformationFrom() { - Workspace workspace1 = createWorkspace(); - - Workspace workspace2 = createWorkspace(); - Person person2 = workspace2.getModel().getPersonWithName("Person"); - SoftwareSystem softwareSystem2 = workspace2.getModel().getSoftwareSystemWithName("Software System"); - DynamicView view2 = workspace2.getViews().createDynamicView("context", "Description"); - view2.add(person2, softwareSystem2); - - workspace2.getViews().copyLayoutInformationFrom(workspace1.getViews()); - assertEquals(0, view2.getElements().iterator().next().getX()); // default - assertNull(view2.getPaperSize()); // default - } - - @Test - public void test_createSystemContextView_DoesNotThrowAnException_WhenEveryViewHasADifferentKey() { - Workspace workspace = new Workspace("Name", "Description"); - SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Name", "Description"); - - workspace.getViews().createSystemContextView(softwareSystem, "context1", "Description"); - workspace.getViews().createSystemContextView(softwareSystem, "context2", "Description"); - } - - @Test - public void test_createSystemContextView_ThrowsAnException_WhenADuplicateKeyIsUsed() { - Workspace workspace = new Workspace("Name", "Description"); - SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Name", "Description"); - - workspace.getViews().createSystemContextView(softwareSystem, "context", "Description"); - try { - workspace.getViews().createSystemContextView(softwareSystem, "context", "Description"); - fail(); - } catch (IllegalArgumentException iae) { - assertEquals("A view with the key context already exists.", iae.getMessage()); - } - } - - @Test - public void test_createContainerView_DoesNotThrowAnException_WhenEveryViewHasADifferentKey() { - Workspace workspace = new Workspace("Name", "Description"); - SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Name", "Description"); - Container container = softwareSystem.addContainer("Name", "Description", "Technology"); - - workspace.getViews().createContainerView(softwareSystem, "containers1", "Description"); - workspace.getViews().createContainerView(softwareSystem, "containers2", "Description"); - } - - @Test - public void test_createContainerView_ThrowsAnException_WhenADuplicateKeyIsUsed() { - Workspace workspace = new Workspace("Name", "Description"); - SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Name", "Description"); - Container container = softwareSystem.addContainer("Name", "Description", "Technology"); - - workspace.getViews().createContainerView(softwareSystem, "containers", "Description"); - try { - workspace.getViews().createContainerView(softwareSystem, "containers", "Description"); - fail(); - } catch (IllegalArgumentException iae) { - assertEquals("A view with the key containers already exists.", iae.getMessage()); - } - } - - @Test - public void test_createComponentView_DoesNotThrowAnException_WhenEveryViewHasADifferentKey() { - Workspace workspace = new Workspace("Name", "Description"); - SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Name", "Description"); - Container container = softwareSystem.addContainer("Name", "Description", "Technology"); - Component component = container.addComponent("Name", "Description", "Technology"); - - workspace.getViews().createComponentView(container, "components1", "Description"); - workspace.getViews().createComponentView(container, "components2", "Description"); - } - - @Test - public void test_createComponentView_ThrowsAnException_WhenADuplicateKeyIsUsed() { - Workspace workspace = new Workspace("Name", "Description"); - SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Name", "Description"); - Container container = softwareSystem.addContainer("Name", "Description", "Technology"); - Component component = container.addComponent("Name", "Description", "Technology"); - - workspace.getViews().createComponentView(container, "components", "Description"); - try { - workspace.getViews().createComponentView(container, "components", "Description"); - fail(); - } catch (IllegalArgumentException iae) { - assertEquals("A view with the key components already exists.", iae.getMessage()); - } - } - - @Test(expected = IllegalArgumentException.class) - public void test_createDynamicView_ThrowsAnException_WhenANullSoftwareSystemIsSpecified() { - new Workspace("Name", "Description").getViews().createDynamicView((SoftwareSystem)null, "key", "Description"); - } - - @Test(expected = IllegalArgumentException.class) - public void test_createDynamicView_ThrowsAnException_WhenANullContainerIsSpecified() { - new Workspace("Name", "Description").getViews().createDynamicView((Container)null, "key", "Description"); - } - - @Test - public void test_createDynamicView_DoesNotThrowAnException_WhenEveryViewHasADifferentKey() { - Workspace workspace = new Workspace("Name", "Description"); - SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Name", "Description"); - - workspace.getViews().createDynamicView(softwareSystem, "dynamic1", "Description"); - workspace.getViews().createDynamicView(softwareSystem, "dynamic2", "Description"); - } - - @Test - public void test_createDynamicView_ThrowsAnException_WhenADuplicateKeyIsUsed() { - Workspace workspace = new Workspace("Name", "Description"); - SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Name", "Description"); - - workspace.getViews().createDynamicView(softwareSystem, "dynamic", "Description"); - try { - workspace.getViews().createDynamicView(softwareSystem, "dynamic", "Description"); - fail(); - } catch (IllegalArgumentException iae) { - assertEquals("A view with the key dynamic already exists.", iae.getMessage()); - } - } - -} diff --git a/structurizr-core/test/unit/com/structurizr/view/ViewTests.java b/structurizr-core/test/unit/com/structurizr/view/ViewTests.java deleted file mode 100644 index fbb18ed82..000000000 --- a/structurizr-core/test/unit/com/structurizr/view/ViewTests.java +++ /dev/null @@ -1,368 +0,0 @@ -package com.structurizr.view; - -import com.structurizr.AbstractWorkspaceTestBase; -import com.structurizr.Workspace; -import com.structurizr.model.*; -import org.junit.Test; - -import java.util.Arrays; -import java.util.Iterator; - -import static org.junit.Assert.*; - -public class ViewTests extends AbstractWorkspaceTestBase { - - @Test - public void test_construction() { - SoftwareSystem softwareSystem = model.addSoftwareSystem(Location.Internal, "The System", "Description"); - StaticView view = new SystemContextView(softwareSystem, "key", "Description"); - assertEquals("key", view.getKey()); - assertEquals("Description", view.getDescription()); - } - - @Test - public 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()); - view.addAllSoftwareSystems(); - assertEquals(1, view.getElements().size()); - } - - @Test - public 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"); - SoftwareSystem softwareSystemC = model.addSoftwareSystem(Location.Unspecified, "System C", "Description"); - - StaticView view = new SystemContextView(softwareSystem, "context", "Description"); - view.addAllSoftwareSystems(); - - assertEquals(4, view.getElements().size()); - Iterator it = view.getElements().iterator(); - assertSame(softwareSystem, it.next().getElement()); - assertSame(softwareSystemA, it.next().getElement()); - assertSame(softwareSystemB, it.next().getElement()); - assertSame(softwareSystemC, it.next().getElement()); - } - - @Test - public void test_addSoftwareSystem_DoesNothing_WhenGivenNull() { - SoftwareSystem softwareSystem = model.addSoftwareSystem(Location.Internal, "The System", "Description"); - StaticView view = new SystemContextView(softwareSystem, "context", "Description"); - view.add((SoftwareSystem)null); - assertEquals(1, view.getElements().size()); - } - - @Test - public void test_addSoftwareSystem_DoesNothing_WhenTheSoftwareSystemIsNotInTheModel() { - SoftwareSystem softwareSystem = model.addSoftwareSystem(Location.Internal, "The System", "Description"); - StaticView view = new SystemContextView(softwareSystem, "context", "Description"); - - Model model2 = new Model(); - SoftwareSystem softwareSystemA = model2.addSoftwareSystem(Location.Unspecified, "System A", "Description"); - view.add(softwareSystemA); - assertEquals(1, view.getElements().size()); - } - - @Test - public void test_addSoftwareSystem_AddsTheSoftwareSystem_WhenTheSoftwareSystemIsInTheModel() { - SoftwareSystem softwareSystem = model.addSoftwareSystem(Location.Internal, "The System", "Description"); - SoftwareSystem softwareSystemA = model.addSoftwareSystem(Location.Unspecified, "System A", "Description"); - - StaticView view = new SystemContextView(softwareSystem, "context", "Description"); - view.add(softwareSystemA); - assertEquals(2, view.getElements().size()); - Iterator it = view.getElements().iterator(); - assertSame(softwareSystem, it.next().getElement()); - assertSame(softwareSystemA, it.next().getElement()); - } - - @Test - public void test_addAllPeople_DoesNothing_WhenThereAreNoPeopleInTheModel() { - SoftwareSystem softwareSystem = model.addSoftwareSystem(Location.Internal, "The System", "Description"); - - StaticView view = new SystemContextView(softwareSystem, "context", "Description"); - assertEquals(1, view.getElements().size()); - - view.addAllPeople(); - assertEquals(1, view.getElements().size()); - } - - @Test - public 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"); - Person person3 = model.addPerson(Location.Unspecified, "Person 3", "Description"); - - StaticView view = new SystemContextView(softwareSystem, "context", "Description"); - view.addAllPeople(); - - assertEquals(4, view.getElements().size()); - Iterator it = view.getElements().iterator(); - assertSame(softwareSystem, it.next().getElement()); - assertSame(person1, it.next().getElement()); - assertSame(person2, it.next().getElement()); - assertSame(person3, it.next().getElement()); - } - - @Test - public void test_addPerson_DoesNothing_WhenGivenNull() { - SoftwareSystem softwareSystem = model.addSoftwareSystem(Location.Internal, "The System", "Description"); - StaticView view = new SystemContextView(softwareSystem, "context", "Description"); - view.add((Person)null); - assertEquals(1, view.getElements().size()); - } - - @Test - public void test_addPerson_DoesNothing_WhenThePersonIsNotInTheModel() { - SoftwareSystem softwareSystem = model.addSoftwareSystem(Location.Internal, "The System", "Description"); - StaticView view = new SystemContextView(softwareSystem, "context", "Description"); - - Model model2 = new Model(); - Person person1 = model2.addPerson(Location.Unspecified, "Person 1", "Description"); - view.add(person1); - assertEquals(1, view.getElements().size()); - } - - @Test - public void test_addPerson_AddsThePerson_WhenThPersonIsInTheModel() { - SoftwareSystem softwareSystem = model.addSoftwareSystem(Location.Internal, "The System", "Description"); - StaticView view = new SystemContextView(softwareSystem, "context", "Description"); - - Person person1 = model.addPerson(Location.Unspecified, "Person 1", "Description"); - view.add(person1); - - assertEquals(2, view.getElements().size()); - Iterator it = view.getElements().iterator(); - assertSame(softwareSystem, it.next().getElement()); - assertSame(person1, it.next().getElement()); - } - - @Test - public void test_removeElementsWithNoRelationships_RemovesAllElements_WhenTheViewHasNoRelationshipsBetweenElements() { - SoftwareSystem softwareSystem = model.addSoftwareSystem(Location.Internal, "The System", "Description"); - SoftwareSystem softwareSystemA = model.addSoftwareSystem(Location.Unspecified, "System A", "Description"); - Person person1 = model.addPerson(Location.Unspecified, "Person 1", "Description"); - - StaticView view = new SystemContextView(softwareSystem, "context", "Description"); - view.addAllSoftwareSystems(); - view.addAllPeople(); - view.removeElementsWithNoRelationships(); - - assertEquals(0, view.getElements().size()); - } - - @Test - public 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"); - Person person1 = model.addPerson(Location.Unspecified, "Person 1", "Description"); - Person person2 = model.addPerson(Location.Unspecified, "Person 2", "Description"); - StaticView view = new SystemContextView(softwareSystem, "context", "Description"); - - softwareSystem.uses(softwareSystemA, "uses"); - person1.uses(softwareSystem, "uses"); - - view.addAllSoftwareSystems(); - view.addAllPeople(); - assertEquals(5, view.getElements().size()); - - view.removeElementsWithNoRelationships(); - assertEquals(3, view.getElements().size()); - } - - @Test - public void test_copyLayoutInformationFrom() { - Workspace workspace1 = new Workspace("", ""); - Model model1 = workspace1.getModel(); - SoftwareSystem softwareSystem1A = model1.addSoftwareSystem("System A", "Description"); - SoftwareSystem softwareSystem1B = model1.addSoftwareSystem("System B", "Description"); - Person person1 = model1.addPerson("Person", "Description"); - Relationship personUsesSoftwareSystem1 = person1.uses(softwareSystem1A, "Uses"); - - // create a view with SystemA and Person (locations are set for both, relationship has vertices) - StaticView view1 = new SystemContextView(softwareSystem1A, "context", "Description"); - view1.add(softwareSystem1B); - view1.getElementView(softwareSystem1B).setX(123); - view1.getElementView(softwareSystem1B).setY(321); - view1.add(person1); - view1.getElementView(person1).setX(456); - view1.getElementView(person1).setY(654); - view1.getRelationshipView(personUsesSoftwareSystem1).setVertices(Arrays.asList(new Vertex(123, 456))); - - Workspace workspace2 = new Workspace("", ""); - Model model2 = workspace2.getModel(); - // creating these in the opposite order will cause them to get different internal IDs - SoftwareSystem softwareSystem2B = model2.addSoftwareSystem("System B", "Description"); - SoftwareSystem softwareSystem2A = model2.addSoftwareSystem("System A", "Description"); - Person person2 = model2.addPerson("Person", "Description"); - Relationship personUsesSoftwareSystem2 = person2.uses(softwareSystem2A, "Uses"); - - // create a view with SystemB and Person (locations are 0,0 for both) - StaticView view2 = new SystemContextView(softwareSystem2A, "context", "Description"); - view2.add(softwareSystem2B); - view2.add(person2); - assertEquals(0, view2.getElementView(softwareSystem2B).getX()); - assertEquals(0, view2.getElementView(softwareSystem2B).getY()); - assertEquals(0, view2.getElementView(softwareSystem2B).getX()); - assertEquals(0, view2.getElementView(softwareSystem2B).getY()); - assertEquals(0, view2.getElementView(person2).getX()); - assertEquals(0, view2.getElementView(person2).getY()); - assertTrue(view2.getRelationshipView(personUsesSoftwareSystem2).getVertices().isEmpty()); - - view2.copyLayoutInformationFrom(view1); - assertEquals(0, view2.getElementView(softwareSystem2A).getX()); - assertEquals(0, view2.getElementView(softwareSystem2A).getY()); - assertEquals(123, view2.getElementView(softwareSystem2B).getX()); - assertEquals(321, view2.getElementView(softwareSystem2B).getY()); - assertEquals(456, view2.getElementView(person2).getX()); - assertEquals(654, view2.getElementView(person2).getY()); - Vertex vertex = view2.getRelationshipView(personUsesSoftwareSystem2).getVertices().iterator().next(); - assertEquals(123, vertex.getX()); - assertEquals(456, vertex.getY()); - } - - @Test - public 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_removeElementsThatCantBeReachedFrom_DoesNothing_WhenANullElementIsSpecified() { - SoftwareSystem softwareSystem = model.addSoftwareSystem(Location.Internal, "The System", "Description"); - StaticView view = new SystemContextView(softwareSystem, "context", "Description"); - view.removeElementsThatCantBeReachedFrom(null); - } - - @Test - public void test_removeElementsThatCantBeReachedFrom_DoesNothing_WhenAllElementsCanBeReached() { - SoftwareSystem softwareSystem = model.addSoftwareSystem("The System", "Description"); - SoftwareSystem softwareSystemA = model.addSoftwareSystem("System A", ""); - SoftwareSystem softwareSystemB = model.addSoftwareSystem("System B", ""); - - softwareSystem.uses(softwareSystemA, "uses"); - softwareSystemA.uses(softwareSystemB, "uses"); - - StaticView view = new SystemContextView(softwareSystem, "context", "Description"); - view.addAllElements(); - assertEquals(3, view.getElements().size()); - - view.removeElementsThatCantBeReachedFrom(softwareSystem); - assertEquals(3, view.getElements().size()); - } - - @Test - public void test_removeElementsThatCantBeReachedFrom_RemovesOrphanedElements_WhenThereAreSomeOrphanedElements() { - SoftwareSystem softwareSystem = model.addSoftwareSystem("The System", "Description"); - SoftwareSystem softwareSystemA = model.addSoftwareSystem("System A", ""); - SoftwareSystem softwareSystemB = model.addSoftwareSystem("System B", ""); - SoftwareSystem softwareSystemC = model.addSoftwareSystem("System C", ""); - - softwareSystem.uses(softwareSystemA, "uses"); - softwareSystemA.uses(softwareSystemB, "uses"); - - StaticView view = new SystemContextView(softwareSystem, "context", "Description"); - view.addAllElements(); - assertEquals(4, view.getElements().size()); - - view.removeElementsThatCantBeReachedFrom(softwareSystem); - assertEquals(3, view.getElements().size()); - assertFalse(view.getElements().contains(new ElementView(softwareSystemC))); - } - - @Test - public void test_removeElementsThatCantBeReachedFrom_RemovesUnreachableElements_WhenThereAreSomeUnreachableElements() { - SoftwareSystem softwareSystem = model.addSoftwareSystem("The System", "Description"); - SoftwareSystem softwareSystemA = model.addSoftwareSystem("System A", ""); - SoftwareSystem softwareSystemB = model.addSoftwareSystem("System B", ""); - - softwareSystem.uses(softwareSystemA, "uses"); - softwareSystemA.uses(softwareSystemB, "uses"); - - StaticView view = new SystemContextView(softwareSystem, "context", "Description"); - view.addAllElements(); - assertEquals(3, view.getElements().size()); - - view.removeElementsThatCantBeReachedFrom(softwareSystemA); - assertEquals(2, view.getElements().size()); - assertFalse(view.getElements().contains(new ElementView(softwareSystem))); - } - - @Test - public void test_removeElementsThatCantBeReachedFrom_DoesntIncludeAllElements_WhenThereIsACyclicGraph() { - SoftwareSystem softwareSystem1 = model.addSoftwareSystem("Software System 1", "Description"); - SoftwareSystem softwareSystem2 = model.addSoftwareSystem("Software System 2", "Description"); - Person user = model.addPerson("User", ""); - - user.uses(softwareSystem1, ""); - user.uses(softwareSystem2, ""); - softwareSystem1.delivers(user, ""); - - StaticView view = new SystemContextView(softwareSystem1, "context", "Description"); - view.addAllElements(); - assertEquals(3, view.getElements().size()); - - // this should remove software system 2 - view.removeElementsThatCantBeReachedFrom(softwareSystem1); - assertEquals(2, view.getElements().size()); - assertTrue(view.getElements().contains(new ElementView(softwareSystem1))); - assertTrue(view.getElements().contains(new ElementView(user))); - } - - @Test - public 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"); - - softwareSystem1.uses(softwareSystem2, "Uses"); - softwareSystem2.uses(softwareSystem3, "Uses"); - softwareSystem3.uses(softwareSystem1, "Uses"); - - StaticView view = new SystemContextView(softwareSystem1, "context", "Description"); - view.addAllElements(); - - assertEquals(3, view.getRelationships().size()); - view.remove((Relationship)null); - } - - @Test - public 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"); - - Relationship relationship12 = softwareSystem1.uses(softwareSystem2, "Uses"); - Relationship relationship23 = softwareSystem2.uses(softwareSystem3, "Uses"); - Relationship relationship31 = softwareSystem3.uses(softwareSystem1, "Uses"); - - StaticView view = new SystemContextView(softwareSystem1, "context", "Description"); - view.addAllElements(); - - assertEquals(3, view.getRelationships().size()); - view.remove(relationship31); - - assertEquals(2, view.getRelationships().size()); - assertTrue(view.getRelationships().contains(new RelationshipView(relationship12))); - 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(expected = IllegalArgumentException.class) - public void test_setKey_ThrowsAnException_WhenAnEmptyKeyIsSpecified() { - SoftwareSystem softwareSystem = model.addSoftwareSystem("Name", "Description"); - new SystemContextView(softwareSystem, " ", "Description"); - } - -} diff --git a/structurizr-core/test/unit/test/DefaultTypeRepository/SomeAbstractClass.java b/structurizr-core/test/unit/test/DefaultTypeRepository/SomeAbstractClass.java deleted file mode 100644 index 91f01fc4f..000000000 --- a/structurizr-core/test/unit/test/DefaultTypeRepository/SomeAbstractClass.java +++ /dev/null @@ -1,4 +0,0 @@ -package test.DefaultTypeRepository; - -abstract class SomeAbstractClass implements SomeInterface { -} diff --git a/structurizr-core/test/unit/test/DefaultTypeRepository/SomeClass.java b/structurizr-core/test/unit/test/DefaultTypeRepository/SomeClass.java deleted file mode 100644 index c0e3fbe05..000000000 --- a/structurizr-core/test/unit/test/DefaultTypeRepository/SomeClass.java +++ /dev/null @@ -1,10 +0,0 @@ -package test.DefaultTypeRepository; - -import com.structurizr.annotation.Component; - -@Component -class SomeClass extends SomeAbstractClass { - - private SomeEnum someEnum; - -} diff --git a/structurizr-core/test/unit/test/DefaultTypeRepository/SomeEnum.java b/structurizr-core/test/unit/test/DefaultTypeRepository/SomeEnum.java deleted file mode 100644 index ddb081dd1..000000000 --- a/structurizr-core/test/unit/test/DefaultTypeRepository/SomeEnum.java +++ /dev/null @@ -1,4 +0,0 @@ -package test.DefaultTypeRepository; - -public enum SomeEnum { -} diff --git a/structurizr-core/test/unit/test/DefaultTypeRepository/SomeInterface.java b/structurizr-core/test/unit/test/DefaultTypeRepository/SomeInterface.java deleted file mode 100644 index 46651dfe3..000000000 --- a/structurizr-core/test/unit/test/DefaultTypeRepository/SomeInterface.java +++ /dev/null @@ -1,4 +0,0 @@ -package test.DefaultTypeRepository; - -public interface SomeInterface { -} diff --git a/structurizr-core/test/unit/test/SourceCodeComponentFinderStrategy/SomeComponent.java b/structurizr-core/test/unit/test/SourceCodeComponentFinderStrategy/SomeComponent.java deleted file mode 100644 index 72d727f81..000000000 --- a/structurizr-core/test/unit/test/SourceCodeComponentFinderStrategy/SomeComponent.java +++ /dev/null @@ -1,10 +0,0 @@ -package test.SourceCodeComponentFinderStrategy; - -/** - * A component that does something. - */ -public interface SomeComponent { - - void doSomething(); - -} \ No newline at end of file diff --git a/structurizr-core/test/unit/test/SourceCodeComponentFinderStrategy/SomeComponentImpl.java b/structurizr-core/test/unit/test/SourceCodeComponentFinderStrategy/SomeComponentImpl.java deleted file mode 100644 index 764a78133..000000000 --- a/structurizr-core/test/unit/test/SourceCodeComponentFinderStrategy/SomeComponentImpl.java +++ /dev/null @@ -1,10 +0,0 @@ -package test.SourceCodeComponentFinderStrategy; - -class SomeComponentImpl implements SomeComponent { - - @Override - public void doSomething() { - System.out.println("Doing something"); - } - -} \ No newline at end of file diff --git a/structurizr-core/test/unit/test/StructurizrAnnotationsComponentFinderStrategy/Controller.java b/structurizr-core/test/unit/test/StructurizrAnnotationsComponentFinderStrategy/Controller.java deleted file mode 100644 index ae153d3f5..000000000 --- a/structurizr-core/test/unit/test/StructurizrAnnotationsComponentFinderStrategy/Controller.java +++ /dev/null @@ -1,18 +0,0 @@ -package test.StructurizrAnnotationsComponentFinderStrategy; - -import com.structurizr.annotation.*; - -@Component(description = "Does something.") -@UsedByPerson(name = "Anonymous User", description = "Uses to do something", technology = "HTTPS") -@UsedByPerson(name = "Authenticated User", description = "Uses to do something too") -@UsedBySoftwareSystem(name = "External 1", description = "Uses to do something", technology = "HTTPS") -@UsedBySoftwareSystem(name = "External 2", description = "Uses to do something too") -@UsedByContainer(name = "Software System/Web Browser", description = "Makes calls to", technology = "HTTPS") // an example of using a canonical name -@UsedByContainer(name = "API Client", description = "Makes API calls to", technology = "HTTPS") // an example of not using a canonical name -@UsesSoftwareSystem(name = "External 1", description = "Sends information to", technology = "HTTPS") -public class Controller { - - @UsesComponent(description = "Reads from and writes to", technology = "Just a method call") - protected Repository repository; - -} diff --git a/structurizr-core/test/unit/test/StructurizrAnnotationsComponentFinderStrategy/Repository.java b/structurizr-core/test/unit/test/StructurizrAnnotationsComponentFinderStrategy/Repository.java deleted file mode 100644 index ac90151a5..000000000 --- a/structurizr-core/test/unit/test/StructurizrAnnotationsComponentFinderStrategy/Repository.java +++ /dev/null @@ -1,10 +0,0 @@ -package test.StructurizrAnnotationsComponentFinderStrategy; - -import com.structurizr.annotation.Component; - -@Component(description = "Manages some data.") -public interface Repository { - - void getData(); - -} \ No newline at end of file diff --git a/structurizr-core/test/unit/test/StructurizrAnnotationsComponentFinderStrategy/RepositoryImpl.java b/structurizr-core/test/unit/test/StructurizrAnnotationsComponentFinderStrategy/RepositoryImpl.java deleted file mode 100644 index 92bfce223..000000000 --- a/structurizr-core/test/unit/test/StructurizrAnnotationsComponentFinderStrategy/RepositoryImpl.java +++ /dev/null @@ -1,12 +0,0 @@ -package test.StructurizrAnnotationsComponentFinderStrategy; - -import com.structurizr.annotation.UsesContainer; - -@UsesContainer(name = "Database", description = "Reads from and writes to", technology = "JDBC") -class RepositoryImpl implements Repository { - - @Override - public void getData() { - } - -} \ No newline at end of file diff --git a/structurizr-core/test/unit/test/TypeMatcherComponentFinderStrategy/MyController.java b/structurizr-core/test/unit/test/TypeMatcherComponentFinderStrategy/MyController.java deleted file mode 100644 index 8a9ce1ae6..000000000 --- a/structurizr-core/test/unit/test/TypeMatcherComponentFinderStrategy/MyController.java +++ /dev/null @@ -1,7 +0,0 @@ -package test.TypeMatcherComponentFinderStrategy; - -public class MyController { - - private MyRepository myRepository = new MyRepositoryImpl(); - -} diff --git a/structurizr-core/test/unit/test/TypeMatcherComponentFinderStrategy/MyRepository.java b/structurizr-core/test/unit/test/TypeMatcherComponentFinderStrategy/MyRepository.java deleted file mode 100644 index f3a29c5b1..000000000 --- a/structurizr-core/test/unit/test/TypeMatcherComponentFinderStrategy/MyRepository.java +++ /dev/null @@ -1,4 +0,0 @@ -package test.TypeMatcherComponentFinderStrategy; - -public interface MyRepository { -} diff --git a/structurizr-core/test/unit/test/TypeMatcherComponentFinderStrategy/MyRepositoryImpl.java b/structurizr-core/test/unit/test/TypeMatcherComponentFinderStrategy/MyRepositoryImpl.java deleted file mode 100644 index d95dfb5fb..000000000 --- a/structurizr-core/test/unit/test/TypeMatcherComponentFinderStrategy/MyRepositoryImpl.java +++ /dev/null @@ -1,4 +0,0 @@ -package test.TypeMatcherComponentFinderStrategy; - -public class MyRepositoryImpl implements MyRepository { -} diff --git a/structurizr-core/test/unit/test/TypeUtils/AnotherClass.java b/structurizr-core/test/unit/test/TypeUtils/AnotherClass.java deleted file mode 100644 index 5df64fe1a..000000000 --- a/structurizr-core/test/unit/test/TypeUtils/AnotherClass.java +++ /dev/null @@ -1,4 +0,0 @@ -package test.TypeUtils; - -public class AnotherClass { -} diff --git a/structurizr-core/test/unit/test/TypeUtils/SomeAbstractClass.java b/structurizr-core/test/unit/test/TypeUtils/SomeAbstractClass.java deleted file mode 100644 index 83ae95523..000000000 --- a/structurizr-core/test/unit/test/TypeUtils/SomeAbstractClass.java +++ /dev/null @@ -1,4 +0,0 @@ -package test.TypeUtils; - -abstract class SomeAbstractClass implements SomeInterface { -} diff --git a/structurizr-core/test/unit/test/TypeUtils/SomeClass.java b/structurizr-core/test/unit/test/TypeUtils/SomeClass.java deleted file mode 100644 index 4344ba4fd..000000000 --- a/structurizr-core/test/unit/test/TypeUtils/SomeClass.java +++ /dev/null @@ -1,10 +0,0 @@ -package test.TypeUtils; - -import com.structurizr.annotation.Component; - -@Component -class SomeClass extends SomeAbstractClass { - - private SomeEnum someEnum; - -} diff --git a/structurizr-core/test/unit/test/TypeUtils/SomeEnum.java b/structurizr-core/test/unit/test/TypeUtils/SomeEnum.java deleted file mode 100644 index 00b590946..000000000 --- a/structurizr-core/test/unit/test/TypeUtils/SomeEnum.java +++ /dev/null @@ -1,4 +0,0 @@ -package test.TypeUtils; - -public enum SomeEnum { -} diff --git a/structurizr-core/test/unit/test/TypeUtils/SomeInterface.java b/structurizr-core/test/unit/test/TypeUtils/SomeInterface.java deleted file mode 100644 index 834aaea50..000000000 --- a/structurizr-core/test/unit/test/TypeUtils/SomeInterface.java +++ /dev/null @@ -1,4 +0,0 @@ -package test.TypeUtils; - -public interface SomeInterface { -} diff --git a/structurizr-dot/build.gradle b/structurizr-dot/build.gradle deleted file mode 100644 index 1d828ea6d..000000000 --- a/structurizr-dot/build.gradle +++ /dev/null @@ -1,6 +0,0 @@ -dependencies { - - compile project(':structurizr-core') - compile 'io.github.livingdocumentation:dot-diagram:1.1' - -} \ No newline at end of file diff --git a/structurizr-dot/etc/graphviz-dot.properties b/structurizr-dot/etc/graphviz-dot.properties deleted file mode 100644 index cb9d6f6d5..000000000 --- a/structurizr-dot/etc/graphviz-dot.properties +++ /dev/null @@ -1,13 +0,0 @@ -# The path where to output generated documentation, with ending slash -outpath= - -# The path to the dot tool (Graphviz), with ending slash -dotpath=/usr/local/bin/ - -# The image extension, with the leading dot -imageextension=.png - - -# $0: outpath -# $1: outpath+filename -commandline={0}dot -Tpng {1}.dot -o {1}.png -Gdpi=72 -Gsize="6,8.5" \ No newline at end of file diff --git a/structurizr-dot/src/com/structurizr/io/dot/DotWriter.java b/structurizr-dot/src/com/structurizr/io/dot/DotWriter.java deleted file mode 100644 index 949666c9d..000000000 --- a/structurizr-dot/src/com/structurizr/io/dot/DotWriter.java +++ /dev/null @@ -1,67 +0,0 @@ -package com.structurizr.io.dot; - -import com.structurizr.Workspace; -import com.structurizr.io.WorkspaceWriter; -import com.structurizr.model.Element; -import com.structurizr.model.Relationship; -import com.structurizr.view.ElementView; -import com.structurizr.view.RelationshipView; -import com.structurizr.view.View; -import io.github.livingdocumentation.dotdiagram.DotGraph; - -import java.io.IOException; -import java.io.Writer; - -/** - * This is a simple implementation of a workspace writer that outputs - * to the Graphviz DOT format. You will need graphviz installed and - * correctly configured. See https://github.com/cyriux/dot-diagram - * for more information. - */ -public class DotWriter implements WorkspaceWriter { - - @Override - public void write(Workspace workspace, Writer writer) { - workspace.getViews().getSystemContextViews().forEach(v -> write(v, null, writer)); - workspace.getViews().getContainerViews().forEach(v -> write(v, v.getSoftwareSystem(), writer)); - workspace.getViews().getComponentViews().forEach(v -> write(v, v.getContainer(), writer)); - } - - private void write(View view, Element clusterElement, Writer writer) { - try { - DotGraph graph = new DotGraph(view.getName()); - DotGraph.Digraph digraph = graph.getDigraph(); - DotGraph.Cluster cluster = null; - - if (clusterElement != null) { - cluster = digraph.addCluster(clusterElement.getId()); - cluster.setLabel(clusterElement.getName()); - } - - for (ElementView elementView : view.getElements()) { - Element element = elementView.getElement(); - - if (clusterElement != null && element.getParent() == clusterElement) { - cluster.addNode(element.getId()).setLabel(element.getName()); - } else { - digraph.addNode(element.getId()).setLabel(element.getName()); - } - } - - for (RelationshipView relationshipView : view.getRelationships()) { - Relationship relationship = relationshipView.getRelationship(); - digraph.addAssociation( - relationship.getSourceId(), - relationship.getDestinationId()).setLabel(relationship.getDescription()); - } - - String output = graph.render().trim(); - writer.write(output); - writer.write(System.lineSeparator()); - writer.write(System.lineSeparator()); - } catch (IOException ioe) { - ioe.printStackTrace(); - } - } - -} \ No newline at end of file diff --git a/structurizr-dot/src/com/structurizr/io/dot/DotWriterExample.java b/structurizr-dot/src/com/structurizr/io/dot/DotWriterExample.java deleted file mode 100644 index c66d400a0..000000000 --- a/structurizr-dot/src/com/structurizr/io/dot/DotWriterExample.java +++ /dev/null @@ -1,37 +0,0 @@ -package com.structurizr.io.dot; - -import com.structurizr.Workspace; -import com.structurizr.model.Model; -import com.structurizr.model.Person; -import com.structurizr.model.SoftwareSystem; -import com.structurizr.view.SystemContextView; -import com.structurizr.view.ViewSet; - -import java.io.StringWriter; - -/** - * Demonstrates the export to DOT. Paste graphs into https://stamm-wilbrandt.de/GraphvizFiddle/ - * to visualise them. - */ -public class DotWriterExample { - - public static void main(String[] args) { - 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(); - - StringWriter stringWriter = new StringWriter(); - DotWriter dotWriter = new DotWriter(); - dotWriter.write(workspace, stringWriter); - System.out.println(stringWriter); - } - -} diff --git a/structurizr-dsl/README.md b/structurizr-dsl/README.md new file mode 100644 index 000000000..2108a6503 --- /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 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) +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..11cd9dd1a --- /dev/null +++ b/structurizr-dsl/build.gradle @@ -0,0 +1,14 @@ +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.25' + testImplementation 'org.jetbrains.kotlin:kotlin-scripting-jsr223:1.9.25' + testImplementation 'org.jruby:jruby-core:9.4.12.0' + +} + +description = 'Structurizr DSL' \ 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..d45b3a2f6 --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/AbstractParser.java @@ -0,0 +1,4 @@ +package com.structurizr.dsl; + +abstract class AbstractParser { +} \ 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..556ac6a7e --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/AbstractRelationshipParser.java @@ -0,0 +1,49 @@ +package com.structurizr.dsl; + +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) { + 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; + } + + 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/AbstractViewParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/AbstractViewParser.java new file mode 100644 index 000000000..c47bfabe8 --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/AbstractViewParser.java @@ -0,0 +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/main/java/com/structurizr/dsl/Archetype.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/Archetype.java new file mode 100644 index 000000000..71a92fa0a --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/Archetype.java @@ -0,0 +1,155 @@ +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, PerspectivesHolder { + + private final String name; + private final String type; + private String metadata = ""; + private String description = ""; + private String technology = ""; + private final Set tags = new LinkedHashSet<>(); + + private final Map properties = new HashMap<>(); + private final Set perspectives = new TreeSet<>(); + + 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 getMetadata() { + return metadata; + } + + void setMetadata(String metadata) { + this.metadata = metadata; + } + + 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); + } + + /** + * 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) + */ + public Map getProperties() { + return Collections.unmodifiableMap(properties); + } + + /** + * Adds a name-value pair property to this archetype. + * + * @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); + } + + /** + * 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/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..1b5c00668 --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ArchetypeParser.java @@ -0,0 +1,76 @@ +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 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)) { + 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/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..add3a4712 --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/BrandingParser.java @@ -0,0 +1,87 @@ +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; + +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) { + // 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/")) { + 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 (context.getFeatures().isEnabled(Features.FILE_SYSTEM)) { + 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); + } catch (Exception e) { + throw new RuntimeException(e); + } + } else { + throw new RuntimeException(path + " does not exist"); + } + } else { + throw new FeatureNotEnabledException(Features.FILE_SYSTEM, "!branding is not permitted"); + } + } + } 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/ComponentArchetypeDslContext.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/ComponentArchetypeDslContext.java new file mode 100644 index 000000000..f81a925cb --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ComponentArchetypeDslContext.java @@ -0,0 +1,21 @@ +package com.structurizr.dsl; + +final class ComponentArchetypeDslContext extends ElementArchetypeDslContext { + + ComponentArchetypeDslContext(Archetype archetype) { + super(archetype); + } + + @Override + protected String[] getPermittedTokens() { + return new String[] { + StructurizrDslTokens.DESCRIPTION_TOKEN, + StructurizrDslTokens.TECHNOLOGY_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/ComponentDslContext.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/ComponentDslContext.java new file mode 100644 index 000000000..f207e3ebe --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ComponentDslContext.java @@ -0,0 +1,45 @@ +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.DOCS_TOKEN, + StructurizrDslTokens.DECISIONS_TOKEN, + StructurizrDslTokens.DESCRIPTION_TOKEN, + StructurizrDslTokens.TECHNOLOGY_TOKEN, + StructurizrDslTokens.TAGS_TOKEN, + StructurizrDslTokens.URL_TOKEN, + StructurizrDslTokens.PROPERTIES_TOKEN, + StructurizrDslTokens.PERSPECTIVES_TOKEN, + StructurizrDslTokens.GROUP_TOKEN, + StructurizrDslTokens.RELATIONSHIP_TOKEN + }; + } + +} \ 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 new file mode 100644 index 000000000..60eaf4a32 --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ComponentFinderDslContext.java @@ -0,0 +1,47 @@ +package com.structurizr.dsl; + +import com.structurizr.component.ComponentFinderBuilder; +import com.structurizr.model.Component; + +import java.util.Set; + +final class ComponentFinderDslContext extends DslContext { + + private final ComponentFinderBuilder componentFinderBuilder = new ComponentFinderBuilder(); + + private final StructurizrDslParser dslParser; + private final ContainerDslContext containerDslContext; + + ComponentFinderDslContext(StructurizrDslParser dslParser, ContainerDslContext containerDslContext) { + this.dslParser = dslParser; + this.containerDslContext = containerDslContext; + componentFinderBuilder.forContainer(containerDslContext.getContainer()); + setDslPortable(false); + } + + @Override + protected String[] getPermittedTokens() { + return new String[] { + StructurizrDslTokens.COMPONENT_FINDER_CLASSES_TOKEN, + StructurizrDslTokens.COMPONENT_FINDER_SOURCE_TOKEN, + StructurizrDslTokens.COMPONENT_FINDER_STRATEGY_TOKEN + }; + } + + ContainerDslContext getContainerDslContext() { + return containerDslContext; + } + + ComponentFinderBuilder getComponentFinderBuilder() { + return this.componentFinderBuilder; + } + + @Override + void end() { + Set components = componentFinderBuilder.build().run(); + 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/ComponentFinderParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/ComponentFinderParser.java new file mode 100644 index 000000000..f0931bc5a --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ComponentFinderParser.java @@ -0,0 +1,66 @@ +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 + + 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)); + } + + 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/ComponentFinderStrategyDslContext.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/ComponentFinderStrategyDslContext.java new file mode 100644 index 000000000..805de90b3 --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ComponentFinderStrategyDslContext.java @@ -0,0 +1,39 @@ +package com.structurizr.dsl; + +import com.structurizr.component.ComponentFinderStrategyBuilder; + +final class ComponentFinderStrategyDslContext extends DslContext { + + private final ComponentFinderDslContext componentFinderDslContext; + private final ComponentFinderStrategyBuilder componentFinderStrategyBuilder = new ComponentFinderStrategyBuilder(); + + ComponentFinderStrategyDslContext(ComponentFinderDslContext componentFinderDslContext) { + this.componentFinderDslContext = componentFinderDslContext; + } + + @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, + StructurizrDslTokens.COMPONENT_FINDER_STRATEGY_NAME_TOKEN, + StructurizrDslTokens.COMPONENT_FINDER_STRATEGY_FOREACH_TOKEN + }; + } + + ComponentFinderStrategyBuilder getComponentFinderStrategyBuilder() { + return this.componentFinderStrategyBuilder; + } + + ComponentFinderDslContext getComponentFinderDslContext() { + return this.componentFinderDslContext; + } + + @Override + void end() { + 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 new file mode 100644 index 000000000..fe765bf44 --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ComponentFinderStrategyForEachDslContext.java @@ -0,0 +1,35 @@ +package com.structurizr.dsl; + +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 { + 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); + } + }); + } + + void addLine(String line) { + this.dslLines.add(line); + } + + @Override + protected String[] getPermittedTokens() { + return new ComponentDslContext(null).getPermittedTokens(); + } + +} \ 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..9c73a68b9 --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ComponentFinderStrategyParser.java @@ -0,0 +1,284 @@ +package com.structurizr.dsl; + +import com.structurizr.component.description.FirstSentenceDescriptionStrategy; +import com.structurizr.component.description.TruncatedDescriptionStrategy; +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; +import com.structurizr.component.naming.FullyQualifiedNamingStrategy; +import com.structurizr.component.supporting.*; +import com.structurizr.component.url.PrefixSourceUrlStrategy; + +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_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 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_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_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"; + 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 "; + + 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) { + throw new RuntimeException("Expected: " + TECHNOLOGY_GRAMMAR); + } + + String name = tokens.get(1); + context.getComponentFinderStrategyBuilder().withTechnology(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); + switch (type.toLowerCase()) { + case MATCHER_ANNOTATION: + if (tokens.size() == 3) { + String name = tokens.get(2); + + context.getComponentFinderStrategyBuilder().matchedBy(new AnnotationTypeMatcher(name)); + } else { + throw new RuntimeException("Expected: " + MATCHER_ANNOTATION_GRAMMAR); + } + break; + case MATCHER_EXTENDS: + if (tokens.size() == 3) { + String name = tokens.get(2); + + context.getComponentFinderStrategyBuilder().matchedBy(new ExtendsTypeMatcher(name)); + } else { + throw new RuntimeException("Expected: " + MATCHER_EXTENDS_GRAMMAR); + } + break; + case MATCHER_IMPLEMENTS: + if (tokens.size() == 3) { + String name = tokens.get(2); + + context.getComponentFinderStrategyBuilder().matchedBy(new ImplementsTypeMatcher(name)); + } else { + throw new RuntimeException("Expected: " + MATCHER_IMPLEMENTS_GRAMMAR); + } + break; + case MATCHER_NAME_SUFFIX: + if (tokens.size() == 3) { + String suffix = tokens.get(2); + + context.getComponentFinderStrategyBuilder().matchedBy(new NameSuffixTypeMatcher(suffix)); + } else { + throw new RuntimeException("Expected: " + MATCHER_NAMESUFFIX_GRAMMAR); + } + break; + case MATCHER_FQN_REGEX: + if (tokens.size() == 3) { + String regex = tokens.get(2); + + context.getComponentFinderStrategyBuilder().matchedBy(new RegexTypeMatcher(regex)); + } else { + throw new RuntimeException("Expected: " + MATCHER_REGEX_GRAMMAR); + } + break; + default: + try { + Class typeMatcherClass = context.loadClass(type, dslFile); + + 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()); + } + } + } + + void parseFilter(ComponentFinderStrategyDslContext context, Tokens tokens, File dslFile) { + 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.getComponentFinderStrategyBuilder().filteredBy(new IncludeFullyQualifiedNameRegexFilter(regex)); + } else { + context.getComponentFinderStrategyBuilder().filteredBy(new ExcludeFullyQualifiedNameRegexFilter(regex)); + } + } else { + throw new RuntimeException("Expected: " + FILTER_GRAMMAR); + } + break; + default: + throw new IllegalArgumentException("Unknown filter: " + type); + } + } + + void parseSupportingTypes(ComponentFinderStrategyDslContext context, Tokens tokens, File dslFile) { + if (tokens.size() < 2) { + throw new RuntimeException("Too few tokens, expected: " + SUPPORTING_TYPES_GRAMMAR); + } + + String type = tokens.get(1).toLowerCase(); + switch (type) { + case SUPPORTING_TYPES_ALL_REFERENCED: + context.getComponentFinderStrategyBuilder().supportedBy(new AllReferencedTypesSupportingTypesStrategy()); + break; + case SUPPORTING_TYPES_REFERENCED_IN_PACKAGE: + context.getComponentFinderStrategyBuilder().supportedBy(new AllReferencedTypesInPackageSupportingTypesStrategy()); + break; + case SUPPORTING_TYPES_IN_PACKAGE: + context.getComponentFinderStrategyBuilder().supportedBy(new AllTypesInPackageSupportingTypesStrategy()); + break; + 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; + default: + throw new IllegalArgumentException("Unknown supporting types strategy: " + type); + } + } + + void parseName(ComponentFinderStrategyDslContext context, Tokens tokens, File dslFile) { + if (tokens.size() < 2) { + throw new RuntimeException("Too few tokens, expected: " + NAME_GRAMMAR); + } + + String type = tokens.get(1).toLowerCase(); + switch (type) { + case NAME_TYPE_NAME: + context.getComponentFinderStrategyBuilder().withName(new TypeNamingStrategy()); + break; + case NAME_FQN: + context.getComponentFinderStrategyBuilder().withName(new FullyQualifiedNamingStrategy()); + break; + 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 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 description strategy: " + type); + } + } + + 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_SRC: + if (tokens.size() < 3) { + throw new RuntimeException("Too few tokens, expected: " + URL_PREFIX_SRC_GRAMMAR); + } + + String prefix = tokens.get(2); + context.getComponentFinderStrategyBuilder().withUrl(new PrefixSourceUrlStrategy(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/ComponentParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/ComponentParser.java new file mode 100644 index 000000000..cb3f88098 --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ComponentParser.java @@ -0,0 +1,87 @@ +package com.structurizr.dsl; + +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]"; + + 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, Archetype archetype) { + // 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); + } + + String description = archetype.getDescription(); + if (tokens.includes(DESCRIPTION_INDEX)) { + description = tokens.get(DESCRIPTION_INDEX); + } + component.setDescription(description); + + String technology = archetype.getTechnology(); + if (tokens.includes(TECHNOLOGY_INDEX)) { + technology = tokens.get(TECHNOLOGY_INDEX); + } + component.setTechnology(technology); + + List tags = new ArrayList<>(archetype.getTags()); + if (tokens.includes(TAGS_INDEX)) { + tags.addAll(Arrays.asList(tokens.get(TAGS_INDEX).split(","))); + } + component.addTags(tags.toArray(new String[0])); + + component.addProperties(archetype.getProperties()); + component.addPerspectives(archetype.getPerspectives()); + + 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..ef90c6963 --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ComponentViewParser.java @@ -0,0 +1,57 @@ +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 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); + validateViewKey(key); + } + + if (tokens.includes(DESCRIPTION_INDEX)) { + description = tokens.get(DESCRIPTION_INDEX); + } + + ComponentView view = workspace.getViews().createComponentView(container, key, description); + + 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/ContainerArchetypeDslContext.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/ContainerArchetypeDslContext.java new file mode 100644 index 000000000..676f1d548 --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ContainerArchetypeDslContext.java @@ -0,0 +1,21 @@ +package com.structurizr.dsl; + +final class ContainerArchetypeDslContext extends ElementArchetypeDslContext { + + ContainerArchetypeDslContext(Archetype archetype) { + super(archetype); + } + + @Override + protected String[] getPermittedTokens() { + return new String[] { + StructurizrDslTokens.DESCRIPTION_TOKEN, + StructurizrDslTokens.TECHNOLOGY_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/ContainerDslContext.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/ContainerDslContext.java new file mode 100644 index 000000000..6a30bf792 --- /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.DECISIONS_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..a8f1bc8bd --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ContainerInstanceDslContext.java @@ -0,0 +1,48 @@ +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 final ContainerInstance containerInstance; + + ContainerInstanceDslContext(ContainerInstance containerInstance) { + this.containerInstance = containerInstance; + } + + ContainerInstance getContainerInstance() { + return containerInstance; + } + + @Override + ModelItem getModelItem() { + return getContainerInstance(); + } + + @Override + Element getElement() { + 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..04e52c35e --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ContainerInstanceParser.java @@ -0,0 +1,59 @@ +package com.structurizr.dsl; + +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 StaticStructureInstanceParser { + + 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)) { + deploymentGroups = getDeploymentGroups(context, tokens.get(DEPLOYMENT_GROUPS_TOKEN)); + } + + 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..33e81e553 --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ContainerParser.java @@ -0,0 +1,87 @@ +package com.structurizr.dsl; + +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]"; + + 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, Archetype archetype) { + // 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); + } + + String description = archetype.getDescription(); + if (tokens.includes(DESCRIPTION_INDEX)) { + description = tokens.get(DESCRIPTION_INDEX); + } + container.setDescription(description); + + String technology = archetype.getTechnology(); + if (tokens.includes(TECHNOLOGY_INDEX)) { + technology = tokens.get(TECHNOLOGY_INDEX); + } + container.setTechnology(technology); + + List tags = new ArrayList<>(archetype.getTags()); + if (tokens.includes(TAGS_INDEX)) { + tags.addAll(Arrays.asList(tokens.get(TAGS_INDEX).split(","))); + } + container.addTags(tags.toArray(new String[0])); + + container.addProperties(archetype.getProperties()); + container.addPerspectives(archetype.getPerspectives()); + + 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..48de8e65c --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ContainerViewParser.java @@ -0,0 +1,57 @@ +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 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); + validateViewKey(key); + } + + if (tokens.includes(DESCRIPTION_INDEX)) { + description = tokens.get(DESCRIPTION_INDEX); + } + + ContainerView view = workspace.getViews().createContainerView(softwareSystem, key, description); + + return view; + } + +} \ No newline at end of file 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/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..06d9e6e65 --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/CustomElementParser.java @@ -0,0 +1,58 @@ +package com.structurizr.dsl; + +import com.structurizr.model.CustomElement; +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]"; + + 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(ModelDslContext context, Tokens tokens, Archetype archetype) { + // 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 = archetype.getMetadata(); + if (tokens.includes(METADATA_INDEX)) { + metadata = tokens.get(METADATA_INDEX); + } + + String description = archetype.getDescription(); + if (tokens.includes(DESCRIPTION_INDEX)) { + description = tokens.get(DESCRIPTION_INDEX); + } + + CustomElement customElement = context.getWorkspace().getModel().addCustomElement(name, metadata, description); + + List tags = new ArrayList<>(archetype.getTags()); + if (tokens.includes(TAGS_INDEX)) { + tags.addAll(Arrays.asList(tokens.get(TAGS_INDEX).split(","))); + } + customElement.addTags(tags.toArray(new String[0])); + + 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..e0e32202b --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/CustomViewAnimationStepParser.java @@ -0,0 +1,70 @@ +package com.structurizr.dsl; + +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|element expression...] + + if (!tokens.includes(1)) { + throw new RuntimeException("Expected: animationStep " + GRAMMAR); + } + + parse(context, context.getCustomView(), tokens, 1); + } + + void parse(CustomViewAnimationDslContext context, Tokens tokens) { + // [identifier|element expression...] + + if (!tokens.includes(0)) { + throw new RuntimeException("Expected: " + GRAMMAR); + } + + parse(context, context.getView(), tokens, 0); + } + + void parse(DslContext context, CustomView view, Tokens tokens, int startIndex) { + List customElements = new ArrayList<>(); + + for (int i = startIndex; i < tokens.size(); i++) { + String token = tokens.get(i); + + 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 (elements.isEmpty()) { + throw new RuntimeException("The element \"" + token + "\" does not exist"); + } + + for (ModelItem element : elements) { + if (element instanceof CustomElement) { + customElements.add((CustomElement)element); + } + } + } + } + + 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/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..16e5be56c --- /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 ExpressionParser { + + @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..bd7ffdcb3 --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/CustomViewParser.java @@ -0,0 +1,44 @@ +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 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); + 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/DecisionsParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/DecisionsParser.java new file mode 100644 index 000000000..cf380e068 --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/DecisionsParser.java @@ -0,0 +1,95 @@ +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; +import java.util.HashMap; +import java.util.Map; + +final class DecisionsParser extends AbstractParser { + + private static final Map DECISION_IMPORTERS = new HashMap<>(); + + 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"); + } + + private static final String GRAMMAR = "!decisions "; + + private static final int PATH_INDEX = 1; + private static final int TYPE_OR_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 + + context.setDslPortable(false); + + if (tokens.hasMoreThan(TYPE_OR_FQN_INDEX)) { + throw new RuntimeException("Too many tokens, expected: " + GRAMMAR); + } + + if (!tokens.includes(PATH_INDEX)) { + throw new RuntimeException("Expected: " + GRAMMAR); + } + + 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) { + 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(TYPE_OR_FQN_INDEX)) { + DefaultImageImporter imageImporter = new DefaultImageImporter(); + imageImporter.importDocumentation(documentable, path); + } + } catch (ClassNotFoundException cnfe) { + throw new RuntimeException("Error importing decisions from " + path.getAbsolutePath() + ": " + fullyQualifiedClassName + " was not found"); + } catch (Exception e) { + throw new RuntimeException("Error importing decisions from " + path.getAbsolutePath() + ": " + e.getMessage()); + } + } + } + +} \ 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..ff54ccfe8 --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/DeploymentEnvironmentDslContext.java @@ -0,0 +1,42 @@ +package com.structurizr.dsl; + +class DeploymentEnvironmentDslContext extends DslContext implements GroupableDslContext { + + private final DeploymentEnvironment environment; + private final ElementGroup group; + + DeploymentEnvironmentDslContext(String environment) { + this.environment = new DeploymentEnvironment(environment); + this.group = null; + } + + DeploymentEnvironmentDslContext(String environment, ElementGroup group) { + this.environment = new DeploymentEnvironment(environment); + this.group = group; + } + + DeploymentEnvironment getEnvironment() { + return environment; + } + + @Override + public boolean hasGroup() { + return group != null; + } + + @Override + public ElementGroup getGroup() { + return group; + } + + @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..a89e9fb49 --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/DeploymentGroup.java @@ -0,0 +1,37 @@ +package com.structurizr.dsl; + +import com.structurizr.model.Element; + +import java.util.Set; + +class DeploymentGroup extends Element { + + private final Element parent; + private final String name; + + DeploymentGroup(DeploymentEnvironment deploymentEnvironment, String name) { + this.parent = deploymentEnvironment; + this.name = name; + } + + @Override + public String getName() { + return name; + } + + @Override + public String getCanonicalName() { + return name; + } + + @Override + public Element getParent() { + return parent; + } + + @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/DeploymentNodeArchetypeDslContext.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/DeploymentNodeArchetypeDslContext.java new file mode 100644 index 000000000..0a5b3dc4a --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/DeploymentNodeArchetypeDslContext.java @@ -0,0 +1,21 @@ +package com.structurizr.dsl; + +final class DeploymentNodeArchetypeDslContext extends ElementArchetypeDslContext { + + DeploymentNodeArchetypeDslContext(Archetype archetype) { + super(archetype); + } + + @Override + protected String[] getPermittedTokens() { + return new String[] { + StructurizrDslTokens.DESCRIPTION_TOKEN, + StructurizrDslTokens.TECHNOLOGY_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/DeploymentNodeDslContext.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/DeploymentNodeDslContext.java new file mode 100644 index 000000000..a347f75c7 --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/DeploymentNodeDslContext.java @@ -0,0 +1,55 @@ +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.INSTANCE_OF_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..a6aa7d859 --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/DeploymentNodeParser.java @@ -0,0 +1,119 @@ +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 { + + 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, Archetype archetype) { + 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)) { + 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); + } + + if (deploymentEnvironmentDslContext != null) { + // add a root deployment node + deploymentNode = deploymentEnvironmentDslContext.getWorkspace().getModel().addDeploymentNode(deploymentEnvironmentDslContext.getEnvironment().getName(), name, description, technology); + + 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.addAll(Arrays.asList(tokens.get(TAGS_INDEX).split(","))); + } + deploymentNode.addTags(tags.toArray(new String[0])); + + deploymentNode.addProperties(archetype.getProperties()); + deploymentNode.addPerspectives(archetype.getPerspectives()); + + String instances = "1"; + if (tokens.includes(INSTANCES_INDEX)) { + instances = tokens.get(INSTANCES_INDEX); + deploymentNode.setInstances(instances); + } + + 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..c85e23c89 --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/DeploymentViewAnimationStepParser.java @@ -0,0 +1,79 @@ +package com.structurizr.dsl; + +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|element expression...] + + if (!tokens.includes(1)) { + throw new RuntimeException("Expected: animationStep " + GRAMMAR); + } + + parse(context, context.getView(), tokens, 1); + } + + void parse(DeploymentViewAnimationDslContext context, Tokens tokens) { + // [identifier|element expression...] + + if (!tokens.includes(0)) { + throw new RuntimeException("Expected: " + GRAMMAR); + } + + parse(context, context.getView(), tokens, 0); + } + + 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 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"); + } + } + +} \ 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..755f50e14 --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/DeploymentViewContentParser.java @@ -0,0 +1,153 @@ +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|expression> [*|identifier|expression...]"); + } + + DeploymentView view = context.getView(); + + 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|expression...]"); + } + + DeploymentView view = context.getView(); + + + 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..ea6266d86 --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/DeploymentViewExpressionParser.java @@ -0,0 +1,89 @@ +package com.structurizr.dsl; + +import com.structurizr.model.*; + +import java.util.LinkedHashSet; +import java.util.Set; +import java.util.stream.Collectors; + +import static com.structurizr.dsl.StructurizrDslExpressions.ELEMENT_TYPE_EQUALS_EXPRESSION; + +final class DeploymentViewExpressionParser extends ExpressionParser { + + @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; + } + + @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<>(); + + 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..32e493a71 --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/DeploymentViewParser.java @@ -0,0 +1,79 @@ +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 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); + 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); + 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..8cb4e6d51 --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/DocsParser.java @@ -0,0 +1,78 @@ +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 + + context.setDslPortable(false); + + 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"); + } + + 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) && path.isDirectory()) { + 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..3c31f3e76 --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/DslContext.java @@ -0,0 +1,166 @@ +package com.structurizr.dsl; + +import com.structurizr.Workspace; +import com.structurizr.http.HttpClient; +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; + private boolean dslPortable = true; + + protected IdentifiersRegister identifiersRegister = new IdentifiersRegister(); + + private Features features = new Features(); + private HttpClient httpClient = new HttpClient(); + + Workspace getWorkspace() { + return workspace; + } + + void setWorkspace(Workspace workspace) { + this.workspace = workspace; + } + + boolean isExtendingWorkspace() { + return extendingWorkspace; + } + + 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; + } + + String findIdentifier(Element element) { + return identifiersRegister.findIdentifier(element); + } + + 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) { + 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(); + } + } + + if (elementDslContext != null) { + Element parent = elementDslContext.getElement(); + 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 = 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()); + } + + 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]; + + 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 (Class) 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..b39952fa5 --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/DslLine.java @@ -0,0 +1,29 @@ +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 processedSource, int lineNumber) { + this.source = processedSource; + this.lineNumber = lineNumber; + } + + String getSource() { + return source; + } + + 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/DslParserContext.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/DslParserContext.java new file mode 100644 index 000000000..8713345de --- /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 final StructurizrDslParser parser; + private final File file; + + DslParserContext(StructurizrDslParser parser, File file) { + this.parser = parser; + this.file = file; + } + + StructurizrDslParser getParser() { + return parser; + } + + File getFile() { + return file; + } + + 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..33662ac87 --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/DslUtils.java @@ -0,0 +1,54 @@ +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 { + + 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. + * + * @param workspace a Workspace object + * @return a DSL string + */ + public static String getDsl(Workspace workspace) { + String base64 = workspace.getProperties().get(STRUCTURIZR_DSL_PROPERTY_NAME); + if (!StringUtils.isNullOrEmpty(base64)) { + return new String(Base64.getDecoder().decode(base64)); + } + + return ""; + } + + /** + * 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) { + if (!StringUtils.isNullOrEmpty(dsl)) { + String base64 = Base64.getEncoder().encodeToString(dsl.getBytes(StandardCharsets.UTF_8)); + workspace.addProperty(STRUCTURIZR_DSL_PROPERTY_NAME, base64); + } + } + + /** + * 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/DynamicViewContentParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/DynamicViewContentParser.java new file mode 100644 index 000000000..1843c148e --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/DynamicViewContentParser.java @@ -0,0 +1,120 @@ +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.util.StringUtils; +import com.structurizr.view.DynamicView; +import com.structurizr.view.RelationshipView; + +final class DynamicViewContentParser extends AbstractParser { + + 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; + 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(); + 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] + 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) { + relationshipView = view.add((StaticStructureElement) sourceElement, description, technology, (StaticStructureElement) destinationElement); + } else if (sourceElement instanceof StaticStructureElement && destinationElement instanceof CustomElement) { + relationshipView = view.add((StaticStructureElement) sourceElement, description, technology, (CustomElement) destinationElement); + } else if (sourceElement instanceof CustomElement && destinationElement instanceof StaticStructureElement) { + relationshipView = view.add((CustomElement) sourceElement, description, technology, (StaticStructureElement) destinationElement); + } else if (sourceElement instanceof CustomElement && destinationElement instanceof CustomElement) { + relationshipView = view.add((CustomElement) sourceElement, description, technology, (CustomElement) destinationElement); + } + } else { + // [order] [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); + } + + 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"); + } + +} \ 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..16ec26fcf --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/DynamicViewParser.java @@ -0,0 +1,76 @@ +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; + +class DynamicViewParser extends AbstractViewParser { + + private static final String GRAMMAR = "dynamic <*|software system identifier|container identifier> [key] [description] {"; + + 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); + 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); + validateViewKey(key); + } + + view = workspace.getViews().createDynamicView((SoftwareSystem)element, key, description); + } else if (element instanceof Container) { + if (tokens.includes(KEY_INDEX)) { + key = tokens.get(KEY_INDEX); + validateViewKey(key); + } + + view = workspace.getViews().createDynamicView((Container)element, key, description); + } else { + throw new RuntimeException("The element \"" + scopeIdentifier + "\" is not a software system or container"); + } + } + + 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/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/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 new file mode 100644 index 000000000..7c5895cef --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ElementGroup.java @@ -0,0 +1,64 @@ +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 Element parent; + private final ElementGroup parentGroup; + private final String name; + + private final Set elements = new HashSet<>(); + + ElementGroup(String name) { + this.name = name; + this.parentGroup = null; + } + + ElementGroup(String name, String groupSeparator, ElementGroup parentGroup) { + 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..f99834d07 --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ElementStyleDslContext.java @@ -0,0 +1,47 @@ +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_ICON_POSITION_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..4fe23bde3 --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ElementStyleParser.java @@ -0,0 +1,367 @@ +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; +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(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)) { + 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, context.getColorScheme()); + if (elementStyle == null) { + elementStyle = workspace.getViews().getConfiguration().getStyles().addElementStyle(tag, context.getColorScheme()); + } + + 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) { + 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/")) { + 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 (context.getFeatures().isEnabled(Features.FILE_SYSTEM)) { + File file = new File(context.getFile().getParent(), path); + if (file.exists() && !file.isDirectory()) { + try { + style.setIcon(ImageUtils.getImageAsDataUri(file)); + context.setDslPortable(false); + } catch (IOException e) { + throw new RuntimeException(e); + } + } else { + throw new RuntimeException(path + " does not exist"); + } + } else { + throw new FeatureNotEnabledException(Features.FILE_SYSTEM, "!icon is not permitted"); + } + } + } else { + throw new RuntimeException("Expected: icon "); + } + } + + 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/ElementsDslContext.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/ElementsDslContext.java new file mode 100644 index 000000000..3ef8706e3 --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ElementsDslContext.java @@ -0,0 +1,42 @@ +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[] { + StructurizrDslTokens.RELATIONSHIP_TOKEN, + StructurizrDslTokens.DESCRIPTION_TOKEN, + StructurizrDslTokens.TECHNOLOGY_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/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/ExplicitRelationshipParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/ExplicitRelationshipParser.java new file mode 100644 index 000000000..2158d596d --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ExplicitRelationshipParser.java @@ -0,0 +1,194 @@ +package com.structurizr.dsl; + +import com.structurizr.model.*; + +import javax.lang.model.util.Elements; +import java.util.*; + +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; + + 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); + } + + if (!tokens.includes(DESTINATION_IDENTIFIER_INDEX)) { + throw new RuntimeException("Expected: " + GRAMMAR); + } + + 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"); + } + + 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()); + if (tokens.includes(TAGS_INDEX)) { + tags.addAll(Arrays.asList(tokens.get(TAGS_INDEX).split(","))); + } + + 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.addAll(findSoftwareSystemInstances((SoftwareSystem)sourceElement, deploymentEnvironment)); + } else if (sourceElement instanceof Container) { + // find the container instances in the deployment environment + sourceElements.addAll(findContainerInstances((Container)sourceElement, deploymentEnvironment)); + } else { + sourceElements.add(sourceElement); + } + + if (destinationElement instanceof SoftwareSystem) { + // find the software system instances in the deployment environment + destinationElements.addAll(findSoftwareSystemInstances((SoftwareSystem)destinationElement, deploymentEnvironment)); + } else if (destinationElement instanceof Container) { + // find the container instances in the deployment environment + destinationElements.addAll(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()); + + relationships.add(relationship); + } + + return relationships; + } + + Set parse(ElementsDslContext context, Tokens tokens, Archetype archetype) { + // -> [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"); + } + + 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); + } + + String[] tags = archetype.getTags().toArray(new String[0]); + if (tokens.includes(TAGS_INDEX)) { + tags = tokens.get(TAGS_INDEX).split(","); + } + + Set relationships = new LinkedHashSet<>(); + for (Element sourceElement : sourceElements) { + for (Element destinationElement : destinationElements) { + Relationship relationship = createRelationship(sourceElement, description, technology, tags, destinationElement); + relationship.addProperties(archetype.getProperties()); + relationship.addPerspectives(archetype.getPerspectives()); + + relationships.add(relationship); + } + } + + return relationships; + } + + 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/ExpressionParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/ExpressionParser.java new file mode 100644 index 000000000..31c39eb98 --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ExpressionParser.java @@ -0,0 +1,463 @@ +package com.structurizr.dsl; + +import com.structurizr.model.*; +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.*; + +class ExpressionParser { + + private static final String WILDCARD = "*"; + + 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(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); + } + + + 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 modelItems = new HashSet<>(modelItems1); + modelItems.addAll(modelItems2); + + return modelItems; + } 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(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()); + + 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.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); + + 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 -> { + 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.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<>(); + + 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); + } + }); + } else { + // fallback that the expression is an identifier + Set elements = getElements(expr, context); + if (!elements.isEmpty()) { + modelItems.addAll(elements); + } + } + + return modelItems; + } + + 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; + + 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; + } + + 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 Set findAfferentCouplings(Element element) { + return new LinkedHashSet<>(findAfferentCouplings(element, Element.class)); + } + + 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 Set findEfferentCouplings(Element element) { + return new LinkedHashSet<>(findEfferentCouplings(element, Element.class)); + } + + 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; + } + } + + protected 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/ExternalScriptDslContext.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/ExternalScriptDslContext.java new file mode 100644 index 000000000..b8e8c4070 --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ExternalScriptDslContext.java @@ -0,0 +1,35 @@ +package com.structurizr.dsl; + +import java.io.File; + +class ExternalScriptDslContext extends ScriptDslContext { + + private final String filename; + + ExternalScriptDslContext(DslContext parentContext, File dslFile, StructurizrDslParser dslParser, String filename) { + super(parentContext, dslFile, dslParser); + + 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"); + } + + run(this, scriptFile); + } 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/Features.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/Features.java new file mode 100644 index 000000000..4e96d07ac --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/Features.java @@ -0,0 +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/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..8f03f9b4c --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/FilteredViewParser.java @@ -0,0 +1,84 @@ +package com.structurizr.dsl; + +import com.structurizr.Workspace; +import com.structurizr.util.StringUtils; +import com.structurizr.view.*; + +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 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 = ""; + + View 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"); + } + + baseView = workspace.getViews().getViewWithKey(baseKey); + if (baseView == null) { + throw new RuntimeException("The view \"" + baseKey + "\" does not exist"); + } + + if (tokens.includes(KEY_INDEX)) { + key = tokens.get(KEY_INDEX); + validateViewKey(key); + } + + 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/main/java/com/structurizr/dsl/FindElementParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/FindElementParser.java new file mode 100644 index 000000000..a6737a585 --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/FindElementParser.java @@ -0,0 +1,47 @@ +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); + } + + Element element; + + String s = tokens.get(IDENTIFIER_INDEX); + 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/FindElementsParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/FindElementsParser.java new file mode 100644 index 000000000..47dc0f748 --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/FindElementsParser.java @@ -0,0 +1,34 @@ +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 FindElementsParser 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); + + 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/FindRelationshipParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/FindRelationshipParser.java new file mode 100644 index 000000000..9643e57b8 --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/FindRelationshipParser.java @@ -0,0 +1,39 @@ +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 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); + } + + Relationship relationship; + + 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"); + } + + return relationship; + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/FindRelationshipsParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/FindRelationshipsParser.java new file mode 100644 index 000000000..2bc4c7d31 --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/FindRelationshipsParser.java @@ -0,0 +1,34 @@ +package com.structurizr.dsl; + +import com.structurizr.model.ModelItem; +import com.structurizr.model.Relationship; + +import java.util.Set; +import java.util.stream.Collectors; + +final class FindRelationshipsParser 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); + + 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/GroupParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/GroupParser.java new file mode 100644 index 000000000..251500fb6 --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/GroupParser.java @@ -0,0 +1,76 @@ +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_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 parseContext(GroupableDslContext dslContext, Tokens tokens) { + // group { + + if (tokens.hasMoreThan(BRACE_INDEX)) { + throw new RuntimeException("Too many tokens, expected: " + GRAMMAR_AS_CONTEXT); + } + + if (!tokens.includes(BRACE_INDEX)) { + throw new RuntimeException("Expected: " + GRAMMAR_AS_CONTEXT); + } + + if (!DslContext.CONTEXT_START_TOKEN.equalsIgnoreCase(tokens.get(BRACE_INDEX))) { + throw new RuntimeException("Expected: " + GRAMMAR_AS_CONTEXT); + } + + ElementGroup group; + if (dslContext.hasGroup()) { + 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(tokens.get(NAME_INDEX)); + } + + 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/GroupableDslContext.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/GroupableDslContext.java new file mode 100644 index 000000000..594ac14b6 --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/GroupableDslContext.java @@ -0,0 +1,9 @@ +package com.structurizr.dsl; + +interface GroupableDslContext { + + boolean hasGroup(); + + 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 new file mode 100644 index 000000000..bc6b337d1 --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/GroupableElementDslContext.java @@ -0,0 +1,29 @@ +package com.structurizr.dsl; + +import com.structurizr.model.GroupableElement; + +abstract class GroupableElementDslContext extends ElementDslContext implements GroupableDslContext { + + private final ElementGroup group; + + GroupableElementDslContext() { + this.group = null; + } + + GroupableElementDslContext(ElementGroup group) { + this.group = group; + } + + @Override + public boolean hasGroup() { + return group != null; + } + + @Override + public ElementGroup getGroup() { + return 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/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..3ef9f1a37 --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/IdentifiersRegister.java @@ -0,0 +1,250 @@ +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) { + for (String key : elementsByIdentifier.keySet()) { + if (key.equalsIgnoreCase(identifier)) { + return elementsByIdentifier.get(key); + } + } + + return null; + } + + /** + * 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(); + } + + 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.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"); + } else { + throw new RuntimeException("The element is already registered with an identifier of \"" + id + "\""); + } + } + } + + Element e = getElement(identifier); + Relationship r = getRelationship(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) { + for (String key : relationshipsByIdentifier.keySet()) { + if (key.equalsIgnoreCase(identifier)) { + return relationshipsByIdentifier.get(key); + } + } + + return null; + } + + /** + * 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(); + } + + // 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 = getElement(identifier); + Relationship r = getRelationship(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_-"); + } + } + + 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/ImageViewContentParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/ImageViewContentParser.java new file mode 100644 index 000000000..e78d72ba3 --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ImageViewContentParser.java @@ -0,0 +1,258 @@ +package com.structurizr.dsl; + +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; +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 KROKI_GRAMMAR = "kroki "; + 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; + + ImageViewContentParser() { + } + + 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); + } + + String source = tokens.get(PLANTUML_SOURCE_INDEX); + ColorScheme colorScheme = context.getColorScheme(); + + try { + if (source.contains("\n")) { + // inline source + 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(context.getHttpClient()).importDiagram(context.getView(), plantumlLight, ColorScheme.Light); + + String plantumlDark = new StructurizrPlantUMLExporter(ColorScheme.Dark).export((ModelView) viewWithKey).getDefinition(); + new PlantUMLImporter(context.getHttpClient()).importDiagram(context.getView(), plantumlDark, ColorScheme.Dark); + + if (!StringUtils.isNullOrEmpty(viewWithKey.getTitle())) { + context.getView().setTitle(viewWithKey.getTitle()); + } else { + context.getView().setTitle(viewWithKey.getName()); + } + context.getView().setDescription(viewWithKey.getDescription()); + } else { + 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 (context.getFeatures().isEnabled(Features.FILE_SYSTEM)) { + File file = new File(dslFile.getParentFile(), source); + if (file.exists()) { + context.setDslPortable(false); + new PlantUMLImporter(context.getHttpClient()).importDiagram(context.getView(), file, colorScheme); + } else { + throw new RuntimeException("The file at " + file.getAbsolutePath() + " does not exist"); + } + } else { + throw new FeatureNotEnabledException(Features.FILE_SYSTEM, "plantuml is not permitted"); + } + } + } + } + } 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); + } + + String source = tokens.get(MERMAID_SOURCE_INDEX); + ColorScheme colorScheme = context.getColorScheme(); + + try { + if (source.contains("\n")) { + // inline source + 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(context.getHttpClient()).importDiagram(context.getView(), mermaid, colorScheme); + + if (!StringUtils.isNullOrEmpty(viewWithKey.getTitle())) { + context.getView().setTitle(viewWithKey.getTitle()); + } else { + context.getView().setTitle(viewWithKey.getName()); + } + context.getView().setDescription(viewWithKey.getDescription()); + } else { + 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 (context.getFeatures().isEnabled(Features.FILE_SYSTEM)) { + File file = new File(dslFile.getParentFile(), source); + if (file.exists()) { + context.setDslPortable(false); + new MermaidImporter(context.getHttpClient()).importDiagram(context.getView(), file, colorScheme); + } else { + throw new RuntimeException("The file at " + file.getAbsolutePath() + " does not exist"); + } + } else { + throw new FeatureNotEnabledException(Features.FILE_SYSTEM, "mermaid is not permitted"); + } + } + } + } + } 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); + } + + 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(context.getHttpClient()).importDiagram(context.getView(), format, source, colorScheme); + } else { + 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 (context.getFeatures().isEnabled(Features.FILE_SYSTEM)) { + File file = new File(dslFile.getParentFile(), source); + if (file.exists()) { + context.setDslPortable(false); + 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 FeatureNotEnabledException(Features.FILE_SYSTEM, "kroki " + format + " is not permitted"); + } + } + } + } 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); + } + + String source = tokens.get(IMAGE_SOURCE_INDEX); + ColorScheme colorScheme = context.getColorScheme(); + + try { + 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 (context.getFeatures().isEnabled(Features.FILE_SYSTEM)) { + File file = new File(dslFile.getParentFile(), source); + if (file.exists()) { + context.setDslPortable(false); + new ImageImporter(context.getHttpClient()).importDiagram(context.getView(), file, colorScheme); + } else { + throw new RuntimeException("The file at " + file.getAbsolutePath() + " does not exist"); + } + } else { + throw new FeatureNotEnabledException(Features.FILE_SYSTEM, "image is not permitted"); + } + } + } catch (Exception e) { + throw new RuntimeException(e.getMessage()); + } + } + +} \ 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..b33c4cbe7 --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ImageViewDslContext.java @@ -0,0 +1,54 @@ +package com.structurizr.dsl; + +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); + } + + ImageView getView() { + return (ImageView)super.getView(); + } + + ColorScheme getColorScheme() { + return colorScheme; + } + + void setColorScheme(ColorScheme colorScheme) { + this.colorScheme = colorScheme; + } + + @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(); + + 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/ImageViewParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/ImageViewParser.java new file mode 100644 index 000000000..aa2c1a817 --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ImageViewParser.java @@ -0,0 +1,52 @@ +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 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); + 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..6d761096a --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ImplicitRelationshipParser.java @@ -0,0 +1,140 @@ +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.*; + +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; + + 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); + } + + if (!tokens.includes(DESTINATION_IDENTIFIER_INDEX)) { + throw new RuntimeException("Expected: " + GRAMMAR); + } + + String destinationId = tokens.get(DESTINATION_IDENTIFIER_INDEX); + + Element sourceElement = context.getElement(); + + Element destinationElement = context.getElement(destinationId); + if (destinationElement == null) { + throw new RuntimeException("The destination element \"" + destinationId + "\" does not exist"); + } + + 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); + } + + List tags = new ArrayList<>(archetype.getTags()); + if (tokens.includes(TAGS_INDEX)) { + tags.addAll(Arrays.asList(tokens.get(TAGS_INDEX).split(","))); + } + + 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.addAll(findSoftwareSystemInstances((SoftwareSystem)destinationElement, deploymentEnvironment)); + } else if (destinationElement instanceof Container) { + // find the container instances in the deployment environment + destinationElements.addAll(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()); + + relationships.add(relationship); + } + + return relationships; + } + + Set parse(ElementsDslContext context, Tokens tokens, Archetype archetype) { + // -> [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 = 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); + } + + String[] tags = archetype.getTags().toArray(new String[0]); + if (tokens.includes(TAGS_INDEX)) { + tags = tokens.get(TAGS_INDEX).split(","); + } + + Set relationships = new LinkedHashSet<>(); + for (Element sourceElement : sourceElements) { + Relationship relationship = createRelationship(sourceElement, description, technology, tags, destinationElement); + relationship.addProperties(archetype.getProperties()); + relationship.addPerspectives(archetype.getPerspectives()); + + relationships.add(relationship); + } + + return relationships; + } + +} \ 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..086fdb9b3 --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ImpliedRelationshipsParser.java @@ -0,0 +1,67 @@ +package com.structurizr.dsl; + +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; +import java.util.Set; + +final class ImpliedRelationshipsParser extends AbstractParser { + + private static final String GRAMMAR = "!impliedRelationships "; + + 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, File dslFile) { + // impliedRelationships + + if (tokens.hasMoreThan(OPTION_INDEX)) { + throw new RuntimeException("Too many tokens, expected: " + GRAMMAR); + } + + if (!tokens.includes(OPTION_INDEX)) { + throw new RuntimeException("Expected: " + GRAMMAR); + } + + String option = tokens.get(OPTION_INDEX); + + if (option.equalsIgnoreCase(FALSE)) { + context.getWorkspace().getModel().setImpliedRelationshipsStrategy(new DefaultImpliedRelationshipsStrategy()); + } else if (option.equalsIgnoreCase(TRUE)) { + context.getWorkspace().getModel().setImpliedRelationshipsStrategy(new CreateImpliedRelationshipsUnlessAnyRelationshipExistsStrategy()); + } else { + if (!context.getFeatures().isEnabled(Features.PLUGINS)) { + if (!BUILT_IN_IMPLIED_RELATIONSHIPS_STRATEGIES.contains(option)) { + throw new FeatureNotEnabledException(Features.PLUGINS, "The implied relationships strategy " + option + " is not available"); + } + } + + if (!BUILT_IN_IMPLIED_RELATIONSHIPS_STRATEGIES.contains(option)) { + context.setDslPortable(false); + } + + 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()); + } + } + } + +} \ 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..d8ac5caef --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/IncludeParser.java @@ -0,0 +1,107 @@ +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; +import java.nio.file.Files; +import java.util.ArrayList; +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; + + 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)) { + 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 (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 (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()); + } + } + } else { + throw new FeatureNotEnabledException(Features.FILE_SYSTEM, "!include is not permitted"); + } + } + + return includedFiles; + } + + private List readFiles(File path) throws IOException { + List includedFiles = new ArrayList<>(); + + if (path.isHidden() || path.getName().startsWith(".")) { + // ignore + return includedFiles; + } + + if (path.isDirectory()) { + File[] files = path.listFiles(); + if (files != null) { + Arrays.sort(files); + + for (File file : files) { + includedFiles.addAll(readFiles(file)); + } + } + } else { + try { + 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/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/InfrastructureNodeArchetypeDslContext.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/InfrastructureNodeArchetypeDslContext.java new file mode 100644 index 000000000..fc6f06ae5 --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/InfrastructureNodeArchetypeDslContext.java @@ -0,0 +1,21 @@ +package com.structurizr.dsl; + +final class InfrastructureNodeArchetypeDslContext extends ElementArchetypeDslContext { + + InfrastructureNodeArchetypeDslContext(Archetype archetype) { + super(archetype); + } + + @Override + protected String[] getPermittedTokens() { + return new String[] { + StructurizrDslTokens.DESCRIPTION_TOKEN, + StructurizrDslTokens.TECHNOLOGY_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/InfrastructureNodeDslContext.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/InfrastructureNodeDslContext.java new file mode 100644 index 000000000..bda1154ee --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/InfrastructureNodeDslContext.java @@ -0,0 +1,42 @@ +package com.structurizr.dsl; + +import com.structurizr.model.GroupableElement; +import com.structurizr.model.InfrastructureNode; +import com.structurizr.model.ModelItem; + +final class InfrastructureNodeDslContext extends GroupableElementDslContext { + + private final InfrastructureNode infrastructureNode; + + InfrastructureNodeDslContext(InfrastructureNode infrastructureNode) { + this.infrastructureNode = infrastructureNode; + } + + InfrastructureNode getInfrastructureNode() { + return infrastructureNode; + } + + @Override + ModelItem getModelItem() { + return getInfrastructureNode(); + } + + @Override + GroupableElement getElement() { + return infrastructureNode; + } + + @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..8d8165a65 --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/InfrastructureNodeParser.java @@ -0,0 +1,79 @@ +package com.structurizr.dsl; + +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]"; + + 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, Archetype archetype) { + // 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 = 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); + } + + infrastructureNode = deploymentNode.addInfrastructureNode(name, description, technology); + + List tags = new ArrayList<>(archetype.getTags()); + if (tokens.includes(TAGS_INDEX)) { + tags.addAll(Arrays.asList(tokens.get(TAGS_INDEX).split(","))); + } + infrastructureNode.addTags(tags.toArray(new String[0])); + + infrastructureNode.addProperties(archetype.getProperties()); + infrastructureNode.addPerspectives(archetype.getPerspectives()); + + 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..adabd3693 --- /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, StructurizrDslParser dslParser, String language) { + super(parentContext, dslFile, dslParser); + + 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/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/ModelDslContext.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/ModelDslContext.java new file mode 100644 index 000000000..56460da9f --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ModelDslContext.java @@ -0,0 +1,38 @@ +package com.structurizr.dsl; + +final class ModelDslContext extends DslContext implements GroupableDslContext { + + private ElementGroup group; + + ModelDslContext() { + } + + ModelDslContext(ElementGroup group) { + this.group = group; + } + + @Override + public boolean hasGroup() { + return group != null; + } + + @Override + public ElementGroup getGroup() { + return group; + } + + @Override + protected String[] getPermittedTokens() { + return new String[] { + StructurizrDslTokens.ARCHETYPES_TOKEN, + StructurizrDslTokens.IDENTIFIERS_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..823702ffc --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ModelItemDslContext.java @@ -0,0 +1,13 @@ +package com.structurizr.dsl; + +import com.structurizr.model.ModelItem; + +abstract class ModelItemDslContext extends DslContext { + + ModelItemDslContext() { + super(); + } + + 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..e4c48ac90 --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ModelItemParser.java @@ -0,0 +1,51 @@ +package com.structurizr.dsl; + +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; + + 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(ElementDslContext 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); + context.getElement().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); + } + +} \ 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..b4bedc9bc --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ModelItemsDslContext.java @@ -0,0 +1,30 @@ +package com.structurizr.dsl; + +import com.structurizr.model.ModelItem; + +import java.util.Set; + +abstract class ModelItemsDslContext extends DslContext { + + private final DslContext parentDslContext; + + public ModelItemsDslContext() { + this.parentDslContext = null; + } + + 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/ModelItemsParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/ModelItemsParser.java new file mode 100644 index 000000000..2c44be60b --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ModelItemsParser.java @@ -0,0 +1,41 @@ +package com.structurizr.dsl; + +import com.structurizr.model.ModelItem; + +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] + 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(",")); + } + } + } + + 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/ModelViewContentParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/ModelViewContentParser.java new file mode 100644 index 000000000..4f16ebcdb --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ModelViewContentParser.java @@ -0,0 +1,20 @@ +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 WILDCARD_RELUCTANT = "*?"; + protected static final String ELEMENT_WILDCARD = "element==*"; + + protected boolean isExpression(String token) { + return ExpressionParser.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/NameValuePair.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/NameValuePair.java new file mode 100644 index 000000000..d415d50e5 --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/NameValuePair.java @@ -0,0 +1,38 @@ +package com.structurizr.dsl; + +class NameValuePair { + + private NameValueType type; + private final String name; + private final String value; + + NameValuePair(String name, String value) { + this.name = name; + this.value = value; + } + + NameValueType getType() { + return type; + } + + void setType(NameValueType type) { + this.type = type; + } + + String getName() { + return name; + } + + String getValue() { + return value; + } + +} + +enum NameValueType { + + Constant, + Variable, + TextBlock + +} \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/NameValueParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/NameValueParser.java new file mode 100644 index 000000000..ad16b1fc7 --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/NameValueParser.java @@ -0,0 +1,49 @@ +package com.structurizr.dsl; + +final class NameValueParser extends AbstractParser { + + private static final String GRAMMAR = "%s "; + + private static final int KEYWORD_INDEX = 0; + private static final int NAME_INDEX = 1; + private static final int VALUE_INDEX = 2; + + private static final String NAME_REGEX = "[a-zA-Z0-9-_.]+"; + + NameValuePair parseConstant(Tokens tokens) { + NameValuePair nvp = parse(tokens); + nvp.setType(NameValueType.Constant); + + return nvp; + } + + NameValuePair parseVariable(Tokens tokens) { + NameValuePair nvp = parse(tokens); + nvp.setType(NameValueType.Variable); + + return nvp; + } + + private NameValuePair parse(Tokens tokens) { + // !const name value + // !var name value + + if (tokens.hasMoreThan(VALUE_INDEX)) { + throw new RuntimeException("Too many tokens, expected: " + String.format(GRAMMAR, tokens.get(KEYWORD_INDEX))); + } + + if (!tokens.includes(VALUE_INDEX)) { + throw new RuntimeException("Expected: " + String.format(GRAMMAR, tokens.get(KEYWORD_INDEX))); + } + + String name = tokens.get(NAME_INDEX); + String value = tokens.get(VALUE_INDEX); + + if (!name.matches(NAME_REGEX)) { + throw new RuntimeException("Constant/variable names must only contain the following characters: a-zA-Z0-9-_."); + } + + return new NameValuePair(name, value); + } + +} \ No newline at end of file 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/PersonArchetypeDslContext.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/PersonArchetypeDslContext.java new file mode 100644 index 000000000..b8eefe9f8 --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/PersonArchetypeDslContext.java @@ -0,0 +1,20 @@ +package com.structurizr.dsl; + +final class PersonArchetypeDslContext extends ElementArchetypeDslContext { + + PersonArchetypeDslContext(Archetype archetype) { + super(archetype); + } + + @Override + protected String[] getPermittedTokens() { + return new String[] { + 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/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..992fbf6ca --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/PersonParser.java @@ -0,0 +1,62 @@ +package com.structurizr.dsl; + +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]"; + + private final static int NAME_INDEX = 1; + private final static int DESCRIPTION_INDEX = 2; + private final static int TAGS_INDEX = 3; + + Person parse(ModelDslContext context, Tokens tokens, Archetype archetype) { + // 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 = archetype.getDescription(); + if (tokens.includes(DESCRIPTION_INDEX)) { + description = tokens.get(DESCRIPTION_INDEX); + } + person.setDescription(description); + + List tags = new ArrayList<>(archetype.getTags()); + if (tokens.includes(TAGS_INDEX)) { + tags.addAll(Arrays.asList(tokens.get(TAGS_INDEX).split(","))); + } + person.addTags(tags.toArray(new String[0])); + + person.addProperties(archetype.getProperties()); + person.addPerspectives(archetype.getPerspectives()); + + 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/PerspectiveParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/PerspectiveParser.java new file mode 100644 index 000000000..6fcc92c1d --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/PerspectiveParser.java @@ -0,0 +1,35 @@ +package com.structurizr.dsl; + +import com.structurizr.PerspectivesHolder; + +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 (PerspectivesHolder perspectivesHolder : context.getPerspectivesHolders()) { + perspectivesHolder.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..fe1338f57 --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/PerspectivesDslContext.java @@ -0,0 +1,30 @@ +package com.structurizr.dsl; + +import com.structurizr.PerspectivesHolder; +import com.structurizr.model.ModelItem; + +import java.util.ArrayList; +import java.util.Collection; + +final class PerspectivesDslContext extends DslContext { + + private final Collection perspectivesHolders = new ArrayList<>(); + + PerspectivesDslContext(PerspectivesHolder perspectivesHolder) { + this.perspectivesHolders.add(perspectivesHolder); + } + + PerspectivesDslContext(Collection perspectivesHolders) { + this.perspectivesHolders.addAll(perspectivesHolders); + } + + Collection getPerspectivesHolders() { + return this.perspectivesHolders; + } + + @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 new file mode 100644 index 000000000..7885db081 --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/PluginDslContext.java @@ -0,0 +1,43 @@ +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; + setDslPortable(false); + } + + 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..6b31503de --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/PropertiesDslContext.java @@ -0,0 +1,29 @@ +package com.structurizr.dsl; + +import com.structurizr.PropertyHolder; + +import java.util.ArrayList; +import java.util.Collection; + +final class PropertiesDslContext extends DslContext { + + private final Collection propertyHolders = new ArrayList<>(); + + public PropertiesDslContext(PropertyHolder propertyHolder) { + this.propertyHolders.add(propertyHolder); + } + + public PropertiesDslContext(Collection propertyHolders) { + this.propertyHolders.addAll(propertyHolders); + } + + Collection getPropertyHolders() { + return this.propertyHolders; + } + + @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..0a1ba6fb5 --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/PropertyParser.java @@ -0,0 +1,29 @@ +package com.structurizr.dsl; + +import com.structurizr.PropertyHolder; + +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); + + 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/RelationshipArchetypeDslContext.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/RelationshipArchetypeDslContext.java new file mode 100644 index 000000000..d25d329ae --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/RelationshipArchetypeDslContext.java @@ -0,0 +1,21 @@ +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, + StructurizrDslTokens.PROPERTIES_TOKEN, + StructurizrDslTokens.PERSPECTIVES_TOKEN + }; + } + +} \ 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..c126aae43 --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/RelationshipStyleParser.java @@ -0,0 +1,260 @@ +package com.structurizr.dsl; + +import com.structurizr.Workspace; +import com.structurizr.util.StringUtils; +import com.structurizr.view.*; + +import java.util.HashMap; +import java.util.Map; + +final class RelationshipStyleParser extends AbstractParser { + + private static final int FIRST_PROPERTY_INDEX = 1; + + RelationshipStyle parseRelationshipStyle(StylesDslContext 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, context.getColorScheme()); + if (relationshipStyle == null) { + relationshipStyle = workspace.getViews().getConfiguration().getStyles().addRelationshipStyle(tag, context.getColorScheme()); + } + + 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 "); + } + } + + void parseJump(RelationshipStyleDslContext context, Tokens tokens) { + // jump + 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/RelationshipsDslContext.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/RelationshipsDslContext.java new file mode 100644 index 000000000..9a830de7f --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/RelationshipsDslContext.java @@ -0,0 +1,38 @@ +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[] { + 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/ScriptDslContext.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/ScriptDslContext.java new file mode 100644 index 000000000..ee25451ec --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ScriptDslContext.java @@ -0,0 +1,101 @@ +package com.structurizr.dsl; + +import com.structurizr.model.Element; +import com.structurizr.model.Relationship; + +import javax.script.*; +import java.io.File; +import java.io.FileReader; +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 StructurizrDslParser dslParser; + + private final Map parameters = new HashMap<>(); + + ScriptDslContext(DslContext parentContext, File dslFile, StructurizrDslParser dslParser) { + this.parentContext = parentContext; + this.dslFile = dslFile; + this.dslParser = dslParser; + setDslPortable(false); + } + + 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(); + populateBindings(bindings, context); + + engine.eval(script.toString(), bindings); + } else { + throw new RuntimeException("Could not load a scripting engine for extension \"" + extension + "\""); + } + } + + 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); + + 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 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/SoftwareSystemArchetypeDslContext.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/SoftwareSystemArchetypeDslContext.java new file mode 100644 index 000000000..0283d5716 --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/SoftwareSystemArchetypeDslContext.java @@ -0,0 +1,20 @@ +package com.structurizr.dsl; + +final class SoftwareSystemArchetypeDslContext extends ElementArchetypeDslContext { + + SoftwareSystemArchetypeDslContext(Archetype archetype) { + super(archetype); + } + + @Override + protected String[] getPermittedTokens() { + return new String[] { + 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/SoftwareSystemDslContext.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/SoftwareSystemDslContext.java new file mode 100644 index 000000000..d86cbdaeb --- /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.DECISIONS_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..607db5485 --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/SoftwareSystemInstanceDslContext.java @@ -0,0 +1,48 @@ +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 final SoftwareSystemInstance softwareSystemInstance; + + SoftwareSystemInstanceDslContext(SoftwareSystemInstance softwareSystemInstance) { + this.softwareSystemInstance = softwareSystemInstance; + } + + SoftwareSystemInstance getSoftwareSystemInstance() { + return softwareSystemInstance; + } + + @Override + ModelItem getModelItem() { + return getSoftwareSystemInstance(); + } + + @Override + Element getElement() { + 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..88f5da70d --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/SoftwareSystemInstanceParser.java @@ -0,0 +1,60 @@ +package com.structurizr.dsl; + +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 StaticStructureInstanceParser { + + 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)) { + deploymentGroups = getDeploymentGroups(context, tokens.get(DEPLOYMENT_GROUPS_TOKEN)); + } + + 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..ffae2403e --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/SoftwareSystemParser.java @@ -0,0 +1,62 @@ +package com.structurizr.dsl; + +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]"; + + private final static int NAME_INDEX = 1; + private final static int DESCRIPTION_INDEX = 2; + private final static int TAGS_INDEX = 3; + + SoftwareSystem parse(ModelDslContext context, Tokens tokens, Archetype archetype) { + // 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 = archetype.getDescription(); + if (tokens.includes(DESCRIPTION_INDEX)) { + description = tokens.get(DESCRIPTION_INDEX); + } + softwareSystem.setDescription(description); + + List tags = new ArrayList<>(archetype.getTags()); + if (tokens.includes(TAGS_INDEX)) { + tags.addAll(Arrays.asList(tokens.get(TAGS_INDEX).split(","))); + } + softwareSystem.addTags(tags.toArray(new String[0])); + + softwareSystem.addProperties(archetype.getProperties()); + softwareSystem.addPerspectives(archetype.getPerspectives()); + + 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..7066961c3 --- /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 ElementDslContext { + + abstract StaticStructureElementInstance getElementInstance(); + +} \ No newline at end of file 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/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..9ce90cfee --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/StaticViewAnimationStepParser.java @@ -0,0 +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|element expression...] + + if (!tokens.includes(1)) { + throw new RuntimeException("Expected: animationStep " + GRAMMAR); + } + + parse(context, context.getView(), tokens, 1); + } + + void parse(StaticViewAnimationDslContext context, Tokens tokens) { + // [identifier|element expression...] + + if (!tokens.includes(0)) { + throw new RuntimeException("Expected: " + GRAMMAR); + } + + parse(context, context.getView(), tokens, 0); + } + + private void parse(DslContext context, StaticView view, Tokens tokens, int startIndex) { + List staticStructureElements = new ArrayList<>(); + + for (int i = startIndex; i < tokens.size(); i++) { + 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); + } + } + } + } + + 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/main/java/com/structurizr/dsl/StaticViewContentParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/StaticViewContentParser.java new file mode 100644 index 000000000..a6c653d0b --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/StaticViewContentParser.java @@ -0,0 +1,121 @@ +package com.structurizr.dsl; + +import com.structurizr.model.*; +import com.structurizr.util.StringUtils; +import com.structurizr.view.*; + +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)) { + 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(); + + // 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(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 { + 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..dfa32f147 --- /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 ExpressionParser { + + @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..ba9ae26fb --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslExpressions.java @@ -0,0 +1,27 @@ +package com.structurizr.dsl; + +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\\[.*]==.*"; + + 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=="; + 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=="; + + 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..faf98ce36 --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java @@ -0,0 +1,1612 @@ +package com.structurizr.dsl; + +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; +import org.apache.commons.logging.LogFactory; + +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 Log log = LogFactory.getLog(StructurizrDslParser.class); + + 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 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"; + + private Charset characterEncoding = StandardCharsets.UTF_8; + private IdentifierScope identifierScope = IdentifierScope.Flat; + private final Stack contextStack; + private final Set parsedTokens = new HashSet<>(); + private final IdentifiersRegister identifiersRegister; + private Map constantsAndVariables; + private Features features = new Features(); + private HttpClient httpClient = new HttpClient(); + + 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<>(), + StructurizrDslTokens.COMPONENT_TOKEN, new HashMap<>(), + StructurizrDslTokens.DEPLOYMENT_NODE_TOKEN, new HashMap<>(), + StructurizrDslTokens.INFRASTRUCTURE_NODE_TOKEN, new HashMap<>(), + StructurizrDslTokens.RELATIONSHIP_TOKEN, new HashMap<>() + ); + + private boolean dslPortable = true; + private final List dslSourceLines = new ArrayList<>(); + private Workspace workspace; + private boolean extendingWorkspace = false; + + /** + * Creates a new instance of the parser. + */ + 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) { + setIdentifierScope(parser.getIdentifierScope()); + archetypes = parser.archetypes; + constantsAndVariables = parser.constantsAndVariables; + } + + /** + * 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; + } + + private 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 + */ + @Deprecated + public void setRestricted(boolean 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); + } + + /** + * Gets the workspace that has been created by parsing the Structurizr DSL. + * + * @return a Workspace instance + */ + public Workspace getWorkspace() { + if (workspace != null) { + if (dslPortable) { + String value = workspace.getProperties().get(DslUtils.STRUCTURIZR_DSL_RETAIN_SOURCE_PROPERTY_NAME); + if (value == null || value.equalsIgnoreCase("true")) { + DslUtils.setDsl(workspace, getParsedDsl()); + } + } + } + + return workspace; + } + + private String getParsedDsl() { + return String.join(System.lineSeparator(), dslSourceLines); + } + + void parse(DslParserContext context, File path) throws StructurizrDslParserException { + parse(path); + + context.copyFrom(identifiersRegister); + } + + /** + * Parses the specified Structurizr DSL file. + * + * @param dslFile a File object representing a DSL file + * @throws StructurizrDslParserException when something goes wrong + */ + public void parse(File dslFile) throws StructurizrDslParserException { + if (dslFile == null) { + throw new StructurizrDslParserException("A file must be specified"); + } + + if (!dslFile.exists()) { + throw new StructurizrDslParserException("The file at " + dslFile.getAbsolutePath() + " does not exist"); + } + + try { + parse(Files.readAllLines(dslFile.toPath(), characterEncoding), dslFile, false, true); + } 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, adding the parsed content to the workspace. + * + * @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, dslFile, false, true); + } + + void parse(List lines, DslContext dslContext) throws StructurizrDslParserException { + startContext(dslContext); + parse(lines, null, true, false); + endContext(); + } + + /** + * 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, boolean fragment, boolean includeInDslSourceLines) throws StructurizrDslParserException { + if (includeInDslSourceLines) { + dslSourceLines.addAll(lines); + } + + List dslLines = preProcessLines(lines); + + for (DslLine dslLine : dslLines) { + 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)) { + 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); + } + + parse(paddedLines, includedFile.getFile(), true, false); + } + + } else if (PLUGIN_TOKEN.equalsIgnoreCase(firstToken)) { + if (features.isEnabled(Features.PLUGINS)) { + 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 { + 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 (features.isEnabled(Features.SCRIPTS)) { + ScriptParser scriptParser = new ScriptParser(); + if (scriptParser.isInlineScript(tokens)) { + String language = scriptParser.parseInline(tokens.withoutContextStartToken()); + startContext(new InlineScriptDslContext(getContext(), dslFile, this, language)); + } else { + String filename = scriptParser.parseExternal(tokens.withoutContextStartToken()); + startContext(new ExternalScriptDslContext(getContext(), dslFile, this, filename)); + + if (shouldStartContext(tokens)) { + // we'll wait for parameters before executing the script + } else { + endContext(); + } + } + } else { + throw new FeatureNotEnabledException(Features.SCRIPTS, firstToken + " is not permitted"); + } + + } 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 + Archetype archetype = getArchetype(RELATIONSHIP_TOKEN, tokens.get(1)); + Set relationships = new ExplicitRelationshipParser().parse(getContext(), tokens.withoutContextStartToken(), archetype); + + if (relationships.size() == 1) { + Relationship relationship = relationships.iterator().next(); + 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(0)); + Set relationships = new ImplicitRelationshipParser().parse(getContext(ElementDslContext.class), tokens.withoutContextStartToken(), archetype); + + if (relationships.size() == 1) { + Relationship relationship = relationships.iterator().next(); + 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)); + Set relationships = new ExplicitRelationshipParser().parse(getContext(ElementsDslContext.class), tokens.withoutContextStartToken(), archetype); + + if (shouldStartContext(tokens)) { + startContext(new RelationshipsDslContext(getContext(), relationships)); + } + + } 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)); + } + + } 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)) { + 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)) { + 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)) { + modelItem = new FindRelationshipParser().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 (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(DeploymentEnvironmentDslContext.class) || inContext(ElementDslContext.class))) { + Set relationships = new FindRelationshipsParser().parse(getContext(), tokens.withoutContextStartToken()); + + if (shouldStartContext(tokens)) { + startContext(new RelationshipsDslContext(getContext(), relationships)); + } + + } 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)); + } + + registerIdentifier(identifier, customElement); + + } else if (isElementKeywordOrArchetype(firstToken, PERSON_TOKEN) && (inContext(ModelDslContext.class))) { + Archetype archetype = getArchetype(PERSON_TOKEN, firstToken); + Person person = new PersonParser().parse(getContext(ModelDslContext.class), tokens.withoutContextStartToken(), archetype); + + if (shouldStartContext(tokens)) { + startContext(new PersonDslContext(person)); + } + + registerIdentifier(identifier, person); + + } else if (isElementKeywordOrArchetype(firstToken, SOFTWARE_SYSTEM_TOKEN) && (inContext(ModelDslContext.class))) { + 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)); + } + + registerIdentifier(identifier, softwareSystem); + + } else if (isElementKeywordOrArchetype(firstToken, CONTAINER_TOKEN) && inContext(SoftwareSystemDslContext.class)) { + Archetype archetype = getArchetype(CONTAINER_TOKEN, firstToken); + Container container = new ContainerParser().parse(getContext(SoftwareSystemDslContext.class), tokens.withoutContextStartToken(), archetype); + + if (shouldStartContext(tokens)) { + startContext(new ContainerDslContext(container)); + } + + registerIdentifier(identifier, container); + + } else if (isElementKeywordOrArchetype(firstToken, COMPONENT_TOKEN) && inContext(ContainerDslContext.class)) { + Archetype archetype = getArchetype(COMPONENT_TOKEN, firstToken); + Component component = new ComponentParser().parse(getContext(ContainerDslContext.class), tokens.withoutContextStartToken(), archetype); + + if (shouldStartContext(tokens)) { + startContext(new ComponentDslContext(component)); + } + + registerIdentifier(identifier, component); + + } else if (COMPONENT_FINDER_TOKEN.equalsIgnoreCase(firstToken) && inContext(ContainerDslContext.class)) { + if (features.isEnabled(Features.COMPONENT_FINDER)) { + if (shouldStartContext(tokens)) { + startContext(new ComponentFinderDslContext(this, getContext(ContainerDslContext.class))); + } + } else { + throw new FeatureNotEnabledException(Features.COMPONENT_FINDER, firstToken + " is not permitted"); + } + + } 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_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))); + } + + } 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, dslFile); + + } else if (COMPONENT_FINDER_STRATEGY_FILTER_TOKEN.equalsIgnoreCase(firstToken) && inContext(ComponentFinderStrategyDslContext.class)) { + 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, 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_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)); + } + + } 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)"); + + } else if (isElementKeywordOrArchetype(firstToken, GROUP_TOKEN) && inContext(ModelDslContext.class)) { + 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().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().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().parseContext(getContext(DeploymentEnvironmentDslContext.class), tokens); + + 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); + + 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) && !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(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); + + } 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 (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); + + } 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)); + + } else if (PROPERTIES_TOKEN.equalsIgnoreCase(firstToken) && inContext(ModelDslContext.class)) { + startContext(new PropertiesDslContext(workspace.getModel())); + + } 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) && !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())); + + } 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())); + + } 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) && !isGroup(getContext())) { + 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(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"); + } + 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(); + 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); + + } 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 (ARCHETYPES_TOKEN.equalsIgnoreCase(firstToken) && inContext(ModelDslContext.class)) { + 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, 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); + 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 (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 (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); + + } 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)) { + 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 (PROPERTIES_TOKEN.equalsIgnoreCase(firstToken) && inContext(ArchetypeDslContext.class)) { + Archetype archetype = getContext(ArchetypeDslContext.class).getArchetype(); + startContext(new PropertiesDslContext(archetype)); + + } 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)) { + 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); + + } 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 (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(StylesDslContext.class), 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); + + } 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(StylesDslContext.class), 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 (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()); + + 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(getContext(DeploymentEnvironmentDslContext.class).getEnvironment(), group)); + + } else if (isElementKeywordOrArchetype(firstToken, DEPLOYMENT_NODE_TOKEN) && inContext(DeploymentEnvironmentDslContext.class)) { + 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)); + } + + registerIdentifier(identifier, deploymentNode); + } else if (isElementKeywordOrArchetype(firstToken, DEPLOYMENT_NODE_TOKEN) && inContext(DeploymentNodeDslContext.class)) { + 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)); + } + + registerIdentifier(identifier, deploymentNode); + } else if (isElementKeywordOrArchetype(firstToken, INFRASTRUCTURE_NODE_TOKEN) && inContext(DeploymentNodeDslContext.class)) { + 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)); + } + + 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()); + + 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().parsePlantUML(getContext(ImageViewDslContext.class), dslFile, tokens); + + } else if (MERMAID_TOKEN.equalsIgnoreCase(firstToken) && inContext(ImageViewDslContext.class)) { + new ImageViewContentParser().parseMermaid(getContext(ImageViewDslContext.class), dslFile, tokens); + + } else if (KROKI_TOKEN.equalsIgnoreCase(firstToken) && inContext(ImageViewDslContext.class)) { + new ImageViewContentParser().parseKroki(getContext(ImageViewDslContext.class), dslFile, tokens); + + } else if (IMAGE_VIEW_TOKEN.equalsIgnoreCase(firstToken) && inContext(ImageViewDslContext.class)) { + 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); + 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); + + 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(), dslFile, tokens); + + } else if (THEMES_TOKEN.equalsIgnoreCase(firstToken) && (inContext(ViewsDslContext.class) || inContext(StylesDslContext.class))) { + new ThemeParser().parseThemes(getContext(), dslFile, tokens); + + } else if (TERMINOLOGY_TOKEN.equalsIgnoreCase(firstToken) && inContext(ViewsDslContext.class)) { + startContext(new TerminologyDslContext()); + + } 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 (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()); + + } 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 (features.isEnabled(Features.DOCUMENTATION)) { + new DocsParser().parse(getContext(WorkspaceDslContext.class), dslFile, tokens); + } else { + throw new FeatureNotEnabledException(Features.DOCUMENTATION, firstToken + " is not permitted"); + } + + } else if (DOCS_TOKEN.equalsIgnoreCase(firstToken) && inContext(SoftwareSystemDslContext.class)) { + if (features.isEnabled(Features.DOCUMENTATION)) { + new DocsParser().parse(getContext(SoftwareSystemDslContext.class), dslFile, tokens); + } else { + throw new FeatureNotEnabledException(Features.DOCUMENTATION, firstToken + " is not permitted"); + } + + } else if (DOCS_TOKEN.equalsIgnoreCase(firstToken) && inContext(ContainerDslContext.class)) { + if (features.isEnabled(Features.DOCUMENTATION)) { + new DocsParser().parse(getContext(ContainerDslContext.class), dslFile, tokens); + } else { + throw new FeatureNotEnabledException(Features.DOCUMENTATION, firstToken + " is not permitted"); + } + + } else if (DOCS_TOKEN.equalsIgnoreCase(firstToken) && inContext(ComponentDslContext.class)) { + if (features.isEnabled(Features.DOCUMENTATION)) { + new DocsParser().parse(getContext(ComponentDslContext.class), dslFile, tokens); + } else { + throw new FeatureNotEnabledException(Features.DOCUMENTATION, firstToken + " is not permitted"); + } + + } else if ((ADRS_TOKEN.equalsIgnoreCase(firstToken) || DECISIONS_TOKEN.equalsIgnoreCase(firstToken)) && inContext(WorkspaceDslContext.class)) { + if (features.isEnabled(Features.DECISIONS)) { + new DecisionsParser().parse(getContext(WorkspaceDslContext.class), dslFile, tokens); + } else { + throw new FeatureNotEnabledException(Features.DECISIONS, firstToken + " is not permitted"); + } + + } else if ((ADRS_TOKEN.equalsIgnoreCase(firstToken) || DECISIONS_TOKEN.equalsIgnoreCase(firstToken)) && inContext(SoftwareSystemDslContext.class)) { + if (features.isEnabled(Features.DECISIONS)) { + new DecisionsParser().parse(getContext(SoftwareSystemDslContext.class), dslFile, tokens); + } else { + throw new FeatureNotEnabledException(Features.DECISIONS, firstToken + " is not permitted"); + } + + } else if ((ADRS_TOKEN.equalsIgnoreCase(firstToken) || DECISIONS_TOKEN.equalsIgnoreCase(firstToken)) && inContext(ContainerDslContext.class)) { + if (features.isEnabled(Features.DECISIONS)) { + new DecisionsParser().parse(getContext(ContainerDslContext.class), dslFile, tokens); + } else { + throw new FeatureNotEnabledException(Features.DECISIONS, firstToken + " is not permitted"); + } + + } else if ((ADRS_TOKEN.equalsIgnoreCase(firstToken) || DECISIONS_TOKEN.equalsIgnoreCase(firstToken)) && inContext(ComponentDslContext.class)) { + if (features.isEnabled(Features.DECISIONS)) { + new DecisionsParser().parse(getContext(ComponentDslContext.class), dslFile, tokens); + } else { + throw new FeatureNotEnabledException(Features.DECISIONS, firstToken + " is not permitted"); + } + + } else if (CONSTANT_TOKEN.equalsIgnoreCase(firstToken)) { + 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); + try { + addConstant(nameValuePair); + } catch (IllegalArgumentException e) { + throw new StructurizrDslParserException(e.getMessage()); + } + + } else if (VAR_TOKEN.equalsIgnoreCase(firstToken)) { + NameValuePair nameValuePair = new NameValueParser().parseVariable(tokens); + addVariable(nameValuePair); + + } else if (IDENTIFIERS_TOKEN.equalsIgnoreCase(firstToken) && (inContext(WorkspaceDslContext.class) || inContext(ModelDslContext.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"); + } + } + } + } 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); + } + } + } + + if (!fragment && !contextStack.empty()) { + throw new StructurizrDslParserException("Unexpected end of DSL content - are one or more closing curly braces missing?"); + } + } + + private List preProcessLines(List lines) { + List dslLines = new ArrayList<>(); + + int lineNumber = 1; + StringBuilder buf = new StringBuilder(); + boolean lineComplete = true; + boolean textBlock = false; + int textBlockLeadingSpace = -1; + + for (String line : lines) { + if (textBlock) { + if (line.endsWith(TEXT_BLOCK_MARKER)) { + buf.append(TEXT_BLOCK_MARKER); + 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; + } + } + } + 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()); + lineComplete = false; + textBlock = true; + } else { + if (lineComplete) { + buf.append(line); + } else { + buf.append(line.stripLeading()); + lineComplete = true; + } + } + + 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 source = buf.toString(); + + if (source.endsWith(TEXT_BLOCK_MARKER)) { + String[] parts = source.split(TEXT_BLOCK_MARKER); + 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)); + } + + buf = new StringBuilder(); + } + + lineNumber++; + } + + return dslLines; + } + + 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 (constantsAndVariables.containsKey(name)) { + NameValuePair nameValuePair = constantsAndVariables.get(name); + + if (nameValuePair.getType() == NameValueType.TextBlock) { + after = substituteStrings(nameValuePair.getValue()); + } else { + after = nameValuePair.getValue(); + } + } else { + if (getFeatures().isEnabled(Features.ENVIRONMENT)) { + 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); + context.setFeatures(features); + context.setHttpClient(httpClient); + 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(); + + dslPortable &= context.isDslPortable(); + } else { + throw new StructurizrDslParserException("Unexpected end of context"); + } + } + + private boolean isGroup(DslContext context) { + if (context instanceof GroupableDslContext) { + return ((GroupableDslContext)context).hasGroup(); + } + + return false; + } + + 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; + } else { + return (archetypes.get(keyword).containsKey(token.toLowerCase())); + } + } + + private boolean isRelationshipKeywordOrArchetype(String token) { + 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 false; + } + + private void addArchetype(Archetype archetype) { + archetypes.get(archetype.getType()).put(archetype.getName(), archetype); + } + + private Archetype getArchetype(String archetypeType, String archetypeName) { + Archetype archetype = null; + + 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()); + + if (archetype == null) { + archetype = new Archetype(archetypeName, archetypeType); + } + + return archetype; + } + + 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])); + } + + /** + * 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; + } + + void registerIdentifier(String identifier, Element element) { + identifiersRegister.register(identifier, element); + element.addProperty(STRUCTURIZR_DSL_IDENTIFIER_PROPERTY_NAME, identifiersRegister.findIdentifier(element)); + } + + void registerIdentifier(String identifier, Relationship relationship) { + identifiersRegister.register(identifier, relationship); + + if (!StringUtils.isNullOrEmpty(identifier)) { + 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) { + 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 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; + } + + 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..70dc54d43 --- /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 && dslFile.isFile() ? " 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..85c03f352 --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslScriptContext.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 scripts when they are executed. + */ +public class StructurizrDslScriptContext { + + 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 script + * @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(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 script context. + * + * @return a StructurizrDslParser instance + */ + public StructurizrDslParser getDslParser() { + return dslParser; + } + + /** + * 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..9ddcd5958 --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslTokens.java @@ -0,0 +1,146 @@ +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 NO_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 METADATA_TOKEN = "metadata"; + 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 TAG_TOKEN = "tag"; + 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 ARCHETYPES_TOKEN = "archetypes"; + 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 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"; + 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 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"; + 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_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"; + 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_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"; + 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 METADATA_SYMBOLS_TOKEN = "metadata"; + 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 DECISIONS_TOKEN = "!decisions"; + static final String CONSTANT_TOKEN = "!constant"; + static final String CONST_TOKEN = "!const"; + 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"; // 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"; + + 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"; + 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_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"; + + static final String RELATIONSHIP_ARCHETYPE_PREFIX = "--"; + static final String RELATIONSHIP_ARCHETYPE_SUFFIX = "->"; + +} \ 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..a374a54df --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/StylesDslContext.java @@ -0,0 +1,29 @@ +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[] { + 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..20235e418 --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/SystemContextViewParser.java @@ -0,0 +1,55 @@ +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 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); + validateViewKey(key); + } + + if (tokens.includes(DESCRIPTION_INDEX)) { + description = tokens.get(DESCRIPTION_INDEX); + } + + return workspace.getViews().createSystemContextView(softwareSystem, key, description); + } + +} \ 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..94495b111 --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/SystemLandscapeViewParser.java @@ -0,0 +1,36 @@ +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 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); + validateViewKey(key); + } + + if (tokens.includes(DESCRIPTION_INDEX)) { + description = tokens.get(DESCRIPTION_INDEX); + } + + 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 new file mode 100644 index 000000000..32bc07e81 --- /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.PERSON_TOKEN, + StructurizrDslTokens.SOFTWARE_SYSTEM_TOKEN, + StructurizrDslTokens.CONTAINER_TOKEN, + StructurizrDslTokens.COMPONENT_TOKEN, + StructurizrDslTokens.DEPLOYMENT_NODE_TOKEN, + StructurizrDslTokens.INFRASTRUCTURE_NODE_TOKEN, + StructurizrDslTokens.TERMINOLOGY_RELATIONSHIP_TOKEN, + StructurizrDslTokens.METADATA_SYMBOLS_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..1fdaab43b --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/TerminologyParser.java @@ -0,0 +1,101 @@ +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 + 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)); + } + + 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/main/java/com/structurizr/dsl/ThemeParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/ThemeParser.java new file mode 100644 index 000000000..90b5e3e73 --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ThemeParser.java @@ -0,0 +1,73 @@ +package com.structurizr.dsl; + +import com.structurizr.util.FeatureNotEnabledException; +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, File dslFile, 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, dslFile, tokens.get(FIRST_THEME_INDEX)); + } + + 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)); + } + } + + private void addTheme(DslContext context, File dslFile, String theme) { + if (DEFAULT_THEME_NAME.equalsIgnoreCase(theme)) { + theme = DEFAULT_THEME_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 { + if (context.getFeatures().isEnabled(Features.FILE_SYSTEM)) { + 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() + " does not exist"); + } + } else { + throw new FeatureNotEnabledException(Features.FILE_SYSTEM, "File-based themes are not permitted"); + } + } + } + +} \ 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..55fe452ba --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/Tokens.java @@ -0,0 +1,45 @@ +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).replaceAll("\\\\\"", "\"").replaceAll("\\\\n", "\n"); + } + + void remove(int index) { + tokens.remove(index); + } + + 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..74fbbe6f8 --- /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.DECISIONS_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..59ebc4a54 --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/WorkspaceParser.java @@ -0,0 +1,167 @@ +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; + +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"); + + 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 (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.toLowerCase().endsWith(".json") || remoteContent.getContentType().startsWith(RemoteContent.CONTENT_TYPE_JSON)) { + String json = remoteContent.getContentAsString(); + workspace = WorkspaceUtils.fromJson(json); + registerIdentifiers(workspace, context); + } else { + String dsl = remoteContent.getContentAsString(); + + StructurizrDslParser structurizrDslParser = createParser(context); + structurizrDslParser.parse(context, dsl); + + workspace = structurizrDslParser.getWorkspace(); + context.getParser().configureFrom(structurizrDslParser); + } + } else { + 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) { + 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.toLowerCase().endsWith(".json")) { + workspace = WorkspaceUtils.loadWorkspaceFromJson(file); + registerIdentifiers(workspace, context); + } else { + StructurizrDslParser structurizrDslParser = createParser(context); + structurizrDslParser.parse(context, file); + + workspace = structurizrDslParser.getWorkspace(); + context.getParser().configureFrom(structurizrDslParser); + } + + DslUtils.clearDsl(workspace); + context.setDslPortable(false); + } + } + } 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)); + } + } + } + + workspace.getModel().setImpliedRelationshipsStrategy(new CreateImpliedRelationshipsUnlessAnyRelationshipExistsStrategy()); + + 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)) { + 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/AbstractTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/AbstractTests.java new file mode 100644 index 000000000..005d8aca8 --- /dev/null +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/AbstractTests.java @@ -0,0 +1,32 @@ +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(); + + AbstractTests() { + model.setImpliedRelationshipsStrategy(new CreateImpliedRelationshipsUnlessAnyRelationshipExistsStrategy()); + } + + protected ModelDslContext context() { + 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/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 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..1bf47b5a1 --- /dev/null +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/BrandingParserTests.java @@ -0,0 +1,173 @@ +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")); + 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")); + fail(); + } catch (Exception e) { + assertEquals("Expected: logo <path|url>", e.getMessage()); + } + } + + @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")); + fail(); + } catch (Exception e) { + assertEquals("hello.png does not exist", e.getMessage()); + } + } + + @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")); + 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); + context.getFeatures().enable(Features.FILE_SYSTEM); + + parser.parseLogo(context, tokens("logo", "src/test/resources/dsl/logo.png")); + 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")); + 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")); + 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")); + 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/ComponentFinderParserTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/ComponentFinderParserTests.java new file mode 100644 index 000000000..ec43b6f57 --- /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, new ContainerDslContext(null)); + + @Test + void test_parseFilter_ThrowsAnException_WhenNoModeAndTypeAreSpecified() { + try { + parser.parseFilter(context, tokens("filter")); + fail(); + } catch (Exception e) { + assertEquals("Too few tokens, expected: filter <include|exclude> <fqn-regex> [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 <include|exclude> <fqn-regex> [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 <include|exclude> <fqn-regex> [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 new file mode 100644 index 000000000..a136790ee --- /dev/null +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/ComponentFinderStrategyParserTests.java @@ -0,0 +1,336 @@ +package com.structurizr.dsl; + +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 ComponentFinderStrategyParserTests extends AbstractTests { + + private final ComponentFinderStrategyParser parser = new ComponentFinderStrategyParser(); + private final ComponentFinderStrategyDslContext context = new ComponentFinderStrategyDslContext(new ComponentFinderDslContext(null, new ContainerDslContext(null))); + + @Test + void test_parseTechnology_ThrowsAnException_WhenThereAreTooFewTokens() { + try { + parser.parseTechnology(context, tokens("technology")); + fail(); + } catch (Exception e) { + assertEquals("Expected: technology <name>", e.getMessage()); + } + } + + @Test + void test_parseTechnology_ThrowsAnException_WhenThereAreTooManyTokens() { + try { + parser.parseTechnology(context, tokens("technology", "name", "extra")); + fail(); + } catch (Exception e) { + assertEquals("Expected: technology <name>", e.getMessage()); + } + } + + @Test + void test_parseTechnology() { + parser.parseTechnology(context, tokens("technology", "name")); + assertEquals("ComponentFinderStrategyBuilder{technology='name', typeMatcher=null, typeFilter=null, supportingTypesStrategy=null, namingStrategy=null, descriptionStrategy=null, urlStrategy=null, componentVisitor=null}", 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 <annotation|extends|implements|name-suffix|fqn-regex> [parameters]", e.getMessage()); + } + } + + @Test + void test_parseMatcher_WhenTheAnnotationTypeMatcherIsUsedAndThereAreTooFewTokens() { + try { + parser.parseMatcher(context, tokens("matcher", "annotation"), null); + fail(); + } catch (Exception e) { + assertEquals("Expected: matcher annotation <fqn>", 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=null, supportingTypesStrategy=null, namingStrategy=null, descriptionStrategy=null, urlStrategy=null, componentVisitor=null}", context.getComponentFinderStrategyBuilder().toString()); + } + + @Test + void test_parseMatcher_WhenTheExtendsTypeMatcherIsUsedAndThereAreTooFewTokens() { + try { + parser.parseMatcher(context, tokens("matcher", "extends"), null); + fail(); + } catch (Exception e) { + assertEquals("Expected: matcher extends <fqn>", 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=null, supportingTypesStrategy=null, namingStrategy=null, descriptionStrategy=null, urlStrategy=null, componentVisitor=null}", context.getComponentFinderStrategyBuilder().toString()); + } + + @Test + void test_parseMatcher_WhenTheImplementsTypeMatcherIsUsedAndThereAreTooFewTokens() { + try { + parser.parseMatcher(context, tokens("matcher", "implements"), null); + fail(); + } catch (Exception e) { + assertEquals("Expected: matcher implements <fqn>", 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=null, supportingTypesStrategy=null, namingStrategy=null, descriptionStrategy=null, urlStrategy=null, componentVisitor=null}", context.getComponentFinderStrategyBuilder().toString()); + } + + @Test + void test_parseMatcher_WhenTheNameSuffixTypeMatcherIsUsedAndThereAreTooFewTokens() { + try { + parser.parseMatcher(context, tokens("matcher", "name-suffix"), null); + fail(); + } catch (Exception e) { + assertEquals("Expected: matcher name-suffix <name>", e.getMessage()); + } + } + + @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, urlStrategy=null, componentVisitor=null}", context.getComponentFinderStrategyBuilder().toString()); + } + + @Test + void test_parseMatcher_WhenTheFullyQualifiedNameRegexTypeMatcherIsUsedAndThereAreTooFewTokens() { + try { + parser.parseMatcher(context, tokens("matcher", "fqn-regex"), null); + fail(); + } catch (Exception e) { + assertEquals("Expected: matcher fqn-regex <regex>", e.getMessage()); + } + } + + @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, urlStrategy=null, componentVisitor=null}", 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_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, 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, urlStrategy=null, componentVisitor=null}", context.getComponentFinderStrategyBuilder().toString()); + } + + @Test + void test_parseFilter_ThrowsAnException_WhenNoModeAndTypeAreSpecified() { + try { + parser.parseFilter(context, tokens("filter"), null); + fail(); + } catch (Exception e) { + assertEquals("Too few tokens, expected: filter <include|exclude> <fqn-regex> [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 <include|exclude> <fqn-regex> [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 <include|exclude> <fqn-regex> [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=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=ExcludeFullyQualifiedNameRegexFilter{regex='.*'}, supportingTypesStrategy=null, namingStrategy=null, descriptionStrategy=null, urlStrategy=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 <all-referenced|referenced-in-package|in-package|under-package|implementation-prefix|implementation-suffix|none> [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 <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 <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 { + parser.parseName(context, tokens("name"), null); + fail(); + } catch (Exception e) { + assertEquals("Too few tokens, expected: name <type-name|fqn|package>", 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, urlStrategy=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 <first-sentence|truncated>", 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{}, urlStrategy=null, 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 <maxLength>", 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}, 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 <prefix-src>", e.getMessage()); + } + } + + @Test + void test_parseUrl_Prefix_ThrowsAnException_WhenNoPrefixIsSpecified() { + try { + parser.parseUrl(context, tokens("url", "prefix-src"), null); + fail(); + } catch (Exception e) { + assertEquals("Too few tokens, expected: url prefix-src <prefix>", e.getMessage()); + } + } + + @Test + void test_parseUrl_Prefix() { + 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()); + } + +} \ 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..babac9aab --- /dev/null +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/ComponentParserTests.java @@ -0,0 +1,148 @@ +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(); + 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"), archetype); + 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"), archetype); + 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"), archetype); + + assertEquals(3, model.getElements().size()); + Component component = container.getComponentWithName("Name"); + assertNotNull(component); + assertEquals("", component.getDescription()); + assertEquals("", 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"), archetype); + + assertEquals(3, model.getElements().size()); + Component component = container.getComponentWithName("Name"); + assertNotNull(component); + assertEquals("Description", component.getDescription()); + assertEquals("", 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"), archetype); + + 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"), archetype); + + 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_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 { + 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..aac6950e9 --- /dev/null +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/ComponentViewParserTests.java @@ -0,0 +1,108 @@ +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()); + } + + @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()); + } + + @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()); + } + +} \ 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/ContainerInstanceParserTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/ContainerInstanceParserTests.java new file mode 100644 index 000000000..98dde6ece --- /dev/null +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/ContainerInstanceParserTests.java @@ -0,0 +1,153 @@ +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 final 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("live", new DeploymentEnvironment("Live")); + elements.register("group", new DeploymentGroup(new DeploymentEnvironment("Live"), "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("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")); + + 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..3215df02c --- /dev/null +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/ContainerParserTests.java @@ -0,0 +1,142 @@ +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(); + 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"), archetype); + 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"), archetype); + 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"), archetype); + + 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"), archetype); + + 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"), archetype); + + 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"), archetype); + + 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_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 { + 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..5d24c2a9d --- /dev/null +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/ContainerViewParserTests.java @@ -0,0 +1,108 @@ +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()); + } + + @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()); + } + + @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()); + } + +} \ 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..27d48b963 --- /dev/null +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/CustomElementParserTests.java @@ -0,0 +1,96 @@ +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(); + private Archetype archetype = new Archetype("name", "type"); + + @Test + void test_parse_ThrowsAnException_WhenThereAreTooManyTokens() { + try { + parser.parse(context(), tokens("element", "name", "metadata", "description", "tags", "extra"), archetype); + 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"), archetype); + fail(); + } catch (Exception e) { + assertEquals("Expected: element <name> [metadata] [description] [tags]", e.getMessage()); + } + } + + @Test + void test_parse_CreatesACustomElement() { + parser.parse(context(), tokens("element", "Name"), archetype); + + 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"), archetype); + + 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"), archetype); + + 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"), archetype); + + 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()); + } + + @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/CustomViewAnimationStepParserTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/CustomViewAnimationStepParserTests.java new file mode 100644 index 000000000..b1cd19d46 --- /dev/null +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/CustomViewAnimationStepParserTests.java @@ -0,0 +1,49 @@ +package com.structurizr.dsl; + +import com.structurizr.view.CustomView; +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|element expression> [identifier|element expression...]", e.getMessage()); + } + } + + @Test + void test_parseImplicit_ThrowsAnException_WhenElementsAreMissing() { + try { + parser.parse((CustomViewAnimationDslContext) null, tokens()); + fail(); + } catch (Exception e) { + assertEquals("Expected: <identifier|element expression> [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()); + } + } + +} \ 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/DecisionsParserTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/DecisionsParserTests.java new file mode 100644 index 000000000..e1c913609 --- /dev/null +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/DecisionsParserTests.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 DecisionsParserTests extends AbstractTests { + + private final DecisionsParser parser = new DecisionsParser(); + + @Test + void test_parse_ThrowsAnException_WhenThereAreTooManyTokens() { + try { + parser.parse(new WorkspaceDslContext(), null, tokens("decisions", "path", "fqn", "extra")); + fail(); + } catch (Exception e) { + assertEquals("Too many tokens, expected: !decisions <path> <type|fqn>", e.getMessage()); + } + } + +} \ 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..f5694274e --- /dev/null +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/DeploymentNodeParserTests.java @@ -0,0 +1,266 @@ +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(); + 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"), archetype); + 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"), archetype); + 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"), archetype); + + 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"), archetype); + + 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"), archetype); + + 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"), archetype); + + 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"), archetype); + + 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_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"); + context.setWorkspace(workspace); + + try { + 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) { + 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"), archetype); + + 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_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 { + 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..a11bbf992 --- /dev/null +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/DeploymentViewAnimationStepParserTests.java @@ -0,0 +1,69 @@ +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; +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|element expression> [identifier|element expression...]", e.getMessage()); + } + } + + @Test + void test_parseImplicit_ThrowsAnException_WhenElementsAreMissing() { + try { + parser.parse((DeploymentViewAnimationDslContext)null, tokens()); + fail(); + } catch (Exception e) { + assertEquals("Expected: <identifier|element expression> [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()); + } + } + +} \ 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..4bf5d0f30 --- /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|expression> [*|identifier|expression...]", 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|expression> [identifier|expression...]", 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..95eef0d04 --- /dev/null +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/DeploymentViewExpressionParserTests.java @@ -0,0 +1,273 @@ +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)); + } + + @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<ModelItem> 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<ModelItem> 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 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..55e03d01e --- /dev/null +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java @@ -0,0 +1,1946 @@ +package com.structurizr.dsl; + +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.Tag; +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.*; + +import static org.junit.jupiter.api.Assertions.*; + +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()); + } + + @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(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(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(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("", DslUtils.getDsl(workspace)); + } + + @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("", DslUtils.getDsl(workspace)); + } + + @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 + @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(); + Model model = workspace.getModel(); + + assertEquals(1, model.getSoftwareSystems().size()); + assertNotNull(model.getSoftwareSystemWithName("Software System")); + + assertEquals(""" + workspace { + + model { + !include https://raw.githubusercontent.com/structurizr/java/refs/heads/master/structurizr-dsl/src/test/resources/dsl/include/model.dsl + } + + }""", DslUtils.getDsl(workspace)); + } + + @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") + 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(); + 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(""" + 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 + 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("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()); + } + } + + @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(); + parser.getHttpClient().allow(".*"); + parser.parse(new File(dslFile)); + + Workspace workspace = parser.getWorkspace(); + assertEquals(IdentifierScope.Hierarchical, parser.getIdentifierScope()); + + 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(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("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()); + } + } + + @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_findElement() throws Exception { + StructurizrDslParser parser = new StructurizrDslParser(); + 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_Hierarchical() 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_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(); + 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_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<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()); + + 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()); + + 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()); + } + + @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_plugin_ThrowsAnException_WhenPluginsAreNotEnabled() { + try { + StructurizrDslParser parser = new StructurizrDslParser(); + 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 permitted (feature structurizr.feature.dsl.plugins is not enabled)")); + } + } + + @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")); + + // check source isn't retained + assertEquals("", DslUtils.getDsl(parser.getWorkspace())); + } + + @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")); + + // check source isn't retained + assertEquals("", DslUtils.getDsl(parser.getWorkspace())); + } + + @Test + void test_script_ThrowsAnException_WhenScriptsAreNotEnabled() { + try { + StructurizrDslParser parser = new StructurizrDslParser(); + parser.getFeatures().disable(Features.SCRIPTS); + parser.parse(new File("src/test/resources/dsl/script-external.dsl")); + fail(); + } catch (Exception e) { + System.out.println(e.getMessage()); + assertTrue(e.getMessage().startsWith("!script is not permitted (feature structurizr.feature.dsl.scripts is not enabled)")); + } + } + + @Test + 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_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 + 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()); + + // check source isn't retained + assertEquals("", DslUtils.getDsl(parser.getWorkspace())); + } + + @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"); + + Collection<Section> 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()); + + // check source isn't retained + assertEquals("", DslUtils.getDsl(parser.getWorkspace())); + } + + @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) { + System.out.println(e.getMessage()); + assertTrue(e.getMessage().startsWith("!docs is not permitted (feature structurizr.feature.dsl.documentation is not enabled)")); + } + } + + @Test + void test_decisions() throws Exception { + StructurizrDslParser parser = new StructurizrDslParser(); + 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()); + + // madr decisions + assertEquals(19, container.getDocumentation().getDecisions().size()); + + // log4brains decisions + assertEquals(4, component.getDocumentation().getDecisions().size()); + + // check source isn't retained + assertEquals("", DslUtils.getDsl(parser.getWorkspace())); + } + + @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) { + System.out.println(e.getMessage()); + assertTrue(e.getMessage().startsWith("!adrs is not permitted (feature structurizr.feature.dsl.decisions is not enabled)")); + } + } + + @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, !decisions, !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, !decisions, 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_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<RelationshipView> relationships = view.getRelationships(); + Iterator<RelationshipView> it = relationships.iterator(); + assertEquals("2", it.next().getOrder()); + assertEquals("3", it.next().getOrder()); + } + + @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(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("rel", relationship.getProperties().get("structurizr.dsl.identifier")); + 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(); + 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(); + assertEquals(5, 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/png/eNpLyUwvSizIUHBXqPZIzcnJ17ULzy_KSanlAgB1EAjQ", krokiView.getContent()); + assertEquals("image/png", krokiView.getContentType()); + + ImageView pngView = (ImageView)workspace.getViews().getViewWithKey("png"); + assertEquals("image.png", pngView.getTitle()); + assertEquals("data:image/png;base64,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", pngView.getContent()); + assertEquals("image/png", pngView.getContentType()); + + ImageView svgView = (ImageView)workspace.getViews().getViewWithKey("svg"); + 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 + @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(); + assertEquals(5, 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 pngView = (ImageView)workspace.getViews().getViewWithKey("png"); + assertEquals("image.png", pngView.getTitle()); + assertEquals("https://raw.githubusercontent.com/structurizr/java/master/structurizr-dsl/src/test/resources/dsl/image-views/image.png", pngView.getContent()); + assertEquals("image/png", pngView.getContentType()); + + ImageView svgView = (ImageView)workspace.getViews().getViewWithKey("svg"); + 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 + 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(); + 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, !decisions, 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); + } + + @Test + void test_Enterprise() { + File dslFile = new File("src/test/resources/dsl/enterprise.dsl"); + + try { + StructurizrDslParser parser = new StructurizrDslParser(); + parser.parse(dslFile); + fail(); + } catch (StructurizrDslParserException e) { + assertEquals("The enterprise keyword was previously deprecated, and has now been removed - please use group instead (https://docs.structurizr.com/dsl/language#group) at line 4 of " + dslFile.getAbsolutePath() + ": enterprise \"Name\" {", e.getMessage()); + } + } + + @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_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 { + StructurizrDslParser parser = new StructurizrDslParser(); + parser.parse(""" + workspace { + model { + person "User" + } + """); + fail(); + } catch (StructurizrDslParserException e) { + assertEquals("Unexpected end of DSL content - are one or more closing curly braces missing?", e.getMessage()); + } + } + + @Test + void test_Const() { + try { + StructurizrDslParser parser = new StructurizrDslParser(); + parser.parse(""" + workspace { + !const name value1 + !const name value2 + } + """); + fail(); + } catch (StructurizrDslParserException e) { + assertEquals("A constant/variable \"name\" already exists at line 3: !const name value2", e.getMessage()); + } + } + + @Test + void test_Var_CannotOverrideConst() { + try { + StructurizrDslParser parser = new StructurizrDslParser(); + parser.parse(""" + workspace { + !const name value1 + !var name value2 + } + """); + fail(); + } catch (StructurizrDslParserException e) { + assertEquals("A constant \"name\" already exists at line 3: !var name value2", e.getMessage()); + } + } + + @Test + void springPetClinic() throws Exception { + String springPetClinicHome = System.getenv().getOrDefault("SPRING_PETCLINIC_HOME", ""); + System.out.println(springPetClinicHome); + 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 permitted (feature structurizr.feature.dsl.componentfinder is not enabled)")); + } + + File workspaceFile = new File("src/test/resources/dsl/spring-petclinic/workspace.dsl"); + StructurizrDslParser parser = new StructurizrDslParser(); + 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"); + 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()); + assertSame(welcomeController, parser.getIdentifiersRegister().getElement("springPetClinic.webApplication.welcomecontroller")); + assertEquals("Web Controllers", welcomeController.getGroup()); + 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()); + assertSame(ownerController, parser.getIdentifiersRegister().getElement("springPetClinic.webApplication.ownerController")); + assertEquals("Web Controllers", ownerController.getGroup()); + 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()); + assertSame(petController, parser.getIdentifiersRegister().getElement("springPetClinic.webApplication.petcontroller")); + assertEquals("Web Controllers", petController.getGroup()); + 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()); + assertSame(vetController, parser.getIdentifiersRegister().getElement("springPetClinic.webApplication.vetcontroller")); + assertEquals("Web Controllers", vetController.getGroup()); + 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()); + assertSame(visitController, parser.getIdentifiersRegister().getElement("springPetClinic.webApplication.visitcontroller")); + assertEquals("Web Controllers", visitController.getGroup()); + 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()); + 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"); + 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()); + 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()); + assertNotNull(petController.getEfferentRelationshipWith(ownerRepository)); + assertNotNull(visitController.getEfferentRelationshipWith(ownerRepository)); + assertNotNull(ownerController.getEfferentRelationshipWith(ownerRepository)); + assertNotNull(vetController.getEfferentRelationshipWith(vetRepository)); + + // checks that source isn't retained + assertEquals("", DslUtils.getDsl(workspace)); + + } else { + System.out.println("Skipping Spring PetClinic example..."); + } + } + + @Test + void test_bulkOperations() throws Exception { + File dslFile = new File("src/test/resources/dsl/bulk-operations.dsl"); + + StructurizrDslParser parser = new StructurizrDslParser(); + 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("image <file> is not permitted (feature structurizr.feature.dsl.filesystem is not enabled) at line 5 of " + dslFile.getAbsolutePath() + ": image image.png", e.getMessage()); + } + } + + @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)); + + // 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("", 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)); + } + + @Test + void test_archetypes() throws Exception { + File parentDslFile = new File("src/test/resources/dsl/archetypes.dsl"); + StructurizrDslParser parser = new StructurizrDslParser(); + parser.parse(parentDslFile); + Workspace workspace = parser.getWorkspace(); + + 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()); + + Relationship relationship = workspace.getModel().getSoftwareSystemWithName("A").getEfferentRelationshipWith(workspace.getModel().getSoftwareSystemWithName("X")); + 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"); + 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_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"); + StructurizrDslParser parser = new StructurizrDslParser(); + 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("Synchronous")); + 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(); + parser.parse(new File("src/test/resources/dsl/text-block.dsl")); + + workspace = parser.getWorkspace(); + + ImageView view = (ImageView)workspace.getViews().getViewWithKey("image"); + assertEquals("https://plantuml.com/plantuml/svg/SoWkIImgAStDuUAoAIwfp4cruohApozHgEPI00AdnEJizAByqhmKv_oS_28h1UKqCB3cgkMoqOUgvqhEIImkLl2jT0RHN0wfUIb0ym00", view.getContent()); + + assertEquals(""" + workspace { + + views { + properties { + "plantuml.url" "https://plantuml.com/plantuml" + } + + !const SOURCE ""\" + class MyClass + ""\" + + !var STYLES ""\" + <style> + root { + BackgroundColor: #ffffff; + } + </style> + ""\" + + image * "image" { + plantuml ""\" + @startuml + + ${STYLES} + + ${SOURCE} + @enduml + ""\" + } + } + + }""", DslUtils.getDsl(workspace)); + } + + @Test + void test_customViewAnimation() throws Exception { + StructurizrDslParser parser = new StructurizrDslParser(); + 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"); + 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())); + } + } + + @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)); + } + + @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()); + } + + @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/DynamicViewContentParserTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/DynamicViewContentParserTests.java new file mode 100644 index 000000000..a94047a89 --- /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: [order:] <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: [order:] <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..65baa3c09 --- /dev/null +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/ElementStyleParserTests.java @@ -0,0 +1,641 @@ +package com.structurizr.dsl; + +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; + +import java.io.File; + +import static org.junit.jupiter.api.Assertions.*; + +class ElementStyleParserTests extends AbstractTests { + + private final 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; + } + + private StylesDslContext stylesDslContext() { + StylesDslContext context = new StylesDslContext(); + context.setWorkspace(workspace); + + return context; + } + + @Test + void test_parseElementStyle_ThrowsAnException_WhenThereAreTooManyTokens() { + try { + parser.parseElementStyle(stylesDslContext(), 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(stylesDslContext(), tokens("element")); + fail(); + } catch (Exception e) { + assertEquals("Expected: element <tag> {", e.getMessage()); + } + } + + @Test + void test_parseElementStyle_ThrowsAnException_WhenTheTagIsEmpty() { + try { + parser.parseElementStyle(stylesDslContext(), tokens("element", "")); + fail(); + } catch (Exception e) { + assertEquals("A tag must be specified", e.getMessage()); + } + } + + @Test + void test_parseElementStyle_CreatesAnElementStyle() { + parser.parseElementStyle(stylesDslContext(), 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(stylesDslContext(), 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|Bucket|Pipe|Person|Robot|Folder|WebBrowser|Window|Terminal|Shell|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|Bucket|Pipe|Person|Robot|Folder|WebBrowser|Window|Terminal|Shell|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")); + 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")); + fail(); + } catch (Exception e) { + assertEquals("Expected: icon <file|url>", e.getMessage()); + } + } + + @Test + void test_parseIcon_ThrowsAnException_WhenTheIconDoesNotExist() { + try { + 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()); + } + } + + @Test + void test_parseIcon_SetsTheIconFromADataUri() { + 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() { + 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() { + 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() { + 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 <file> is not permitted (feature structurizr.feature.dsl.filesystem is not enabled)", e.getMessage()); + } + } + + @Test + void test_parseIconPosition_ThrowsAnException_WhenThereAreTooManyTokens() { + try { + parser.parseIconPosition(elementStyleDslContext(), tokens("iconPosition", "top", "extra")); + fail(); + } catch (Exception e) { + assertEquals("Too many tokens, expected: iconPosition <Top|Bottom|Left>", e.getMessage()); + } + } + + @Test + void test_parseIconPosition_ThrowsAnException_WhenTheShapeIsMissing() { + try { + parser.parseIconPosition(elementStyleDslContext(), tokens("iconPosition")); + fail(); + } catch (Exception e) { + assertEquals("Expected: iconPosition <Top|Bottom|Left>", 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 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 <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 <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 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..ac4b7cead --- /dev/null +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/ExplicitRelationshipParserTests.java @@ -0,0 +1,448 @@ +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 ExplicitRelationshipParserTests extends AbstractTests { + + private final 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"), archetype); + 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", "->"), archetype); + 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"), archetype); + 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"), archetype); + 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"), archetype); + + 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"), archetype); + + 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"), archetype); + + 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"), archetype); + + 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_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"); + 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"), archetype); + 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"), archetype); + + 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"), archetype); + + 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_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<Relationship> 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<Relationship> 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<Relationship> 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<Relationship> 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)); + } + + @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/ExpressionParserTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/ExpressionParserTests.java new file mode 100644 index 000000000..83bb4a3ab --- /dev/null +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/ExpressionParserTests.java @@ -0,0 +1,587 @@ +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 ExpressionParserTests extends AbstractTests { + + private final ExpressionParser parser = new ExpressionParser(); + + @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_ReturnsElementsAndElementInstances_WhenUsingAnElementTagEqualsExpression() { + model.addPerson("User"); + SoftwareSystem ss = model.addSoftwareSystem("Software System"); + SoftwareSystemInstance ssi = model.addDeploymentNode("DN").add(ss); + + 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_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<ModelItem> 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"); + 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"); + + DeploymentNode deploymentNode = model.addDeploymentNode("Deployment Node"); + SoftwareSystemInstance ai = deploymentNode.add(a); + SoftwareSystemInstance bi = deploymentNode.add(b); + SoftwareSystemInstance ci = deploymentNode.add(c); + + Set<ModelItem> elements = parser.parseExpression("element.properties[Technical Debt]==High", context()); + assertEquals(2, elements.size()); + 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 + 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_ReturnsRelationshipsAndImpliedRelationships_WhenUsingARelationshipTagEqualsExpression() { + Person user = model.addPerson("User"); + SoftwareSystem a = model.addSoftwareSystem("A"); + Container aa = a.addContainer("AA"); + SoftwareSystem b = model.addSoftwareSystem("B"); + Container bb = b.addContainer("BB"); + SoftwareSystem c = model.addSoftwareSystem("C"); + Container cc = c.addContainer("CC"); + + 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<ModelItem> 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_ReturnsRelationshipsAndImpliedRelationships_WhenUsingARelationshipPropertyEqualsExpression() { + Person user = model.addPerson("User"); + SoftwareSystem a = model.addSoftwareSystem("A"); + Container aa = a.addContainer("AA"); + SoftwareSystem b = model.addSoftwareSystem("B"); + Container bb = b.addContainer("BB"); + SoftwareSystem c = model.addSoftwareSystem("C"); + Container cc = c.addContainer("CC"); + + 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"); + + assertEquals(6, model.getRelationships().size()); + + Set<ModelItem> relationships = parser.parseExpression("relationship.properties[Secure]==Yes", context()); + assertEquals(2, relationships.size()); + assertTrue(relationships.contains(r1)); + + Relationship impliedRelationship = user.getEfferentRelationshipWith(a); + 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<ModelItem> 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<ModelItem> elements = parser.parseExpression("element!=->b", context); + assertEquals(1, elements.size()); + assertTrue(elements.contains(c)); + } + +} \ 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..922d5b287 --- /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"), null, "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..c34afec00 --- /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_WhenTheBaseViewIsNotAStaticOrDeploymentView() { + DslContext context = context(); + 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, Component, or Deployment 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/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 <identifier|canonical name>", e.getMessage()); + } + } + + @Test + void test_parse_ThrowsAnException_WhenTheIdentifierOrCanonicalNameIsNotSpecified() { + try { + parser.parse(context(), tokens("!element")); + fail(); + } catch (Exception e) { + assertEquals("Expected: !element <identifier|canonical name>", 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/FindElementsParserTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/FindElementsParserTests.java new file mode 100644 index 000000000..49bf3ddf2 --- /dev/null +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/FindElementsParserTests.java @@ -0,0 +1,55 @@ +package com.structurizr.dsl; + +import com.structurizr.model.Component; +import com.structurizr.model.Container; +import com.structurizr.model.Element; +import org.junit.jupiter.api.Test; + +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.*; + +class FindElementsParserTests extends AbstractTests { + + private final FindElementsParser parser = new FindElementsParser(); + + @Test + void test_parse_ThrowsAnException_WhenThereAreTooManyTokens() { + try { + parser.parse(context(), tokens("!elements", "expression", "tokens")); + fail(); + } catch (Exception e) { + assertEquals("Too many tokens, expected: !elements <expression>", e.getMessage()); + } + } + + @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"); + Component componentA = application.addComponent("A"); + Component componentB = application.addComponent("B"); + Component componentC = application.addComponent("C"); + + ContainerDslContext context = new ContainerDslContext(application); + context.setWorkspace(workspace); + IdentifiersRegister register = new IdentifiersRegister(); + register.register("application", application); + context.setIdentifierRegister(register); + + Set<Element> 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/java/com/structurizr/dsl/FindRelationshipParserTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/FindRelationshipParserTests.java new file mode 100644 index 000000000..91d9f6dd2 --- /dev/null +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/FindRelationshipParserTests.java @@ -0,0 +1,72 @@ +package com.structurizr.dsl; + +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.*; + +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 <identifier|canonical name>", e.getMessage()); + } + } + + @Test + void test_parse_ThrowsAnException_WhenTheIdentifierOrCanonicalNameIsNotSpecified() { + try { + parser.parse(context(), tokens("!relationship")); + fail(); + } catch (Exception e) { + assertEquals("Expected: !relationship <identifier|canonical name>", 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_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() { + SoftwareSystem a = model.addSoftwareSystem("A"); + SoftwareSystem b = model.addSoftwareSystem("B"); + Relationship relationship = a.uses(b, "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/FindRelationshipsParserTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/FindRelationshipsParserTests.java new file mode 100644 index 000000000..916f23407 --- /dev/null +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/FindRelationshipsParserTests.java @@ -0,0 +1,55 @@ +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 FindRelationshipsParserTests extends AbstractTests { + + private final FindRelationshipsParser parser = new FindRelationshipsParser(); + + @Test + void test_parse_ThrowsAnException_WhenThereAreTooManyTokens() { + try { + parser.parse(context(), tokens("!relationships", "expression", "tokens")); + fail(); + } catch (Exception e) { + assertEquals("Too many tokens, expected: !relationships <expression>", e.getMessage()); + } + } + + @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"); + 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<Relationship> 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/java/com/structurizr/dsl/GroupParserTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/GroupParserTests.java new file mode 100644 index 000000000..b4415f1d1 --- /dev/null +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/GroupParserTests.java @@ -0,0 +1,130 @@ +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 final GroupParser parser = new GroupParser(); + + @Test + void parseContext_ThrowsAnException_WhenThereAreTooManyTokens() { + try { + parser.parseContext(null, tokens("group", "name", "{", "extra")); + fail(); + } catch (Exception e) { + assertEquals("Too many tokens, expected: group <name> {", e.getMessage()); + } + } + + @Test + void parseContext_ThrowsAnException_WhenTheNameIsMissing() { + try { + parser.parseContext(null, tokens("group")); + fail(); + } catch (Exception e) { + assertEquals("Expected: group <name> {", e.getMessage()); + } + } + + @Test + void parseContext_ThrowsAnException_WhenTheBraceIsMissing() { + try { + parser.parseContext(null, tokens("group", "Name", "foo")); + fail(); + } catch (Exception e) { + assertEquals("Expected: group <name> {", e.getMessage()); + } + } + + @Test + void parseContext() { + ElementGroup group = parser.parseContext(context(), tokens("group", "Group 1", "{")); + assertEquals("Group 1", group.getName()); + assertTrue(group.getElements().isEmpty()); + } + + @Test + void parseContext_NestedGroup_ThrowsAnExceptionWhenNestedGroupsAreNotConfigured() { + ModelDslContext context = new ModelDslContext(new ElementGroup("Group 1")); + context.setWorkspace(workspace); + + try { + 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()); + } + } + + @Test + void parseContext_NestedGroup() { + workspace.getModel().addProperty("structurizr.groupSeparator", "/"); + ModelDslContext context = new ModelDslContext(new ElementGroup("Group 1")); + context.setWorkspace(workspace); + + 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 <name>", e.getMessage()); + } + } + + @Test + void parseProperty_ThrowsAnException_WhenTheNameIsMissing() { + try { + parser.parseProperty(null, tokens("group")); + fail(); + } catch (Exception e) { + assertEquals("Expected: group <name>", 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/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..8a141ce65 --- /dev/null +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/IdentifierRegisterTests.java @@ -0,0 +1,123 @@ +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.*; + +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", 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()); + } + } + +} \ 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/ImageViewContentParserTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/ImageViewContentParserTests.java new file mode 100644 index 000000000..40cb94dab --- /dev/null +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/ImageViewContentParserTests.java @@ -0,0 +1,412 @@ +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.Tag; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class ImageViewContentParserTests extends AbstractTests { + + private ImageViewContentParser parser; + private ImageView imageView; + + @BeforeEach + 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(); + parser.parsePlantUML(context, null, tokens("plantuml")); + fail(); + } catch (Exception e) { + assertEquals("Expected: plantuml <source|file|url|viewKey>", e.getMessage()); + } + } + + @Test + void test_parsePlantUML_ThrowsAnException_WhenUsingAFileNameInRestrictedMode() { + try { + ImageViewDslContext context = new ImageViewDslContext(imageView); + context.setWorkspace(workspace); + parser = new ImageViewContentParser(); + parser.parsePlantUML(context, null, tokens("plantuml", "image.puml")); + fail(); + } catch (Exception e) { + assertEquals("plantuml <file> 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()); + } + } + + @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(); + 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(); + 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(); + 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); + 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(); + parser.parsePlantUML(context, null, tokens("plantuml", "SystemLandscape")); + assertEquals("System Landscape View", imageView.getTitle()); + assertEquals("Description", imageView.getDescription()); + 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 + void test_parseMermaid_ThrowsAnException_WithTooFewTokens() { + try { + ImageViewDslContext context = new ImageViewDslContext(imageView); + context.setWorkspace(workspace); + parser = new ImageViewContentParser(); + parser.parseMermaid(context, null, tokens("mermaid")); + fail(); + } catch (Exception e) { + assertEquals("Expected: mermaid <source|file|url|viewKey>", e.getMessage()); + } + } + + @Test + void test_parseMermaid_ThrowsAnException_WhenUsingAFileNameInRestrictedMode() { + try { + ImageViewDslContext context = new ImageViewDslContext(imageView); + context.setWorkspace(workspace); + parser = new ImageViewContentParser(); + parser.parseMermaid(context, null, tokens("mermaid", "image.mmd")); + fail(); + } catch (Exception e) { + assertEquals("mermaid <file> 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("Image views via HTTP are not permitted (feature structurizr.feature.dsl.http is not enabled)", e.getMessage()); + } + } + + @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(); + 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(); + 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(); + 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); + 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(); + parser.parseMermaid(context, null, tokens("mermaid", "SystemLandscape")); + 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 + void test_parseKroki_ThrowsAnException_WithTooFewTokens() { + try { + ImageViewDslContext context = new ImageViewDslContext(imageView); + context.setWorkspace(workspace); + parser = new ImageViewContentParser(); + parser.parseKroki(context, null, tokens("kroki")); + fail(); + } catch (Exception e) { + assertEquals("Expected: kroki <format> <source|file|url>", e.getMessage()); + } + } + + @Test + void test_parseKroki_ThrowsAnException_WhenUsingAFileNameInRestrictedMode() { + try { + parser = new ImageViewContentParser(); + parser.parseKroki(new ImageViewDslContext(imageView), null, tokens("kroki", "plantuml", "image.puml")); + fail(); + } catch (Exception e) { + assertEquals("kroki plantuml <file> 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()); + } + } + + @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(); + 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(); + 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(); + 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 { + parser = new ImageViewContentParser(); + parser.parseImage(new ImageViewDslContext(imageView), null, tokens("image", "image.png")); + fail(); + } catch (Exception e) { + assertEquals("image <file> is not permitted (feature structurizr.feature.dsl.filesystem is not enabled)", e.getMessage()); + } + } + + @Test + @Tag("IntegrationTest") + void test_parseImage() { + 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 + @Tag("IntegrationTest") + void test_parseImage_Url_Light() { + parser = new ImageViewContentParser(); + ImageViewDslContext context = new ImageViewDslContext(imageView); + context.setColorScheme(ColorScheme.Light); + 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 + @Tag("IntegrationTest") + void test_parseImage_Url_Dark() { + parser = new ImageViewContentParser(); + ImageViewDslContext context = new ImageViewDslContext(imageView); + context.setColorScheme(ColorScheme.Dark); + 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/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/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..706454d33 --- /dev/null +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/ImplicitRelationshipParserTests.java @@ -0,0 +1,273 @@ +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 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); + context.setWorkspace(workspace); + model.setImpliedRelationshipsStrategy(new CreateImpliedRelationshipsUnlessAnyRelationshipExistsStrategy()); + + return context; + } + + @Test + void test_parse_ThrowsAnException_WhenThereAreTooManyTokens() { + try { + parser.parse((ElementDslContext)null, tokens("->", "destination", "description", "technology", "tags", "extra"), archetype); + fail(); + } catch (Exception e) { + assertEquals("Too many tokens, expected: -> <identifier> [description] [technology] [tags]", e.getMessage()); + } + } + + @Test + void test_parse_ThrowsAnException_WhenTheDestinationIdentifierIsMissing() { + try { + parser.parse((ElementDslContext)null, tokens("->"), archetype); + fail(); + } catch (Exception e) { + assertEquals("Expected: -> <identifier> [description] [technology] [tags]", e.getMessage()); + } + } + + @Test + void test_parse_ThrowsAnException_WhenTheDestinationElementIsNotDefined() { + Person user = model.addPerson("User", "Description"); + ElementDslContext context = context(user); + IdentifiersRegister elements = new IdentifiersRegister(); + context.setIdentifierRegister(elements); + + try { + parser.parse(context, tokens("->", "destination"), archetype); + 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"); + 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"), archetype); + + 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"); + 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"), archetype); + + 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"); + 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"), archetype); + + 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"); + 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()); + assertEquals("HTTP", r.getTechnology()); + 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"); + SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System", "Description"); + Container container = softwareSystem.addContainer("Container", "Description", "Technology"); + ElementDslContext 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"), archetype); + 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_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<Relationship> 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<Relationship> 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 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..04d17f55a --- /dev/null +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/ImpliedRelationshipsParserTests.java @@ -0,0 +1,97 @@ +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 ImpliedRelationshipsParserTests extends AbstractTests { + + private final ImpliedRelationshipsParser parser = new ImpliedRelationshipsParser(); + + @Test + void test_parse_ThrowsAnException_WhenThereAreTooManyTokens() { + try { + parser.parse(context(), tokens("!impliedRelationships", "boolean", "extra"), null); + fail(); + } catch (Exception e) { + assertEquals("Too many tokens, expected: !impliedRelationships <true|false|fqcn>", e.getMessage()); + } + } + + @Test + void test_parse_ThrowsAnException_WhenNoFlagIsSpecified() { + try { + parser.parse(context(), tokens("!impliedRelationships"), null); + fail(); + } catch (Exception e) { + assertEquals("Expected: !impliedRelationships <true|false|fqcn>", e.getMessage()); + } + } + + @Test + void test_parse_SetsTheStrategy_WhenFalseIsSpecified() { + 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); + + 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(".")); + + assertEquals("com.structurizr.model.CreateImpliedRelationshipsUnlessAnyRelationshipExistsStrategy", workspace.getModel().getImpliedRelationshipsStrategy().getClass().getCanonicalName()); + } + + @Test + 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_WhenACustomStrategyIsUsedAndCustomStrategiesAreNotEnabled() { + try { + 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 (feature structurizr.feature.dsl.plugins is not enabled)", e.getMessage()); + } + } + + @Test + void test_parse_ThrowsAnException_WhenACustomStrategyIsUsedButCannotBeLoaded() { + try { + 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()); + } + } + + @Test + 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()); + } + +} \ 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..803a9a545 --- /dev/null +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/IncludeParserTests.java @@ -0,0 +1,87 @@ +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 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 { + 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 <file|directory|url>", e.getMessage()); + } + } + + @Test + void test_parse_ThrowsAnException_WhenAFileIsNotSpecified() { + try { + DslContext context = context(); + context.getFeatures().enable(Features.INCLUDE); + parser.parse(context, null, tokens("!include")); + fail(); + } catch (Exception e) { + assertEquals("Expected: !include <file|directory|url>", 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 <file> 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/InfrastructureNodeParserTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/InfrastructureNodeParserTests.java new file mode 100644 index 000000000..1fbd90759 --- /dev/null +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/InfrastructureNodeParserTests.java @@ -0,0 +1,157 @@ +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(); + 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"), archetype); + 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"), archetype); + 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"), archetype); + + 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"), archetype); + + 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"), archetype); + + 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"), archetype); + + 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_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 { + 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..01067aa09 --- /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"), null, "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/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 <identifier> [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 <identifier> [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/ModelItemParserTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/ModelItemParserTests.java new file mode 100644 index 000000000..fb1091782 --- /dev/null +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/ModelItemParserTests.java @@ -0,0 +1,107 @@ +package com.structurizr.dsl; + +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 { + SoftwareSystemDslContext 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"); + SoftwareSystemDslContext 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", ""); + SoftwareSystemDslContext 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()); + } + +} \ No newline at end of file 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> [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/java/com/structurizr/dsl/NameValueParserTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/NameValueParserTests.java new file mode 100644 index 000000000..7c6a738ae --- /dev/null +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/NameValueParserTests.java @@ -0,0 +1,68 @@ +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 NameValueParserTests extends AbstractTests { + + private final NameValueParser parser = new NameValueParser(); + + @Test + void test_parseConstant_ThrowsAnException_WhenThereAreTooManyTokens() { + try { + parser.parseConstant(tokens("!const", "name", "value", "extra")); + fail(); + } catch (Exception e) { + assertEquals("Too many tokens, expected: !const <name> <value>", e.getMessage()); + } + } + + @Test + void test_parseConstant_ThrowsAnException_WhenNoNameOrValueIsSpecified() { + try { + parser.parseConstant(tokens("!const")); + fail(); + } catch (Exception e) { + assertEquals("Expected: !const <name> <value>", e.getMessage()); + } + } + + @Test + void test_parseConstant_ThrowsAnException_WhenNoValueIsSpecified() { + try { + parser.parseConstant(tokens("!const", "name")); + fail(); + } catch (Exception e) { + assertEquals("Expected: !const <name> <value>", e.getMessage()); + } + } + + @Test + void test_parseConstant_ThrowsAnException_WhenNameContainsDisallowedCharacters() { + try { + parser.parseConstant(tokens("!const", "${NAME}", "value")); + fail(); + } catch (Exception e) { + assertEquals("Constant/variable names must only contain the following characters: a-zA-Z0-9-_.", e.getMessage()); + } + } + + @Test + void test_parseConstant_CreatesAConstant() { + NameValuePair nameValuePair = parser.parseConstant(tokens("!const", "name", "value")); + assertEquals("name", nameValuePair.getName()); + assertEquals("value", nameValuePair.getValue()); + assertEquals(NameValueType.Constant, nameValuePair.getType()); + } + + @Test + void test_parseVariable_CreatesAVariable() { + NameValuePair nameValuePair = parser.parseVariable(tokens("!var", "name", "value")); + assertEquals("name", nameValuePair.getName()); + assertEquals("value", nameValuePair.getValue()); + assertEquals(NameValueType.Variable, nameValuePair.getType()); + } + +} \ 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: <identifier> -/> <identifier> [description]", e.getMessage()); + } + } + + @Test + void test_parse_ThrowsAnException_WhenTheDestinationIdentifierIsMissing() { + try { + parser.parse(null, tokens("source", "-/>")); + fail(); + } catch (Exception e) { + assertEquals("Not enough tokens, expected: <identifier> -/> <identifier> [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<Relationship> 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<Relationship> 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<Relationship> 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<Relationship> 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<Relationship> 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<Relationship> 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/java/com/structurizr/dsl/PersonParserTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/PersonParserTests.java new file mode 100644 index 000000000..38960e0e0 --- /dev/null +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/PersonParserTests.java @@ -0,0 +1,81 @@ +package com.structurizr.dsl; + +import com.structurizr.model.Person; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +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"), archetype); + 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"), archetype); + fail(); + } catch (Exception e) { + assertEquals("Expected: person <name> [description] [tags]", e.getMessage()); + } + } + + @Test + void test_parse_CreatesAPerson() { + parser.parse(context(), tokens("person", "User"), archetype); + + assertEquals(1, model.getElements().size()); + Person user = model.getPersonWithName("User"); + assertNotNull(user); + assertEquals("", user.getDescription()); + assertEquals("Element,Person", user.getTags()); + } + + @Test + void test_parse_CreatesAPersonWithADescription() { + parser.parse(context(), tokens("person", "User", "Description"), archetype); + + assertEquals(1, model.getElements().size()); + Person user = model.getPersonWithName("User"); + assertNotNull(user); + assertEquals("Description", user.getDescription()); + assertEquals("Element,Person", user.getTags()); + } + + @Test + void test_parse_CreatesAPersonWithADescriptionAndTags() { + 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()); + 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/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: <name> <description> [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: <name> <description> [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: <name> <description> [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/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/RelationshipStyleParserTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/RelationshipStyleParserTests.java new file mode 100644 index 000000000..d59d7081d --- /dev/null +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/RelationshipStyleParserTests.java @@ -0,0 +1,438 @@ +package com.structurizr.dsl; + +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; + } + + private StylesDslContext stylesDslContext() { + StylesDslContext context = new StylesDslContext(); + context.setWorkspace(workspace); + + return context; + } + + @Test + void test_parseRelationshipStyle_ThrowsAnException_WhenThereAreTooManyTokens() { + try { + parser.parseRelationshipStyle(stylesDslContext(), 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(stylesDslContext(), tokens("relationship")); + fail(); + } catch (Exception e) { + assertEquals("Expected: relationship <tag> {", e.getMessage()); + } + } + + @Test + void test_parseRelationshipStyle_ThrowsAnException_WhenTheTagIsEmpty() { + try { + parser.parseRelationshipStyle(stylesDslContext(), tokens("relationship", "")); + fail(); + } catch (Exception e) { + assertEquals("A tag must be specified", e.getMessage()); + } + } + + @Test + void test_parseRelationshipStyle_CreatesAnRelationshipStyle() { + parser.parseRelationshipStyle(stylesDslContext(), 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(stylesDslContext(), 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()); + } + + @Test + void test_parseJump_ThrowsAnException_WhenThereAreTooManyTokens() { + try { + parser.parseJump(relationshipStyleDslContext(), tokens("jump", "boolean", "extra")); + fail(); + } catch (Exception e) { + assertEquals("Too many tokens, expected: jump <true|false>", e.getMessage()); + } + } + + @Test + void test_parseJump_ThrowsAnException_WhenTheValueIsMissing() { + try { + parser.parseJump(relationshipStyleDslContext(), tokens("jump")); + fail(); + } catch (Exception e) { + assertEquals("Expected: jump <true|false>", 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 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..290c56a0b --- /dev/null +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/SoftwareSystemInstanceParserTests.java @@ -0,0 +1,149 @@ +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 final 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("live", new DeploymentEnvironment("Live")); + elements.register("group", new DeploymentGroup(new DeploymentEnvironment("Live"), "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("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")); + + 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..29f9b7840 --- /dev/null +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/SoftwareSystemParserTests.java @@ -0,0 +1,81 @@ +package com.structurizr.dsl; + +import com.structurizr.model.SoftwareSystem; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +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"), archetype); + 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"), archetype); + fail(); + } catch (Exception e) { + assertEquals("Expected: softwareSystem <name> [description] [tags]", e.getMessage()); + } + } + + @Test + void test_parse_CreatesASoftwareSystem() { + parser.parse(context(), tokens("softwareSystem", "Name"), archetype); + + assertEquals(1, model.getElements().size()); + SoftwareSystem softwareSystem = model.getSoftwareSystemWithName("Name"); + assertNotNull(softwareSystem); + assertEquals("", softwareSystem.getDescription()); + assertEquals("Element,Software System", softwareSystem.getTags()); + } + + @Test + void test_parse_CreatesASoftwareSystemWithADescription() { + parser.parse(context(), tokens("softwareSystem", "Name", "Description"), archetype); + + assertEquals(1, model.getElements().size()); + SoftwareSystem softwareSystem = model.getSoftwareSystemWithName("Name"); + assertNotNull(softwareSystem); + assertEquals("Description", softwareSystem.getDescription()); + assertEquals("Element,Software System", softwareSystem.getTags()); + } + + @Test + void test_parse_CreatesASoftwareSystemWithADescriptionAndTags() { + 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()); + 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 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..61b0a0b4a --- /dev/null +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/StaticViewAnimationStepParserTests.java @@ -0,0 +1,49 @@ +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.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|element expression> [identifier|element expression...]", e.getMessage()); + } + } + + @Test + void test_parseImplicit_ThrowsAnException_WhenElementsAreMissing() { + try { + parser.parse((StaticViewAnimationDslContext) null, tokens()); + fail(); + } catch (Exception e) { + assertEquals("Expected: <identifier|element expression> [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()); + } + } + +} \ 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..043a723fe --- /dev/null +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/StaticViewContentParserTests.java @@ -0,0 +1,830 @@ +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_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"); + 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_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"); + 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_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"); + SystemContextViewDslContext context = new SystemContextViewDslContext(view); + context.setWorkspace(workspace); + + 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()); + } + + @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..d0438c6b6 --- /dev/null +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/TerminologyParserTests.java @@ -0,0 +1,173 @@ +package com.structurizr.dsl; + +import com.structurizr.view.MetadataSymbols; +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_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()); + } + + @Test + void test_parseMetadataSymbols_ThrowsAnException_WhenNoSymbolTypeIsSpecified() { + try { + parser.parseMetadataSymbols(context(), tokens("metadata")); + fail(); + } catch (Exception e) { + assertEquals("Expected: metadata <square|round|curly|angle|double-angle|none>", 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/java/com/structurizr/dsl/ThemeParserTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/ThemeParserTests.java new file mode 100644 index 000000000..28d3daa2b --- /dev/null +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/ThemeParserTests.java @@ -0,0 +1,143 @@ +package com.structurizr.dsl; + +import org.junit.jupiter.api.Test; + +import java.io.File; + +import static org.junit.jupiter.api.Assertions.*; + +class ThemeParserTests extends AbstractTests { + + private final ThemeParser parser = new ThemeParser(); + + @Test + void test_parseTheme_ThrowsAnException_WhenThereAreTooManyTokens() { + try { + parser.parseTheme(context(), null, tokens("theme", "url", "extra")); + fail(); + } catch (Exception e) { + assertEquals("Too many tokens, expected: theme <url|file>", e.getMessage()); + } + } + + @Test + void test_parseTheme_ThrowsAnException_WhenNoThemeIsSpecified() { + try { + parser.parseTheme(context(), null, tokens("theme")); + fail(); + } catch (Exception e) { + assertEquals("Expected: theme <url|file>", e.getMessage()); + } + } + + @Test + void test_parseTheme_AddsTheTheme_WhenAThemeIsSpecified() { + 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]); + } + + @Test + void test_parseTheme_AddsTheTheme_WhenTheDefaultThemeIsSpecified() { + 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]); + } + + @Test + void test_parseThemes_ThrowsAnException_WhenNoThemesAreSpecified() { + try { + parser.parseThemes(context(), null, tokens("themes")); + fail(); + } catch (Exception e) { + assertEquals("Expected: themes <url|file> [url|file] ... [url|file]", e.getMessage()); + } + } + + @Test + void test_parseThemes_AddsTheTheme_WhenOneThemeIsSpecified() { + 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]); + } + + @Test + void test_parseThemes_AddsTheThemes_WhenMultipleThemesAreSpecified() { + 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]); + 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(), 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() { + try { + 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")); + } + } + + @Test + void test_parseTheme_ThrowsAnException_WhenTheThemeFileIsADirectory() { + try { + 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")); + } + } + + @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")); + + 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_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")); + fail(); + } catch (Exception e) { + assertEquals("File-based themes are not permitted (feature structurizr.feature.dsl.filesystem is not enabled)", e.getMessage()); + } + } + +} \ 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..3f3ba44b4 --- /dev/null +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/WorkspaceParserTests.java @@ -0,0 +1,131 @@ +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 final 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_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 { + 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/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/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/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/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 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..cc6d71aee --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/archetypes-for-defaults.dsl @@ -0,0 +1,38 @@ +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" + } + } + } + + 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..105e25b44 --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/archetypes-for-extension.dsl @@ -0,0 +1,33 @@ +workspace { + + model { + archetypes { + softwaresystem { + tag "Default Tag" + } + + externalSoftwareSystem = softwareSystem { + tag "External Software System" + } + + -> { + technology "Default Technology" + tag "Default Tag" + } + + sync = -> { + tag "Synchronous" + } + + https = --sync-> { + 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-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 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 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..d892dcb5d --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/archetypes.dsl @@ -0,0 +1,51 @@ +workspace { + + model { + archetypes { + application = container { + tag "Application" + } + datastore = container { + tag "Datastore" + } + + 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" + } + + 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" { + a --https-> this "Makes API calls using" + } + customerRepository = repository "Customer Repository" { + customerController -> this + this -> db + } + } + } + } + } + +} \ 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..6cca65aed --- /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" + + 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" + + 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..ec0dc243b --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/big-bank-plc/internet-banking-system.dsl @@ -0,0 +1,189 @@ +!const 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..d5d051be7 --- /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" + +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" + + 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..ac7dbf561 --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/big-bank-plc/system-landscape.dsl @@ -0,0 +1,23 @@ +!const 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/bulk-operations.dsl b/structurizr-dsl/src/test/resources/dsl/bulk-operations.dsl new file mode 100644 index 000000000..d1833badc --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/bulk-operations.dsl @@ -0,0 +1,71 @@ +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" { + tags "Tag" + } + } + + !elements "element.parent==application1" { + -> databaseSchema1 "Uses 2" { + tags "Tag" + } + } + } + + !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" { + tags "Tag" + } + } + + !elements "element.parent==application2" { + this -> databaseSchema2 "Uses 2" { + tags "Tag" + } + } + + !elements "element.parent==application2" { + -> softwareSystem2.databaseSchema2 "Uses 3" { + tags "Tag" + } + } + + !elements "element.parent==application2" { + -> databaseSchema2 "Uses 4" { + tags "Tag" + } + } + } + } + +} \ No newline at end of file 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/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 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 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/decisions/adrtools/0001-record-architecture-decisions.md b/structurizr-dsl/src/test/resources/dsl/decisions/adrtools/0001-record-architecture-decisions.md new file mode 100644 index 000000000..f30860000 --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/decisions/adrtools/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/decisions/adrtools/0002-implement-as-shell-scripts.md b/structurizr-dsl/src/test/resources/dsl/decisions/adrtools/0002-implement-as-shell-scripts.md new file mode 100644 index 000000000..8e6ea15e6 --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/decisions/adrtools/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/decisions/adrtools/0003-single-command-with-subcommands.md b/structurizr-dsl/src/test/resources/dsl/decisions/adrtools/0003-single-command-with-subcommands.md new file mode 100644 index 000000000..f64db8da1 --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/decisions/adrtools/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/decisions/adrtools/0004-markdown-format.md b/structurizr-dsl/src/test/resources/dsl/decisions/adrtools/0004-markdown-format.md new file mode 100644 index 000000000..86a21bf7e --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/decisions/adrtools/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/decisions/adrtools/0005-help-comments.md b/structurizr-dsl/src/test/resources/dsl/decisions/adrtools/0005-help-comments.md new file mode 100644 index 000000000..b19bf0fb1 --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/decisions/adrtools/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/decisions/adrtools/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 new file mode 100644 index 000000000..4a3485f79 --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/decisions/adrtools/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/decisions/adrtools/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 new file mode 100644 index 000000000..a649b2356 --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/decisions/adrtools/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/decisions/adrtools/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 new file mode 100644 index 000000000..4146f11df --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/decisions/adrtools/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/decisions/adrtools/0009-help-scripts.md b/structurizr-dsl/src/test/resources/dsl/decisions/adrtools/0009-help-scripts.md new file mode 100644 index 000000000..0146d127c --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/decisions/adrtools/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/decisions/adrtools/0010-asciidoc-format.md b/structurizr-dsl/src/test/resources/dsl/decisions/adrtools/0010-asciidoc-format.md new file mode 100644 index 000000000..edb613d1a --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/decisions/adrtools/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/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 <https://github.com/joelparkerhenderson/architecture_decision_record> +- 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/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 <https://github.com/joelparkerhenderson/architecture_decision_record> +* 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 <https://help.github.com/articles/licensing-a-repository/>. + +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 <https://adr.github.io/madr/tooling.html>. 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: <https://github.com/npryce/adr-tools/blob/master/tests/generate-contents-with-header-and-footer.sh>. + +### 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 <https://github.com/joelparkerhenderson/architecture_decision_record#adr-file-name-conventions> + +## 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: <https://github.com/adr/madr/issues/2> + +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 <https://shields.io> 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 000000000..20ced2835 Binary files /dev/null and b/structurizr-dsl/src/test/resources/dsl/decisions/madr/0008-example-badge.png differ diff --git a/structurizr-dsl/src/test/resources/dsl/decisions/madr/0008-example-separate-heading.png b/structurizr-dsl/src/test/resources/dsl/decisions/madr/0008-example-separate-heading.png new file mode 100644 index 000000000..8a898033e Binary files /dev/null and b/structurizr-dsl/src/test/resources/dsl/decisions/madr/0008-example-separate-heading.png differ 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 000000000..768c2d145 Binary files /dev/null and b/structurizr-dsl/src/test/resources/dsl/decisions/madr/0008-example-table.png differ diff --git a/structurizr-dsl/src/test/resources/dsl/decisions/madr/0009-support-links-between-adrs-inside-an-adrs.md b/structurizr-dsl/src/test/resources/dsl/decisions/madr/0009-support-links-between-adrs-inside-an-adrs.md new file mode 100644 index 000000000..f08efe328 --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/decisions/madr/0009-support-links-between-adrs-inside-an-adrs.md @@ -0,0 +1,79 @@ +--- +parent: Decisions +nav_order: 9 +--- +# Support Links To Other ADRs Inside an ADR + +## Context and Problem Statement + +A decision might point to another decision. +For instance, if a decision is a follow-up to another decision. +This should be supported by MADR, too. + +Technical Story: <https://github.com/adr/madr/issues/9> + +## 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 <https://github.com/adr/madr/issues/2>) + +### 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 <https://gist.github.com/FaKeller/2f9c63b6e1d436abb7358b68bf396f57>) +* 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: `<option 1>` + +Idea taken from <https://github.com/schubmat/DecisionCapture/blob/master/templates/captureTemplate_full.md> + +* Good, because kept in Markdown as is +* Bad, because could be mixed up with an HTML element diff --git a/structurizr-dsl/src/test/resources/dsl/decisions/madr/0013-example.png b/structurizr-dsl/src/test/resources/dsl/decisions/madr/0013-example.png new file mode 100644 index 000000000..9facf8226 Binary files /dev/null and b/structurizr-dsl/src/test/resources/dsl/decisions/madr/0013-example.png differ diff --git a/structurizr-dsl/src/test/resources/dsl/decisions/madr/0013-use-yaml-front-matter-for-meta-data.md b/structurizr-dsl/src/test/resources/dsl/decisions/madr/0013-use-yaml-front-matter-for-meta-data.md new file mode 100644 index 000000000..2788effc9 --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/decisions/madr/0013-use-yaml-front-matter-for-meta-data.md @@ -0,0 +1,65 @@ +--- +parent: Decisions +nav_order: 13 +--- +# Use YAML front matter for metadata + +## Context and Problem Statement + +MADR offers the fields "Status", "Deciders", and "Date". +These are a kind of metadata fields. +Should this data be included in the ADR directly, or should it be separated somehow? + +## Decision Drivers + +* Easy to read +* Easy to write + +## Considered Options + +* Use YAML front matter +* Use plain Markdown everywhere + +## 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 +--- +status: accepted +deciders: +date: +--- + +## Context and problem statement + +We want to record architectural decisions made in this project. +Which format and structure should these records follow? +``` + +Rendered output: + +![adr-013 rendered output](0013-example.png) + +* Good, because it shortens the body (essence of the ADR) +* Good, because tools can handle it more easily +* Good, because indicates the lower importance of the data +* Bad, because pretends to be more accurate than it can be (e.g., possible status values) +* Bad, because rendering not standardized +* Bad, because not all Markdown parsers can parse it + +## Use plain Markdown everywhere + +* Good, because all parsers can handle it +* Bad, because special markdown parsing tooling is needed +* Bad, because metadata is handled the same way as the content + +## More Information + +[ADR-0008](0008-add-status-field.md) reasons on adding metadata (such as status). diff --git a/structurizr-dsl/src/test/resources/dsl/decisions/madr/0014-allow-neutral-arguments.md b/structurizr-dsl/src/test/resources/dsl/decisions/madr/0014-allow-neutral-arguments.md new file mode 100644 index 000000000..bfd221988 --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/decisions/madr/0014-allow-neutral-arguments.md @@ -0,0 +1,62 @@ +--- +parent: Decisions +nav_order: 14 +--- +# Allow "neutral" arguments + +## Context and Problem Statement + +Sometimes, one wants to write down an argument, which is neither pro nor con. + +## Considered Options + +* Neutral, because … +* OK, because … +* Good/bad, because … +* Interesting, because … +* Indifferent, because … + +## Decision Outcome + +Chosen option: "Neutral, because …", because + +* it fits best. +* it is consistent to patterns: +/0/- + +## Pros and Cons of the Options + +### Neutral, because … + +In the following, a full pro/con list is given: + +Example: + +* The proposed solution is good, because it resolves the force 1. +* The proposed solution is good, because it addresses decision driver 2. +* The proposed solution is good, because it mitigates the technical risk / dept with respect to decision driver 4. +* The proposed solution is good, because it removes architectural smell … +* The proposed solution is good, because it addresses stakeholder concern … +* The proposed solution is good, because it has a positive effect on the performance (quality property). +* The proposed solution is neutral, because it is indifferent to decision driver 2. +* The proposed solution is bad, because it does not address decision driver 3. +* The proposed solution is bad, because it has a negative effect on the maintainability (quality property). + +Shorter example for pros and cons: + +* The proposed solution is good with respect to resolving the force 1. +* The considered option is a good solution, because it resolves force1 and the non-resolving of force 1 is OK, … + +### OK, because … + +Real world example: <https://github.com/island-is/island.is/blob/main/handbook/technical-overview/adr/0005-error-tracking-and-monitoring.md> + +```markdown +### Bugsnag + +- Good, because it offers a Slack integration for faster feedback. +- Good, because it offers a Github integration to link to possible commits and PRs. +- Good, because it offers bot front-side/server-side/serverless error tracking. +- OK, because it was ranked the **\#5** as the best Javascript (client-side) error logging service in a community survey. +- Bad, because it's expensive. (**\$199/mo** for **450k events** and **15 collaborators**) +- Bad, because it's pricing includes a fixed set of collaborators. +``` diff --git a/structurizr-dsl/src/test/resources/dsl/decisions/madr/0015-include-consulting-informed-of-raci.md b/structurizr-dsl/src/test/resources/dsl/decisions/madr/0015-include-consulting-informed-of-raci.md new file mode 100644 index 000000000..b028fbb7c --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/decisions/madr/0015-include-consulting-informed-of-raci.md @@ -0,0 +1,44 @@ +--- +parent: Decisions +nav_order: 15 +--- +# Include "Consulted" and "Informed" of RACI + +## Context and Problem Statement + +We noticed an intersection between MADR and [RACI](https://en.wikipedia.org/wiki/Responsibility_assignment_matrix), and felt the need to add a "consulted" and "informed" field in addition to "deciders". +Would it be beneficial to "upstream" these fields to MADR? + +MADR Issue: [#62](https://github.com/adr/madr/issues/62). + +## Decision Drivers + +* MADR should contain fields important to the ADR decision process +* MADR template should be easy to understand +* MADR should be lightweight + +## Considered Options + +* Include "Consulted" and "Informed" of RACI +* Include all fields of RACI +* Do not include anything of RACI + +## Decision Outcome + +Chosen option: "Include 'Consulted' and 'Informed' of RACI", because comes out best (see below). + +## Pros and Cons of the Options + +### Include "Consulted" and "Informed" of RACI + +* Good, because these two roles of RACI are well understood. +* Good, because we make these fields optional, thus it keeps MADR still lightweight. +* Bad, because it adds two additional fields + +### Include all fields of RACI + +This would add "Responsible", "Accountable", "Consulted", and "Informed" + +* Good, because complete RACI would be included +* Bad, because get confused about who is "accountable" and who is "responsible". +* Bad, because if decisions are mostly taken by consensus in small committees, then there might not be an "accountable" person. diff --git a/structurizr-dsl/src/test/resources/dsl/decisions/madr/0016-outcome-before-detailed-pros-cons.md b/structurizr-dsl/src/test/resources/dsl/decisions/madr/0016-outcome-before-detailed-pros-cons.md new file mode 100644 index 000000000..9caa315e8 --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/decisions/madr/0016-outcome-before-detailed-pros-cons.md @@ -0,0 +1,74 @@ +--- +parent: Decisions +nav_order: 16 +--- +# Outcome before Detailed Pros and Cons + +## Context and Problem Statement + +MADR aims to list pros and cons of each option. +Where should that list be placed? + +## Decision Drivers + +* Most important information should be above the fold. +* MADR should be easy to write. +* MADR should be easy to read. + +## Considered Options + +* Section "Pros and Cons of the Options" after "Decision Outcome" +* Section "Pros and Cons of the Options" before "Decision Outcome" + +## Decision Outcome + +Chosen option: "Section 'Pros and Cons of the Options' after 'Decision Outcome'", because this keeps the available options and the decision outcome close together. +One gets the decision outcome above the fold and preserve a nice logical flow. +Hence, one can see the "Pros and Cons" section as a sort of "appendix" to the considered options section. +It is almost like the "Considered Options" section implies the following sentence: "For a more detailed analysis of these options, refer to pros and cons". + +## Pros and Cons of the Options + +### Section "Pros and Cons of the Options" after "Decision Outcome" + +Illustration: + +```markdown +## Considered Options + +... + +## Decision Outcome + +... + +## Pros and Cons of the Options + +... +``` + +* Good, because this keeps the available options and the decision outcome close together. +* Bad, because readers might be confused, because the logical flow broken: First comes the result, then the detailed arguments. + +### Section "Pros and Cons of the Options" before "Decision Outcome" + +Illustration: + +```markdown +## Considered Options + +... + +## Pros and Cons of the Options + +... + +## Decision Outcome + +... +``` + +* Good, because the logical flow is kept +* Bad, because the decision outcome is not close to the options +* Bad, because "small" MADRs with the list of options and the decision outcome (and not containing the section "Pros and Cons of the Options") +* Bad, because "small" MADRs with the list of options and the decision outcome (and not containing the section "Pros and Cons of the Options") diff --git a/structurizr-dsl/src/test/resources/dsl/decisions/madr/0017-use-same-format-for-outcomes-and-options.md b/structurizr-dsl/src/test/resources/dsl/decisions/madr/0017-use-same-format-for-outcomes-and-options.md new file mode 100644 index 000000000..bbf876996 --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/decisions/madr/0017-use-same-format-for-outcomes-and-options.md @@ -0,0 +1,26 @@ +--- +parent: Decisions +nav_order: 17 +--- +# Use Same Format for Outcomes and Options + +## Context and Problem Statement + +"Outcome" has "Positive Consequences" and "Negative Consequences" sections. "Options" just have a single list with "Good" and "Bad" prefixes. + +Ticket: [issue#75](https://github.com/adr/madr/issues/75) + +## Decision Drivers + +* Consistent design of MADR +* Allow easy copy and paste + +## Considered Options + +* Section "Consequences" listing positive and negative consequences as "Good, because" and "Bad, because" +* Section "Consequences" listing positive and negative consequences as "Positive, because" and "Negative, because" +* No sections "Consequences", "Positive Consequences", and "Negative Consequences" + +## Decision Outcome + +Chosen option: 'Section "Consequences" listing positive and negative consequences as "Good, because" and "Bad, because"', because resolves all forces. diff --git a/structurizr-dsl/src/test/resources/dsl/decisions/madr/0018-use-confirmation-as-heading.md b/structurizr-dsl/src/test/resources/dsl/decisions/madr/0018-use-confirmation-as-heading.md new file mode 100644 index 000000000..230f4f54a --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/decisions/madr/0018-use-confirmation-as-heading.md @@ -0,0 +1,30 @@ +--- +parent: Decisions +nav_order: 18 +--- +<!-- we need to disable MD025, because we use the different heading "ADR Template" in the homepage (see above) than it is foreseen in the template --> +<!-- markdownlint-disable-next-line MD025 --> +# Use "Confirmation" as Heading + +## Context and Problem Statement + +In MADR, we want to include some sort of check that the decision was implemented. +How to name the heading for the explanation? + +## Decision Drivers + +* Consistent with terms used in IT +* Common word + +## Considered Options + +* "Confirmation" +* "Validation" +* "Verification" + +## Decision Outcome + +Chosen option: "Confirmation", because "validation" is out of scope of the template. +There is a process leading to a "valid" ADR. +The other term "Verification" is often bound to a formal tool or formal procedure. +We wanted to enable also less formal checks. diff --git a/structurizr-dsl/src/test/resources/dsl/decisions/workspace.dsl b/structurizr-dsl/src/test/resources/dsl/decisions/workspace.dsl new file mode 100644 index 000000000..4ae222394 --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/decisions/workspace.dsl @@ -0,0 +1,19 @@ +workspace { + + !adrs adrtools com.structurizr.example.ExampleDecisionImporter + + model { + softwareSystem = softwareSystem "Software System" { + !decisions adrtools + + container "Container" { + !decisions madr madr + + component "Component" { + !decisions log4brains log4brains + } + } + } + } + +} \ 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..11566eb50 --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/deployment-environment-empty.dsl @@ -0,0 +1,9 @@ +workspace { + model { + de = deploymentEnvironment "DeploymentEnvironment" + + !element de { + dn = deploymentNode "DeploymentNode" + } + } +} \ No newline at end of file diff --git a/structurizr-dsl/src/test/resources/dsl/deployment-groups-flat.dsl b/structurizr-dsl/src/test/resources/dsl/deployment-groups-flat.dsl new file mode 100644 index 000000000..287b4abe6 --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/deployment-groups-flat.dsl @@ -0,0 +1,36 @@ +workspace { + + model { + softwareSystem = softwareSystem "Software System" { + database = container "DB" + api = container "API" { + -> database "Uses" + } + } + + deploymentEnvironment "WithoutDeploymentGroups" { + deploymentNode "Server 1" { + containerInstance api + containerInstance database + } + deploymentNode "Server 2" { + containerInstance api + containerInstance database + } + } + + deploymentEnvironment "WithDeploymentGroups" { + 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 + } + } + } + +} \ 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 diff --git a/structurizr-dsl/src/test/resources/dsl/deployment-view-animation.dsl b/structurizr-dsl/src/test/resources/dsl/deployment-view-animation.dsl new file mode 100644 index 000000000..a46afbbfd --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/deployment-view-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 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 new file mode 100644 index 000000000..1f2a383f0 --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/docs/workspace.dsl @@ -0,0 +1,31 @@ +workspace { + + !docs docs/workspace com.structurizr.example.ExampleDocumentationImporter + + model { + user = person "User" + softwareSystem = softwareSystem "Software System" { + !docs docs/softwaresystem + + container "Container" { + !docs docs/softwaresystem/container + + component "Component" { + !docs docs/softwaresystem/container/component/1.md + } + } + } + + 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-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 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/enterprise.dsl b/structurizr-dsl/src/test/resources/dsl/enterprise.dsl new file mode 100644 index 000000000..ea537ae91 --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/enterprise.dsl @@ -0,0 +1,9 @@ +workspace { + + model { + enterprise "Name" { + person "User" + } + } + +} \ 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..5465ce720 --- /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 { + !element 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..086f56c12 --- /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 { + !element 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..e43dd69a7 --- /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 { + // !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 + } + +} \ 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..d59fa686c --- /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 { + // !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 + } + +} \ 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/find-element-hierarchical.dsl b/structurizr-dsl/src/test/resources/dsl/find-element-hierarchical.dsl new file mode 100644 index 000000000..ecc74c77d --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/find-element-hierarchical.dsl @@ -0,0 +1,31 @@ +workspace { + + !identifiers hierarchical + + model { + a = softwareSystem "A" { + b = container "B" { + c = component "C" + + !element c { + properties { + "Name1" "Value1" + } + } + } + + !element b.c { + properties { + "Name2" "Value2" + } + } + } + + !element a.b.c { + properties { + "Name3" "Value3" + } + } + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/test/resources/dsl/find-element.dsl b/structurizr-dsl/src/test/resources/dsl/find-element.dsl new file mode 100644 index 000000000..a4b0e66cd --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/find-element.dsl @@ -0,0 +1,45 @@ +workspace extends amazon-web-services.dsl { + + model { + + !element "DeploymentNode://Live/Amazon Web Services" { + deploymentNode "New deployment node" { + infrastructureNode "New infrastructure node" { + -> route53 + } + } + } + + !element "DeploymentNode://Live/Amazon Web Services/US-East-1" { + deploymentNode "New deployment node 1" { + infrastructureNode "New infrastructure node 1" { + -> route53 + } + } + } + + !element region { + deploymentNode "New deployment node 2" { + infrastructureNode "New infrastructure node 2" { + -> route53 + } + } + } + + !element 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/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 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..6a355bb33 --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/groups-nested.dsl @@ -0,0 +1,48 @@ +workspace { + + model { + properties { + structurizr.groupSeparator / + } + + group "Organisation" { + group "Department A" { + a = softwareSystem "A" { + group "Capability 1" { + group "Service A" { + container "A API" { + group "a-api.jar" { + component "API Endpoint" { + group "API Layer" + } + component "Repository" { + group "Data Layer" + } + } + } + container "A Database" + } + group "Service B" { + container "B API" + container "B Database" + } + } + } + } + + group "Department B" { + b = softwareSystem "B" + } + + c = softwareSystem "C" + } + } + + 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-view.dsl b/structurizr-dsl/src/test/resources/dsl/image-view.dsl new file mode 100644 index 000000000..c443c4b61 --- /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 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/image.png b/structurizr-dsl/src/test/resources/dsl/image-views/image.png new file mode 100644 index 000000000..9ecfbfd32 Binary files /dev/null and b/structurizr-dsl/src/test/resources/dsl/image-views/image.png differ diff --git a/structurizr-dsl/src/test/resources/dsl/image-views/image.svg b/structurizr-dsl/src/test/resources/dsl/image-views/image.svg new file mode 100644 index 000000000..fe686329a --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/image-views/image.svg @@ -0,0 +1 @@ +<?xml version="1.0" encoding="us-ascii" standalone="no"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" contentStyleType="text/css" height="120px" preserveAspectRatio="none" style="width:113px;height:120px;background:#FFFFFF;" version="1.1" viewBox="0 0 113 120" width="113px" zoomAndPan="magnify"><defs/><g><line style="stroke:#181818;stroke-width:0.5;stroke-dasharray:5.0,5.0;" x1="26" x2="26" y1="36.2969" y2="85.4297"/><line style="stroke:#181818;stroke-width:0.5;stroke-dasharray:5.0,5.0;" x1="82" x2="82" y1="36.2969" y2="85.4297"/><rect fill="#E2E2F0" height="30.2969" rx="2.5" ry="2.5" style="stroke:#181818;stroke-width:0.5;" width="43" x="5" y="5"/><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="29" x="12" y="24.9951">Bob</text><rect fill="#E2E2F0" height="30.2969" rx="2.5" ry="2.5" style="stroke:#181818;stroke-width:0.5;" width="43" x="5" y="84.4297"/><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="29" x="12" y="104.4248">Bob</text><rect fill="#E2E2F0" height="30.2969" rx="2.5" ry="2.5" style="stroke:#181818;stroke-width:0.5;" width="49" x="58" y="5"/><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="35" x="65" y="24.9951">Alice</text><rect fill="#E2E2F0" height="30.2969" rx="2.5" ry="2.5" style="stroke:#181818;stroke-width:0.5;" width="49" x="58" y="84.4297"/><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="35" x="65" y="104.4248">Alice</text><polygon fill="#181818" points="70.5,63.4297,80.5,67.4297,70.5,71.4297,74.5,67.4297" style="stroke:#181818;stroke-width:1.0;"/><line style="stroke:#181818;stroke-width:1.0;" x1="26.5" x2="76.5" y1="67.4297" y2="67.4297"/><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacing" textLength="30" x="33.5" y="62.3638">hello</text><!--SRC=[SyfFKj2rKt3CoKnELR1Io4ZDoSa70000]--></g></svg> \ No newline at end of file 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..3cfeed7e9 --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/image-views/workspace-via-file.dsl @@ -0,0 +1,32 @@ +workspace { + + views { + properties { + "plantuml.url" "http://localhost:7777" + "mermaid.url" "http://localhost:8888" + "mermaid.compress" "false" + "kroki.url" "http://localhost:9999" + } + + image * "plantuml" { + plantuml diagram.puml + } + + image * "mermaid" { + mermaid diagram.mmd + } + + image * "kroki" { + kroki graphviz diagram.dot + } + + image * "png" { + image image.png + } + + image * "svg" { + image image.svg + } + } + +} \ 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 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..f42f657ac --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/image-views/workspace-via-url.dsl @@ -0,0 +1,35 @@ +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 + } + } + +} \ 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..d402b5e98 --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/include-directory.dsl @@ -0,0 +1,14 @@ +workspace { + + model { + !var SOFTWARE_SYSTEM_NAME "Software System 1" + !include include/model/software-system/model.dsl + + !var SOFTWARE_SYSTEM_NAME "Software System 2" + !include include/model/software-system + + !var 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..e8e813e37 --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/include-url.dsl @@ -0,0 +1,7 @@ +workspace { + + model { + !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-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..71e89755d --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/include/model.dsl @@ -0,0 +1 @@ +softwareSystem = softwareSystem "Software System" \ 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 000000000..763d19bf5 Binary files /dev/null and b/structurizr-dsl/src/test/resources/dsl/logo.png differ 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/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 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/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 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 000000000..42234ce4b Binary files /dev/null and b/structurizr-dsl/src/test/resources/dsl/plugins/structurizr-dsl-plugin-1.0.0.jar differ 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/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 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/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 diff --git a/structurizr-dsl/src/test/resources/dsl/spring-petclinic/workspace.dsl b/structurizr-dsl/src/test/resources/dsl/spring-petclinic/workspace.dsl new file mode 100644 index 000000000..8809a0700 --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/spring-petclinic/workspace.dsl @@ -0,0 +1,103 @@ +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 { + 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" + url "https://github.com/spring-projects/spring-petclinic/tree/main/src/main/resources/db" + 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_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 { + technology "Spring MVC Controller" + matcher annotation "org.springframework.stereotype.Controller" + filter exclude fqn-regex ".*.CrashController" + url prefix-src "https://github.com/spring-projects/spring-petclinic/blob/main/src/main/java" + forEach { + clinicEmployee -> this "Uses" + tag "Spring MVC Controller" + group "Web Controllers" + } + } + strategy { + technology "Spring Data Repository" + matcher name-suffix "Repository" + description first-sentence + 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" + group "Data Repositories" + } + } + } + } + } + } + + 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 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 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..2073a6fc1 --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/test.dsl @@ -0,0 +1,436 @@ +!const ORGANISATION_NAME "Organisation" +!const GROUP_NAME "Group" + +!var name abc +!var name ABC + +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 { + !impliedRelationships false + !impliedRelationships "com.structurizr.model.CreateImpliedRelationshipsUnlessSameRelationshipExistsStrategy" + !impliedRelationships true + + // single line comment with long line split character \ + 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" + } + } + + 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" { + url "https://structurizr.com" + properties { + "Name" "Value" + } + perspectives { + "Security" "A description..." + } + } + + url "https://structurizr.com" + properties { + "Name" "Value" + } + perspectives { + "Security" "A description..." + } + + !elements "element.parent==webApplication && element.technology==Spring MVC Controller" { + tag "Tag 1" + tags "Tag 2, Tag 3" + url "https://example.com" + properties { + "type" "Spring MVC Controller" + } + perspectives { + "Owner" "Team A" + } + } + } + + 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 + } + 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 + } + } + } + + 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 + } + 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" + properties { + "Name" "Value" + } + perspectives { + "Security" "A description..." + } + + } + } + + !relationships "*->*" { + tag "Tag 1" + tags "Tag 2, Tag 3" + url "https://example.com" + properties { + name value + } + perspectives { + name value + } + } + } + + 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" + properties { + "Name" "Value" + } + } + + 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 + } + + image * { + light { + image logo.png + } + dark { + image logo.png + } + image logo.png + } + + styles { + element "Element" { + shape roundedbox + icon logo.png + iconPosition left + 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 + jump true + fontSize 24 + width 400 + position 50 + opacity 50 + properties { + "Name" "Value" + } + } + + 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 + } + + 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 { + person "Person" + softwareSystem "Software System" + container "Container" + component "Component" + deploymentNode "Deployment Node" + infrastructureNode "Infrastructure Node" + relationship "Relationship" + metadata angle + } + + 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/text-block.dsl b/structurizr-dsl/src/test/resources/dsl/text-block.dsl new file mode 100644 index 000000000..1b442a781 --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/text-block.dsl @@ -0,0 +1,32 @@ +workspace { + + views { + properties { + "plantuml.url" "https://plantuml.com/plantuml" + } + + !const SOURCE """ + class MyClass + """ + + !var STYLES """ + <style> + root { + BackgroundColor: #ffffff; + } + </style> + """ + + image * "image" { + plantuml """ + @startuml + + ${STYLES} + + ${SOURCE} + @enduml + """ + } + } + +} \ 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-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 new file mode 100644 index 000000000..b0f7a64b0 --- /dev/null +++ b/structurizr-dsl/src/test/resources/dsl/workspace-with-bom.dsl @@ -0,0 +1,24 @@ +workspace "Getting Started" "This is a model of my software system." { + + !include workspace-with-bom-model.dsl + + 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-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 diff --git a/structurizr-examples/build.gradle b/structurizr-examples/build.gradle deleted file mode 100644 index bc090b8b6..000000000 --- a/structurizr-examples/build.gradle +++ /dev/null @@ -1,19 +0,0 @@ -dependencies { - compile project(':structurizr-annotations') - compile project(':structurizr-core') - compile project(':structurizr-javaee') - compile project(':structurizr-spring') - - compile 'org.slf4j:slf4j-api:1.7.21' - compile 'org.slf4j:slf4j-simple:1.7.21' -} - -task springPetClinic(type:JavaExec) { - main = "com.structurizr.example.spring.petclinic.SpringPetClinic" - classpath( - sourceSets.main.runtimeClasspath, - '/Users/structurizr/Documents/spring-petclinic/target/spring-petclinic-1.0.0-SNAPSHOT/WEB-INF/classes', - fileTree(dir: '/Users/structurizr/Documents/spring-petclinic/target/spring-petclinic-1.0.0-SNAPSHOT/WEB-INF/lib', include: '*.jar') - ) - args '/Users/structurizr/Documents/spring-petclinic' -} \ No newline at end of file diff --git a/structurizr-examples/etc/logging.properties b/structurizr-examples/etc/logging.properties deleted file mode 100644 index cb7270348..000000000 --- a/structurizr-examples/etc/logging.properties +++ /dev/null @@ -1,4 +0,0 @@ -handlers=java.util.logging.ConsoleHandler -java.util.logging.ConsoleHandler.level=ALL - -com.structurizr.level=ALL \ No newline at end of file diff --git a/structurizr-examples/src/com/structurizr/example/Arc42DocumentationExample.java b/structurizr-examples/src/com/structurizr/example/Arc42DocumentationExample.java deleted file mode 100644 index 2284b8f49..000000000 --- a/structurizr-examples/src/com/structurizr/example/Arc42DocumentationExample.java +++ /dev/null @@ -1,81 +0,0 @@ -package com.structurizr.example; - -import com.structurizr.Workspace; -import com.structurizr.api.StructurizrClient; -import com.structurizr.documentation.Arc42DocumentationTemplate; -import com.structurizr.documentation.Format; -import com.structurizr.model.Model; -import com.structurizr.model.Person; -import com.structurizr.model.SoftwareSystem; -import com.structurizr.model.Tags; -import com.structurizr.view.Shape; -import com.structurizr.view.Styles; -import com.structurizr.view.SystemContextView; -import com.structurizr.view.ViewSet; - -import java.io.File; - -/** - * An empty software architecture document using the arc42 template. - * - * See https://structurizr.com/share/27791/documentation for the live version. - */ -public class Arc42DocumentationExample { - - private static final long WORKSPACE_ID = 27791; - private static final String API_KEY = "key"; - private static final String API_SECRET = "secret"; - - public static void main(String[] args) throws Exception { - Workspace workspace = new Workspace("Documentation - arc42", "An empty software architecture document using the arc42 template."); - Model model = workspace.getModel(); - ViewSet views = workspace.getViews(); - - Person user = model.addPerson("User", "A user of my software system."); - SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System", "My software system."); - user.uses(softwareSystem, "Uses"); - - SystemContextView contextView = views.createSystemContextView(softwareSystem, "SystemContext", "An example of a System Context diagram."); - contextView.addAllSoftwareSystems(); - contextView.addAllPeople(); - - Styles styles = views.getConfiguration().getStyles(); - styles.addElementStyle(Tags.PERSON).shape(Shape.Person); - - Arc42DocumentationTemplate template = new Arc42DocumentationTemplate(workspace); - - // this is the Markdown version - 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")); - - // this is the AsciiDoc version -// File documentationRoot = new File("./structurizr-examples/src/com/structurizr/example/documentation/arc42/asciidoc"); -// template.addIntroductionAndGoalsSection(softwareSystem, new File(documentationRoot, "01-introduction-and-goals.adoc")); -// template.addConstraintsSection(softwareSystem, new File(documentationRoot, "02-architecture-constraints.adoc")); -// template.addContextAndScopeSection(softwareSystem, new File(documentationRoot, "03-system-scope-and-context.adoc")); -// template.addSolutionStrategySection(softwareSystem, new File(documentationRoot, "04-solution-strategy.adoc")); -// template.addBuildingBlockViewSection(softwareSystem, new File(documentationRoot, "05-building-block-view.adoc")); -// template.addRuntimeViewSection(softwareSystem, new File(documentationRoot, "06-runtime-view.adoc")); -// template.addDeploymentViewSection(softwareSystem, new File(documentationRoot, "07-deployment-view.adoc")); -// template.addCrosscuttingConceptsSection(softwareSystem, new File(documentationRoot, "08-crosscutting-concepts.adoc")); -// template.addArchitecturalDecisionsSection(softwareSystem, new File(documentationRoot, "09-architecture-decisions.adoc")); -// template.addRisksAndTechnicalDebtSection(softwareSystem, new File(documentationRoot, "10-quality-requirements.adoc")); -// template.addQualityRequirementsSection(softwareSystem, new File(documentationRoot, "11-risks-and-technical-debt.adoc")); -// template.addGlossarySection(softwareSystem, new File(documentationRoot, "12-glossary.adoc")); - - StructurizrClient structurizrClient = new StructurizrClient(API_KEY, API_SECRET); - structurizrClient.putWorkspace(WORKSPACE_ID, workspace); - } - -} \ No newline at end of file diff --git a/structurizr-examples/src/com/structurizr/example/AutomaticDocumentationTemplateExample.java b/structurizr-examples/src/com/structurizr/example/AutomaticDocumentationTemplateExample.java deleted file mode 100644 index 0a29a5f39..000000000 --- a/structurizr-examples/src/com/structurizr/example/AutomaticDocumentationTemplateExample.java +++ /dev/null @@ -1,55 +0,0 @@ -package com.structurizr.example; - -import com.structurizr.Workspace; -import com.structurizr.api.StructurizrClient; -import com.structurizr.documentation.AutomaticDocumentationTemplate; -import com.structurizr.model.Model; -import com.structurizr.model.Person; -import com.structurizr.model.SoftwareSystem; -import com.structurizr.model.Tags; -import com.structurizr.view.Shape; -import com.structurizr.view.Styles; -import com.structurizr.view.SystemContextView; -import com.structurizr.view.ViewSet; - -import java.io.File; - -/** - * An empty software architecture document based upon the Structurizr template, - * created with the automatic document template. - * - * See https://structurizr.com/share/35971/documentation for the live version. - */ -public class AutomaticDocumentationTemplateExample { - - private static final long WORKSPACE_ID = 35971; - private static final String API_KEY = "key"; - private static final String API_SECRET = "secret"; - - public static void main(String[] args) throws Exception { - Workspace workspace = new Workspace("Documentation - Automatic", "An empty software architecture document using the Structurizr template."); - Model model = workspace.getModel(); - ViewSet views = workspace.getViews(); - - Person user = model.addPerson("User", "A user of my software system."); - SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System", "My software system."); - user.uses(softwareSystem, "Uses"); - - SystemContextView contextView = views.createSystemContextView(softwareSystem, "SystemContext", "An example of a System Context diagram."); - contextView.addAllSoftwareSystems(); - contextView.addAllPeople(); - - Styles styles = views.getConfiguration().getStyles(); - styles.addElementStyle(Tags.PERSON).shape(Shape.Person); - - // this directory includes a mix of Markdown and AsciiDoc files - File documentationRoot = new File("./structurizr-examples/src/com/structurizr/example/documentation/automatic"); - - AutomaticDocumentationTemplate template = new AutomaticDocumentationTemplate(workspace); - template.addSections(softwareSystem, documentationRoot); - - StructurizrClient structurizrClient = new StructurizrClient(API_KEY, API_SECRET); - structurizrClient.putWorkspace(WORKSPACE_ID, workspace); - } - -} \ No newline at end of file diff --git a/structurizr-examples/src/com/structurizr/example/BigBankPlc.java b/structurizr-examples/src/com/structurizr/example/BigBankPlc.java deleted file mode 100644 index e2f7123c9..000000000 --- a/structurizr-examples/src/com/structurizr/example/BigBankPlc.java +++ /dev/null @@ -1,187 +0,0 @@ -package com.structurizr.example; - -import com.structurizr.Workspace; -import com.structurizr.api.StructurizrClient; -import com.structurizr.documentation.Format; -import com.structurizr.documentation.StructurizrDocumentationTemplate; -import com.structurizr.model.*; -import com.structurizr.util.MapUtils; -import com.structurizr.view.*; - -/** - * This is an example workspace to illustrate the key features of Structurizr, - * based around a fictional Internet Banking System for Big Bank plc. - * - * The live workspace is available to view at https://structurizr.com/share/36141 - */ -public class BigBankPlc { - - private static final long WORKSPACE_ID = 36141; - private static final String API_KEY = "key"; - private static final String API_SECRET = "secret"; - - private static final String DATABASE_TAG = "Database"; - - private static Workspace create(boolean usePaidFeatures) { - Workspace workspace = new Workspace("Big Bank plc", "This is an example workspace to illustrate the key features of Structurizr, based around a fictional online banking system."); - Model model = workspace.getModel(); - ViewSet views = workspace.getViews(); - - model.setEnterprise(new Enterprise("Big Bank plc")); - - // people and software systems - Person customer = model.addPerson(Location.External, "Customer", "A customer of the bank."); - - SoftwareSystem internetBankingSystem = model.addSoftwareSystem(Location.Internal, "Internet Banking System", "Allows customers to view information about their bank accounts and make payments."); - customer.uses(internetBankingSystem, "Uses"); - - SoftwareSystem mainframeBankingSystem = model.addSoftwareSystem(Location.Internal, "Mainframe Banking System", "Stores all of the core banking information about customers, accounts, transactions, etc."); - internetBankingSystem.uses(mainframeBankingSystem, "Uses"); - - SoftwareSystem atm = model.addSoftwareSystem(Location.Internal, "ATM", "Allows customers to withdraw cash."); - atm.uses(mainframeBankingSystem, "Uses"); - customer.uses(atm, "Withdraws cash using"); - - Person bankStaff = model.addPerson(Location.Internal, "Bank Staff", "Staff within the bank."); - bankStaff.uses(mainframeBankingSystem, "Uses"); - - // containers - Container webApplication = internetBankingSystem.addContainer("Web Application", "Provides all of the Internet banking functionality to customers.", "Java and Spring MVC"); - Container database = internetBankingSystem.addContainer("Database", "Stores interesting data.", "Relational Database Schema"); - database.addTags(DATABASE_TAG); - - customer.uses(webApplication, "Uses", "HTTPS"); - webApplication.uses(database, "Reads from and writes to", "JDBC"); - webApplication.uses(mainframeBankingSystem, "Uses", "XML/HTTPS"); - - // components - // - for a real-world software system, you would probably want to extract the components using - // - static analysis/reflection rather than manually specifying them all - Component homePageController = webApplication.addComponent("Home Page Controller", "Serves up the home page.", "Spring MVC Controller"); - Component signinController = webApplication.addComponent("Sign In Controller", "Allows users to sign in to the Internet Banking System.", "Spring MVC Controller"); - Component accountsSummaryController = webApplication.addComponent("Accounts Summary Controller", "Provides customers with an summary of their bank accounts.", "Spring MVC Controller"); - Component securityComponent = webApplication.addComponent("Security Component", "Provides functionality related to signing in, changing passwords, etc.", "Spring Bean"); - Component mainframeBankingSystemFacade = webApplication.addComponent("Mainframe Banking System Facade", "A facade onto the mainframe banking system.", "Spring Bean"); - - webApplication.getComponents().stream().filter(c -> "Spring MVC Controller".equals(c.getTechnology())).forEach(c -> customer.uses(c, "Uses", "HTTPS")); - signinController.uses(securityComponent, "Uses"); - accountsSummaryController.uses(mainframeBankingSystemFacade, "Uses"); - securityComponent.uses(database, "Reads from and writes to", "JDBC"); - mainframeBankingSystemFacade.uses(mainframeBankingSystem, "Uses", "XML/HTTPS"); - - // deployment nodes and container instances - DeploymentNode developerLaptop = model.addDeploymentNode("Developer Laptop", "A developer laptop.", "Windows 7 or 10"); - developerLaptop.addDeploymentNode("Docker Container - Web Server", "A Docker container.", "Docker") - .addDeploymentNode("Apache Tomcat", "An open source Java EE web server.", "Apache Tomcat 8.x", 1, MapUtils.create("Xmx=512M", "Xms=1024M", "Java Version=8")) - .add(webApplication); - - developerLaptop.addDeploymentNode("Docker Container - Database Server", "A Docker container.", "Docker") - .addDeploymentNode("Database Server", "A development database.", "Oracle 12c") - .add(database); - - DeploymentNode liveWebServer = model.addDeploymentNode("bigbank-web***", "A web server residing in the web server farm, accessed via F5 BIG-IP LTMs.", "Ubuntu 16.04 LTS", 8, MapUtils.create("Location=London")); - liveWebServer.addDeploymentNode("Apache Tomcat", "An open source Java EE web server.", "Apache Tomcat 8.x", 1, MapUtils.create("Xmx=512M", "Xms=1024M", "Java Version=8")) - .add(webApplication); - - DeploymentNode primaryDatabaseServer = model.addDeploymentNode("bigbank-db01", "The primary database server.", "Ubuntu 16.04 LTS", 1, MapUtils.create("Location=London")) - .addDeploymentNode("Oracle - Primary", "The primary, live database server.", "Oracle 12c"); - primaryDatabaseServer.add(database); - - DeploymentNode secondaryDatabaseServer = model.addDeploymentNode("bigbank-db02", "The secondary database server.", "Ubuntu 16.04 LTS", 1, MapUtils.create("Location=Reading")) - .addDeploymentNode("Oracle - Secondary", "A secondary, standby database server, used for failover purposes only.", "Oracle 12c"); - ContainerInstance secondaryDatabase = secondaryDatabaseServer.add(database); - - model.getRelationships().stream().filter(r -> r.getDestination().equals(secondaryDatabase)).forEach(r -> r.addTags("Failover")); - Relationship dataReplicationRelationship = primaryDatabaseServer.uses(secondaryDatabaseServer, "Replicates data to", ""); - secondaryDatabase.addTags("Failover"); - - // views/diagrams - EnterpriseContextView enterpriseContextView = views.createEnterpriseContextView("SystemLandscape", "The system landscape diagram for Big Bank plc."); - enterpriseContextView.addAllElements(); - enterpriseContextView.setPaperSize(PaperSize.A5_Landscape); - - SystemContextView systemContextView = views.createSystemContextView(internetBankingSystem, "SystemContext", "The system context diagram for the Internet Banking System."); - systemContextView.addNearestNeighbours(internetBankingSystem); - systemContextView.setPaperSize(PaperSize.A5_Landscape); - - ContainerView containerView = views.createContainerView(internetBankingSystem, "Containers", "The container diagram for the Internet Banking System."); - containerView.add(customer); - containerView.addAllContainers(); - containerView.add(mainframeBankingSystem); - containerView.setPaperSize(PaperSize.A5_Landscape); - - ComponentView componentView = views.createComponentView(webApplication, "Components", "The component diagram for the Web Application."); - componentView.addAllContainers(); - componentView.addAllComponents(); - componentView.add(customer); - componentView.add(mainframeBankingSystem); - componentView.setPaperSize(PaperSize.A5_Landscape); - - if (usePaidFeatures) { - // dynamic diagrams, deployment diagrams and corporate branding are not available with the Free Plan - DynamicView dynamicView = views.createDynamicView(webApplication, "SignIn", "Summarises how the sign in feature works."); - dynamicView.add(customer, "Requests /signin from", signinController); - dynamicView.add(customer, "Submits credentials to", signinController); - dynamicView.add(signinController, "Calls isAuthenticated() on", securityComponent); - dynamicView.add(securityComponent, "select * from users u where username = ?", database); - dynamicView.setPaperSize(PaperSize.A5_Landscape); - - DeploymentView developmentDeploymentView = views.createDeploymentView(internetBankingSystem, "DevelopmentDeployment", "An example development deployment scenario for the Internet Banking System."); - developmentDeploymentView.add(developerLaptop); - developmentDeploymentView.setPaperSize(PaperSize.A5_Landscape); - - DeploymentView liveDeploymentView = views.createDeploymentView(internetBankingSystem, "LiveDeployment", "An example live deployment scenario for the Internet Banking System."); - liveDeploymentView.add(liveWebServer); - liveDeploymentView.add(primaryDatabaseServer); - liveDeploymentView.add(secondaryDatabaseServer); - liveDeploymentView.add(dataReplicationRelationship); - liveDeploymentView.setPaperSize(PaperSize.A5_Landscape); - } - - // colours, shapes and other diagram styling - Styles styles = views.getConfiguration().getStyles(); - styles.addElementStyle(Tags.ELEMENT).color("#ffffff"); - styles.addElementStyle(Tags.SOFTWARE_SYSTEM).background("#1168bd"); - styles.addElementStyle(Tags.CONTAINER).background("#438dd5"); - styles.addElementStyle(Tags.COMPONENT).background("#85bbf0").color("#000000"); - styles.addElementStyle(Tags.PERSON).background("#08427b").shape(Shape.Person); - styles.addElementStyle(DATABASE_TAG).shape(Shape.Cylinder); - styles.addElementStyle("Failover").opacity(25); - styles.addRelationshipStyle("Failover").opacity(25).position(70); - - // documentation - // - usually the documentation would be included from separate Markdown/AsciiDoc files, but this is just an example - StructurizrDocumentationTemplate template = new StructurizrDocumentationTemplate(workspace); - template.addContextSection(internetBankingSystem, Format.Markdown, - "Here is some context about the Internet Banking System...\n" + - "![](embed:EnterpriseContext)\n" + - "![](embed:SystemContext)\n" + - "### Internet Banking System\n...\n" + - "### Mainframe Banking System\n...\n"); - template.addContainersSection(internetBankingSystem, Format.Markdown, - "Here is some information about the containers within the Internet Banking System...\n" + - "![](embed:Containers)\n" + - "### Web Application\n...\n" + - "### Database\n...\n"); - template.addComponentsSection(webApplication, Format.Markdown, - "Here is some information about the Web Application...\n" + - "![](embed:Components)\n" + - "### Sign in process\n" + - "Here is some information about the Sign In Controller, including how the sign in process works...\n" + - "![](embed:SignIn)"); - template.addDevelopmentEnvironmentSection(internetBankingSystem, Format.AsciiDoc, - "Here is some information about how to set up a development environment for the Internet Banking System...\n" + - "image::embed:DevelopmentDeployment[]"); - template.addDeploymentSection(internetBankingSystem, Format.AsciiDoc, - "Here is some information about the live deployment environment for the Internet Banking System...\n" + - "image::embed:LiveDeployment[]"); - - return workspace; - } - - public static void main(String[] args) throws Exception { - StructurizrClient structurizrClient = new StructurizrClient(API_KEY, API_SECRET); - structurizrClient.putWorkspace(WORKSPACE_ID, create(true)); - } - -} \ No newline at end of file diff --git a/structurizr-examples/src/com/structurizr/example/ClientSideEncryption.java b/structurizr-examples/src/com/structurizr/example/ClientSideEncryption.java deleted file mode 100644 index 34bccf231..000000000 --- a/structurizr-examples/src/com/structurizr/example/ClientSideEncryption.java +++ /dev/null @@ -1,49 +0,0 @@ -package com.structurizr.example; - -import com.structurizr.Workspace; -import com.structurizr.api.StructurizrClient; -import com.structurizr.encryption.AesEncryptionStrategy; -import com.structurizr.model.Model; -import com.structurizr.model.Person; -import com.structurizr.model.SoftwareSystem; -import com.structurizr.model.Tags; -import com.structurizr.view.Shape; -import com.structurizr.view.Styles; -import com.structurizr.view.SystemContextView; -import com.structurizr.view.ViewSet; - -/** - * This is an example of how to use client-side encryption. - * - * You can see the workspace online at https://structurizr.com/share/41 - * (the passphrase is "password") - */ -public class ClientSideEncryption { - - private static final long WORKSPACE_ID = 41; - private static final String API_KEY = "key"; - private static final String API_SECRET = "secret"; - - public static void main(String[] args) throws Exception { - Workspace workspace = new Workspace("Client-side encrypted workspace", "This is a client-side encrypted workspace. The passphrase is 'password'."); - 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(); - - Styles styles = views.getConfiguration().getStyles(); - styles.addElementStyle(Tags.SOFTWARE_SYSTEM).background("#d34407").color("#ffffff"); - styles.addElementStyle(Tags.PERSON).background("#f86628").color("#ffffff").shape(Shape.Person); - - StructurizrClient structurizrClient = new StructurizrClient(API_KEY, API_SECRET); - structurizrClient.setEncryptionStrategy(new AesEncryptionStrategy("password")); - structurizrClient.putWorkspace(WORKSPACE_ID, workspace); - } - -} \ No newline at end of file diff --git a/structurizr-examples/src/com/structurizr/example/CorporateBranding.java b/structurizr-examples/src/com/structurizr/example/CorporateBranding.java deleted file mode 100644 index 42209778c..000000000 --- a/structurizr-examples/src/com/structurizr/example/CorporateBranding.java +++ /dev/null @@ -1,71 +0,0 @@ -package com.structurizr.example; - -import com.structurizr.Workspace; -import com.structurizr.api.StructurizrClient; -import com.structurizr.documentation.Format; -import com.structurizr.documentation.StructurizrDocumentationTemplate; -import com.structurizr.model.Model; -import com.structurizr.model.Person; -import com.structurizr.model.SoftwareSystem; -import com.structurizr.model.Tags; -import com.structurizr.util.ImageUtils; -import com.structurizr.view.*; - -import java.io.File; - -/** - * <p> - * This is a simple example that illustrates the corporate branding features of Structurizr, including: - * </p> - * - * <ul> - * <li>A colour scheme, which is applied to model elements and documentation navigation.</li> - * <li>A logo, which is included in diagrams and documentation.</li> - * </ul> - * - * <p> - * You can see the live workspace at https://structurizr.com/share/35031 - * </p> - */ -public class CorporateBranding { - - private static final long WORKSPACE_ID = 35031; - private static final String API_KEY = ""; - private static final String API_SECRET = ""; - - public static void main(String[] args) throws Exception { - Workspace workspace = new Workspace("Corporate Branding", "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(); - - Styles styles = views.getConfiguration().getStyles(); - styles.addElementStyle(Tags.PERSON).shape(Shape.Person); - - StructurizrDocumentationTemplate template = new StructurizrDocumentationTemplate(workspace); - template.addContextSection(softwareSystem, Format.Markdown, "Here is some context about the software system...\n\n![](embed:SystemContext)"); - template.addQualityAttributesSection(softwareSystem, Format.Markdown, "Here is some information about the quality attributes..."); - template.addSoftwareArchitectureSection(softwareSystem, Format.Markdown, "Here is some information about the software architecture..."); - template.addOperationAndSupportSection(softwareSystem, Format.Markdown, "Here is some information about how to operate and support the software..."); - template.addDecisionLogSection(softwareSystem, Format.Markdown, "Here is some information about the decisions made..."); - - Branding branding = views.getConfiguration().getBranding(); - branding.setColor1(new ColorPair("#02172c", "#ffffff")); - branding.setColor2(new ColorPair("#08427b", "#ffffff")); - branding.setColor3(new ColorPair("#1168bd", "#ffffff")); - branding.setColor4(new ColorPair("#438dd5", "#ffffff")); - branding.setColor5(new ColorPair("#85bbf0", "#ffffff")); - branding.setLogo(ImageUtils.getImageAsDataUri(new File("./docs/images/structurizr-logo.png"))); - - StructurizrClient structurizrClient = new StructurizrClient(API_KEY, API_SECRET); - structurizrClient.putWorkspace(WORKSPACE_ID, workspace); - } - -} diff --git a/structurizr-examples/src/com/structurizr/example/EmptyWorkspace.java b/structurizr-examples/src/com/structurizr/example/EmptyWorkspace.java deleted file mode 100644 index 17d44fcb7..000000000 --- a/structurizr-examples/src/com/structurizr/example/EmptyWorkspace.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.structurizr.example; - -import com.structurizr.Workspace; -import com.structurizr.api.StructurizrClient; - -/** - * An example of uploading an empty workspace, for when you want to use - * Structurizr's local storage mode. - */ -public class EmptyWorkspace { - - private static final long WORKSPACE_ID = 123456; - private static final String API_KEY = ""; - private static final String API_SECRET = ""; - - public static void main(String[] args) throws Exception { - Workspace workspace = new Workspace("Name", "Description"); - StructurizrClient structurizrClient = new StructurizrClient(API_KEY, API_SECRET); - structurizrClient.putWorkspace(WORKSPACE_ID, workspace); - } - -} diff --git a/structurizr-examples/src/com/structurizr/example/FilteredViews.java b/structurizr-examples/src/com/structurizr/example/FilteredViews.java deleted file mode 100644 index fe651862b..000000000 --- a/structurizr-examples/src/com/structurizr/example/FilteredViews.java +++ /dev/null @@ -1,54 +0,0 @@ -package com.structurizr.example; - -import com.structurizr.Workspace; -import com.structurizr.api.StructurizrClient; -import com.structurizr.model.Model; -import com.structurizr.model.Person; -import com.structurizr.model.SoftwareSystem; -import com.structurizr.model.Tags; -import com.structurizr.view.*; - -/** - * An example of how to use filtered views to show "before" and "after" views of a software system. - * - * You can see the live diagrams at https://structurizr.com/public/19911 - */ -public class FilteredViews { - - private static final long WORKSPACE_ID = 19911; - private static final String API_KEY = "key"; - private static final String API_SECRET = "secret"; - - private static final String CURRENT_STATE = "Current State"; - private static final String FUTURE_STATE = "Future State"; - - public static void main(String[] args) throws Exception { - Workspace workspace = new Workspace("Filtered Views", "An example of using filtered views."); - Model model = workspace.getModel(); - - 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(); - EnterpriseContextView enterpriseContextView = views.createEnterpriseContextView("EnterpriseContext", "An example Enterprise Context diagram."); - enterpriseContextView.addAllElements(); - - views.createFilteredView(enterpriseContextView, "CurrentState", "The current context.", FilterMode.Exclude, FUTURE_STATE); - views.createFilteredView(enterpriseContextView, "FutureState", "The future state context after Software System B is live.", FilterMode.Exclude, CURRENT_STATE); - - Styles styles = views.getConfiguration().getStyles(); - styles.addElementStyle(Tags.ELEMENT).color("#ffffff"); - styles.addElementStyle(Tags.SOFTWARE_SYSTEM).background("#91a437").shape(Shape.RoundedBox); - styles.addElementStyle(Tags.PERSON).background("#6a7b15").shape(Shape.Person); - - StructurizrClient structurizrClient = new StructurizrClient(API_KEY, API_SECRET); - structurizrClient.putWorkspace(WORKSPACE_ID, workspace); - } - -} \ No newline at end of file diff --git a/structurizr-examples/src/com/structurizr/example/FinancialRiskSystem.java b/structurizr-examples/src/com/structurizr/example/FinancialRiskSystem.java deleted file mode 100644 index 2c57657e4..000000000 --- a/structurizr-examples/src/com/structurizr/example/FinancialRiskSystem.java +++ /dev/null @@ -1,90 +0,0 @@ -package com.structurizr.example; - -import com.structurizr.Workspace; -import com.structurizr.api.StructurizrClient; -import com.structurizr.documentation.Format; -import com.structurizr.documentation.StructurizrDocumentationTemplate; -import com.structurizr.model.*; -import com.structurizr.view.*; - -import java.io.File; - -/** - * 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 - * - * You can see the workspace online at https://structurizr.com/public/31 - */ -public class FinancialRiskSystem { - - private static final long WORKSPACE_ID = 31; - private static final String API_KEY = ""; - private static final String API_SECRET = ""; - - private static final String TAG_ALERT = "Alert"; - - public static void main(String[] args) throws Exception { - Workspace workspace = new 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 model = workspace.getModel(); - - SoftwareSystem financialRiskSystem = model.addSoftwareSystem("Financial Risk System", "Calculates the bank's exposure to risk for product X."); - - Person businessUser = model.addPerson("Business User", "A regular business user."); - businessUser.uses(financialRiskSystem, "Views reports using"); - - Person configurationUser = model.addPerson("Configuration User", "A regular business user who can also configure the parameters used in the risk calculations."); - configurationUser.uses(financialRiskSystem, "Configures parameters using"); - - SoftwareSystem tradeDataSystem = model.addSoftwareSystem("Trade Data System", "The system of record for trades of type X."); - financialRiskSystem.uses(tradeDataSystem, "Gets trade data from"); - - SoftwareSystem referenceDataSystem = model.addSoftwareSystem("Reference Data System", "Manages reference data for all counterparties the bank interacts with."); - financialRiskSystem.uses(referenceDataSystem, "Gets counterparty data from"); - - SoftwareSystem referenceDataSystemV2 = model.addSoftwareSystem("Reference Data System v2.0", "Manages reference data for all counterparties the bank interacts with."); - referenceDataSystemV2.addTags("Future State"); - financialRiskSystem.uses(referenceDataSystemV2, "Gets counterparty data from").addTags("Future State"); - - SoftwareSystem emailSystem = model.addSoftwareSystem("E-mail system", "The bank's Microsoft Exchange system."); - financialRiskSystem.uses(emailSystem, "Sends a notification that a report is ready to"); - emailSystem.delivers(businessUser, "Sends a notification that a report is ready to", "E-mail message", InteractionStyle.Asynchronous); - - SoftwareSystem centralMonitoringService = model.addSoftwareSystem("Central Monitoring Service", "The bank's central monitoring and alerting dashboard."); - financialRiskSystem.uses(centralMonitoringService, "Sends critical failure alerts to", "SNMP", InteractionStyle.Asynchronous).addTags(TAG_ALERT); - - SoftwareSystem activeDirectory = model.addSoftwareSystem("Active Directory", "The bank's authentication and authorisation system."); - financialRiskSystem.uses(activeDirectory, "Uses for user authentication and authorisation"); - - ViewSet views = workspace.getViews(); - SystemContextView contextView = views.createSystemContextView(financialRiskSystem, "Context", "An example System Context diagram for the Financial Risk System architecture kata."); - contextView.addAllSoftwareSystems(); - contextView.addAllPeople(); - - Styles styles = views.getConfiguration().getStyles(); - financialRiskSystem.addTags("Risk System"); - - styles.addElementStyle(Tags.ELEMENT).color("#ffffff").fontSize(34); - styles.addElementStyle("Risk System").background("#550000").color("#ffffff"); - styles.addElementStyle(Tags.SOFTWARE_SYSTEM).width(650).height(400).background("#801515").shape(Shape.RoundedBox); - styles.addElementStyle(Tags.PERSON).width(550).background("#d46a6a").shape(Shape.Person); - - styles.addRelationshipStyle(Tags.RELATIONSHIP).thickness(4).dashed(false).fontSize(32).width(400); - styles.addRelationshipStyle(Tags.SYNCHRONOUS).dashed(false); - styles.addRelationshipStyle(Tags.ASYNCHRONOUS).dashed(true); - styles.addRelationshipStyle(TAG_ALERT).color("#ff0000"); - - styles.addElementStyle("Future State").opacity(30).border(Border.Dashed); - styles.addRelationshipStyle("Future State").opacity(30).dashed(true); - - StructurizrDocumentationTemplate template = new StructurizrDocumentationTemplate(workspace); - File documentationRoot = new File("./structurizr-examples/src/com/structurizr/example/financialrisksystem"); - template.addContextSection(financialRiskSystem, new File(documentationRoot, "context.adoc")); - template.addFunctionalOverviewSection(financialRiskSystem, new File(documentationRoot, "functional-overview.md")); - template.addQualityAttributesSection(financialRiskSystem, new File(documentationRoot, "quality-attributes.md")); - template.addImages(documentationRoot); - - StructurizrClient structurizrClient = new StructurizrClient(API_KEY, API_SECRET); - structurizrClient.putWorkspace(WORKSPACE_ID, workspace); - } - -} \ No newline at end of file diff --git a/structurizr-examples/src/com/structurizr/example/GettingStarted.java b/structurizr-examples/src/com/structurizr/example/GettingStarted.java deleted file mode 100644 index 6177216b9..000000000 --- a/structurizr-examples/src/com/structurizr/example/GettingStarted.java +++ /dev/null @@ -1,52 +0,0 @@ -package com.structurizr.example; - -import com.structurizr.Workspace; -import com.structurizr.api.StructurizrClient; -import com.structurizr.model.Model; -import com.structurizr.model.Person; -import com.structurizr.model.SoftwareSystem; -import com.structurizr.model.Tags; -import com.structurizr.view.Shape; -import com.structurizr.view.Styles; -import com.structurizr.view.SystemContextView; -import com.structurizr.view.ViewSet; - -/** - * A "getting started" example that illustrates how to - * create a software architecture diagram using code. - * - * The live workspace is available to view at https://structurizr.com/share/25441 - */ -public class GettingStarted { - - private static final long WORKSPACE_ID = 25441; - private static final String API_KEY = ""; - private static final String API_SECRET = ""; - - public static void main(String[] args) throws Exception { - // all software architecture models belong to a workspace - Workspace workspace = new Workspace("Getting Started", "This is a model of my software system."); - Model model = workspace.getModel(); - - // create a model to describe a user using a software system - Person user = model.addPerson("User", "A user of my software system."); - SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System", "My software system."); - user.uses(softwareSystem, "Uses"); - - // create a system context diagram showing people and software systems - ViewSet views = workspace.getViews(); - SystemContextView contextView = views.createSystemContextView(softwareSystem, "SystemContext", "An example of a System Context diagram."); - contextView.addAllSoftwareSystems(); - contextView.addAllPeople(); - - // add some styling to the diagram elements - 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); - - // upload to structurizr.com (you'll need your own workspace ID, API key and API secret) - StructurizrClient structurizrClient = new StructurizrClient(API_KEY, API_SECRET); - structurizrClient.putWorkspace(WORKSPACE_ID, workspace); - } - -} \ No newline at end of file diff --git a/structurizr-examples/src/com/structurizr/example/MicroservicesExample.java b/structurizr-examples/src/com/structurizr/example/MicroservicesExample.java deleted file mode 100644 index 5a5946b10..000000000 --- a/structurizr-examples/src/com/structurizr/example/MicroservicesExample.java +++ /dev/null @@ -1,96 +0,0 @@ -package com.structurizr.example; - -import com.structurizr.Workspace; -import com.structurizr.api.StructurizrClient; -import com.structurizr.model.*; -import com.structurizr.view.*; - -/** - * A simple example of what a microservices architecture might look like. This workspace also - * includes a dynamic view that demonstrates parallel sequences of events. - * - * The live version of the diagrams can be found at https://structurizr.com/public/4241 - */ -public class MicroservicesExample { - - private static final String MICROSERVICE_TAG = "Microservice"; - private static final String MESSAGE_BUS_TAG = "Message Bus"; - private static final String DATASTORE_TAG = "Database"; - - public static void main(String[] args) throws Exception { - Workspace workspace = new Workspace("Microservices example", "An example of a microservices architecture, which includes asynchronous and parallel behaviour."); - Model model = workspace.getModel(); - - SoftwareSystem mySoftwareSystem = model.addSoftwareSystem("Customer Information System", "Stores information "); - Person customer = model.addPerson("Customer", "A customer"); - Container customerApplication = mySoftwareSystem.addContainer("Customer Application", "Allows customers to manage their profile.", "Angular"); - - Container customerService = mySoftwareSystem.addContainer("Customer Service", "The point of access for customer information.", "Java and Spring Boot"); - customerService.addTags(MICROSERVICE_TAG); - Container customerDatabase = mySoftwareSystem.addContainer("Customer Database", "Stores customer information.", "Oracle 12c"); - customerDatabase.addTags(DATASTORE_TAG); - - Container reportingService = mySoftwareSystem.addContainer("Reporting Service", "Creates normalised data for reporting purposes.", "Ruby"); - reportingService.addTags(MICROSERVICE_TAG); - Container reportingDatabase = mySoftwareSystem.addContainer("Reporting Database", "Stores a normalised version of all business data for ad hoc reporting purposes.", "MySQL"); - reportingDatabase.addTags(DATASTORE_TAG); - - Container auditService = mySoftwareSystem.addContainer("Audit Service", "Provides organisation-wide auditing facilities.", "C# .NET"); - auditService.addTags(MICROSERVICE_TAG); - Container auditStore = mySoftwareSystem.addContainer("Audit Store", "Stores information about events that have happened.", "Event Store"); - auditStore.addTags(DATASTORE_TAG); - - Container messageBus = mySoftwareSystem.addContainer("Message Bus", "Transport for business events.", "RabbitMQ"); - messageBus.addTags(MESSAGE_BUS_TAG); - - customer.uses(customerApplication, "Uses"); - customerApplication.uses(customerService, "Updates customer information using", "JSON/HTTPS", InteractionStyle.Synchronous); - customerService.uses(messageBus, "Sends customer update events to", "", InteractionStyle.Asynchronous); - customerService.uses(customerDatabase, "Stores data in", "JDBC", InteractionStyle.Synchronous); - customerService.uses(customerApplication, "Sends events to", "WebSocket", InteractionStyle.Asynchronous); - messageBus.uses(reportingService, "Sends customer update events to", "", InteractionStyle.Asynchronous); - messageBus.uses(auditService, "Sends customer update events to", "", InteractionStyle.Asynchronous); - reportingService.uses(reportingDatabase, "Stores data in", "", InteractionStyle.Synchronous); - auditService.uses(auditStore, "Stores events in", "", InteractionStyle.Synchronous); - - ViewSet views = workspace.getViews(); - - ContainerView containerView = views.createContainerView(mySoftwareSystem, "Containers", null); - containerView.addAllElements(); - - DynamicView dynamicView = views.createDynamicView(mySoftwareSystem, "CustomerUpdateEvent", "This diagram shows what happens when a customer updates their details."); - dynamicView.add(customer, customerApplication); - dynamicView.add(customerApplication, customerService); - - dynamicView.add(customerService, customerDatabase); - dynamicView.add(customerService, messageBus); - - dynamicView.startParallelSequence(); - dynamicView.add(messageBus, reportingService); - dynamicView.add(reportingService, reportingDatabase); - dynamicView.endParallelSequence(); - - dynamicView.startParallelSequence(); - dynamicView.add(messageBus, auditService); - dynamicView.add(auditService, auditStore); - dynamicView.endParallelSequence(); - - dynamicView.add(customerService, "Confirms update to", customerApplication); - - Styles styles = views.getConfiguration().getStyles(); - styles.addElementStyle(Tags.ELEMENT).color("#000000"); - styles.addElementStyle(Tags.PERSON).background("#ffbf00").shape(Shape.Person); - styles.addElementStyle(Tags.CONTAINER).background("#facc2E"); - styles.addElementStyle(MESSAGE_BUS_TAG).width(1600).shape(Shape.Pipe); - styles.addElementStyle(MICROSERVICE_TAG).shape(Shape.Hexagon); - styles.addElementStyle(DATASTORE_TAG).background("#f5da81").shape(Shape.Cylinder); - styles.addRelationshipStyle(Tags.RELATIONSHIP).routing(Routing.Orthogonal); - - styles.addRelationshipStyle(Tags.ASYNCHRONOUS).dashed(true); - styles.addRelationshipStyle(Tags.SYNCHRONOUS).dashed(false); - - StructurizrClient client = new StructurizrClient("key", "secret"); - client.putWorkspace(4241, workspace); - } - -} diff --git a/structurizr-examples/src/com/structurizr/example/PlantUML.java b/structurizr-examples/src/com/structurizr/example/PlantUML.java deleted file mode 100644 index aad0ea146..000000000 --- a/structurizr-examples/src/com/structurizr/example/PlantUML.java +++ /dev/null @@ -1,53 +0,0 @@ -package com.structurizr.example; - -import com.structurizr.Workspace; -import com.structurizr.io.plantuml.PlantUMLWriter; -import com.structurizr.model.Model; -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.Styles; -import com.structurizr.view.SystemContextView; -import com.structurizr.view.ViewSet; - -import java.io.StringWriter; - -/** - * An example of how to use the PlantUML writer. Run this program and copy/paste - * the output into http://www.plantuml.com/plantuml/ - */ -public class PlantUML { - - 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(); - contextView.setPaperSize(PaperSize.Slide_16_9); - - 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); - - StringWriter stringWriter = new StringWriter(); - PlantUMLWriter plantUMLWriter = new PlantUMLWriter(); - - // if you're using dark background colours, you might need to explicitly set the foreground colour using skin params - // e.g. rectangleFontColor, rectangleFontColor<<Software System>>, etc - plantUMLWriter.addSkinParam("rectangleFontColor", "#ffffff"); - plantUMLWriter.addSkinParam("rectangleStereotypeFontColor", "#ffffff"); - - plantUMLWriter.write(workspace, stringWriter); - System.out.println(stringWriter.toString()); - } - -} diff --git a/structurizr-examples/src/com/structurizr/example/Shapes.java b/structurizr-examples/src/com/structurizr/example/Shapes.java deleted file mode 100644 index 056e0e632..000000000 --- a/structurizr-examples/src/com/structurizr/example/Shapes.java +++ /dev/null @@ -1,56 +0,0 @@ -package com.structurizr.example; - -import com.structurizr.Workspace; -import com.structurizr.api.StructurizrClient; -import com.structurizr.model.Model; -import com.structurizr.model.Tags; -import com.structurizr.view.*; - -/** - * An example illustrating all of the shapes available in Structurizr. - * - * The live workspace is available to view at https://structurizr.com/share/12541 - */ -public class Shapes { - - private static final long WORKSPACE_ID = 12541; - private static final String API_KEY = ""; - private static final String API_SECRET = ""; - - public static void main(String[] args) throws Exception { - Workspace workspace = new Workspace("Shapes", "An example of all shapes available in Structurizr."); - Model model = workspace.getModel(); - - model.addSoftwareSystem("Box", "Description").addTags("Box"); - model.addSoftwareSystem("RoundedBox", "Description").addTags("RoundedBox"); - model.addSoftwareSystem("Ellipse", "Description").addTags("Ellipse"); - model.addSoftwareSystem("Circle", "Description").addTags("Circle"); - model.addSoftwareSystem("Hexagon", "Description").addTags("Hexagon"); - model.addSoftwareSystem("Cylinder", "Description").addTags("Cylinder"); - model.addSoftwareSystem("Pipe", "Description").addTags("Pipe"); - model.addSoftwareSystem("Folder", "Description").addTags("Folder"); - model.addPerson("Person", "Description").addTags("Person"); - - ViewSet views = workspace.getViews(); - EnterpriseContextView view = views.createEnterpriseContextView("shapes", "An example of all shapes available in Structurizr."); - view.addAllElements(); - view.setPaperSize(PaperSize.A5_Landscape); - - Styles styles = views.getConfiguration().getStyles(); - - styles.addElementStyle(Tags.ELEMENT).color("#ffffff").background("#438dd5").fontSize(34).width(650).height(400); - styles.addElementStyle("Box").shape(Shape.Box); - styles.addElementStyle("RoundedBox").shape(Shape.RoundedBox); - styles.addElementStyle("Ellipse").shape(Shape.Ellipse); - styles.addElementStyle("Circle").shape(Shape.Circle); - styles.addElementStyle("Cylinder").shape(Shape.Cylinder); - styles.addElementStyle("Pipe").shape(Shape.Pipe); - styles.addElementStyle("Folder").shape(Shape.Folder); - styles.addElementStyle("Hexagon").shape(Shape.Hexagon); - styles.addElementStyle("Person").shape(Shape.Person).width(550); - - StructurizrClient structurizrClient = new StructurizrClient(API_KEY, API_SECRET); - structurizrClient.putWorkspace(WORKSPACE_ID, workspace); - } - -} \ No newline at end of file diff --git a/structurizr-examples/src/com/structurizr/example/SpringBootPetClinic.java b/structurizr-examples/src/com/structurizr/example/SpringBootPetClinic.java deleted file mode 100644 index b7764b479..000000000 --- a/structurizr-examples/src/com/structurizr/example/SpringBootPetClinic.java +++ /dev/null @@ -1,143 +0,0 @@ -package com.structurizr.example; - -import com.structurizr.Workspace; -import com.structurizr.analysis.ComponentFinder; -import com.structurizr.analysis.ReferencedTypesSupportingTypesStrategy; -import com.structurizr.analysis.SourceCodeComponentFinderStrategy; -import com.structurizr.analysis.SpringComponentFinderStrategy; -import com.structurizr.api.StructurizrClient; -import com.structurizr.model.*; -import com.structurizr.view.*; - -import java.io.File; - -/** - * This is a C4 representation of the Spring Boot version of the PetClinic sample app - * (https://github.com/spring-projects/spring-petclinic/tree/ffa967c94b65a70ea6d3b44275632821838d9fd3) and you can see - * the resulting diagrams at https://www.structurizr.com/public/6031 - * - * Please note, you will need to modify the paths specified in the structurizr-examples/build.gradle file. - */ -public class SpringBootPetClinic { - - private static final String VERSION = "ffa967c94b65a70ea6d3b44275632821838d9fd3"; - - private static final long WORKSPACE_ID = 6031; - private static final String API_KEY = ""; - private static final String API_SECRET = ""; - - public static void main(String[] args) throws Exception { - if (args.length == 0) { - System.out.println("The path to the Spring Boot PetClinic source code must be provided."); - System.exit(-1); - } - File sourceRoot = new File(args[0]); - - try { - ClassLoader.getSystemClassLoader().loadClass("org.springframework.samples.petclinic.vet.Vet"); - } catch (ClassNotFoundException e) { - System.err.println("Please check that the compiled version of the Spring Boot PetClinic application is on the classpath"); - System.exit(-1); - } - - Workspace workspace = new Workspace("Spring Boot PetClinic", - "This is a C4 representation of the Spring Boot PetClinic sample app (https://github.com/spring-projects/spring-petclinic/)"); - workspace.setVersion(VERSION); - Model model = workspace.getModel(); - - // create the basic model (the stuff we can't get from the code) - SoftwareSystem springPetClinic = model.addSoftwareSystem("Spring PetClinic", "Allows employees to view and manage information regarding the veterinarians, the clients, and their pets."); - Person clinicEmployee = model.addPerson("Clinic Employee", "An employee of the clinic"); - clinicEmployee.uses(springPetClinic, "Uses"); - - Container webApplication = springPetClinic.addContainer( - "Web Application", "Allows employees to view and manage information regarding the veterinarians, the clients, and their pets.", "Apache Tomcat 7.x"); - Container relationalDatabase = springPetClinic.addContainer( - "Relational Database", "Stores information regarding the veterinarians, the clients, and their pets.", "HSQLDB"); - clinicEmployee.uses(webApplication, "Uses", "HTTP"); - webApplication.uses(relationalDatabase, "Reads from and writes to", "JDBC, port 9001"); - - SpringComponentFinderStrategy springComponentFinderStrategy = new SpringComponentFinderStrategy(new ReferencedTypesSupportingTypesStrategy(false)); - springComponentFinderStrategy.setIncludePublicTypesOnly(false); - - // and now automatically find all Spring @Controller, @Component, @Service and @Repository components - ComponentFinder componentFinder = new ComponentFinder( - webApplication, - "org.springframework.samples.petclinic", - springComponentFinderStrategy, - new SourceCodeComponentFinderStrategy(new File(sourceRoot, "/src/main/java/"), 150)); - - componentFinder.exclude(".*Formatter.*"); - componentFinder.findComponents(); - - // connect the user to all of the Spring MVC controllers - webApplication.getComponents().stream() - .filter(c -> c.getTechnology().equals(SpringComponentFinderStrategy.SPRING_MVC_CONTROLLER)) - .forEach(c -> clinicEmployee.uses(c, "Uses", "HTTP")); - - // connect all of the repository components to the relational database - webApplication.getComponents().stream() - .filter(c -> c.getTechnology().equals(SpringComponentFinderStrategy.SPRING_REPOSITORY)) - .forEach(c -> c.uses(relationalDatabase, "Reads from and writes to", "JDBC")); - - // finally create some views - ViewSet views = workspace.getViews(); - SystemContextView contextView = views.createSystemContextView(springPetClinic, "context", "The System Context diagram for the Spring PetClinic system."); - contextView.addAllSoftwareSystems(); - contextView.addAllPeople(); - - ContainerView containerView = views.createContainerView(springPetClinic, "containers", "The Containers diagram for the Spring PetClinic system."); - containerView.addAllPeople(); - containerView.addAllSoftwareSystems(); - containerView.addAllContainers(); - - ComponentView componentView = views.createComponentView(webApplication, "components", "The Components diagram for the Spring PetClinic web application."); - componentView.addAllComponents(); - componentView.addAllPeople(); - componentView.add(relationalDatabase); - - // link the architecture model with the code - for (Component component : webApplication.getComponents()) { - for (CodeElement codeElement : component.getCode()) { - String sourcePath = codeElement.getUrl(); - if (sourcePath != null) { - codeElement.setUrl(sourcePath.replace( - sourceRoot.toURI().toString(), - "https://github.com/spring-projects/spring-petclinic/tree/" + VERSION + "/")); - } - } - } - - // rather than creating a component model for the database, let's simply link to the DDL - // (this is really just an example of linking an arbitrary element in the model to an external resource) - relationalDatabase.setUrl("https://github.com/spring-projects/spring-petclinic/tree/" + VERSION + "/src/main/resources/db/hsqldb"); - - // tag and style some elements - springPetClinic.addTags("Spring PetClinic"); - webApplication.getComponents().stream().filter(c -> c.getTechnology().equals(SpringComponentFinderStrategy.SPRING_MVC_CONTROLLER)).forEach(c -> c.addTags("Spring MVC Controller")); - webApplication.getComponents().stream().filter(c -> c.getTechnology().equals(SpringComponentFinderStrategy.SPRING_SERVICE)).forEach(c -> c.addTags("Spring Service")); - webApplication.getComponents().stream().filter(c -> c.getTechnology().equals(SpringComponentFinderStrategy.SPRING_REPOSITORY)).forEach(c -> c.addTags("Spring Repository")); - relationalDatabase.addTags("Database"); - - Component vetController = webApplication.getComponentWithName("VetController"); - Component vetRepository = webApplication.getComponentWithName("VetRepository"); - - DynamicView dynamicView = views.createDynamicView(webApplication, "viewListOfVets", "Shows how the \"view list of vets\" feature works."); - dynamicView.add(clinicEmployee, "Requests list of vets from /vets", vetController); - dynamicView.add(vetController, "Calls findAll", vetRepository); - dynamicView.add(vetRepository, "select * from vets", relationalDatabase); - - Styles styles = views.getConfiguration().getStyles(); - styles.addElementStyle("Spring PetClinic").background("#6CB33E").color("#ffffff"); - styles.addElementStyle(Tags.PERSON).background("#519823").color("#ffffff").shape(Shape.Person); - styles.addElementStyle(Tags.CONTAINER).background("#91D366").color("#ffffff"); - styles.addElementStyle("Database").shape(Shape.Cylinder); - styles.addElementStyle("Spring MVC Controller").background("#D4F3C0").color("#000000"); - styles.addElementStyle("Spring Service").background("#6CB33E").color("#000000"); - styles.addElementStyle("Spring Repository").background("#95D46C").color("#000000"); - - StructurizrClient structurizrClient = new StructurizrClient(API_KEY, API_SECRET); - structurizrClient.putWorkspace(WORKSPACE_ID, workspace); - } - -} \ No newline at end of file diff --git a/structurizr-examples/src/com/structurizr/example/SpringPetClinic.java b/structurizr-examples/src/com/structurizr/example/SpringPetClinic.java deleted file mode 100644 index 3804f7d3f..000000000 --- a/structurizr-examples/src/com/structurizr/example/SpringPetClinic.java +++ /dev/null @@ -1,201 +0,0 @@ -package com.structurizr.example; - -import com.structurizr.Workspace; -import com.structurizr.api.StructurizrClient; -import com.structurizr.analysis.ComponentFinder; -import com.structurizr.analysis.ReferencedTypesSupportingTypesStrategy; -import com.structurizr.analysis.SourceCodeComponentFinderStrategy; -import com.structurizr.analysis.SpringComponentFinderStrategy; -import com.structurizr.documentation.Format; -import com.structurizr.documentation.StructurizrDocumentationTemplate; -import com.structurizr.model.*; -import com.structurizr.util.MapUtils; -import com.structurizr.view.*; - -import java.io.File; - -/** - * This is a C4 representation of the original Spring PetClinic sample app - * (https://github.com/spring-projects/spring-petclinic/tree/95de1d9f8bf63560915331664b27a4a75ce1f1f6) and you can see - * the resulting diagrams at https://www.structurizr.com/public/1 - * - * Use the following command to run this example: ./gradlew :structurizr-example:springPetClinic - * - * Please note, you will need to modify the paths specified in the structurizr-examples/build.gradle file. - */ -public class SpringPetClinic { - - private static final String VERSION = "95de1d9f8bf63560915331664b27a4a75ce1f1f6"; - - private static final long WORKSPACE_ID = 1; - private static final String API_KEY = ""; - private static final String API_SECRET = ""; - - public static void main(String[] args) throws Exception { - if (args.length == 0) { - System.out.println("The path to the Spring PetClinic source code must be provided."); - System.exit(-1); - } - File sourceRoot = new File(args[0]); - - try { - ClassLoader.getSystemClassLoader().loadClass("org.springframework.samples.petclinic.model.Vet"); - } catch (ClassNotFoundException e) { - System.err.println("Please check that the compiled version of the Spring PetClinic application is on the classpath"); - System.exit(-1); - } - - Workspace workspace = new Workspace("Spring PetClinic", - "This is a C4 representation of the Spring PetClinic sample app (https://github.com/spring-projects/spring-petclinic/)"); - workspace.setVersion(VERSION); - Model model = workspace.getModel(); - - // create the basic model (the stuff we can't get from the code) - SoftwareSystem springPetClinic = model.addSoftwareSystem("Spring PetClinic", "Allows employees to view and manage information regarding the veterinarians, the clients, and their pets."); - Person clinicEmployee = model.addPerson("Clinic Employee", "An employee of the clinic"); - clinicEmployee.uses(springPetClinic, "Uses"); - - Container webApplication = springPetClinic.addContainer( - "Web Application", "Allows employees to view and manage information regarding the veterinarians, the clients, and their pets.", "Java and Spring"); - webApplication.addProperty("Deployable artifact name", "petclinic.war"); - Container relationalDatabase = springPetClinic.addContainer( - "Database", "Stores information regarding the veterinarians, the clients, and their pets.", "Relational Database Schema"); - clinicEmployee.uses(webApplication, "Uses", "HTTPS"); - webApplication.uses(relationalDatabase, "Reads from and writes to", "JDBC"); - - // and now automatically find all Spring @Controller, @Component, @Service and @Repository components - ComponentFinder componentFinder = new ComponentFinder( - webApplication, - "org.springframework.samples.petclinic", - new SpringComponentFinderStrategy( - new ReferencedTypesSupportingTypesStrategy(false) - ), - new SourceCodeComponentFinderStrategy(new File(sourceRoot, "/src/main/java/"), 150)); - componentFinder.findComponents(); - - // connect the user to all of the Spring MVC controllers - webApplication.getComponents().stream() - .filter(c -> c.getTechnology().equals(SpringComponentFinderStrategy.SPRING_MVC_CONTROLLER)) - .forEach(c -> clinicEmployee.uses(c, "Uses", "HTTP")); - - // connect all of the repository components to the relational database - webApplication.getComponents().stream() - .filter(c -> c.getTechnology().equals(SpringComponentFinderStrategy.SPRING_REPOSITORY)) - .forEach(c -> c.uses(relationalDatabase, "Reads from and writes to", "JDBC")); - - // finally create some views - ViewSet views = workspace.getViews(); - SystemContextView contextView = views.createSystemContextView(springPetClinic, "context", "The System Context diagram for the Spring PetClinic system."); - contextView.addAllSoftwareSystems(); - contextView.addAllPeople(); - - ContainerView containerView = views.createContainerView(springPetClinic, "containers", "The Container diagram for the Spring PetClinic system."); - containerView.addAllPeople(); - containerView.addAllSoftwareSystems(); - containerView.addAllContainers(); - - ComponentView componentView = views.createComponentView(webApplication, "components", "The Component diagram for the Spring PetClinic web application."); - componentView.addAllComponents(); - componentView.addAllPeople(); - componentView.add(relationalDatabase); - - // link the architecture model with the code - for (Component component : webApplication.getComponents()) { - for (CodeElement codeElement : component.getCode()) { - String sourcePath = codeElement.getUrl(); - if (sourcePath != null) { - codeElement.setUrl(sourcePath.replace( - sourceRoot.toURI().toString(), - "https://github.com/spring-projects/spring-petclinic/tree/" + VERSION + "/")); - } - } - } - - // rather than creating a component model for the database, let's simply link to the DDL - // (this is really just an example of linking an arbitrary element in the model to an external resource) - relationalDatabase.setUrl("https://github.com/spring-projects/spring-petclinic/tree/" + VERSION + "/src/main/resources/db/hsqldb"); - relationalDatabase.addProperty("Schema name", "petclinic"); - - // tag and style some elements - springPetClinic.addTags("Spring PetClinic"); - webApplication.getComponents().stream().filter(c -> c.getTechnology().equals(SpringComponentFinderStrategy.SPRING_MVC_CONTROLLER)).forEach(c -> c.addTags("Spring MVC Controller")); - webApplication.getComponents().stream().filter(c -> c.getTechnology().equals(SpringComponentFinderStrategy.SPRING_SERVICE)).forEach(c -> c.addTags("Spring Service")); - webApplication.getComponents().stream().filter(c -> c.getTechnology().equals(SpringComponentFinderStrategy.SPRING_REPOSITORY)).forEach(c -> c.addTags("Spring Repository")); - relationalDatabase.addTags("Database"); - - Component vetController = webApplication.getComponentWithName("VetController"); - Component clinicService = webApplication.getComponentWithName("ClinicService"); - Component vetRepository = webApplication.getComponentWithName("VetRepository"); - - DynamicView dynamicView = views.createDynamicView(webApplication, "viewListOfVets", "Shows how the \"view list of vets\" feature works."); - dynamicView.add(clinicEmployee, "Requests list of vets from /vets", vetController); - dynamicView.add(vetController, "Calls findVets", clinicService); - dynamicView.add(clinicService, "Calls findAll", vetRepository); - dynamicView.add(vetRepository, "select * from vets", relationalDatabase); - - DeploymentNode developerLaptop = model.addDeploymentNode("Developer Laptop", "A developer laptop.", "Windows 7+ or macOS"); - developerLaptop.addDeploymentNode("Docker Container - Web Server", "A Docker container.", "Docker") - .addDeploymentNode("Apache Tomcat", "An open source Java EE web server.", "Apache Tomcat 7.x", 1, MapUtils.create("Xmx=256M", "Xms=512M", "Java Version=8")) - .add(webApplication); - - developerLaptop.addDeploymentNode("Docker Container - Database Server", "A Docker container.", "Docker") - .addDeploymentNode("Database Server", "A development database.", "HSQLDB") - .add(relationalDatabase); - - DeploymentNode stagingServer = model.addDeploymentNode("Staging Server", "A server hosted at Amazon AWS EC2", "Ubuntu 12.04 LTS", 1, MapUtils.create("AWS instance type=t2.medium", "AWS region=us-west-1")); - stagingServer.addDeploymentNode("Apache Tomcat", "An open source Java EE web server.", "Apache Tomcat 7.x", 1, MapUtils.create("Xmx=512M", "Xms=1024M", "Java Version=8")) - .add(webApplication); - stagingServer.addDeploymentNode("MySQL", "The staging database server.", "MySQL 5.5.x", 1) - .add(relationalDatabase);; - - DeploymentNode liveWebServer = model.addDeploymentNode("Web Server", "A server hosted at Amazon AWS EC2, accessed via Elastic Load Balancing.", "Ubuntu 12.04 LTS", 2, MapUtils.create("AWS instance type=t2.small", "AWS region=us-west-1")); - liveWebServer.addDeploymentNode("Apache Tomcat", "An open source Java EE web server.", "Apache Tomcat 7.x", 1, MapUtils.create("Xmx=512M", "Xms=1024M", "Java Version=8")) - .add(webApplication); - - DeploymentNode primaryDatabaseServer = model.addDeploymentNode("Database Server - Primary", "A server hosted at Amazon AWS EC2.", "Ubuntu 12.04 LTS", 1, MapUtils.create("AWS instance type=t2.medium", "AWS region=us-west-1")) - .addDeploymentNode("MySQL - Primary", "The primary, live database server.", "MySQL 5.5.x"); - primaryDatabaseServer.add(relationalDatabase); - - DeploymentNode secondaryDatabaseServer = model.addDeploymentNode("Database Server - Secondary", "A server hosted at Amazon AWS EC2.", "Ubuntu 12.04 LTS", 1, MapUtils.create("AWS instance type=t2.small", "AWS region=us-east-1")) - .addDeploymentNode("MySQL - Secondary", "A secondary database server, used for failover purposes.", "MySQL 5.5.x"); - ContainerInstance secondaryDatabase = secondaryDatabaseServer.add(relationalDatabase); - - model.getRelationships().stream().filter(r -> r.getDestination().equals(secondaryDatabase)).forEach(r -> r.addTags("Failover")); - Relationship dataReplicationRelationship = primaryDatabaseServer.uses(secondaryDatabaseServer, "Replicates data to", ""); - secondaryDatabase.addTags("Failover"); - - DeploymentView developmentDeploymentView = views.createDeploymentView(springPetClinic, "developmentDeployment", "An example development deployment scenario for the Spring PetClinic software system."); - developmentDeploymentView.add(developerLaptop); - - DeploymentView stagingDeploymentView = views.createDeploymentView(springPetClinic, "stagingDeployment", "An example staging deployment scenario for the Spring PetClinic software system."); - stagingDeploymentView.add(stagingServer); - - DeploymentView liveDeploymentView = views.createDeploymentView(springPetClinic, "liveDeployment", "An example live deployment scenario for the Spring PetClinic software system."); - liveDeploymentView.add(liveWebServer); - liveDeploymentView.add(primaryDatabaseServer); - liveDeploymentView.add(secondaryDatabaseServer); - liveDeploymentView.add(dataReplicationRelationship); - - StructurizrDocumentationTemplate template = new StructurizrDocumentationTemplate(workspace); - template.addContextSection(springPetClinic, Format.Markdown, "This is the context section for the Spring PetClinic System...\n![](embed:context)"); - template.addContainersSection(springPetClinic, Format.Markdown, "This is the containers section for the Spring PetClinic System...\n![](embed:containers)"); - template.addComponentsSection(webApplication, Format.Markdown, "This is the components section for the Spring PetClinic web application...\n![](embed:components)"); - template.addDeploymentSection(springPetClinic, Format.Markdown, "This is the deployment section for the Spring PetClinic web application...\n### Staging environment\n![](embed:stagingDeployment)\n### Live environment\n![](embed:liveDeployment)"); - template.addDevelopmentEnvironmentSection(springPetClinic, Format.Markdown, "This is the development environment section for the Spring PetClinic web application...\n![](embed:developmentDeployment)"); - - Styles styles = views.getConfiguration().getStyles(); - styles.addElementStyle("Spring PetClinic").background("#6CB33E").color("#ffffff"); - styles.addElementStyle(Tags.PERSON).background("#519823").color("#ffffff").shape(Shape.Person); - styles.addElementStyle(Tags.CONTAINER).background("#91D366").color("#ffffff"); - styles.addElementStyle("Database").shape(Shape.Cylinder); - styles.addElementStyle("Spring MVC Controller").background("#D4F3C0").color("#000000"); - styles.addElementStyle("Spring Service").background("#6CB33E").color("#000000"); - styles.addElementStyle("Spring Repository").background("#95D46C").color("#000000"); - styles.addElementStyle("Failover").opacity(25); - styles.addRelationshipStyle("Failover").opacity(25).position(70); - - StructurizrClient structurizrClient = new StructurizrClient(API_KEY, API_SECRET); - structurizrClient.putWorkspace(WORKSPACE_ID, workspace); - } - -} \ No newline at end of file diff --git a/structurizr-examples/src/com/structurizr/example/StructurizrAnnotations.java b/structurizr-examples/src/com/structurizr/example/StructurizrAnnotations.java deleted file mode 100644 index 91be1b4a3..000000000 --- a/structurizr-examples/src/com/structurizr/example/StructurizrAnnotations.java +++ /dev/null @@ -1,65 +0,0 @@ -package com.structurizr.example; - -import com.structurizr.Workspace; -import com.structurizr.analysis.ComponentFinder; -import com.structurizr.analysis.StructurizrAnnotationsComponentFinderStrategy; -import com.structurizr.api.StructurizrClient; -import com.structurizr.model.*; -import com.structurizr.view.*; - -/** - * An small example that illustrates how to use the Structurizr annotations - * in conjunction with the StructurizrAnnotationsComponentFinderStrategy. - * - * The live workspace is available to view at https://structurizr.com/share/36571 - */ -public class StructurizrAnnotations { - - private static final String DATABASE_TAG = "Database"; - - private static final long WORKSPACE_ID = 36571; - private static final String API_KEY = ""; - private static final String API_SECRET = ""; - - public static void main(String[] args) throws Exception { - Workspace workspace = new Workspace("Structurizr for Java Annotations", "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."); - - Container webApplication = softwareSystem.addContainer("Web Application", "Provides users with information.", "Java"); - Container database = softwareSystem.addContainer("Database", "Stores information.", "Relational database schema"); - database.addTags(DATABASE_TAG); - - ComponentFinder componentFinder = new ComponentFinder( - webApplication, - "com.structurizr.example.annotations", - new StructurizrAnnotationsComponentFinderStrategy() - ); - componentFinder.findComponents(); - model.addImplicitRelationships(); - - ViewSet views = workspace.getViews(); - SystemContextView contextView = views.createSystemContextView(softwareSystem, "SystemContext", "An example of a System Context diagram."); - contextView.addAllElements(); - - ContainerView containerView = views.createContainerView(softwareSystem, "Containers", "The container diagram from my software system."); - containerView.addAllElements(); - - ComponentView componentView = views.createComponentView(webApplication, "Components", "The component diagram for the web application."); - componentView.addAllElements(); - - Styles styles = views.getConfiguration().getStyles(); - styles.addElementStyle(Tags.ELEMENT).color("#ffffff"); - styles.addElementStyle(Tags.SOFTWARE_SYSTEM).background("#1168bd"); - styles.addElementStyle(Tags.CONTAINER).background("#438dd5"); - styles.addElementStyle(Tags.COMPONENT).background("#85bbf0").color("#000000"); - styles.addElementStyle(Tags.PERSON).background("#08427b").shape(Shape.Person); - styles.addElementStyle(DATABASE_TAG).shape(Shape.Cylinder); - - StructurizrClient structurizrClient = new StructurizrClient(API_KEY, API_SECRET); - structurizrClient.putWorkspace(WORKSPACE_ID, workspace); - } - -} \ No newline at end of file diff --git a/structurizr-examples/src/com/structurizr/example/StructurizrDocumentationExample.java b/structurizr-examples/src/com/structurizr/example/StructurizrDocumentationExample.java deleted file mode 100644 index 12d2a8fa7..000000000 --- a/structurizr-examples/src/com/structurizr/example/StructurizrDocumentationExample.java +++ /dev/null @@ -1,81 +0,0 @@ -package com.structurizr.example; - -import com.structurizr.Workspace; -import com.structurizr.api.StructurizrClient; -import com.structurizr.documentation.Format; -import com.structurizr.documentation.StructurizrDocumentationTemplate; -import com.structurizr.model.Model; -import com.structurizr.model.Person; -import com.structurizr.model.SoftwareSystem; -import com.structurizr.model.Tags; -import com.structurizr.view.Shape; -import com.structurizr.view.Styles; -import com.structurizr.view.SystemContextView; -import com.structurizr.view.ViewSet; - -import java.io.File; - -/** - * An empty software architecture document using the Structurizr template. - * - * See https://structurizr.com/share/14181/documentation for the live version. - */ -public class StructurizrDocumentationExample { - - private static final long WORKSPACE_ID = 14181; - private static final String API_KEY = "key"; - private static final String API_SECRET = "secret"; - - public static void main(String[] args) throws Exception { - Workspace workspace = new Workspace("Documentation - Structurizr", "An empty software architecture document using the Structurizr template."); - Model model = workspace.getModel(); - ViewSet views = workspace.getViews(); - - Person user = model.addPerson("User", "A user of my software system."); - SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System", "My software system."); - user.uses(softwareSystem, "Uses"); - - SystemContextView contextView = views.createSystemContextView(softwareSystem, "SystemContext", "An example of a System Context diagram."); - contextView.addAllSoftwareSystems(); - contextView.addAllPeople(); - - Styles styles = views.getConfiguration().getStyles(); - styles.addElementStyle(Tags.PERSON).shape(Shape.Person); - - StructurizrDocumentationTemplate template = new StructurizrDocumentationTemplate(workspace); - - // this is the Markdown version - 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")); - - // this is the AsciiDoc version -// File documentationRoot = new File("./structurizr-examples/src/com/structurizr/example/documentation/structurizr/asciidoc"); -// template.addContextSection(softwareSystem, new File(documentationRoot, "01-context.adoc")); -// template.addFunctionalOverviewSection(softwareSystem, new File(documentationRoot, "02-functional-overview.adoc")); -// template.addQualityAttributesSection(softwareSystem, new File(documentationRoot, "03-quality-attributes.adoc")); -// template.addConstraintsSection(softwareSystem, new File(documentationRoot, "04-constraints.adoc")); -// template.addPrinciplesSection(softwareSystem, new File(documentationRoot, "05-principles.adoc")); -// template.addSoftwareArchitectureSection(softwareSystem, new File(documentationRoot, "06-software-architecture.adoc")); -// template.addDataSection(softwareSystem, new File(documentationRoot, "07-data.adoc")); -// template.addInfrastructureArchitectureSection(softwareSystem, new File(documentationRoot, "08-infrastructure-architecture.adoc")); -// template.addDeploymentSection(softwareSystem, new File(documentationRoot, "09-deployment.adoc")); -// template.addDevelopmentEnvironmentSection(softwareSystem, new File(documentationRoot, "10-development-environment.adoc")); -// template.addOperationAndSupportSection(softwareSystem, new File(documentationRoot, "11-operation-and-support.adoc")); -// template.addDecisionLogSection(softwareSystem, new File(documentationRoot, "12-decision-log.adoc")); - - StructurizrClient structurizrClient = new StructurizrClient(API_KEY, API_SECRET); - structurizrClient.putWorkspace(WORKSPACE_ID, workspace); - } - -} \ No newline at end of file diff --git a/structurizr-examples/src/com/structurizr/example/StylingElements.java b/structurizr-examples/src/com/structurizr/example/StylingElements.java deleted file mode 100644 index 6b8c215c0..000000000 --- a/structurizr-examples/src/com/structurizr/example/StylingElements.java +++ /dev/null @@ -1,60 +0,0 @@ -package com.structurizr.example; - -import com.structurizr.Workspace; -import com.structurizr.api.StructurizrClient; -import com.structurizr.model.Container; -import com.structurizr.model.Model; -import com.structurizr.model.Person; -import com.structurizr.model.SoftwareSystem; -import com.structurizr.view.ContainerView; -import com.structurizr.view.Styles; -import com.structurizr.view.ViewSet; - -/** - * An example of how to style elements on diagrams. - * - * The live workspace is available to view at https://structurizr.com/share/36111 - */ -public class StylingElements { - - private static final long WORKSPACE_ID = 36111; - private static final String API_KEY = ""; - private static final String API_SECRET = ""; - - public static void main(String[] args) throws Exception { - Workspace workspace = new Workspace("Styling Elements", "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."); - Container webApplication = softwareSystem.addContainer("Web Application", "My web application.", "Java and Spring MVC"); - Container database = softwareSystem.addContainer("Database", "My database.", "Relational database schema"); - user.uses(webApplication, "Uses", "HTTPS"); - webApplication.uses(database, "Reads from and writes to", "JDBC"); - - ViewSet views = workspace.getViews(); - ContainerView containerView = views.createContainerView(softwareSystem, "containers", "An example of a container diagram."); - containerView.addAllElements(); - - Styles styles = workspace.getViews().getConfiguration().getStyles(); - - // example 1 -// styles.addElementStyle(Tags.ELEMENT).background("#438dd5").color("#ffffff"); - - // example 2 -// styles.addElementStyle(Tags.ELEMENT).color("#ffffff"); -// styles.addElementStyle(Tags.PERSON).background("#08427b"); -// styles.addElementStyle(Tags.CONTAINER).background("#438dd5"); - - // example 3 -// 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); - - StructurizrClient structurizrClient = new StructurizrClient(API_KEY, API_SECRET); - structurizrClient.putWorkspace(WORKSPACE_ID, workspace); - } - -} \ No newline at end of file diff --git a/structurizr-examples/src/com/structurizr/example/StylingRelationships.java b/structurizr-examples/src/com/structurizr/example/StylingRelationships.java deleted file mode 100644 index 7aaa97ffb..000000000 --- a/structurizr-examples/src/com/structurizr/example/StylingRelationships.java +++ /dev/null @@ -1,54 +0,0 @@ -package com.structurizr.example; - -import com.structurizr.Workspace; -import com.structurizr.api.StructurizrClient; -import com.structurizr.model.Container; -import com.structurizr.model.Model; -import com.structurizr.model.Person; -import com.structurizr.model.SoftwareSystem; -import com.structurizr.view.ContainerView; -import com.structurizr.view.Styles; -import com.structurizr.view.ViewSet; - -/** - * An example of how to style relationships on diagrams. - * - * The live workspace is available to view at https://structurizr.com/share/36131 - */ -public class StylingRelationships { - - private static final long WORKSPACE_ID = 36131; - private static final String API_KEY = ""; - private static final String API_SECRET = ""; - - public static void main(String[] args) throws Exception { - Workspace workspace = new Workspace("Styling Relationships", "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."); - Container webApplication = softwareSystem.addContainer("Web Application", "My web application.", "Java and Spring MVC"); - Container database = softwareSystem.addContainer("Database", "My database.", "Relational database schema"); - user.uses(webApplication, "Uses", "HTTPS"); - webApplication.uses(database, "Reads from and writes to", "JDBC"); - - ViewSet views = workspace.getViews(); - ContainerView containerView = views.createContainerView(softwareSystem, "containers", "An example of a container diagram."); - containerView.addAllElements(); - - Styles styles = workspace.getViews().getConfiguration().getStyles(); - - // example 1 -// styles.addRelationshipStyle(Tags.RELATIONSHIP).color("#ff0000"); - - // example 2 -// 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"); - - StructurizrClient structurizrClient = new StructurizrClient(API_KEY, API_SECRET); - structurizrClient.putWorkspace(WORKSPACE_ID, workspace); - } - -} \ No newline at end of file diff --git a/structurizr-examples/src/com/structurizr/example/ViewpointsAndPerspectivesDocumentationExample.java b/structurizr-examples/src/com/structurizr/example/ViewpointsAndPerspectivesDocumentationExample.java deleted file mode 100644 index 0371aaf37..000000000 --- a/structurizr-examples/src/com/structurizr/example/ViewpointsAndPerspectivesDocumentationExample.java +++ /dev/null @@ -1,71 +0,0 @@ -package com.structurizr.example; - -import com.structurizr.Workspace; -import com.structurizr.api.StructurizrClient; -import com.structurizr.documentation.Format; -import com.structurizr.documentation.ViewpointsAndPerspectivesDocumentationTemplate; -import com.structurizr.model.Model; -import com.structurizr.model.Person; -import com.structurizr.model.SoftwareSystem; -import com.structurizr.model.Tags; -import com.structurizr.view.Shape; -import com.structurizr.view.Styles; -import com.structurizr.view.SystemContextView; -import com.structurizr.view.ViewSet; - -import java.io.File; - -/** - * An empty software architecture document using the "Viewpoints and Perspectives" template. - * - * See https://structurizr.com/share/36371/documentation for the live version. - */ -public class ViewpointsAndPerspectivesDocumentationExample { - - private static final long WORKSPACE_ID = 36371; - private static final String API_KEY = "key"; - private static final String API_SECRET = "secret"; - - public static void main(String[] args) throws Exception { - Workspace workspace = new Workspace("Documentation - Viewpoints and Perspectives", "An empty software architecture document using the Viewpoints and Perspectives template."); - Model model = workspace.getModel(); - ViewSet views = workspace.getViews(); - - Person user = model.addPerson("User", "A user of my software system."); - SoftwareSystem softwareSystem = model.addSoftwareSystem("Software System", "My software system."); - user.uses(softwareSystem, "Uses"); - - SystemContextView contextView = views.createSystemContextView(softwareSystem, "SystemContext", "An example of a System Context diagram."); - contextView.addAllSoftwareSystems(); - contextView.addAllPeople(); - - Styles styles = views.getConfiguration().getStyles(); - styles.addElementStyle(Tags.PERSON).shape(Shape.Person); - - ViewpointsAndPerspectivesDocumentationTemplate template = new ViewpointsAndPerspectivesDocumentationTemplate(workspace); - - // this is the Markdown version - 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")); - - // this is the AsciiDoc version -// File documentationRoot = new File("./structurizr-examples/src/com/structurizr/example/documentation/viewpointsandperspectives/asciidoc"); -// template.addIntroductionSection(softwareSystem, new File(documentationRoot, "01-introduction.adoc")); -// template.addGlossarySection(softwareSystem, new File(documentationRoot, "02-glossary.adoc")); -// template.addSystemStakeholdersAndRequirementsSection(softwareSystem, new File(documentationRoot, "03-system-stakeholders-and-requirements.adoc")); -// template.addArchitecturalForcesSection(softwareSystem, new File(documentationRoot, "04-architectural-forces.adoc")); -// template.addArchitecturalViewsSection(softwareSystem, new File(documentationRoot, "05-architectural-views")); -// template.addSystemQualitiesSection(softwareSystem, new File(documentationRoot, "06-system-qualities.adoc")); -// template.addAppendicesSection(softwareSystem, new File(documentationRoot, "07-appendices.adoc")); - - StructurizrClient structurizrClient = new StructurizrClient(API_KEY, API_SECRET); - structurizrClient.putWorkspace(WORKSPACE_ID, workspace); - } - -} \ No newline at end of file diff --git a/structurizr-examples/src/com/structurizr/example/WidgetsLimited.java b/structurizr-examples/src/com/structurizr/example/WidgetsLimited.java deleted file mode 100644 index ed06424b2..000000000 --- a/structurizr-examples/src/com/structurizr/example/WidgetsLimited.java +++ /dev/null @@ -1,86 +0,0 @@ -package com.structurizr.example; - -import com.structurizr.Workspace; -import com.structurizr.api.StructurizrClient; -import com.structurizr.documentation.Format; -import com.structurizr.documentation.StructurizrDocumentationTemplate; -import com.structurizr.model.*; -import com.structurizr.view.*; - -/** - * This workspace contains a number of diagrams for a fictional reseller of widgets online. - * - * You can see the workspace online at https://structurizr.com/public/14471 - */ -public class WidgetsLimited { - - private static final long WORKSPACE_ID = 14471; - private static final String API_KEY = ""; - private static final String API_SECRET = ""; - - private static final String EXTERNAL_TAG = "External"; - private static final String INTERNAL_TAG = "Internal"; - - public static void main(String[] args) throws Exception { - Workspace workspace = new Workspace("Widgets Limited", "Sells widgets to customers online."); - Model model = workspace.getModel(); - ViewSet views = workspace.getViews(); - Styles styles = views.getConfiguration().getStyles(); - - model.setEnterprise(new Enterprise("Widgets Limited")); - - Person customer = model.addPerson(Location.External, "Customer", "A customer of Widgets Limited."); - Person customerServiceUser = model.addPerson(Location.Internal, "Customer Service Agent", "Deals with customer enquiries."); - SoftwareSystem ecommerceSystem = model.addSoftwareSystem(Location.Internal, "E-commerce System", "Allows customers to buy widgets online via the widgets.com website."); - SoftwareSystem fulfilmentSystem = model.addSoftwareSystem(Location.Internal, "Fulfilment System", "Responsible for processing and shipping of customer orders."); - SoftwareSystem taxamo = model.addSoftwareSystem(Location.External, "Taxamo", "Calculates local tax (for EU B2B customers) and acts as a front-end for Braintree Payments."); - taxamo.setUrl("https://www.taxamo.com"); - SoftwareSystem braintreePayments = model.addSoftwareSystem(Location.External, "Braintree Payments", "Processes credit card payments on behalf of Widgets Limited."); - braintreePayments.setUrl("https://www.braintreepayments.com"); - SoftwareSystem jerseyPost = model.addSoftwareSystem(Location.External, "Jersey Post", "Calculates worldwide shipping costs for packages."); - - model.getPeople().stream().filter(p -> p.getLocation() == Location.External).forEach(p -> p.addTags(EXTERNAL_TAG)); - model.getPeople().stream().filter(p -> p.getLocation() == Location.Internal).forEach(p -> p.addTags(INTERNAL_TAG)); - - model.getSoftwareSystems().stream().filter(ss -> ss.getLocation() == Location.External).forEach(ss -> ss.addTags(EXTERNAL_TAG)); - model.getSoftwareSystems().stream().filter(ss -> ss.getLocation() == Location.Internal).forEach(ss -> ss.addTags(INTERNAL_TAG)); - - customer.interactsWith(customerServiceUser, "Asks questions to", "Telephone"); - customerServiceUser.uses(ecommerceSystem, "Looks up order information using"); - customer.uses(ecommerceSystem, "Places orders for widgets using"); - ecommerceSystem.uses(fulfilmentSystem, "Sends order information to"); - fulfilmentSystem.uses(jerseyPost, "Gets shipping charges from"); - ecommerceSystem.uses(taxamo, "Delegates credit card processing to"); - taxamo.uses(braintreePayments, "Uses for credit card processing"); - - EnterpriseContextView enterpriseContextView = views.createEnterpriseContextView("SystemLandscape", "The system landscape for Widgets Limited."); - enterpriseContextView.addAllElements(); - - SystemContextView ecommerceSystemContext = views.createSystemContextView(ecommerceSystem, "EcommerceSystemContext", "The system context diagram for the Widgets Limited e-commerce system."); - ecommerceSystemContext.addNearestNeighbours(ecommerceSystem); - ecommerceSystemContext.remove(customer.getEfferentRelationshipWith(customerServiceUser)); - - SystemContextView fulfilmentSystemContext = views.createSystemContextView(fulfilmentSystem, "FulfilmentSystemContext", "The system context diagram for the Widgets Limited fulfilment system."); - fulfilmentSystemContext.addNearestNeighbours(fulfilmentSystem); - - DynamicView dynamicView = views.createDynamicView("CustomerSupportCall", "A high-level overview of the customer support call process."); - dynamicView.add(customer, customerServiceUser); - dynamicView.add(customerServiceUser, ecommerceSystem); - - StructurizrDocumentationTemplate template = new StructurizrDocumentationTemplate(workspace); - template.addSection("System Landscape", 1, Format.Markdown, "Here is some information about the Widgets Limited system landscape... ![](embed:SystemLandscape)"); - template.addContextSection(ecommerceSystem, Format.Markdown, "This is the context section for the E-commerce System... ![](embed:EcommerceSystemContext)"); - template.addContextSection(fulfilmentSystem, Format.Markdown, "This is the context section for the Fulfilment System... ![](embed:FulfilmentSystemContext)"); - - styles.addElementStyle(Tags.SOFTWARE_SYSTEM).shape(Shape.RoundedBox); - styles.addElementStyle(Tags.PERSON).shape(Shape.Person); - - styles.addElementStyle(Tags.ELEMENT).color("#ffffff"); - styles.addElementStyle(EXTERNAL_TAG).background("#EC5381").border(Border.Dashed); - styles.addElementStyle(INTERNAL_TAG).background("#B60037"); - - StructurizrClient structurizrClient = new StructurizrClient(API_KEY, API_SECRET); - structurizrClient.putWorkspace(WORKSPACE_ID, workspace); - } - -} \ No newline at end of file diff --git a/structurizr-examples/src/com/structurizr/example/annotations/HtmlController.java b/structurizr-examples/src/com/structurizr/example/annotations/HtmlController.java deleted file mode 100644 index 9a4a1e2f4..000000000 --- a/structurizr-examples/src/com/structurizr/example/annotations/HtmlController.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.structurizr.example.annotations; - -import com.structurizr.annotation.Component; -import com.structurizr.annotation.UsedByPerson; -import com.structurizr.annotation.UsesComponent; - -@Component(description = "Serves HTML pages to users.", technology = "Java") -@UsedByPerson(name = "User", description = "Uses", technology = "HTTPS") -class HtmlController { - - @UsesComponent(description = "Gets data using") - private Repository repository = new JdbcRepository(); - -} \ No newline at end of file diff --git a/structurizr-examples/src/com/structurizr/example/annotations/JdbcRepository.java b/structurizr-examples/src/com/structurizr/example/annotations/JdbcRepository.java deleted file mode 100644 index fa21c78f9..000000000 --- a/structurizr-examples/src/com/structurizr/example/annotations/JdbcRepository.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.structurizr.example.annotations; - -import com.structurizr.annotation.UsesContainer; - -@UsesContainer(name = "Database", description = "Reads from", technology = "JDBC") -class JdbcRepository implements Repository { - - public String getData(long id) { - return "..."; - } - -} \ No newline at end of file diff --git a/structurizr-examples/src/com/structurizr/example/annotations/Repository.java b/structurizr-examples/src/com/structurizr/example/annotations/Repository.java deleted file mode 100644 index 57d458441..000000000 --- a/structurizr-examples/src/com/structurizr/example/annotations/Repository.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.structurizr.example.annotations; - -import com.structurizr.annotation.Component; - -@Component(description = "Provides access to data stored in the database.", technology = "Java and JPA") -public interface Repository { - - String getData(long id); - -} \ No newline at end of file diff --git a/structurizr-examples/src/com/structurizr/example/documentation/arc42/asciidoc/01-introduction-and-goals.adoc b/structurizr-examples/src/com/structurizr/example/documentation/arc42/asciidoc/01-introduction-and-goals.adoc deleted file mode 100644 index 4ff008131..000000000 --- a/structurizr-examples/src/com/structurizr/example/documentation/arc42/asciidoc/01-introduction-and-goals.adoc +++ /dev/null @@ -1,7 +0,0 @@ -== Introduction and Goals - -=== Requirements Overview - -=== Quality Goals - -=== Stakeholders diff --git a/structurizr-examples/src/com/structurizr/example/documentation/arc42/asciidoc/02-architecture-constraints.adoc b/structurizr-examples/src/com/structurizr/example/documentation/arc42/asciidoc/02-architecture-constraints.adoc deleted file mode 100644 index e0637beb3..000000000 --- a/structurizr-examples/src/com/structurizr/example/documentation/arc42/asciidoc/02-architecture-constraints.adoc +++ /dev/null @@ -1 +0,0 @@ -== Architecture Constraints diff --git a/structurizr-examples/src/com/structurizr/example/documentation/arc42/asciidoc/03-system-scope-and-context.adoc b/structurizr-examples/src/com/structurizr/example/documentation/arc42/asciidoc/03-system-scope-and-context.adoc deleted file mode 100644 index 06cfd6537..000000000 --- a/structurizr-examples/src/com/structurizr/example/documentation/arc42/asciidoc/03-system-scope-and-context.adoc +++ /dev/null @@ -1,8 +0,0 @@ -== Context and Scope - -image::embed:SystemContext[] - -=== Business Context - -=== Technical Context - diff --git a/structurizr-examples/src/com/structurizr/example/documentation/arc42/asciidoc/04-solution-strategy.adoc b/structurizr-examples/src/com/structurizr/example/documentation/arc42/asciidoc/04-solution-strategy.adoc deleted file mode 100644 index 3d425dfac..000000000 --- a/structurizr-examples/src/com/structurizr/example/documentation/arc42/asciidoc/04-solution-strategy.adoc +++ /dev/null @@ -1 +0,0 @@ -== Solution Strategy diff --git a/structurizr-examples/src/com/structurizr/example/documentation/arc42/asciidoc/05-building-block-view.adoc b/structurizr-examples/src/com/structurizr/example/documentation/arc42/asciidoc/05-building-block-view.adoc deleted file mode 100644 index 4053cea52..000000000 --- a/structurizr-examples/src/com/structurizr/example/documentation/arc42/asciidoc/05-building-block-view.adoc +++ /dev/null @@ -1,7 +0,0 @@ -== Building Block View - -=== Level 1 - -=== Level 2 - -=== Level 3 diff --git a/structurizr-examples/src/com/structurizr/example/documentation/arc42/asciidoc/06-runtime-view.adoc b/structurizr-examples/src/com/structurizr/example/documentation/arc42/asciidoc/06-runtime-view.adoc deleted file mode 100644 index 85b36e5d4..000000000 --- a/structurizr-examples/src/com/structurizr/example/documentation/arc42/asciidoc/06-runtime-view.adoc +++ /dev/null @@ -1,2 +0,0 @@ -== Runtime View - diff --git a/structurizr-examples/src/com/structurizr/example/documentation/arc42/asciidoc/07-deployment-view.adoc b/structurizr-examples/src/com/structurizr/example/documentation/arc42/asciidoc/07-deployment-view.adoc deleted file mode 100644 index de1b72ca7..000000000 --- a/structurizr-examples/src/com/structurizr/example/documentation/arc42/asciidoc/07-deployment-view.adoc +++ /dev/null @@ -1 +0,0 @@ -== Deployment View diff --git a/structurizr-examples/src/com/structurizr/example/documentation/arc42/asciidoc/08-crosscutting-concepts.adoc b/structurizr-examples/src/com/structurizr/example/documentation/arc42/asciidoc/08-crosscutting-concepts.adoc deleted file mode 100644 index 01f6c0c0a..000000000 --- a/structurizr-examples/src/com/structurizr/example/documentation/arc42/asciidoc/08-crosscutting-concepts.adoc +++ /dev/null @@ -1,2 +0,0 @@ -== Crosscutting Concepts - diff --git a/structurizr-examples/src/com/structurizr/example/documentation/arc42/asciidoc/09-architecture-decisions.adoc b/structurizr-examples/src/com/structurizr/example/documentation/arc42/asciidoc/09-architecture-decisions.adoc deleted file mode 100644 index ffabd62e9..000000000 --- a/structurizr-examples/src/com/structurizr/example/documentation/arc42/asciidoc/09-architecture-decisions.adoc +++ /dev/null @@ -1,2 +0,0 @@ -== Architecture Decisions - diff --git a/structurizr-examples/src/com/structurizr/example/documentation/arc42/asciidoc/10-quality-requirements.adoc b/structurizr-examples/src/com/structurizr/example/documentation/arc42/asciidoc/10-quality-requirements.adoc deleted file mode 100644 index 6930d6a20..000000000 --- a/structurizr-examples/src/com/structurizr/example/documentation/arc42/asciidoc/10-quality-requirements.adoc +++ /dev/null @@ -1,6 +0,0 @@ -== Quality Requirements - -=== Quality Tree - -=== Quality Scenarios - diff --git a/structurizr-examples/src/com/structurizr/example/documentation/arc42/asciidoc/11-risks-and-technical-debt.adoc b/structurizr-examples/src/com/structurizr/example/documentation/arc42/asciidoc/11-risks-and-technical-debt.adoc deleted file mode 100644 index 196489aa6..000000000 --- a/structurizr-examples/src/com/structurizr/example/documentation/arc42/asciidoc/11-risks-and-technical-debt.adoc +++ /dev/null @@ -1 +0,0 @@ -== Risks and Technical Debt diff --git a/structurizr-examples/src/com/structurizr/example/documentation/arc42/asciidoc/12-glossary.adoc b/structurizr-examples/src/com/structurizr/example/documentation/arc42/asciidoc/12-glossary.adoc deleted file mode 100644 index 0c393c5a8..000000000 --- a/structurizr-examples/src/com/structurizr/example/documentation/arc42/asciidoc/12-glossary.adoc +++ /dev/null @@ -1 +0,0 @@ -== Glossary diff --git a/structurizr-examples/src/com/structurizr/example/documentation/arc42/markdown/01-introduction-and-goals.md b/structurizr-examples/src/com/structurizr/example/documentation/arc42/markdown/01-introduction-and-goals.md deleted file mode 100644 index 30f9e0aa6..000000000 --- a/structurizr-examples/src/com/structurizr/example/documentation/arc42/markdown/01-introduction-and-goals.md +++ /dev/null @@ -1,7 +0,0 @@ -## Introduction and Goals - -### Requirements Overview - -### Quality Goals - -### Stakeholders diff --git a/structurizr-examples/src/com/structurizr/example/documentation/arc42/markdown/02-architecture-constraints.md b/structurizr-examples/src/com/structurizr/example/documentation/arc42/markdown/02-architecture-constraints.md deleted file mode 100644 index 38ca06000..000000000 --- a/structurizr-examples/src/com/structurizr/example/documentation/arc42/markdown/02-architecture-constraints.md +++ /dev/null @@ -1 +0,0 @@ -## Architecture Constraints diff --git a/structurizr-examples/src/com/structurizr/example/documentation/arc42/markdown/03-system-scope-and-context.md b/structurizr-examples/src/com/structurizr/example/documentation/arc42/markdown/03-system-scope-and-context.md deleted file mode 100644 index 275ed2030..000000000 --- a/structurizr-examples/src/com/structurizr/example/documentation/arc42/markdown/03-system-scope-and-context.md +++ /dev/null @@ -1,8 +0,0 @@ -## Context and Scope - -![](embed:SystemContext) - -### Business Context - -### Technical Context - diff --git a/structurizr-examples/src/com/structurizr/example/documentation/arc42/markdown/04-solution-strategy.md b/structurizr-examples/src/com/structurizr/example/documentation/arc42/markdown/04-solution-strategy.md deleted file mode 100644 index ae7819aa8..000000000 --- a/structurizr-examples/src/com/structurizr/example/documentation/arc42/markdown/04-solution-strategy.md +++ /dev/null @@ -1 +0,0 @@ -## Solution Strategy diff --git a/structurizr-examples/src/com/structurizr/example/documentation/arc42/markdown/05-building-block-view.md b/structurizr-examples/src/com/structurizr/example/documentation/arc42/markdown/05-building-block-view.md deleted file mode 100644 index c6e769d56..000000000 --- a/structurizr-examples/src/com/structurizr/example/documentation/arc42/markdown/05-building-block-view.md +++ /dev/null @@ -1,7 +0,0 @@ -## Building Block View - -### Level 1 - -### Level 2 - -### Level 3 diff --git a/structurizr-examples/src/com/structurizr/example/documentation/arc42/markdown/06-runtime-view.md b/structurizr-examples/src/com/structurizr/example/documentation/arc42/markdown/06-runtime-view.md deleted file mode 100644 index bff782219..000000000 --- a/structurizr-examples/src/com/structurizr/example/documentation/arc42/markdown/06-runtime-view.md +++ /dev/null @@ -1,2 +0,0 @@ -## Runtime View - diff --git a/structurizr-examples/src/com/structurizr/example/documentation/arc42/markdown/07-deployment-view.md b/structurizr-examples/src/com/structurizr/example/documentation/arc42/markdown/07-deployment-view.md deleted file mode 100644 index 0600694ef..000000000 --- a/structurizr-examples/src/com/structurizr/example/documentation/arc42/markdown/07-deployment-view.md +++ /dev/null @@ -1 +0,0 @@ -## Deployment View diff --git a/structurizr-examples/src/com/structurizr/example/documentation/arc42/markdown/08-crosscutting-concepts.md b/structurizr-examples/src/com/structurizr/example/documentation/arc42/markdown/08-crosscutting-concepts.md deleted file mode 100644 index 52b1d38eb..000000000 --- a/structurizr-examples/src/com/structurizr/example/documentation/arc42/markdown/08-crosscutting-concepts.md +++ /dev/null @@ -1,2 +0,0 @@ -## Crosscutting Concepts - diff --git a/structurizr-examples/src/com/structurizr/example/documentation/arc42/markdown/09-architecture-decisions.md b/structurizr-examples/src/com/structurizr/example/documentation/arc42/markdown/09-architecture-decisions.md deleted file mode 100644 index 4d176d1a0..000000000 --- a/structurizr-examples/src/com/structurizr/example/documentation/arc42/markdown/09-architecture-decisions.md +++ /dev/null @@ -1,2 +0,0 @@ -## Architecture Decisions - diff --git a/structurizr-examples/src/com/structurizr/example/documentation/arc42/markdown/10-quality-requirements.md b/structurizr-examples/src/com/structurizr/example/documentation/arc42/markdown/10-quality-requirements.md deleted file mode 100644 index 4e98604cf..000000000 --- a/structurizr-examples/src/com/structurizr/example/documentation/arc42/markdown/10-quality-requirements.md +++ /dev/null @@ -1,6 +0,0 @@ -## Quality Requirements - -### Quality Tree - -### Quality Scenarios - diff --git a/structurizr-examples/src/com/structurizr/example/documentation/arc42/markdown/11-risks-and-technical-debt.md b/structurizr-examples/src/com/structurizr/example/documentation/arc42/markdown/11-risks-and-technical-debt.md deleted file mode 100644 index 96f39bb62..000000000 --- a/structurizr-examples/src/com/structurizr/example/documentation/arc42/markdown/11-risks-and-technical-debt.md +++ /dev/null @@ -1 +0,0 @@ -## Risks and Technical Debt diff --git a/structurizr-examples/src/com/structurizr/example/documentation/arc42/markdown/12-glossary.md b/structurizr-examples/src/com/structurizr/example/documentation/arc42/markdown/12-glossary.md deleted file mode 100644 index a4ea737a8..000000000 --- a/structurizr-examples/src/com/structurizr/example/documentation/arc42/markdown/12-glossary.md +++ /dev/null @@ -1 +0,0 @@ -## Glossary diff --git a/structurizr-examples/src/com/structurizr/example/documentation/automatic/01-context.md b/structurizr-examples/src/com/structurizr/example/documentation/automatic/01-context.md deleted file mode 100644 index fb2565d41..000000000 --- a/structurizr-examples/src/com/structurizr/example/documentation/automatic/01-context.md +++ /dev/null @@ -1,3 +0,0 @@ -## Context - -![](embed:SystemContext) \ No newline at end of file diff --git a/structurizr-examples/src/com/structurizr/example/documentation/automatic/02-functional-overview.md b/structurizr-examples/src/com/structurizr/example/documentation/automatic/02-functional-overview.md deleted file mode 100644 index 2997b6e7b..000000000 --- a/structurizr-examples/src/com/structurizr/example/documentation/automatic/02-functional-overview.md +++ /dev/null @@ -1 +0,0 @@ -## Functional Overview diff --git a/structurizr-examples/src/com/structurizr/example/documentation/automatic/03-quality-attributes.md b/structurizr-examples/src/com/structurizr/example/documentation/automatic/03-quality-attributes.md deleted file mode 100644 index 45b817d4f..000000000 --- a/structurizr-examples/src/com/structurizr/example/documentation/automatic/03-quality-attributes.md +++ /dev/null @@ -1 +0,0 @@ -## Quality Attributes diff --git a/structurizr-examples/src/com/structurizr/example/documentation/automatic/04-constraints.md b/structurizr-examples/src/com/structurizr/example/documentation/automatic/04-constraints.md deleted file mode 100644 index f6f45c85a..000000000 --- a/structurizr-examples/src/com/structurizr/example/documentation/automatic/04-constraints.md +++ /dev/null @@ -1 +0,0 @@ -## Constraints diff --git a/structurizr-examples/src/com/structurizr/example/documentation/automatic/05-principles.md b/structurizr-examples/src/com/structurizr/example/documentation/automatic/05-principles.md deleted file mode 100644 index 497b190ce..000000000 --- a/structurizr-examples/src/com/structurizr/example/documentation/automatic/05-principles.md +++ /dev/null @@ -1 +0,0 @@ -## Principles diff --git a/structurizr-examples/src/com/structurizr/example/documentation/automatic/06-software-architecture.md b/structurizr-examples/src/com/structurizr/example/documentation/automatic/06-software-architecture.md deleted file mode 100644 index ffd3ad203..000000000 --- a/structurizr-examples/src/com/structurizr/example/documentation/automatic/06-software-architecture.md +++ /dev/null @@ -1 +0,0 @@ -## Software Architecture \ No newline at end of file diff --git a/structurizr-examples/src/com/structurizr/example/documentation/automatic/07-data.adoc b/structurizr-examples/src/com/structurizr/example/documentation/automatic/07-data.adoc deleted file mode 100644 index 1fa400652..000000000 --- a/structurizr-examples/src/com/structurizr/example/documentation/automatic/07-data.adoc +++ /dev/null @@ -1 +0,0 @@ -== Data diff --git a/structurizr-examples/src/com/structurizr/example/documentation/automatic/08-infrastructure-architecture.adoc b/structurizr-examples/src/com/structurizr/example/documentation/automatic/08-infrastructure-architecture.adoc deleted file mode 100644 index a119ffd2e..000000000 --- a/structurizr-examples/src/com/structurizr/example/documentation/automatic/08-infrastructure-architecture.adoc +++ /dev/null @@ -1 +0,0 @@ -== Infrastructure Architecture diff --git a/structurizr-examples/src/com/structurizr/example/documentation/automatic/09-deployment.adoc b/structurizr-examples/src/com/structurizr/example/documentation/automatic/09-deployment.adoc deleted file mode 100644 index 2cabaa3be..000000000 --- a/structurizr-examples/src/com/structurizr/example/documentation/automatic/09-deployment.adoc +++ /dev/null @@ -1 +0,0 @@ -== Deployment diff --git a/structurizr-examples/src/com/structurizr/example/documentation/automatic/10-development-environment.adoc b/structurizr-examples/src/com/structurizr/example/documentation/automatic/10-development-environment.adoc deleted file mode 100644 index 05f84c570..000000000 --- a/structurizr-examples/src/com/structurizr/example/documentation/automatic/10-development-environment.adoc +++ /dev/null @@ -1 +0,0 @@ -== Development Environment diff --git a/structurizr-examples/src/com/structurizr/example/documentation/automatic/11-operation-and-support.adoc b/structurizr-examples/src/com/structurizr/example/documentation/automatic/11-operation-and-support.adoc deleted file mode 100644 index fe570a9d3..000000000 --- a/structurizr-examples/src/com/structurizr/example/documentation/automatic/11-operation-and-support.adoc +++ /dev/null @@ -1 +0,0 @@ -== Operation and Support diff --git a/structurizr-examples/src/com/structurizr/example/documentation/automatic/12-decision-log.adoc b/structurizr-examples/src/com/structurizr/example/documentation/automatic/12-decision-log.adoc deleted file mode 100644 index 0b98ca9df..000000000 --- a/structurizr-examples/src/com/structurizr/example/documentation/automatic/12-decision-log.adoc +++ /dev/null @@ -1 +0,0 @@ -== Decision Log diff --git a/structurizr-examples/src/com/structurizr/example/documentation/structurizr/asciidoc/01-context.adoc b/structurizr-examples/src/com/structurizr/example/documentation/structurizr/asciidoc/01-context.adoc deleted file mode 100644 index 079aeba93..000000000 --- a/structurizr-examples/src/com/structurizr/example/documentation/structurizr/asciidoc/01-context.adoc +++ /dev/null @@ -1,3 +0,0 @@ -== Context - -image::embed:SystemContext[] diff --git a/structurizr-examples/src/com/structurizr/example/documentation/structurizr/asciidoc/02-functional-overview.adoc b/structurizr-examples/src/com/structurizr/example/documentation/structurizr/asciidoc/02-functional-overview.adoc deleted file mode 100644 index f859f1774..000000000 --- a/structurizr-examples/src/com/structurizr/example/documentation/structurizr/asciidoc/02-functional-overview.adoc +++ /dev/null @@ -1 +0,0 @@ -== Functional Overview diff --git a/structurizr-examples/src/com/structurizr/example/documentation/structurizr/asciidoc/03-quality-attributes.adoc b/structurizr-examples/src/com/structurizr/example/documentation/structurizr/asciidoc/03-quality-attributes.adoc deleted file mode 100644 index 18bf9fc77..000000000 --- a/structurizr-examples/src/com/structurizr/example/documentation/structurizr/asciidoc/03-quality-attributes.adoc +++ /dev/null @@ -1 +0,0 @@ -== Quality Attributes diff --git a/structurizr-examples/src/com/structurizr/example/documentation/structurizr/asciidoc/04-constraints.adoc b/structurizr-examples/src/com/structurizr/example/documentation/structurizr/asciidoc/04-constraints.adoc deleted file mode 100644 index 17efd4403..000000000 --- a/structurizr-examples/src/com/structurizr/example/documentation/structurizr/asciidoc/04-constraints.adoc +++ /dev/null @@ -1 +0,0 @@ -== Constraints diff --git a/structurizr-examples/src/com/structurizr/example/documentation/structurizr/asciidoc/05-principles.adoc b/structurizr-examples/src/com/structurizr/example/documentation/structurizr/asciidoc/05-principles.adoc deleted file mode 100644 index 6602f92a9..000000000 --- a/structurizr-examples/src/com/structurizr/example/documentation/structurizr/asciidoc/05-principles.adoc +++ /dev/null @@ -1 +0,0 @@ -== Principles diff --git a/structurizr-examples/src/com/structurizr/example/documentation/structurizr/asciidoc/06-software-architecture.adoc b/structurizr-examples/src/com/structurizr/example/documentation/structurizr/asciidoc/06-software-architecture.adoc deleted file mode 100644 index 48dafb257..000000000 --- a/structurizr-examples/src/com/structurizr/example/documentation/structurizr/asciidoc/06-software-architecture.adoc +++ /dev/null @@ -1 +0,0 @@ -== Software Architecture \ No newline at end of file diff --git a/structurizr-examples/src/com/structurizr/example/documentation/structurizr/asciidoc/07-data.adoc b/structurizr-examples/src/com/structurizr/example/documentation/structurizr/asciidoc/07-data.adoc deleted file mode 100644 index 1fa400652..000000000 --- a/structurizr-examples/src/com/structurizr/example/documentation/structurizr/asciidoc/07-data.adoc +++ /dev/null @@ -1 +0,0 @@ -== Data diff --git a/structurizr-examples/src/com/structurizr/example/documentation/structurizr/asciidoc/08-infrastructure-architecture.adoc b/structurizr-examples/src/com/structurizr/example/documentation/structurizr/asciidoc/08-infrastructure-architecture.adoc deleted file mode 100644 index a119ffd2e..000000000 --- a/structurizr-examples/src/com/structurizr/example/documentation/structurizr/asciidoc/08-infrastructure-architecture.adoc +++ /dev/null @@ -1 +0,0 @@ -== Infrastructure Architecture diff --git a/structurizr-examples/src/com/structurizr/example/documentation/structurizr/asciidoc/09-deployment.adoc b/structurizr-examples/src/com/structurizr/example/documentation/structurizr/asciidoc/09-deployment.adoc deleted file mode 100644 index 2cabaa3be..000000000 --- a/structurizr-examples/src/com/structurizr/example/documentation/structurizr/asciidoc/09-deployment.adoc +++ /dev/null @@ -1 +0,0 @@ -== Deployment diff --git a/structurizr-examples/src/com/structurizr/example/documentation/structurizr/asciidoc/10-development-environment.adoc b/structurizr-examples/src/com/structurizr/example/documentation/structurizr/asciidoc/10-development-environment.adoc deleted file mode 100644 index 05f84c570..000000000 --- a/structurizr-examples/src/com/structurizr/example/documentation/structurizr/asciidoc/10-development-environment.adoc +++ /dev/null @@ -1 +0,0 @@ -== Development Environment diff --git a/structurizr-examples/src/com/structurizr/example/documentation/structurizr/asciidoc/11-operation-and-support.adoc b/structurizr-examples/src/com/structurizr/example/documentation/structurizr/asciidoc/11-operation-and-support.adoc deleted file mode 100644 index fe570a9d3..000000000 --- a/structurizr-examples/src/com/structurizr/example/documentation/structurizr/asciidoc/11-operation-and-support.adoc +++ /dev/null @@ -1 +0,0 @@ -== Operation and Support diff --git a/structurizr-examples/src/com/structurizr/example/documentation/structurizr/asciidoc/12-decision-log.adoc b/structurizr-examples/src/com/structurizr/example/documentation/structurizr/asciidoc/12-decision-log.adoc deleted file mode 100644 index 0b98ca9df..000000000 --- a/structurizr-examples/src/com/structurizr/example/documentation/structurizr/asciidoc/12-decision-log.adoc +++ /dev/null @@ -1 +0,0 @@ -== Decision Log diff --git a/structurizr-examples/src/com/structurizr/example/documentation/structurizr/markdown/01-context.md b/structurizr-examples/src/com/structurizr/example/documentation/structurizr/markdown/01-context.md deleted file mode 100644 index fb2565d41..000000000 --- a/structurizr-examples/src/com/structurizr/example/documentation/structurizr/markdown/01-context.md +++ /dev/null @@ -1,3 +0,0 @@ -## Context - -![](embed:SystemContext) \ No newline at end of file diff --git a/structurizr-examples/src/com/structurizr/example/documentation/structurizr/markdown/02-functional-overview.md b/structurizr-examples/src/com/structurizr/example/documentation/structurizr/markdown/02-functional-overview.md deleted file mode 100644 index 2997b6e7b..000000000 --- a/structurizr-examples/src/com/structurizr/example/documentation/structurizr/markdown/02-functional-overview.md +++ /dev/null @@ -1 +0,0 @@ -## Functional Overview diff --git a/structurizr-examples/src/com/structurizr/example/documentation/structurizr/markdown/03-quality-attributes.md b/structurizr-examples/src/com/structurizr/example/documentation/structurizr/markdown/03-quality-attributes.md deleted file mode 100644 index 45b817d4f..000000000 --- a/structurizr-examples/src/com/structurizr/example/documentation/structurizr/markdown/03-quality-attributes.md +++ /dev/null @@ -1 +0,0 @@ -## Quality Attributes diff --git a/structurizr-examples/src/com/structurizr/example/documentation/structurizr/markdown/04-constraints.md b/structurizr-examples/src/com/structurizr/example/documentation/structurizr/markdown/04-constraints.md deleted file mode 100644 index f6f45c85a..000000000 --- a/structurizr-examples/src/com/structurizr/example/documentation/structurizr/markdown/04-constraints.md +++ /dev/null @@ -1 +0,0 @@ -## Constraints diff --git a/structurizr-examples/src/com/structurizr/example/documentation/structurizr/markdown/05-principles.md b/structurizr-examples/src/com/structurizr/example/documentation/structurizr/markdown/05-principles.md deleted file mode 100644 index 497b190ce..000000000 --- a/structurizr-examples/src/com/structurizr/example/documentation/structurizr/markdown/05-principles.md +++ /dev/null @@ -1 +0,0 @@ -## Principles diff --git a/structurizr-examples/src/com/structurizr/example/documentation/structurizr/markdown/06-software-architecture.md b/structurizr-examples/src/com/structurizr/example/documentation/structurizr/markdown/06-software-architecture.md deleted file mode 100644 index ffd3ad203..000000000 --- a/structurizr-examples/src/com/structurizr/example/documentation/structurizr/markdown/06-software-architecture.md +++ /dev/null @@ -1 +0,0 @@ -## Software Architecture \ No newline at end of file diff --git a/structurizr-examples/src/com/structurizr/example/documentation/structurizr/markdown/07-data.md b/structurizr-examples/src/com/structurizr/example/documentation/structurizr/markdown/07-data.md deleted file mode 100644 index 834b75ed8..000000000 --- a/structurizr-examples/src/com/structurizr/example/documentation/structurizr/markdown/07-data.md +++ /dev/null @@ -1 +0,0 @@ -## Data diff --git a/structurizr-examples/src/com/structurizr/example/documentation/structurizr/markdown/08-infrastructure-architecture.md b/structurizr-examples/src/com/structurizr/example/documentation/structurizr/markdown/08-infrastructure-architecture.md deleted file mode 100644 index 5ee3fd094..000000000 --- a/structurizr-examples/src/com/structurizr/example/documentation/structurizr/markdown/08-infrastructure-architecture.md +++ /dev/null @@ -1 +0,0 @@ -## Infrastructure Architecture diff --git a/structurizr-examples/src/com/structurizr/example/documentation/structurizr/markdown/09-deployment.md b/structurizr-examples/src/com/structurizr/example/documentation/structurizr/markdown/09-deployment.md deleted file mode 100644 index 9a331e2ef..000000000 --- a/structurizr-examples/src/com/structurizr/example/documentation/structurizr/markdown/09-deployment.md +++ /dev/null @@ -1 +0,0 @@ -## Deployment diff --git a/structurizr-examples/src/com/structurizr/example/documentation/structurizr/markdown/10-development-environment.md b/structurizr-examples/src/com/structurizr/example/documentation/structurizr/markdown/10-development-environment.md deleted file mode 100644 index 86976762e..000000000 --- a/structurizr-examples/src/com/structurizr/example/documentation/structurizr/markdown/10-development-environment.md +++ /dev/null @@ -1 +0,0 @@ -## Development Environment diff --git a/structurizr-examples/src/com/structurizr/example/documentation/structurizr/markdown/11-operation-and-support.md b/structurizr-examples/src/com/structurizr/example/documentation/structurizr/markdown/11-operation-and-support.md deleted file mode 100644 index 6de7d37b7..000000000 --- a/structurizr-examples/src/com/structurizr/example/documentation/structurizr/markdown/11-operation-and-support.md +++ /dev/null @@ -1 +0,0 @@ -## Operation and Support diff --git a/structurizr-examples/src/com/structurizr/example/documentation/structurizr/markdown/12-decision-log.md b/structurizr-examples/src/com/structurizr/example/documentation/structurizr/markdown/12-decision-log.md deleted file mode 100644 index 31371f635..000000000 --- a/structurizr-examples/src/com/structurizr/example/documentation/structurizr/markdown/12-decision-log.md +++ /dev/null @@ -1 +0,0 @@ -## Decision Log diff --git a/structurizr-examples/src/com/structurizr/example/documentation/viewpointsandperspectives/asciidoc/01-introduction.adoc b/structurizr-examples/src/com/structurizr/example/documentation/viewpointsandperspectives/asciidoc/01-introduction.adoc deleted file mode 100644 index 0f2b68f0a..000000000 --- a/structurizr-examples/src/com/structurizr/example/documentation/viewpointsandperspectives/asciidoc/01-introduction.adoc +++ /dev/null @@ -1,9 +0,0 @@ -== Introduction - -=== Purpose and Scope - -=== Audience - -=== Status - -=== Architectural Design Approach diff --git a/structurizr-examples/src/com/structurizr/example/documentation/viewpointsandperspectives/asciidoc/02-glossary.adoc b/structurizr-examples/src/com/structurizr/example/documentation/viewpointsandperspectives/asciidoc/02-glossary.adoc deleted file mode 100644 index 0c393c5a8..000000000 --- a/structurizr-examples/src/com/structurizr/example/documentation/viewpointsandperspectives/asciidoc/02-glossary.adoc +++ /dev/null @@ -1 +0,0 @@ -== Glossary diff --git a/structurizr-examples/src/com/structurizr/example/documentation/viewpointsandperspectives/asciidoc/03-system-stakeholders-and-requirements.adoc b/structurizr-examples/src/com/structurizr/example/documentation/viewpointsandperspectives/asciidoc/03-system-stakeholders-and-requirements.adoc deleted file mode 100644 index 96e0b9b85..000000000 --- a/structurizr-examples/src/com/structurizr/example/documentation/viewpointsandperspectives/asciidoc/03-system-stakeholders-and-requirements.adoc +++ /dev/null @@ -1,7 +0,0 @@ -== System Stakeholders and Requirements - -=== Stakeholders - -=== Overview of Requirements - -=== System Scenarios diff --git a/structurizr-examples/src/com/structurizr/example/documentation/viewpointsandperspectives/asciidoc/04-architectural-forces.adoc b/structurizr-examples/src/com/structurizr/example/documentation/viewpointsandperspectives/asciidoc/04-architectural-forces.adoc deleted file mode 100644 index 35c1b2d69..000000000 --- a/structurizr-examples/src/com/structurizr/example/documentation/viewpointsandperspectives/asciidoc/04-architectural-forces.adoc +++ /dev/null @@ -1,7 +0,0 @@ -== Architectural Forces - -=== Goals - -=== Constraints - -=== Architectural Principles diff --git a/structurizr-examples/src/com/structurizr/example/documentation/viewpointsandperspectives/asciidoc/05-architectural-views/01-architectural-views.adoc b/structurizr-examples/src/com/structurizr/example/documentation/viewpointsandperspectives/asciidoc/05-architectural-views/01-architectural-views.adoc deleted file mode 100644 index b7ef607d9..000000000 --- a/structurizr-examples/src/com/structurizr/example/documentation/viewpointsandperspectives/asciidoc/05-architectural-views/01-architectural-views.adoc +++ /dev/null @@ -1 +0,0 @@ -== Architectural Views diff --git a/structurizr-examples/src/com/structurizr/example/documentation/viewpointsandperspectives/asciidoc/05-architectural-views/02-context-view.adoc b/structurizr-examples/src/com/structurizr/example/documentation/viewpointsandperspectives/asciidoc/05-architectural-views/02-context-view.adoc deleted file mode 100644 index c8d31fc94..000000000 --- a/structurizr-examples/src/com/structurizr/example/documentation/viewpointsandperspectives/asciidoc/05-architectural-views/02-context-view.adoc +++ /dev/null @@ -1,3 +0,0 @@ -=== Context View - -image::embed:SystemContext[] diff --git a/structurizr-examples/src/com/structurizr/example/documentation/viewpointsandperspectives/asciidoc/05-architectural-views/03-functional-view.adoc b/structurizr-examples/src/com/structurizr/example/documentation/viewpointsandperspectives/asciidoc/05-architectural-views/03-functional-view.adoc deleted file mode 100644 index 929b81481..000000000 --- a/structurizr-examples/src/com/structurizr/example/documentation/viewpointsandperspectives/asciidoc/05-architectural-views/03-functional-view.adoc +++ /dev/null @@ -1 +0,0 @@ -=== Functional View diff --git a/structurizr-examples/src/com/structurizr/example/documentation/viewpointsandperspectives/asciidoc/05-architectural-views/04-information-view.adoc b/structurizr-examples/src/com/structurizr/example/documentation/viewpointsandperspectives/asciidoc/05-architectural-views/04-information-view.adoc deleted file mode 100644 index 92aef8ac3..000000000 --- a/structurizr-examples/src/com/structurizr/example/documentation/viewpointsandperspectives/asciidoc/05-architectural-views/04-information-view.adoc +++ /dev/null @@ -1 +0,0 @@ -=== Information View diff --git a/structurizr-examples/src/com/structurizr/example/documentation/viewpointsandperspectives/asciidoc/05-architectural-views/05-concurrency-view.adoc b/structurizr-examples/src/com/structurizr/example/documentation/viewpointsandperspectives/asciidoc/05-architectural-views/05-concurrency-view.adoc deleted file mode 100644 index 5bd240ab1..000000000 --- a/structurizr-examples/src/com/structurizr/example/documentation/viewpointsandperspectives/asciidoc/05-architectural-views/05-concurrency-view.adoc +++ /dev/null @@ -1 +0,0 @@ -=== Concurrency View diff --git a/structurizr-examples/src/com/structurizr/example/documentation/viewpointsandperspectives/asciidoc/05-architectural-views/06-deployment-view.adoc b/structurizr-examples/src/com/structurizr/example/documentation/viewpointsandperspectives/asciidoc/05-architectural-views/06-deployment-view.adoc deleted file mode 100644 index 52a5994a4..000000000 --- a/structurizr-examples/src/com/structurizr/example/documentation/viewpointsandperspectives/asciidoc/05-architectural-views/06-deployment-view.adoc +++ /dev/null @@ -1 +0,0 @@ -=== Deployment View diff --git a/structurizr-examples/src/com/structurizr/example/documentation/viewpointsandperspectives/asciidoc/05-architectural-views/07-development-view.adoc b/structurizr-examples/src/com/structurizr/example/documentation/viewpointsandperspectives/asciidoc/05-architectural-views/07-development-view.adoc deleted file mode 100644 index 169dfc1e8..000000000 --- a/structurizr-examples/src/com/structurizr/example/documentation/viewpointsandperspectives/asciidoc/05-architectural-views/07-development-view.adoc +++ /dev/null @@ -1 +0,0 @@ -=== Development View diff --git a/structurizr-examples/src/com/structurizr/example/documentation/viewpointsandperspectives/asciidoc/05-architectural-views/08-operational-view.adoc b/structurizr-examples/src/com/structurizr/example/documentation/viewpointsandperspectives/asciidoc/05-architectural-views/08-operational-view.adoc deleted file mode 100644 index 8e2f48706..000000000 --- a/structurizr-examples/src/com/structurizr/example/documentation/viewpointsandperspectives/asciidoc/05-architectural-views/08-operational-view.adoc +++ /dev/null @@ -1 +0,0 @@ -=== Operational View diff --git a/structurizr-examples/src/com/structurizr/example/documentation/viewpointsandperspectives/asciidoc/06-system-qualities.adoc b/structurizr-examples/src/com/structurizr/example/documentation/viewpointsandperspectives/asciidoc/06-system-qualities.adoc deleted file mode 100644 index d58efa14d..000000000 --- a/structurizr-examples/src/com/structurizr/example/documentation/viewpointsandperspectives/asciidoc/06-system-qualities.adoc +++ /dev/null @@ -1,11 +0,0 @@ -== System Qualities - -=== Performance and Scalability - -=== Security - -=== Availability and Resilience - -=== Evolution - -=== Other Qualities diff --git a/structurizr-examples/src/com/structurizr/example/documentation/viewpointsandperspectives/asciidoc/07-appendices.adoc b/structurizr-examples/src/com/structurizr/example/documentation/viewpointsandperspectives/asciidoc/07-appendices.adoc deleted file mode 100644 index d9e93d4a8..000000000 --- a/structurizr-examples/src/com/structurizr/example/documentation/viewpointsandperspectives/asciidoc/07-appendices.adoc +++ /dev/null @@ -1,7 +0,0 @@ -== Appendices - -=== Decisions and Alternatives - -=== Questions and Answers - -=== References diff --git a/structurizr-examples/src/com/structurizr/example/documentation/viewpointsandperspectives/markdown/01-introduction.md b/structurizr-examples/src/com/structurizr/example/documentation/viewpointsandperspectives/markdown/01-introduction.md deleted file mode 100644 index c2a8dc7f5..000000000 --- a/structurizr-examples/src/com/structurizr/example/documentation/viewpointsandperspectives/markdown/01-introduction.md +++ /dev/null @@ -1,9 +0,0 @@ -## Introduction - -### Purpose and Scope - -### Audience - -### Status - -### Architectural Design Approach diff --git a/structurizr-examples/src/com/structurizr/example/documentation/viewpointsandperspectives/markdown/02-glossary.md b/structurizr-examples/src/com/structurizr/example/documentation/viewpointsandperspectives/markdown/02-glossary.md deleted file mode 100644 index a4ea737a8..000000000 --- a/structurizr-examples/src/com/structurizr/example/documentation/viewpointsandperspectives/markdown/02-glossary.md +++ /dev/null @@ -1 +0,0 @@ -## Glossary diff --git a/structurizr-examples/src/com/structurizr/example/documentation/viewpointsandperspectives/markdown/03-system-stakeholders-and-requirements.md b/structurizr-examples/src/com/structurizr/example/documentation/viewpointsandperspectives/markdown/03-system-stakeholders-and-requirements.md deleted file mode 100644 index 484c2cf23..000000000 --- a/structurizr-examples/src/com/structurizr/example/documentation/viewpointsandperspectives/markdown/03-system-stakeholders-and-requirements.md +++ /dev/null @@ -1,7 +0,0 @@ -## System Stakeholders and Requirements - -### Stakeholders - -### Overview of Requirements - -### System Scenarios diff --git a/structurizr-examples/src/com/structurizr/example/documentation/viewpointsandperspectives/markdown/04-architectural-forces.md b/structurizr-examples/src/com/structurizr/example/documentation/viewpointsandperspectives/markdown/04-architectural-forces.md deleted file mode 100644 index 87d89f279..000000000 --- a/structurizr-examples/src/com/structurizr/example/documentation/viewpointsandperspectives/markdown/04-architectural-forces.md +++ /dev/null @@ -1,7 +0,0 @@ -## Architectural Forces - -### Goals - -### Constraints - -### Architectural Principles diff --git a/structurizr-examples/src/com/structurizr/example/documentation/viewpointsandperspectives/markdown/05-architectural-views/01-architectural-views.md b/structurizr-examples/src/com/structurizr/example/documentation/viewpointsandperspectives/markdown/05-architectural-views/01-architectural-views.md deleted file mode 100644 index b1de7b571..000000000 --- a/structurizr-examples/src/com/structurizr/example/documentation/viewpointsandperspectives/markdown/05-architectural-views/01-architectural-views.md +++ /dev/null @@ -1 +0,0 @@ -## Architectural Views diff --git a/structurizr-examples/src/com/structurizr/example/documentation/viewpointsandperspectives/markdown/05-architectural-views/02-context-view.md b/structurizr-examples/src/com/structurizr/example/documentation/viewpointsandperspectives/markdown/05-architectural-views/02-context-view.md deleted file mode 100644 index f72cc3a6c..000000000 --- a/structurizr-examples/src/com/structurizr/example/documentation/viewpointsandperspectives/markdown/05-architectural-views/02-context-view.md +++ /dev/null @@ -1,3 +0,0 @@ -### Context View - -![](embed:SystemContext) diff --git a/structurizr-examples/src/com/structurizr/example/documentation/viewpointsandperspectives/markdown/05-architectural-views/03-functional-view.md b/structurizr-examples/src/com/structurizr/example/documentation/viewpointsandperspectives/markdown/05-architectural-views/03-functional-view.md deleted file mode 100644 index 09b72c0bd..000000000 --- a/structurizr-examples/src/com/structurizr/example/documentation/viewpointsandperspectives/markdown/05-architectural-views/03-functional-view.md +++ /dev/null @@ -1 +0,0 @@ -### Functional View diff --git a/structurizr-examples/src/com/structurizr/example/documentation/viewpointsandperspectives/markdown/05-architectural-views/04-information-view.md b/structurizr-examples/src/com/structurizr/example/documentation/viewpointsandperspectives/markdown/05-architectural-views/04-information-view.md deleted file mode 100644 index 801d78fbe..000000000 --- a/structurizr-examples/src/com/structurizr/example/documentation/viewpointsandperspectives/markdown/05-architectural-views/04-information-view.md +++ /dev/null @@ -1 +0,0 @@ -### Information View diff --git a/structurizr-examples/src/com/structurizr/example/documentation/viewpointsandperspectives/markdown/05-architectural-views/05-concurrency-view.md b/structurizr-examples/src/com/structurizr/example/documentation/viewpointsandperspectives/markdown/05-architectural-views/05-concurrency-view.md deleted file mode 100644 index 7831cac26..000000000 --- a/structurizr-examples/src/com/structurizr/example/documentation/viewpointsandperspectives/markdown/05-architectural-views/05-concurrency-view.md +++ /dev/null @@ -1 +0,0 @@ -### Concurrency View diff --git a/structurizr-examples/src/com/structurizr/example/documentation/viewpointsandperspectives/markdown/05-architectural-views/06-deployment-view.md b/structurizr-examples/src/com/structurizr/example/documentation/viewpointsandperspectives/markdown/05-architectural-views/06-deployment-view.md deleted file mode 100644 index f978bd2c8..000000000 --- a/structurizr-examples/src/com/structurizr/example/documentation/viewpointsandperspectives/markdown/05-architectural-views/06-deployment-view.md +++ /dev/null @@ -1 +0,0 @@ -### Deployment View diff --git a/structurizr-examples/src/com/structurizr/example/documentation/viewpointsandperspectives/markdown/05-architectural-views/07-development-view.md b/structurizr-examples/src/com/structurizr/example/documentation/viewpointsandperspectives/markdown/05-architectural-views/07-development-view.md deleted file mode 100644 index b2fe7847d..000000000 --- a/structurizr-examples/src/com/structurizr/example/documentation/viewpointsandperspectives/markdown/05-architectural-views/07-development-view.md +++ /dev/null @@ -1 +0,0 @@ -### Development View diff --git a/structurizr-examples/src/com/structurizr/example/documentation/viewpointsandperspectives/markdown/05-architectural-views/08-operational-view.md b/structurizr-examples/src/com/structurizr/example/documentation/viewpointsandperspectives/markdown/05-architectural-views/08-operational-view.md deleted file mode 100644 index fc13034df..000000000 --- a/structurizr-examples/src/com/structurizr/example/documentation/viewpointsandperspectives/markdown/05-architectural-views/08-operational-view.md +++ /dev/null @@ -1 +0,0 @@ -### Operational View diff --git a/structurizr-examples/src/com/structurizr/example/documentation/viewpointsandperspectives/markdown/06-system-qualities.md b/structurizr-examples/src/com/structurizr/example/documentation/viewpointsandperspectives/markdown/06-system-qualities.md deleted file mode 100644 index 4ee8995ab..000000000 --- a/structurizr-examples/src/com/structurizr/example/documentation/viewpointsandperspectives/markdown/06-system-qualities.md +++ /dev/null @@ -1,11 +0,0 @@ -## System Qualities - -### Performance and Scalability - -### Security - -### Availability and Resilience - -### Evolution - -### Other Qualities diff --git a/structurizr-examples/src/com/structurizr/example/documentation/viewpointsandperspectives/markdown/07-appendices.md b/structurizr-examples/src/com/structurizr/example/documentation/viewpointsandperspectives/markdown/07-appendices.md deleted file mode 100644 index 29e1d7fbc..000000000 --- a/structurizr-examples/src/com/structurizr/example/documentation/viewpointsandperspectives/markdown/07-appendices.md +++ /dev/null @@ -1,7 +0,0 @@ -## Appendices - -### Decisions and Alternatives - -### Questions and Answers - -### References diff --git a/structurizr-examples/src/com/structurizr/example/financialrisksystem/context.adoc b/structurizr-examples/src/com/structurizr/example/financialrisksystem/context.adoc deleted file mode 100644 index f1c965507..000000000 --- a/structurizr-examples/src/com/structurizr/example/financialrisksystem/context.adoc +++ /dev/null @@ -1,18 +0,0 @@ -== Context - -A global investment bank based in London, New York and Singapore trades (buys and sells) financial products with other banks (counterparties). When share prices on the stock markets move up or down, the bank either makes money or loses it. At the end of the working day, the bank needs to gain a view of how much risk they are exposed to (e.g. of losing money) by running some calculations on the data held about their trades. The bank has an existing Trade Data System (TDS) and Reference Data System (RDS) but need a new Risk System. - -image::embed:Context[] - -=== Trade Data System - -The Trade Data System maintains a store of all trades made by the bank. It is already configured to generate a file-based XML export of trade data at the close of business (5pm) in New York. The export includes the following information for every trade made by the bank: - -* Trade ID -* Date -* Current trade value in US dollars -* Counterparty ID - -=== Reference Data System - -The Reference Data System maintains all of the reference data needed by the bank. This includes information about counterparties; each of which represents an individual, a bank, etc. A file-based XML export is also available and includes basic information about each counterparty. A new organisation-wide reference data system is due for completion in the next 3 months, with the current system eventually being decommissioned. \ No newline at end of file diff --git a/structurizr-examples/src/com/structurizr/example/financialrisksystem/context.md b/structurizr-examples/src/com/structurizr/example/financialrisksystem/context.md deleted file mode 100644 index 4841c7408..000000000 --- a/structurizr-examples/src/com/structurizr/example/financialrisksystem/context.md +++ /dev/null @@ -1,18 +0,0 @@ -## Context - -A global investment bank based in London, New York and Singapore trades (buys and sells) financial products with other banks (counterparties). When share prices on the stock markets move up or down, the bank either makes money or loses it. At the end of the working day, the bank needs to gain a view of how much risk they are exposed to (e.g. of losing money) by running some calculations on the data held about their trades. The bank has an existing Trade Data System (TDS) and Reference Data System (RDS) but need a new Risk System. - -![](embed:Context) - -### Trade Data System - -The Trade Data System maintains a store of all trades made by the bank. It is already configured to generate a file-based XML export of trade data at the close of business (5pm) in New York. The export includes the following information for every trade made by the bank: - -- Trade ID -- Date -- Current trade value in US dollars -- Counterparty ID - -### Reference Data System - -The Reference Data System maintains all of the reference data needed by the bank. This includes information about counterparties; each of which represents an individual, a bank, etc. A file-based XML export is also available and includes basic information about each counterparty. A new organisation-wide reference data system is due for completion in the next 3 months, with the current system eventually being decommissioned. \ No newline at end of file diff --git a/structurizr-examples/src/com/structurizr/example/financialrisksystem/functional-overview.md b/structurizr-examples/src/com/structurizr/example/financialrisksystem/functional-overview.md deleted file mode 100644 index ee62c2f3e..000000000 --- a/structurizr-examples/src/com/structurizr/example/financialrisksystem/functional-overview.md +++ /dev/null @@ -1,13 +0,0 @@ -## Functional Overview - -The high-level functional requirements for the new Risk System are as follows. - -![Functional overview](images/functional-overview.png) - -1. Import trade data from the Trade Data System. -2. Import counterparty data from the Reference Data System. -3. Join the two sets of data together, enriching the trade data with information about the counterparty. -4. For each counterparty, calculate the risk that the bank is exposed to. -5. Generate a report that can be imported into Microsoft Excel containing the risk figures for all counterparties known by the bank. -6. Distribute the report to the business users before the start of the next trading day (9am) in Singapore. -7. Provide a way for a subset of the business users to configure and maintain the external parameters used by the risk calculations. diff --git a/structurizr-examples/src/com/structurizr/example/financialrisksystem/images/functional-overview.png b/structurizr-examples/src/com/structurizr/example/financialrisksystem/images/functional-overview.png deleted file mode 100644 index 258545639..000000000 Binary files a/structurizr-examples/src/com/structurizr/example/financialrisksystem/images/functional-overview.png and /dev/null differ diff --git a/structurizr-examples/src/com/structurizr/example/financialrisksystem/quality-attributes.md b/structurizr-examples/src/com/structurizr/example/financialrisksystem/quality-attributes.md deleted file mode 100644 index a9af187ca..000000000 --- a/structurizr-examples/src/com/structurizr/example/financialrisksystem/quality-attributes.md +++ /dev/null @@ -1,61 +0,0 @@ -## Quality Attributes - -The quality attributes for the new Financial Risk System are as follows. - -### Performance - -- Risk reports must be generated before 9am the following business day in Singapore. - -### Scalability -- The system must be able to cope with trade volumes for the next 5 years. -- The Trade Data System export includes approximately 5000 trades now and it is anticipated that there will be an additional 10 trades per day. -- The Reference Data System counterparty export includes approximately 20,000 counterparties and growth will be negligible. -- There are 40-50 business users around the world that need access to the report. - -### Availability - -- Risk reports should be available to users 24x7, but a small amount of downtime (less than 30 minutes per day) can be tolerated. - -### Failover - -- Manual failover is sufficient for all system components, provided that the availability targets can be met. - -### Security - -- This system must follow bank policy that states system access is restricted to authenticated and authorised users only. -- Reports must only be distributed to authorised users. -- Only a subset of the authorised users are permitted to modify the parameters used in the risk calculations. -- Although desirable, there are no single sign-on requirements (e.g. integration with Active Directory, LDAP, etc). -- All access to the system and reports will be within the confines of the bank's global network. - -### Audit - -- The following events must be recorded in the system audit logs: - - Report generation. - - Modification of risk calculation parameters. -- It must be possible to understand the input data that was used in calculating risk. - -### Fault Tolerance and Resilience - -- The system should take appropriate steps to recover from an error if possible, but all errors should be logged. -- Errors preventing a counterparty risk calculation being completed should be logged and the process should continue. - -### Internationalization and Localization - -- All user interfaces will be presented in English only. -- All reports will be presented in English only. -- All trading values and risk figures will be presented in US dollars only. - -### Monitoring and Management - -- A Simple Network Management Protocol (SNMP) trap should be sent to the bank's Central Monitoring Service in the following circumstances: - - When there is a fatal error with a system component. - - When reports have not been generated before 9am Singapore time. - -### Data Retention and Archiving - -- Input files used in the risk calculation process must be retained for 1 year. - -### Interoperability - -- Interfaces with existing data systems should conform to and use existing data formats. \ 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..f908d3fb5 --- /dev/null +++ b/structurizr-export/build.gradle @@ -0,0 +1,9 @@ +dependencies { + + api project(':structurizr-core') + + testImplementation project(':structurizr-client') + +} + +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..f982fd5f1 --- /dev/null +++ b/structurizr-export/src/main/java/com/structurizr/export/AbstractDiagramExporter.java @@ -0,0 +1,851 @@ +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"; + + protected final ColorScheme colorScheme; + + private Object frame = null; + + public AbstractDiagramExporter() { + this(ColorScheme.Light); + } + + public AbstractDiagramExporter(ColorScheme colorScheme) { + this.colorScheme = colorScheme != null ? colorScheme : ColorScheme.Light; + } + + /** + * 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(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); + + 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 = getGroupableElements(view, null); + writeElements(view, elements, writer); + + if (!elements.isEmpty()) { + 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; + + IndentingWriter writer = new IndentingWriter(); + writeHeader(view, writer); + + List<GroupableElement> elements = getGroupableElements(view, null); + writeElements(view, elements, writer); + + if (!elements.isEmpty()) { + writer.writeLine(); + } + + writeRelationships(view, writer); + writeFooter(view, writer); + + return createDiagram(view, writer.toString()); + } + + 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; + + IndentingWriter writer = new IndentingWriter(); + writeHeader(view, writer); + + List<GroupableElement> elements = getGroupableElements(view, null); + writeElements(view, elements, writer); + + if (!elements.isEmpty()) { + 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 = getGroupableElements(view, softwareSystem); + 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); + + List<CustomElement> customElements = getCustomElements(view); + for (CustomElement customElement : customElements) { + writeElement(view, customElement, writer); + } + if (!customElements.isEmpty()) { + writer.writeLine(); + } + + List<Person> people = getPeople(view); + for (Person person : people) { + writeElement(view, person, writer); + } + if (!people.isEmpty()) { + writer.writeLine(); + } + + List<SoftwareSystem> softwareSystems = getSoftwareSystems(view); + for (SoftwareSystem softwareSystem : softwareSystems) { + writeElement(view, softwareSystem, writer); + } + if (!softwareSystems.isEmpty()) { + writer.writeLine(); + } + + List<Container> boundaryContainers = getBoundaryContainers(view); + Set<SoftwareSystem> boundarySoftwareSystems = boundaryContainers.stream().map(Container::getSoftwareSystem).collect(Collectors.toCollection(LinkedHashSet::new)); + for (SoftwareSystem softwareSystem : boundarySoftwareSystems) { + + startSoftwareSystemBoundary(view, softwareSystem, writer); + + for (Container container : boundaryContainers) { + if (container.getSoftwareSystem() == softwareSystem) { + startContainerBoundary(view, container, writer); + + List<GroupableElement> scopedElements = getGroupableElements(view, container); + writeElements(view, scopedElements, writer); + + endContainerBoundary(view, writer); + } + } + + for (Container container : getContainers(view)) { + if (container.getSoftwareSystem() == softwareSystem) { + writeElement(view, container, writer); + } + } + + endSoftwareSystemBoundary(view, writer); + } + + writeRelationships(view, writer); + + writeFooter(view, writer); + + return createDiagram(view, writer.toString()); + } + + protected List<CustomElement> 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<Person> 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<SoftwareSystem> 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<Container> getBoundaryContainers(ModelView view) { + 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()); + } + + protected List<Container> 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) { + 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 = getGroupableElements(view, null); + writeElements(view, elements, writer); + + if (!elements.isEmpty()) { + elementsWritten = true; + } + } 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 = getGroupableElements(view, softwareSystem); + 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 + List<CustomElement> customElements = getCustomElements(view); + for (CustomElement customElement : customElements) { + writeElement(view, customElement, writer); + } + if (!customElements.isEmpty()) { + writer.writeLine(); + } + + List<Person> people = getPeople(view); + for (Person person : people) { + writeElement(view, person, writer); + } + if (!people.isEmpty()) { + writer.writeLine(); + } + + List<SoftwareSystem> softwareSystems = getSoftwareSystems(view); + for (SoftwareSystem softwareSystem : softwareSystems) { + writeElement(view, softwareSystem, writer); + } + if (!softwareSystems.isEmpty()) { + writer.writeLine(); + } + + List<Container> boundaryContainers = getBoundaryContainers(view); + Set<SoftwareSystem> boundarySoftwareSystems = boundaryContainers.stream().map(Container::getSoftwareSystem).collect(Collectors.toCollection(LinkedHashSet::new)); + for (SoftwareSystem softwareSystem : boundarySoftwareSystems) { + + startSoftwareSystemBoundary(view, softwareSystem, writer); + + for (Container container : boundaryContainers) { + if (container.getSoftwareSystem() == softwareSystem) { + startContainerBoundary(view, container, writer); + + List<GroupableElement> scopedElements = getGroupableElements(view, container); + writeElements(view, scopedElements, writer); + + endContainerBoundary(view, writer); + } + } + + for (Container container : getContainers(view)) { + if (container.getSoftwareSystem() == softwareSystem) { + writeElement(view, container, writer); + } + } + + endSoftwareSystemBoundary(view, writer); + } + + 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 = getGroupableElements(view, null); + writeElements(view, elements, writer); + + writeRelationships(view, writer); + writeFooter(view, writer); + + return createDiagram(view, writer.toString()); + } + + protected List<String> findGroups(ModelView view, List<GroupableElement> elements) { + 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); + + return groupsAsList; + } + + 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); + List<String> groupsAsList = findGroups(view, elements); + + // first render grouped elements + if (!groupsAsList.isEmpty()) { + 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 Collection<RelationshipView> getRelationshipsInView(ModelView view) { + if (view instanceof DynamicView) { + return view.getRelationships(); + } else { + return view.getRelationships().stream().sorted(Comparator.comparing(rv -> rv.getRelationship().getId())).collect(Collectors.toList()); + } + } + + protected void writeRelationships(ModelView view, IndentingWriter writer) { + Collection<RelationshipView> relationshipList = getRelationshipsInView(view); + + for (RelationshipView relationshipView : relationshipList) { + writeRelationship(view, relationshipView, writer); + } + + if (!relationshipList.isEmpty()) { + writer.writeLine(); + } + } + + protected abstract void writeHeader(ModelView view, IndentingWriter writer); + protected abstract void writeFooter(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) + ); + } + + @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<GroupableElement> getGroupableElements(ModelView view, Element parent) { + List<GroupableElement> 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/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..214fb00e2 --- /dev/null +++ b/structurizr-export/src/main/java/com/structurizr/export/IndentingWriter.java @@ -0,0 +1,69 @@ +package com.structurizr.export; + +public final class IndentingWriter { + + private int indent = 0; + private IndentType indentType = IndentType.Spaces; + private int indentQuantity = 2; + + private final 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"))); + } + + 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(); + 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..708ea4b46 --- /dev/null +++ b/structurizr-export/src/main/java/com/structurizr/export/dot/DOTExporter.java @@ -0,0 +1,423 @@ +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 int DEFAULT_WIDTH = 450; + private static final int DEFAULT_HEIGHT = 300; + 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 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); + + 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(); + + + 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..9a80784e1 --- /dev/null +++ b/structurizr-export/src/main/java/com/structurizr/export/ilograph/IlographExporter.java @@ -0,0 +1,437 @@ +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 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(); + + 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())); + + 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(); + } + + 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) { + String scope = dynamicView.getName(); + scope = scope.substring("Dynamic View".length()); + if (scope.startsWith(": ")) { + scope = scope.substring(2); + } + writer.indent(); + writer.writeLine("- name: Dynamic: " + scope); + 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..3082176a3 --- /dev/null +++ b/structurizr-export/src/main/java/com/structurizr/export/mermaid/MermaidDiagramExporter.java @@ -0,0 +1,397 @@ +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 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/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..d078aa65e --- /dev/null +++ b/structurizr-export/src/main/java/com/structurizr/export/plantuml/AbstractPlantUMLExporter.java @@ -0,0 +1,294 @@ +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.ColorScheme; +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 { + + 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"; + + public static final String DIAGRAM_TITLE_TAG = "Diagram:Title"; + public static final String DIAGRAM_DESCRIPTION_TAG = "Diagram:Description"; + + 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(); + } + + public AbstractPlantUMLExporter() { + this(ColorScheme.Light); + } + + public AbstractPlantUMLExporter(ColorScheme colorScheme) { + super(colorScheme); + } + + 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"); + + if (includeTitle(view)) { + ElementStyle titleStyle = findElementStyle(view, DIAGRAM_TITLE_TAG); + ElementStyle descriptionStyle = findElementStyle(view, DIAGRAM_DESCRIPTION_TAG); + + String title = view.getTitle(); + if (StringUtils.isNullOrEmpty(title)) { + title = view.getName(); + } + + String description = view.getDescription(); + if (StringUtils.isNullOrEmpty(description)) { + writer.writeLine( + String.format( + "title <size:%s>%s</size>", + titleStyle != null ? titleStyle.getFontSize() : DEFAULT_FONT_SIZE, + title + ) + ); + } else { + writer.writeLine( + String.format( + "title <size:%s>%s</size>\\n<size:%s>%s</size>", + titleStyle != null ? titleStyle.getFontSize() : DEFAULT_FONT_SIZE, + title, + descriptionStyle != null ? descriptionStyle.getFontSize() : DEFAULT_FONT_SIZE, + description + ) + ); + } + } + + writer.writeLine(); + 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)) { + String[] includes = commaSeparatedIncludes.split(","); + + for (String include : includes) { + if (!StringUtils.isNullOrEmpty(include)) { + include = include.trim(); + writer.writeLine("!include " + include); + } + } + writer.writeLine(); + } + } + + @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 isSupportedIcon(String icon) { + return !StringUtils.isNullOrEmpty(icon) && icon.startsWith("http"); + } + + protected double calculateIconScale(String iconUrl, int maxIconSize) { + double scale = 0.5; + + try { + URL url = new URL(iconUrl); + BufferedImage bi = ImageIO.read(url); + + int width = bi.getWidth(); + int height = bi.getHeight(); + + 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 + } 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..bba5d53c5 --- /dev/null +++ b/structurizr-export/src/main/java/com/structurizr/export/plantuml/C4PlantUMLExporter.java @@ -0,0 +1,680 @@ +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 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"; + + private static final int MAX_ICON_SIZE = 30; + + /** + * <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() { + } + + public C4PlantUMLExporter(ColorScheme colorScheme) { + super(colorScheme); + } + + @Override + protected void writeHeader(ModelView view, IndentingWriter writer) { + super.writeHeader(view, writer); + groupId = 0; + + if (!renderAsSequenceDiagram(view)) { + 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"); + } + } + + writeSkinParams(writer); + + writer.writeLine("<style>"); + writer.indent(); + + writer.writeLine("root {"); + writer.indent(); + if (colorScheme == ColorScheme.Dark) { + writer.writeLine("BackgroundColor: " + Styles.DEFAULT_BACKGROUND_DARK); + writer.writeLine("FontColor: " + Styles.DEFAULT_COLOR_DARK); + } else { + writer.writeLine("BackgroundColor: " + Styles.DEFAULT_BACKGROUND_LIGHT); + writer.writeLine("FontColor: " + Styles.DEFAULT_COLOR_LIGHT); + } + Font font = view.getViewSet().getConfiguration().getBranding().getFont(); + if (font != null) { + String fontName = font.getName(); + if (!StringUtils.isNullOrEmpty(fontName)) { + writer.writeLine("FontName: " + fontName); + } + } + writer.outdent(); + writer.writeLine("}"); + writer.outdent(); + writer.writeLine("</style>"); + writer.writeLine(); + + 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 (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"); + } + } + } + writer.writeLine(); + + 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()) { + for (String tagList : elementStyles.keySet()) { + ElementStyle elementStyle = elementStyles.get(tagList); + tagList = tagList.replaceFirst("Element,", ""); + + String sprite = ""; + 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); + + int borderThickness = 1; + if (elementStyle.getStrokeWidth() != null) { + borderThickness = elementStyle.getStrokeWidth(); + } + + 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(), + elementStyle.getColor(), + sprite, + elementStyle.getProperties().getOrDefault(C4PLANTUML_SHADOW, ""), + elementStyle.getBorder().toString().toLowerCase(), + borderThickness + ); + + line = line.replace(", $borderThickness=\"1\")", ")"); + writer.writeLine(line); + } + + writer.writeLine(); + } + + if (!relationshipStyles.isEmpty()) { + 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 + )); + } + + writer.writeLine(); + } + + if (!boundaryStyles.isEmpty()) { + for (String tagList : boundaryStyles.keySet()) { + ElementStyle elementStyle = boundaryStyles.get(tagList); + tagList = tagList.replaceFirst("Element,", ""); + + int borderThickness = 1; + if (elementStyle.getStrokeWidth() != null) { + borderThickness = elementStyle.getStrokeWidth(); + } + + String line = 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().toString().toLowerCase(), + borderThickness + ); + + line = line.replace(", $borderThickness=\"1\")", ")"); + writer.writeLine(line); + } + + writer.writeLine(); + } + } + } + + @Override + protected void writeFooter(ModelView view, IndentingWriter writer) { + writer.writeLine("SHOW_LEGEND(" + includeLegend(view) + ")"); + writer.writeLine((includeStereotypes(view) ? "show" : "hide") + " stereotypes"); + + super.writeFooter(view, writer); + } + + @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"; + 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())) { + color = elementStyleForGroup.getStroke(); + } else if (elementStyleForAllGroups != null && !StringUtils.isNullOrEmpty(elementStyleForAllGroups.getStroke())) { + color = 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 + "}>"; +// } + + String line = String.format("AddBoundaryTag(\"%s\", $borderColor=\"%s\", $fontColor=\"%s\", $borderStyle=\"%s\", $borderThickness=\"%s\")", + group, + color, + color, + Border.Dashed.toString().toLowerCase(), + borderThickness); + + line = line.replace(", $borderThickness=\"1\")", ")"); + writer.writeLine(line); + + 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; + + // Person(alias, label, ?descr, ?sprite, ?tags, ?link, ?type) + writer.writeLine( + 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; + + // System(alias, label, ?descr, ?sprite, ?tags, ?link, ?type) + writer.writeLine( + 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; + 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 (!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/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/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/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/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/README.md b/structurizr-export/src/main/java/com/structurizr/export/plantuml/README.md new file mode 100644 index 000000000..8474c6296 --- /dev/null +++ b/structurizr-export/src/main/java/com/structurizr/export/plantuml/README.md @@ -0,0 +1,5 @@ +# PlantUML + +There are two PlantUML exporters in this package - [StructurizrPlantUMLExporter](StructurizrPlantUMLExporter.java) and [C4PlantUMLExporter](C4PlantUMLExporter.java). + +See https://docs.structurizr.com/export for more. 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..79f53c142 --- /dev/null +++ b/structurizr-export/src/main/java/com/structurizr/export/plantuml/StructurizrPlantUMLExporter.java @@ -0,0 +1,757 @@ +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 static java.lang.String.format; + +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; + + private Set<PlantUMLStyle> plantUMLStyles; + + public StructurizrPlantUMLExporter() { + this(ColorScheme.Light); + } + + public StructurizrPlantUMLExporter(ColorScheme colorScheme) { + super(colorScheme); + } + + @Override + protected void writeHeader(ModelView view, IndentingWriter writer) { + plantUMLStyles = new HashSet<>(); + super.writeHeader(view, writer); + + if (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; + } + + // 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"); + } + } + + writer.writeLine("hide stereotype"); + + writeSkinParams(writer); + + writer.writeLine("<style></style>"); + writer.writeLine(); + + String fontName = null; + Font font = view.getViewSet().getConfiguration().getBranding().getFont(); + if (font != null) { + fontName = font.getName(); + if (!StringUtils.isNullOrEmpty(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)); + } + + writeIncludes(view, writer); + } + + @Override + protected void writeFooter(ModelView view, IndentingWriter writer) { + super.writeFooter(view, writer); + writeStyles(writer); + } + + private void writeStyles(IndentingWriter writer) { + StringBuilder styles = new StringBuilder(); + List<PlantUMLStyle> sortedStyles = plantUMLStyles.stream().sorted(Comparator.comparing(PlantUMLStyle::getName)).toList(); + + 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())); + + writer.replace("<style></style>", "<style>\n" + styles + "</style>"); + } + + @Override + protected void startGroupBoundary(ModelView view, String group, IndentingWriter writer) { + 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()); + } + + 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, elementStyle.getFontSize() * MAX_ICON_SIZE_RATIO); + icon = "\\n\\n<img:" + icon + "{scale=" + scale + "}>"; + } else { + icon = ""; + } + + writer.writeLine( + String.format( + "rectangle \"%s%s\" <<%s>> as group%s {", + groupName, + icon, + classSelectorForGroup(group), + Base64.getEncoder().encodeToString(group.getBytes())) + ); + writer.indent(); + } + } + + @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)) { + 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<size:%s>%s</size>\" <<%s>> {", + softwareSystem.getName(), + calculateMetadataFontSize(elementStyle.getFontSize()), + typeOf(view, softwareSystem, true), + plantUMLBoundaryStyle.getClassSelector() + ) + ); + 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)) { + 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<size:%s>%s</size>\" <<%s>> {", + container.getName(), + calculateMetadataFontSize(findBoundaryStyle(view, container).getFontSize()), typeOf(view, container, true), + plantUMLBoundaryStyle.getClassSelector())); + 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); + + PlantUMLDeploymentNodeStyle plantUMLDeploymentNodeStyle = new PlantUMLDeploymentNodeStyle( + elementStyle.getTag(), + 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(plantUMLDeploymentNodeStyle); + + String icon = ""; + if (isSupportedIcon(elementStyle.getIcon())) { + double scale = calculateIconScale(elementStyle.getIcon(), elementStyle.getFontSize() * MAX_ICON_SIZE_RATIO); + 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:%s>%s</size>%s\" <<%s>> as %s%s {", + deploymentNode.getName() + (!"1".equals(deploymentNode.getInstances()) ? " (x" + deploymentNode.getInstances() + ")" : ""), + calculateMetadataFontSize(findElementStyle(view, deploymentNode).getFontSize()), + typeOf(view, deploymentNode, true), + icon, + plantUMLDeploymentNodeStyle.getClassSelector(), + 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); + } + + if (!elements.isEmpty()) { + writer.writeLine(); + } + + writeRelationships(view, writer); + writeFooter(view, writer); + + Diagram diagram = createDiagram(view, writer.toString()); + diagram.setLegend(createLegend(view)); + + return diagram; + } else { + return super.export(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)) { + // 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(), + elementStyle.getWidth(), + elementStyle.getBackground(), + elementStyle.getColor(), + elementStyle.getStroke(), + renderAsSequenceDiagram(view) ? DEFAULT_STROKE_WIDTH : 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 (renderAsSequenceDiagram(view)) { + writer.writeLine(String.format("%s \"%s\\n<size:%s>%s</size>\" as %s <<%s>>", + sequenceDiagramShape, + element.getName(), + metadataFontSize, + typeOf(view, element, true), + idOf(element), + plantUMLElementStyle.getClassSelector() + )); + } else { + String shape = plantUMLShapeOf(view, element); + 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:%s>%s</size>", metadataFontSize, type); + } + + if (isSupportedIcon(elementStyle.getIcon())) { + double scale = calculateIconScale(elementStyle.getIcon(), elementStyle.getFontSize() * MAX_ICON_SIZE_RATIO); + icon = "\\n\\n<img:" + elementStyle.getIcon() + "{scale=" + scale + "}>"; + } + + String classSelector = plantUMLElementStyle.getClassSelector(); + String id = idOf(element); + + writer.writeLine(format("%s \"==%s%s%s%s\" <<%s>> as %s%s", + shape, + name, + type, + icon, + description, + classSelector, + 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); + + PlantUMLRelationshipStyle plantUMLRelationshipStyle = new PlantUMLRelationshipStyle( + style.getTag(), + style.getColor(), + style.getStyle(), + renderAsSequenceDiagram(view) ? DEFAULT_STROKE_WIDTH : style.getThickness(), + style.getFontSize() + ); + plantUMLStyles.add(plantUMLRelationshipStyle); + + int metadataFontSize = calculateMetadataFontSize(style.getFontSize()); + String description = ""; + String technology = relationship.getTechnology(); + + if (!StringUtils.isNullOrEmpty(relationshipView.getOrder())) { + description = relationshipView.getOrder() + ": "; + } + + description += (hasValue(relationshipView.getDescription()) ? relationshipView.getDescription() : hasValue(relationshipView.getRelationship().getDescription()) ? relationshipView.getRelationship().getDescription() : ""); + + if (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%s", + idOf(relationship.getSource()), + arrowStart, + arrowEnd, + idOf(relationship.getDestination()), + plantUMLRelationshipStyle.getClassSelector(), + description, + (StringUtils.isNullOrEmpty(technology) ? "" : "\\n<size:" + metadataFontSize + ">[" + technology + "]</size>"))); + } else { + String arrow; + + if (relationshipView.isResponse() != null && relationshipView.isResponse()) { + arrow = "<--"; + } else { + arrow = "-->"; + } + + // 1 --> 2 : "...\n<size:..>...</size> + writer.writeLine(format("%s %s %s <<%s>> : \"%s%s\"", + idOf(relationship.getSource()), + arrow, + idOf(relationship.getDestination()), + plantUMLRelationshipStyle.getClassSelector(), + description, + (StringUtils.isNullOrEmpty(technology) ? "" : "\\n<size:" + metadataFontSize + ">[" + technology + "]</size>") + )); + } + } + + @Override + protected Legend createLegend(ModelView view) { + IndentingWriter writer = new IndentingWriter(); + + writer.writeLine("@startuml"); + writer.writeLine(); + writer.writeLine("set separator none"); + writer.writeLine("hide stereotype"); + writer.writeLine(); + + writer.writeLine("<style></style>"); + 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<img:" + style.getIcon() + "{scale=" + scale + "}>"; + } + + 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(); + 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<img:" + style.getIcon() + "{scale=" + scale + "}>"; + } + + writer.writeLine(format("%s \"==%s%s\" <<%s>>", + plantUMLShapeOf(style.getShape()), + description, + icon, + style.getClassSelector()) + ); + writer.writeLine(); + }); + + int id = 0; + List<PlantUMLRelationshipStyle> 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.getName(); + if (description.startsWith("Relationship,")) { + description = description.substring("Relationship,".length()); + } + description = description.replaceAll(",", ", "); + + // id --> id : "..." + writer.writeLine(format("rectangle \".\" <<.Element-Transparent>> as %s", id)); + writer.writeLine(format("%s --> %s <<%s>> : \"%s\"", + id, + id, + relationshipStyle.getClassSelector(), + description) + ); + + writer.writeLine(); + }; + + writer.writeLine("@enduml"); + + plantUMLStyles.add(new PlantUMLLegendStyle()); + writeStyles(writer); + + return new Legend(writer.toString()); + } + + protected boolean renderAsSequenceDiagram(ModelView view) { + return view instanceof DynamicView && "true".equalsIgnoreCase(getViewOrViewSetProperty(view, PLANTUML_SEQUENCE_DIAGRAM_PROPERTY, "false")); + } + + 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; + 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) { + 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 && elementStyleForAllGroups.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/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..c7932abcc --- /dev/null +++ b/structurizr-export/src/main/java/com/structurizr/export/websequencediagrams/WebSequenceDiagramsExporter.java @@ -0,0 +1,172 @@ +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) { + if (!StringUtils.isNullOrEmpty(view.getDescription())) { + writer.writeLine("title " + view.getName() + "\n" + view.getDescription()); + } else { + writer.writeLine("title " + view.getName()); + } + writer.writeLine(); + } + + @Override + protected void writeFooter(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/DOTDiagramExporterTests.java b/structurizr-export/src/test/java/com/structurizr/export/dot/DOTDiagramExporterTests.java new file mode 100644 index 000000000..ca459bddd --- /dev/null +++ b/structurizr-export/src/test/java/com/structurizr/export/dot/DOTDiagramExporterTests.java @@ -0,0 +1,1168 @@ +package com.structurizr.export.dot; + +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.*; +import org.junit.jupiter.api.Tag; +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/big-bank-plc.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(); + 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=<<br /><font point-size="34">System Landscape View</font>> + + subgraph "cluster_group_Big Bank plc" { + margin=25 + label=<<font point-size="24"><br />Big Bank plc</font>> + labelloc=b + color="#cccccc" + fontcolor="#cccccc" + fillcolor="#ffffff" + style="dashed" + + 2 [id=2,shape=rect, label=<<font point-size="32">Customer Service Staff</font><br /><font point-size="17">[Person]</font><br /><br /><font point-size="22">Customer service staff within the<br />bank.</font>>, style=filled, color="#6b6b6b", fillcolor="#999999", fontcolor="#ffffff"] + 3 [id=3,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 staff<br />within the bank.</font>>, style=filled, color="#6b6b6b", fillcolor="#999999", 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"] + 5 [id=5,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"] + 6 [id=6,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"] + 7 [id=7,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"] + } + + 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"] + + 1 -> 7 [id=19, label=<<font point-size="24">Views account balances,<br />and makes payments using</font>>, style="dashed", color="#444444", fontcolor="#444444"] + 7 -> 4 [id=20, label=<<font point-size="24">Gets account information<br />from, and makes payments<br />using</font>>, style="dashed", color="#444444", fontcolor="#444444"] + 7 -> 5 [id=21, label=<<font point-size="24">Sends e-mail using</font>>, style="dashed", color="#444444", fontcolor="#444444"] + 5 -> 1 [id=22, label=<<font point-size="24">Sends e-mails to</font>>, style="dashed", color="#444444", fontcolor="#444444"] + 1 -> 2 [id=23, label=<<font point-size="24">Asks questions to</font><br /><font point-size="19">[Telephone]</font>>, style="dashed", color="#444444", fontcolor="#444444"] + 2 -> 4 [id=24, label=<<font point-size="24">Uses</font>>, style="dashed", color="#444444", fontcolor="#444444"] + 1 -> 6 [id=25, label=<<font point-size="24">Withdraws cash using</font>>, style="dashed", color="#444444", fontcolor="#444444"] + 6 -> 4 [id=26, label=<<font point-size="24">Uses</font>>, style="dashed", color="#444444", fontcolor="#444444"] + 3 -> 4 [id=27, label=<<font point-size="24">Uses</font>>, style="dashed", color="#444444", fontcolor="#444444"] + + }""", diagram.getDefinition()); + + diagram = diagrams.stream().filter(md -> md.getKey().equals("SystemContext")).findFirst().get(); + 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=<<br /><font point-size="34">System Context View: Internet Banking System</font><br /><font point-size="24">The system context diagram for the Internet Banking System.</font>> + + subgraph "cluster_group_Big Bank plc" { + margin=25 + label=<<font point-size="24"><br />Big Bank plc</font>> + labelloc=b + color="#cccccc" + fontcolor="#cccccc" + fillcolor="#ffffff" + style="dashed" + + 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"] + 5 [id=5,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"] + 7 [id=7,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"] + } + + 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"] + + 1 -> 7 [id=19, label=<<font point-size="24">Views account balances,<br />and makes payments using</font>>, style="dashed", color="#444444", fontcolor="#444444"] + 7 -> 4 [id=20, label=<<font point-size="24">Gets account information<br />from, and makes payments<br />using</font>>, style="dashed", color="#444444", fontcolor="#444444"] + 7 -> 5 [id=21, label=<<font point-size="24">Sends e-mail using</font>>, style="dashed", color="#444444", fontcolor="#444444"] + 5 -> 1 [id=22, label=<<font point-size="24">Sends e-mails to</font>>, style="dashed", color="#444444", fontcolor="#444444"] + + }""", diagram.getDefinition()); + + diagram = diagrams.stream().filter(md -> md.getKey().equals("Containers")).findFirst().get(); + 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=<<br /><font point-size="34">Container View: Internet Banking System</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"] + 5 [id=5,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_7 { + 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" + + 10 [id=10,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"] + 11 [id=11,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"] + 18 [id=18,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"] + 8 [id=8,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"] + 9 [id=9,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"] + } + + 5 -> 1 [id=22, label=<<font point-size="24">Sends e-mails to</font>>, style="dashed", color="#444444", fontcolor="#444444"] + 1 -> 10 [id=28, label=<<font point-size="24">Visits bigbank.com/ib<br />using</font><br /><font point-size="19">[HTTPS]</font>>, style="dashed", color="#444444", fontcolor="#444444"] + 1 -> 8 [id=29, label=<<font point-size="24">Views account balances,<br />and makes payments using</font>>, style="dashed", color="#444444", fontcolor="#444444"] + 1 -> 9 [id=30, label=<<font point-size="24">Views account balances,<br />and makes payments using</font>>, style="dashed", color="#444444", fontcolor="#444444"] + 10 -> 8 [id=31, label=<<font point-size="24">Delivers to the customer's<br />web browser</font>>, style="dashed", color="#444444", fontcolor="#444444"] + 8 -> 11 [id=33, label=<<font point-size="24">Makes API calls to</font><br /><font point-size="19">[JSON/HTTPS]</font>>, style="dashed", color="#444444", fontcolor="#444444"] + 9 -> 11 [id=37, label=<<font point-size="24">Makes API calls to</font><br /><font point-size="19">[JSON/HTTPS]</font>>, style="dashed", color="#444444", fontcolor="#444444"] + 11 -> 18 [id=45, label=<<font point-size="24">Reads from and writes to</font><br /><font point-size="19">[SQL/TCP]</font>>, style="dashed", color="#444444", fontcolor="#444444"] + 11 -> 4 [id=47, label=<<font point-size="24">Makes API calls to</font><br /><font point-size="19">[XML/HTTPS]</font>>, style="dashed", color="#444444", fontcolor="#444444"] + 11 -> 5 [id=49, label=<<font point-size="24">Sends e-mail using</font>>, style="dashed", color="#444444", fontcolor="#444444"] + + }""", diagram.getDefinition()); + + diagram = diagrams.stream().filter(md -> md.getKey().equals("Components")).findFirst().get(); + 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=<<br /><font point-size="34">Component View: Internet Banking System - API Application</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"] + 5 [id=5,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_7 { + 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" + + subgraph cluster_11 { + 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" + + 12 [id=12,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"] + 13 [id=13,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"] + 14 [id=14,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"] + 15 [id=15,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"] + 16 [id=16,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"] + 17 [id=17,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"] + } + + 18 [id=18,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"] + 8 [id=8,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"] + 9 [id=9,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"] + } + + 8 -> 12 [id=32, label=<<font point-size="24">Makes API calls to</font><br /><font point-size="19">[JSON/HTTPS]</font>>, style="dashed", color="#444444", fontcolor="#444444"] + 8 -> 13 [id=34, label=<<font point-size="24">Makes API calls to</font><br /><font point-size="19">[JSON/HTTPS]</font>>, style="dashed", color="#444444", fontcolor="#444444"] + 8 -> 14 [id=35, label=<<font point-size="24">Makes API calls to</font><br /><font point-size="19">[JSON/HTTPS]</font>>, style="dashed", color="#444444", fontcolor="#444444"] + 9 -> 12 [id=36, label=<<font point-size="24">Makes API calls to</font><br /><font point-size="19">[JSON/HTTPS]</font>>, style="dashed", color="#444444", fontcolor="#444444"] + 9 -> 13 [id=38, label=<<font point-size="24">Makes API calls to</font><br /><font point-size="19">[JSON/HTTPS]</font>>, style="dashed", color="#444444", fontcolor="#444444"] + 9 -> 14 [id=39, label=<<font point-size="24">Makes API calls to</font><br /><font point-size="19">[JSON/HTTPS]</font>>, style="dashed", color="#444444", fontcolor="#444444"] + 12 -> 15 [id=40, label=<<font point-size="24">Uses</font>>, style="dashed", color="#444444", fontcolor="#444444"] + 13 -> 16 [id=41, label=<<font point-size="24">Uses</font>>, style="dashed", color="#444444", fontcolor="#444444"] + 14 -> 15 [id=42, label=<<font point-size="24">Uses</font>>, style="dashed", color="#444444", fontcolor="#444444"] + 14 -> 17 [id=43, label=<<font point-size="24">Uses</font>>, style="dashed", color="#444444", fontcolor="#444444"] + 15 -> 18 [id=44, label=<<font point-size="24">Reads from and writes to</font><br /><font point-size="19">[SQL/TCP]</font>>, style="dashed", color="#444444", fontcolor="#444444"] + 16 -> 4 [id=46, label=<<font point-size="24">Makes API calls to</font><br /><font point-size="19">[XML/HTTPS]</font>>, style="dashed", color="#444444", fontcolor="#444444"] + 17 -> 5 [id=48, label=<<font point-size="24">Sends e-mail using</font>>, style="dashed", color="#444444", fontcolor="#444444"] + + }""", diagram.getDefinition()); + + diagram = diagrams.stream().filter(md -> md.getKey().equals("SignIn")).findFirst().get(); + 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=<<br /><font point-size="34">Dynamic View: Internet Banking System - API Application</font><br /><font point-size="24">Summarises how the sign in feature works in the single-page application.</font>> + + subgraph cluster_7 { + margin=25 + label=<<font point-size="24"><br />Internet Banking System</font><br /><font point-size="19">[Software System]</font>> + labelloc=b + color="#cccccc" + fontcolor="#cccccc" + fillcolor="#cccccc" + + subgraph cluster_11 { + 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" + + 12 [id=12,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"] + 15 [id=15,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"] + } + + 18 [id=18,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"] + 8 [id=8,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"] + } + + 8 [id=8,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=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"] + + 8 -> 12 [id=32, label=<<font point-size="24">1. Submits credentials to</font><br /><font point-size="19">[JSON/HTTPS]</font>>, style="dashed", color="#444444", fontcolor="#444444"] + 12 -> 15 [id=40, label=<<font point-size="24">2. Validates credentials<br />using</font>>, style="dashed", color="#444444", fontcolor="#444444"] + 15 -> 18 [id=44, label=<<font point-size="24">3. select * from users<br />where username = ?</font><br /><font point-size="19">[SQL/TCP]</font>>, style="dashed", color="#444444", fontcolor="#444444"] + 18 -> 15 [id=44, label=<<font point-size="24">4. Returns user data to</font><br /><font point-size="19">[SQL/TCP]</font>>, style="dashed", color="#444444", fontcolor="#444444"] + 15 -> 12 [id=40, label=<<font point-size="24">5. Returns true if the<br />hashed password matches</font>>, style="dashed", color="#444444", fontcolor="#444444"] + 12 -> 8 [id=32, 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="#444444", fontcolor="#444444"] + + }""", diagram.getDefinition()); + + diagram = diagrams.stream().filter(md -> md.getKey().equals("DevelopmentDeployment")).findFirst().get(); + 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=<<br /><font point-size="34">Deployment View: Internet Banking System - 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="#444444" + fontcolor="#444444" + fillcolor="#ffffff" + + subgraph cluster_51 { + 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="#444444" + fontcolor="#444444" + fillcolor="#ffffff" + + 52 [id=52,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_53 { + margin=25 + label=<<font point-size="24">Docker Container - Web Server</font><br /><font point-size="19">[Deployment Node: Docker]</font>> + labelloc=b + color="#444444" + fontcolor="#444444" + fillcolor="#ffffff" + + subgraph cluster_54 { + 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="#444444" + fontcolor="#444444" + fillcolor="#ffffff" + + 55 [id=55,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"] + 57 [id=57,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="#444444" + fontcolor="#444444" + 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="#444444" + fontcolor="#444444" + 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">Big Bank plc</font><br /><font point-size="19">[Deployment Node: Big Bank plc data center]</font>> + labelloc=b + color="#444444" + fontcolor="#444444" + fillcolor="#ffffff" + + subgraph cluster_64 { + margin=25 + label=<<font point-size="24">bigbank-dev001</font><br /><font point-size="19">[Deployment Node]</font>> + labelloc=b + color="#444444" + fontcolor="#444444" + fillcolor="#ffffff" + + 65 [id=65,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"] + } + + } + + 55 -> 52 [id=56, label=<<font point-size="24">Delivers to the customer's<br />web browser</font>>, style="dashed", color="#444444", fontcolor="#444444"] + 52 -> 57 [id=58, label=<<font point-size="24">Makes API calls to</font><br /><font point-size="19">[JSON/HTTPS]</font>>, style="dashed", color="#444444", fontcolor="#444444"] + 57 -> 61 [id=62, label=<<font point-size="24">Reads from and writes to</font><br /><font point-size="19">[SQL/TCP]</font>>, style="dashed", color="#444444", fontcolor="#444444"] + 57 -> 65 [id=66, label=<<font point-size="24">Makes API calls to</font><br /><font point-size="19">[XML/HTTPS]</font>>, style="dashed", color="#444444", fontcolor="#444444"] + + }""", diagram.getDefinition()); + + diagram = diagrams.stream().filter(md -> md.getKey().equals("LiveDeployment")).findFirst().get(); + 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=<<br /><font point-size="34">Deployment View: Internet Banking System - 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="#444444" + fontcolor="#444444" + 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="#444444" + fontcolor="#444444" + 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="#444444" + fontcolor="#444444" + 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="#444444" + fontcolor="#444444" + fillcolor="#ffffff" + + subgraph cluster_73 { + 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="#444444" + fontcolor="#444444" + fillcolor="#ffffff" + + subgraph cluster_74 { + 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="#444444" + fontcolor="#444444" + fillcolor="#ffffff" + + 75 [id=75,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_77 { + 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="#444444" + fontcolor="#444444" + fillcolor="#ffffff" + + subgraph cluster_78 { + 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="#444444" + fontcolor="#444444" + fillcolor="#ffffff" + + 79 [id=79,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_82 { + 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="#444444" + fontcolor="#444444" + fillcolor="#ffffff" + + subgraph cluster_83 { + margin=25 + label=<<font point-size="24">Oracle - Primary</font><br /><font point-size="19">[Deployment Node: Oracle 12c]</font>> + labelloc=b + color="#444444" + fontcolor="#444444" + fillcolor="#ffffff" + + 84 [id=84,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_86 { + 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="#444444" + fontcolor="#444444" + fillcolor="#ffffff" + + subgraph cluster_87 { + margin=25 + label=<<font point-size="24">Oracle - Secondary</font><br /><font point-size="19">[Deployment Node: Oracle 12c]</font>> + labelloc=b + color="#444444" + fontcolor="#444444" + fillcolor="#ffffff" + + 88 [id=88,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_90 { + margin=25 + label=<<font point-size="24">bigbank-prod001</font><br /><font point-size="19">[Deployment Node]</font>> + labelloc=b + color="#444444" + fontcolor="#444444" + fillcolor="#ffffff" + + 91 [id=91,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"] + } + + } + + 75 -> 71 [id=76, label=<<font point-size="24">Delivers to the customer's<br />web browser</font>>, style="dashed", color="#444444", fontcolor="#444444"] + 68 -> 79 [id=80, label=<<font point-size="24">Makes API calls to</font><br /><font point-size="19">[JSON/HTTPS]</font>>, style="dashed", color="#444444", fontcolor="#444444"] + 71 -> 79 [id=81, label=<<font point-size="24">Makes API calls to</font><br /><font point-size="19">[JSON/HTTPS]</font>>, style="dashed", color="#444444", fontcolor="#444444"] + 79 -> 84 [id=85, label=<<font point-size="24">Reads from and writes to</font><br /><font point-size="19">[SQL/TCP]</font>>, style="dashed", color="#444444", fontcolor="#444444"] + 79 -> 88 [id=89, label=<<font point-size="24">Reads from and writes to</font><br /><font point-size="19">[SQL/TCP]</font>>, style="dashed", color="#444444", fontcolor="#444444"] + 79 -> 91 [id=92, label=<<font point-size="24">Makes API calls to</font><br /><font point-size="19">[XML/HTTPS]</font>>, style="dashed", color="#444444", fontcolor="#444444"] + 84 -> 88 [id=93, label=<<font point-size="24">Replicates data to</font>>, 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/amazon-web-services.json")); + 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(); + Collection<Diagram> diagrams = exporter.export(workspace); + assertEquals(1, diagrams.size()); + + Diagram diagram = diagrams.stream().findFirst().get(); + 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=<<br /><font point-size="34">Deployment View: X - 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_10 { + 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_11 { + margin=25 + label=<<font point-size="24">Amazon EC2 - Ubuntu server</font><br /><font point-size="19">[Deployment Node]</font>> + labelloc=b + color="#d86613" + fontcolor="#d86613" + fillcolor="#ffffff" + + 12 [id=12,shape=rect, label=<<font point-size="34">Web Application</font><br /><font point-size="19">[Container: Java and Spring Boot]</font>>, style=filled, color="#444444", fillcolor="#ffffff", fontcolor="#444444"] + } + + } + + subgraph cluster_14 { + 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_15 { + 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" + + 16 [id=16,shape=cylinder, label=<<font point-size="34">Database Schema</font><br /><font point-size="19">[Container]</font>>, style=filled, color="#444444", fillcolor="#ffffff", fontcolor="#444444"] + } + + } + + 7 [id=7,shape=rect, label=<<font point-size="34">DNS router</font><br /><font point-size="19">[Infrastructure Node: Route 53]</font><br /><br /><font point-size="24">Routes incoming requests based<br />upon domain name.</font>>, style=filled, color="#693cc5", fillcolor="#ffffff", fontcolor="#693cc5"] + 8 [id=8,shape=rect, label=<<font point-size="34">Load Balancer</font><br /><font point-size="19">[Infrastructure Node: Elastic Load Balancer]</font><br /><br /><font point-size="24">Automatically distributes<br />incoming application traffic.</font>>, style=filled, color="#693cc5", fillcolor="#ffffff", fontcolor="#693cc5"] + } + + } + + 8 -> 12 [id=13, label=<<font point-size="24">Forwards requests to</font><br /><font point-size="19">[HTTPS]</font>>, style="dashed", color="#444444", fontcolor="#444444"] + 12 -> 16 [id=17, label=<<font point-size="24">Reads from and writes to</font><br /><font point-size="19">[MySQL Protocol/SSL]</font>>, style="dashed", color="#444444", fontcolor="#444444"] + 7 -> 8 [id=9, label=<<font point-size="24">Forwards requests to</font><br /><font point-size="19">[HTTPS]</font>>, style="dashed", color="#444444", fontcolor="#444444"] + + }""", 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(); + 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=<<br /><font point-size="34">System Landscape View</font>> + + 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="#444444", fillcolor="#ffffff", fontcolor="#444444"] + } + + 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" + + 3 [id=3,shape=rect, label=<<font point-size="34">C</font><br /><font point-size="19">[Software System]</font>>, style=filled, color="#444444", fillcolor="#ffffff", fontcolor="#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" + + 4 [id=4,shape=rect, label=<<font point-size="34">D</font><br /><font point-size="19">[Software System]</font>>, style=filled, color="#444444", fillcolor="#ffffff", fontcolor="#444444"] + } + + } + + 1 [id=1,shape=rect, label=<<font point-size="34">A</font><br /><font point-size="19">[Software System]</font>>, 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(); + 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=<<br /><font point-size="34">Container View: D</font>> + + 3 [id=3,shape=rect, label=<<font point-size="34">C</font><br /><font point-size="19">[Software System]</font>>, style=filled, color="#444444", fillcolor="#ffffff", fontcolor="#444444"] + + 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 4" { + margin=25 + label=<<font point-size="24"><br />Group 4</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="#444444", fillcolor="#ffffff", fontcolor="#444444"] + } + + 5 [id=5,shape=rect, label=<<font point-size="34">E</font><br /><font point-size="19">[Container]</font>>, 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(); + 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=<<br /><font point-size="34">Component View: D - F</font>> + + 3 [id=3,shape=rect, label=<<font point-size="34">C</font><br /><font point-size="19">[Software System]</font>>, style=filled, color="#444444", fillcolor="#ffffff", fontcolor="#444444"] + + 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_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 5" { + margin=25 + label=<<font point-size="24"><br />Group 5</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="#444444", fillcolor="#ffffff", fontcolor="#444444"] + } + + 7 [id=7,shape=rect, label=<<font point-size="34">G</font><br /><font point-size="19">[Component]</font>>, 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 + 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(); + 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=<<br /><font point-size="34">System Landscape View</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="#444444", fillcolor="#ffffff", fontcolor="#444444"] + 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="#444444", fillcolor="#ffffff", fontcolor="#444444"] + 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="#444444", fillcolor="#ffffff", fontcolor="#444444"] + } + + 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="#444444", fillcolor="#ffffff", fontcolor="#444444"] + } + + } + + } + + 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="#444444", fillcolor="#ffffff", fontcolor="#444444"] + } + + + }""", 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 { + 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">Container View: Software System 1</font>> + + subgraph cluster_1 { + margin=25 + label=<<font point-size="24"><br />Software System 1</font><br /><font point-size="19">[Software System]</font>> + labelloc=b + color="#444444" + fontcolor="#444444" + fillcolor="#444444" + + 2 [id=2,shape=rect, label=<<font point-size="34">Container 1</font><br /><font point-size="19">[Container]</font>>, style=filled, color="#444444", fillcolor="#ffffff", fontcolor="#444444"] + } + + subgraph cluster_3 { + margin=25 + label=<<font point-size="24"><br />Software System 2</font><br /><font point-size="19">[Software System]</font>> + labelloc=b + color="#cccccc" + fontcolor="#cccccc" + fillcolor="#cccccc" + + 4 [id=4,shape=rect, label=<<font point-size="34">Container 2</font><br /><font point-size="19">[Container]</font>>, style=filled, color="#444444", fillcolor="#ffffff", fontcolor="#444444"] + } + + 2 -> 4 [id=5, label=<<font point-size="24">Uses</font>>, style="dashed", color="#444444", fontcolor="#444444"] + + }""", 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 { + 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">Component View: Software System 1 - Container 1</font>> + + subgraph cluster_1 { + margin=25 + label=<<font point-size="24"><br />Software System 1</font><br /><font point-size="19">[Software System]</font>> + labelloc=b + color="#444444" + fontcolor="#444444" + fillcolor="#444444" + + subgraph cluster_2 { + margin=25 + label=<<font point-size="24"><br />Container 1</font><br /><font point-size="19">[Container]</font>> + labelloc=b + color="#444444" + fontcolor="#444444" + fillcolor="#444444" + + 3 [id=3,shape=rect, label=<<font point-size="34">Component 1</font><br /><font point-size="19">[Component]</font>>, style=filled, color="#444444", fillcolor="#ffffff", fontcolor="#444444"] + } + + } + + subgraph cluster_4 { + margin=25 + label=<<font point-size="24"><br />Software System 2</font><br /><font point-size="19">[Software System]</font>> + labelloc=b + color="#cccccc" + fontcolor="#cccccc" + fillcolor="#cccccc" + + subgraph cluster_5 { + margin=25 + label=<<font point-size="24"><br />Container 2</font><br /><font point-size="19">[Container]</font>> + labelloc=b + color="#cccccc" + fontcolor="#cccccc" + fillcolor="#cccccc" + + 6 [id=6,shape=rect, label=<<font point-size="34">Component 2</font><br /><font point-size="19">[Component]</font>>, style=filled, color="#444444", fillcolor="#ffffff", fontcolor="#444444"] + } + + } + + 3 -> 6 [id=7, label=<<font point-size="24">Uses</font>>, style="dashed", color="#444444", fontcolor="#444444"] + + }""", 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 { + 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 View</font>> + + subgraph "cluster_group_Group 1" { + margin=25 + label=<<font point-size="24"><br />Group 1</font>> + labelloc=b + color="#111111" + fontcolor="#111111" + fillcolor="#ffffff" + style="dashed" + + 1 [id=1,shape=rect, label=<<font point-size="34">User 1</font><br /><font point-size="19">[Person]</font>>, style=filled, color="#444444", fillcolor="#ffffff", fontcolor="#444444"] + } + + subgraph "cluster_group_Group 2" { + margin=25 + label=<<font point-size="24"><br />Group 2</font>> + labelloc=b + color="#222222" + fontcolor="#222222" + fillcolor="#ffffff" + style="dashed" + + 2 [id=2,shape=rect, label=<<font point-size="34">User 2</font><br /><font point-size="19">[Person]</font>>, style=filled, color="#444444", fillcolor="#ffffff", fontcolor="#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" + + 3 [id=3,shape=rect, label=<<font point-size="34">User 3</font><br /><font point-size="19">[Person]</font>>, style=filled, color="#444444", fillcolor="#ffffff", fontcolor="#444444"] + } + + + }""", diagram.getDefinition()); + + workspace.getViews().getConfiguration().getStyles().addElementStyle("Group").color("#aabbcc"); + + diagram = exporter.export(view); + 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=<<br /><font point-size="34">System Landscape View</font>> + + subgraph "cluster_group_Group 1" { + margin=25 + label=<<font point-size="24"><br />Group 1</font>> + labelloc=b + color="#111111" + fontcolor="#111111" + fillcolor="#ffffff" + style="dashed" + + 1 [id=1,shape=rect, label=<<font point-size="34">User 1</font><br /><font point-size="19">[Person]</font>>, style=filled, color="#444444", fillcolor="#ffffff", fontcolor="#444444"] + } + + subgraph "cluster_group_Group 2" { + margin=25 + label=<<font point-size="24"><br />Group 2</font>> + labelloc=b + color="#222222" + fontcolor="#222222" + fillcolor="#ffffff" + style="dashed" + + 2 [id=2,shape=rect, label=<<font point-size="34">User 2</font><br /><font point-size="19">[Person]</font>>, style=filled, color="#444444", fillcolor="#ffffff", fontcolor="#444444"] + } + + subgraph "cluster_group_Group 3" { + margin=25 + label=<<font point-size="24"><br />Group 3</font>> + labelloc=b + color="#aabbcc" + fontcolor="#aabbcc" + fillcolor="#ffffff" + style="dashed" + + 3 [id=3,shape=rect, label=<<font point-size="34">User 3</font><br /><font point-size="19">[Person]</font>>, style=filled, color="#444444", fillcolor="#ffffff", fontcolor="#444444"] + } + + + }""", 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 { + 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">Title</font><br /><font point-size="24">Description</font>> + + 1 [id=1,shape=rect, label=<<font point-size="34">A</font>>, style=filled, color="#444444", fillcolor="#ffffff", fontcolor="#444444"] + 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="#444444", fillcolor="#ffffff", fontcolor="#444444"] + + 1 -> 2 [id=3, label=<<font point-size="24">Uses</font>>, style="dashed", color="#444444", fontcolor="#444444"] + + }""", 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(); + + DOTExporter exporter = new DOTExporter(); + Diagram diagram = exporter.export(view); + 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=<<br /><font point-size="34">Container View: Software System</font>> + + subgraph cluster_1 { + margin=25 + label=<<font point-size="24"><br />Software System</font><br /><font point-size="19">[Software System]</font>> + labelloc=b + color="#444444" + fontcolor="#444444" + fillcolor="#444444" + + 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">Container 1</font><br /><font point-size="19">[Container]</font>>, style=filled, color="#444444", fillcolor="#ffffff", fontcolor="#444444"] + } + + 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" + + 3 [id=3,shape=rect, label=<<font point-size="34">Container 2</font><br /><font point-size="19">[Container]</font>>, 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 new file mode 100644 index 000000000..88d8184f4 --- /dev/null +++ b/structurizr-export/src/test/java/com/structurizr/export/ilograph/IlographExporterTests.java @@ -0,0 +1,837 @@ +package com.structurizr.export.ilograph; + +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; +import com.structurizr.view.ThemeUtils; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +import java.io.File; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class IlographExporterTests extends AbstractExporterTests { + + @Test + public void test_BigBankPlcExample() throws Exception { + Workspace workspace = WorkspaceUtils.loadWorkspaceFromJson(new File("./src/test/resources/big-bank-plc.json")); + IlographExporter ilographExporter = new IlographExporter(); + WorkspaceExport export = ilographExporter.export(workspace); + + 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: Internet Banking System - API Application + 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/amazon-web-services.json")); + workspace.getViews().getConfiguration().getStyles().addElementStyle("Amazon Web Services - Route 53").addProperty(IlographExporter.ILOGRAPH_ICON, "AWS/Networking/Route-53.svg"); + + HttpClient httpClient = new HttpClient(); + httpClient.allow(".*"); + ThemeUtils.loadThemes(workspace, httpClient); + + IlographExporter ilographExporter = new IlographExporter(); + WorkspaceExport export = ilographExporter.export(workspace); + + 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 + void test_renderCustomElements() { + 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: + - 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 + 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 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..1259c57be --- /dev/null +++ b/structurizr-export/src/test/java/com/structurizr/export/mermaid/MermaidDiagramExporterTests.java @@ -0,0 +1,479 @@ +package com.structurizr.export.mermaid; + +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.*; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Tag; +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 + @Tag("IntegrationTest") + public void test_AmazonWebServicesExample() throws Exception { + Workspace workspace = WorkspaceUtils.loadWorkspaceFromJson(new File("./src/test/resources/amazon-web-services.json")); + 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(); + Collection<Diagram> diagrams = exporter.export(workspace); + assertEquals(1, diagrams.size()); + + Diagram diagram = diagrams.stream().findFirst().get(); + assertEquals(""" + graph LR + linkStyle default fill:#ffffff + + subgraph diagram ["Deployment View: X - 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("<div style='font-weight: bold'>Web Application</div><div style='font-size: 70%; margin-top: 0px'>[Container: Java and Spring Boot]</div>") + 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[("<div style='font-weight: bold'>Database Schema</div><div style='font-size: 70%; margin-top: 0px'>[Container]</div>")] + style 16 fill:#ffffff,stroke:#444444,color:#444444 + end + + end + + 7["<div style='font-weight: bold'>DNS router</div><div style='font-size: 70%; margin-top: 0px'>[Infrastructure Node: Route 53]</div><div style='font-size: 80%; margin-top:10px'>Routes incoming requests<br />based upon domain name.</div>"] + style 7 fill:#ffffff,stroke:#693cc5,color:#693cc5 + 8["<div style='font-weight: bold'>Load Balancer</div><div style='font-size: 70%; margin-top: 0px'>[Infrastructure Node: Elastic Load Balancer]</div><div style='font-size: 80%; margin-top:10px'>Automatically distributes<br />incoming application traffic.</div>"] + style 8 fill:#ffffff,stroke:#693cc5,color:#693cc5 + end + + end + + 8-. "<div>Forwards requests to</div><div style='font-size: 70%'>[HTTPS]</div>" .->12 + 12-. "<div>Reads from and writes to</div><div style='font-size: 70%'>[MySQL Protocol/SSL]</div>" .->16 + 7-. "<div>Forwards requests to</div><div style='font-size: 70%'>[HTTPS]</div>" .->8 + + end""", 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(); + assertEquals(""" + graph TB + linkStyle default fill:#ffffff + + subgraph diagram ["System Landscape View"] + style diagram fill:#ffffff,stroke:#ffffff + + subgraph group1 ["Group 1"] + style group1 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:#ffffff,stroke:#444444,color:#444444 + end + + subgraph group2 ["Group 2"] + style group2 fill:#ffffff,stroke:#cccccc,color:#cccccc,stroke-dasharray:5 + + 3["<div style='font-weight: bold'>C</div><div style='font-size: 70%; margin-top: 0px'>[Software System]</div>"] + style 3 fill:#ffffff,stroke:#444444,color:#444444 + subgraph group3 ["Group 3"] + style group3 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:#ffffff,stroke:#444444,color:#444444 + end + + end + + 1["<div style='font-weight: bold'>A</div><div style='font-size: 70%; margin-top: 0px'>[Software System]</div>"] + style 1 fill:#ffffff,stroke:#444444,color:#444444 + + 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""", diagram.getDefinition()); + + diagram = diagrams.stream().filter(md -> md.getKey().equals("Containers")).findFirst().get(); + assertEquals(""" + graph TB + linkStyle default fill:#ffffff + + subgraph diagram ["Container View: D"] + 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:#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["<div style='font-weight: bold'>F</div><div style='font-size: 70%; margin-top: 0px'>[Container]</div>"] + style 6 fill:#ffffff,stroke:#444444,color:#444444 + end + + 5["<div style='font-weight: bold'>E</div><div style='font-size: 70%; margin-top: 0px'>[Container]</div>"] + style 5 fill:#ffffff,stroke:#444444,color:#444444 + end + + 3-. "<div></div><div style='font-size: 70%'></div>" .->5 + 3-. "<div></div><div style='font-size: 70%'></div>" .->6 + + end""", diagram.getDefinition()); + + diagram = diagrams.stream().filter(md -> md.getKey().equals("Components")).findFirst().get(); + assertEquals(""" + graph TB + linkStyle default fill:#ffffff + + subgraph diagram ["Component View: D - F"] + 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:#ffffff,stroke:#444444,color:#444444 + + subgraph 4 ["D"] + style 4 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["<div style='font-weight: bold'>H</div><div style='font-size: 70%; margin-top: 0px'>[Component]</div>"] + style 8 fill:#ffffff,stroke:#444444,color:#444444 + end + + 7["<div style='font-weight: bold'>G</div><div style='font-size: 70%; margin-top: 0px'>[Component]</div>"] + style 7 fill:#ffffff,stroke:#444444,color:#444444 + end + + end + + 3-. "<div></div><div style='font-size: 70%'></div>" .->7 + 3-. "<div></div><div style='font-size: 70%'></div>" .->8 + + end""", 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(); + assertEquals(""" + graph TB + linkStyle default fill:#ffffff + + subgraph diagram ["System Landscape View"] + 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:#ffffff,stroke:#444444,color:#444444 + 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:#ffffff,stroke:#444444,color:#444444 + 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:#ffffff,stroke:#444444,color:#444444 + 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:#ffffff,stroke:#444444,color:#444444 + 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:#ffffff,stroke:#444444,color:#444444 + end + + + end""", 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 + linkStyle default fill:#ffffff + + subgraph diagram ["Container View: Software System 1"] + style diagram fill:#ffffff,stroke:#ffffff + + subgraph 1 ["Software System 1"] + style 1 fill:#ffffff,stroke:#444444,color:#444444 + + 2["<div style='font-weight: bold'>Container 1</div><div style='font-size: 70%; margin-top: 0px'>[Container]</div>"] + style 2 fill:#ffffff,stroke:#444444,color:#444444 + end + + subgraph 3 ["Software System 2"] + style 3 fill:#ffffff,stroke:#444444,color:#444444 + + 4["<div style='font-weight: bold'>Container 2</div><div style='font-size: 70%; margin-top: 0px'>[Container]</div>"] + style 4 fill:#ffffff,stroke:#444444,color:#444444 + end + + 2-. "<div>Uses</div><div style='font-size: 70%'></div>" .->4 + + 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 + linkStyle default fill:#ffffff + + subgraph diagram ["Component View: Software System 1 - Container 1"] + style diagram fill:#ffffff,stroke:#ffffff + + 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["<div style='font-weight: bold'>Component 1</div><div style='font-size: 70%; margin-top: 0px'>[Component]</div>"] + style 3 fill:#ffffff,stroke:#444444,color:#444444 + end + + end + + 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["<div style='font-weight: bold'>Component 2</div><div style='font-size: 70%; margin-top: 0px'>[Component]</div>"] + style 6 fill:#ffffff,stroke:#444444,color:#444444 + end + + end + + 3-. "<div>Uses</div><div style='font-size: 70%'></div>" .->6 + + 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 + linkStyle default fill:#ffffff + + subgraph diagram ["System Landscape View"] + style diagram fill:#ffffff,stroke:#ffffff + + subgraph group1 ["Group 1"] + style group1 fill:#ffffff,stroke:#111111,color:#111111,stroke-dasharray:5 + + 1["<div style='font-weight: bold'>User 1</div><div style='font-size: 70%; margin-top: 0px'>[Person]</div>"] + 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["<div style='font-weight: bold'>User 2</div><div style='font-size: 70%; margin-top: 0px'>[Person]</div>"] + 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["<div style='font-weight: bold'>User 3</div><div style='font-size: 70%; margin-top: 0px'>[Person]</div>"] + 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 View"] + style diagram fill:#ffffff,stroke:#ffffff + + subgraph group1 ["Group 1"] + style group1 fill:#ffffff,stroke:#111111,color:#111111,stroke-dasharray:5 + + 1["<div style='font-weight: bold'>User 1</div><div style='font-size: 70%; margin-top: 0px'>[Person]</div>"] + 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["<div style='font-weight: bold'>User 2</div><div style='font-size: 70%; margin-top: 0px'>[Person]</div>"] + 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["<div style='font-weight: bold'>User 3</div><div style='font-size: 70%; margin-top: 0px'>[Person]</div>"] + style 3 fill:#ffffff,stroke:#444444,color:#444444 + end + + + 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 + linkStyle default fill:#ffffff + + subgraph diagram ["Title"] + style diagram fill:#ffffff,stroke:#ffffff + + 1["<div style='font-weight: bold'>A</div><div style='font-size: 70%; margin-top: 0px'></div>"] + style 1 fill:#ffffff,stroke:#444444,color:#444444 + 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>"] + style 2 fill:#ffffff,stroke:#444444,color:#444444 + + 1-. "<div>Uses</div><div style='font-size: 70%'></div>" .->2 + + end""", diagram.getDefinition()); + } + +} \ 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..9497bb5c9 --- /dev/null +++ b/structurizr-export/src/test/java/com/structurizr/export/plantuml/C4PlantUMLDiagramExporterTests.java @@ -0,0 +1,1798 @@ +package com.structurizr.export.plantuml; + +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.*; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Tag; +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/big-bank-plc.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(); + assertEquals(""" + @startuml + title <size:24>System Landscape View</size> + + set separator none + top to bottom direction + + <style> + root { + BackgroundColor: #ffffff + FontColor: #444444 + } + </style> + + !include <C4/C4> + !include <C4/C4_Context> + + 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(); + assertEquals(""" + @startuml + title <size:24>System Context View: Internet Banking System</size>\\n<size:24>The system context diagram for the Internet Banking System.</size> + + set separator none + top to bottom direction + + <style> + root { + BackgroundColor: #ffffff + FontColor: #444444 + } + </style> + + !include <C4/C4> + !include <C4/C4_Context> + + 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(); + assertEquals(""" + @startuml + title <size:24>Container View: Internet Banking System</size>\\n<size:24>The container diagram for the Internet Banking System.</size> + + set separator none + top to bottom direction + + <style> + root { + BackgroundColor: #ffffff + FontColor: #444444 + } + </style> + + !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") + 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(); + assertEquals(""" + @startuml + title <size:24>Component View: Internet Banking System - API Application</size>\\n<size:24>The component diagram for the API Application.</size> + + set separator none + top to bottom direction + + <style> + root { + BackgroundColor: #ffffff + FontColor: #444444 + } + </style> + + !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") + 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="") + + 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="") + 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(); + assertEquals(""" + @startuml + title <size:24>Dynamic View: Internet Banking System - API Application</size>\\n<size:24>Summarises how the sign in feature works in the single-page application.</size> + + set separator none + top to bottom direction + + <style> + root { + BackgroundColor: #ffffff + FontColor: #444444 + } + </style> + + !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") + 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_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="") + 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(); + assertEquals(""" + @startuml + title <size:24>Deployment View: Internet Banking System - Development</size>\\n<size:24>An example development deployment scenario for the Internet Banking System.</size> + + set separator none + top to bottom direction + + <style> + root { + BackgroundColor: #ffffff + FontColor: #444444 + } + </style> + + !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") + 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(); + assertEquals(""" + @startuml + title <size:24>Deployment View: Internet Banking System - Live</size>\\n<size:24>An example live deployment scenario for the Internet Banking System.</size> + + set separator none + top to bottom direction + + <style> + root { + BackgroundColor: #ffffff + FontColor: #444444 + } + </style> + + !include <C4/C4> + !include <C4/C4_Context> + !include <C4/C4_Container> + !include <C4/C4_Deployment> + + 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(); + assertEquals(""" + @startuml + title <size:24>Dynamic View: Internet Banking System - API Application</size>\\n<size:24>Summarises how the sign in feature works in the single-page application.</size> + + set separator none + + <style> + root { + BackgroundColor: #ffffff + FontColor: #444444 + } + </style> + + !include <C4/C4_Sequence> + + 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, "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()); + } + + @Test + @Tag("IntegrationTest") + public void test_AmazonWebServicesExampleWithoutTags() throws Exception { + Workspace workspace = WorkspaceUtils.loadWorkspaceFromJson(new File("./src/test/resources/amazon-web-services.json")); + 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")); + + C4PlantUMLExporter exporter = new C4PlantUMLExporter(); + Collection<Diagram> diagrams = exporter.export(workspace); + assertEquals(1, diagrams.size()); + + Diagram diagram = diagrams.stream().findFirst().get(); + assertEquals(""" + @startuml + title <size:24>Deployment View: X - Live</size> + + set separator none + left to right direction + + <style> + root { + BackgroundColor: #ffffff + FontColor: #444444 + } + </style> + + !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.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/amazon-web-services.json")); + 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"); + + C4PlantUMLExporter exporter = new C4PlantUMLExporter(); + Collection<Diagram> diagrams = exporter.export(workspace); + assertEquals(1, diagrams.size()); + + Diagram diagram = diagrams.stream().findFirst().get(); + assertEquals(""" + @startuml + title <size:24>Deployment View: X - Live</size> + + set separator none + left to right direction + + <style> + root { + BackgroundColor: #ffffff + FontColor: #444444 + } + </style> + + !include <C4/C4> + !include <C4/C4_Context> + !include <C4/C4_Container> + !include <C4/C4_Deployment> + + 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")); + + 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(); + assertEquals(""" + @startuml + title <size:24>System Landscape View</size> + + set separator none + top to bottom direction + + <style> + root { + BackgroundColor: #ffffff + FontColor: #444444 + } + </style> + + !include <C4/C4> + !include <C4/C4_Context> + + 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(); + assertEquals(""" + @startuml + title <size:24>Container View: D</size> + + set separator none + top to bottom direction + + <style> + root { + BackgroundColor: #ffffff + FontColor: #444444 + } + </style> + + !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 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(); + assertEquals(""" + @startuml + title <size:24>Component View: D - F</size> + + set separator none + top to bottom direction + + <style> + root { + BackgroundColor: #ffffff + FontColor: #444444 + } + </style> + + !include <C4/C4> + !include <C4/C4_Context> + !include <C4/C4_Component> + + System(C, "C", $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="") + } + + } + + 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 + 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(); + assertEquals(""" + @startuml + title <size:24>System Landscape View</size>\\n<size:24>Description</size> + + set separator none + top to bottom direction + + <style> + root { + BackgroundColor: #ffffff + FontColor: #444444 + } + </style> + + !include <C4/C4> + !include <C4/C4_Context> + + 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 + 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(); + assertEquals(""" + @startuml + title <size:24>System Landscape View</size>\\n<size:24>Description</size> + + set separator none + top to bottom direction + + <style> + root { + BackgroundColor: #ffffff + FontColor: #444444 + } + </style> + + !include <C4/C4> + !include <C4/C4_Context> + + 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 + 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, int maxIconSize) { + return 1.0; + } + }; + + Diagram diagram = exporter.export(view); + assertEquals(""" + @startuml + title <size:24>System Landscape View</size> + + set separator none + top to bottom direction + + <style> + root { + BackgroundColor: #ffffff + FontColor: #444444 + } + </style> + + !include <C4/C4> + !include <C4/C4_Context> + + 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); + assertEquals(""" + @startuml + title <size:24>System Landscape View</size> + + set separator none + top to bottom direction + + <style> + root { + BackgroundColor: #ffffff + FontColor: #444444 + } + </style> + + !include <C4/C4> + !include <C4/C4_Context> + + 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 + 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 + title <size:24>Container View: Software System 1</size> + + set separator none + top to bottom direction + + <style> + root { + BackgroundColor: #ffffff + FontColor: #444444 + } + </style> + + !include <C4/C4> + !include <C4/C4_Context> + !include <C4/C4_Container> + + 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 + 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 + title <size:24>Component View: Software System 1 - Container 1</size> + + set separator none + top to bottom direction + + <style> + root { + BackgroundColor: #ffffff + FontColor: #444444 + } + </style> + + !include <C4/C4> + !include <C4/C4_Context> + !include <C4/C4_Component> + + 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="") + } + + } + + 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="") + + SHOW_LEGEND(true) + hide stereotypes + @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 + title <size:24>System Landscape View</size>\\n<size:24>Description</size> + + set separator none + top to bottom direction + + <style> + root { + BackgroundColor: #ffffff + FontColor: #444444 + } + </style> + + !include <C4/C4> + !include <C4/C4_Context> + + System(SoftwareSystem, "Software System", $descr="", $tags="", $link="https://structurizr.com") + + SHOW_LEGEND(true) + hide stereotypes + @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 + title <size:24>System Landscape View</size>\\n<size:24>Description</size> + + set separator none + top to bottom direction + + <style> + root { + BackgroundColor: #ffffff + FontColor: #444444 + } + </style> + + !include <C4/C4> + !include <C4/C4_Context> + + !include styles.puml + + System(SoftwareSystem, "Software System", $descr="", $tags="", $link="") + + SHOW_LEGEND(true) + hide stereotypes + @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 + title <size:24>System Landscape View</size>\\n<size:24>Description</size> + + set separator none + top to bottom direction + + <style> + root { + BackgroundColor: #ffffff + FontColor: #444444 + } + </style> + + !include <C4/C4> + !include <C4/C4_Context> + + System(SoftwareSystem, "Software\\nSystem", $descr="", $tags="", $link="") + + SHOW_LEGEND(true) + hide stereotypes + @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 + title <size:24>Deployment View: Default</size>\\n<size:24>view description</size> + + set separator none + top to bottom direction + + <style> + root { + BackgroundColor: #ffffff + FontColor: #444444 + } + </style> + + !include <C4/C4> + !include <C4/C4_Context> + !include <C4/C4_Deployment> + + 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 + 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); + assertEquals(""" + @startuml + title <size:24>Container View: SoftwareSystem</size> + + set separator none + top to bottom direction + + <style> + root { + BackgroundColor: #ffffff + FontColor: #444444 + } + </style> + + !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) + hide stereotypes + @enduml""", 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); + assertEquals(""" + @startuml + title <size:24>Deployment View: Default</size> + + set separator none + top to bottom direction + + <style> + root { + BackgroundColor: #ffffff + FontColor: #444444 + } + </style> + + !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) + hide stereotypes + @enduml""", 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 + title <size:24>System Landscape View</size>\\n<size:24>Description</size> + + set separator none + top to bottom direction + + <style> + root { + BackgroundColor: #ffffff + FontColor: #444444 + } + </style> + + !include <C4/C4> + !include <C4/C4_Context> + + 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 + title <size:24>System Landscape View</size>\\n<size:24>Description</size> + + set separator none + top to bottom direction + + <style> + root { + BackgroundColor: #ffffff + FontColor: #444444 + } + </style> + + !include <C4/C4> + !include <C4/C4_Context> + + 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 + title <size:24>System Landscape View</size>\\n<size:24>Description</size> + + set separator none + top to bottom direction + + <style> + root { + BackgroundColor: #ffffff + FontColor: #444444 + } + </style> + + !include <C4/C4> + !include <C4/C4_Context> + + 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 + title <size:24>System Landscape View</size>\\n<size:24>Description</size> + + set separator none + top to bottom direction + + <style> + root { + BackgroundColor: #ffffff + FontColor: #444444 + } + </style> + + !include <C4/C4> + !include <C4/C4_Context> + + System(Name, "Name", $descr="", $tags="", $link="") + + SHOW_LEGEND(false) + show stereotypes + @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 + title <size:24>Container View: Software System</size> + + set separator none + top to bottom direction + + <style> + root { + BackgroundColor: #ffffff + FontColor: #444444 + } + </style> + + !include <C4/C4> + !include <C4/C4_Context> + !include <C4/C4_Container> + + 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 + 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 + title <size:24>Component View: Software System - Container</size> + + set separator none + top to bottom direction + + <style> + root { + BackgroundColor: #ffffff + FontColor: #444444 + } + </style> + + !include <C4/C4> + !include <C4/C4_Context> + !include <C4/C4_Component> + + 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) + hide stereotypes + @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 + title <size:24>System Landscape View</size>\\n<size:24>Description</size> + + set separator none + top to bottom direction + + <style> + root { + BackgroundColor: #ffffff + FontColor: #444444 + FontName: Courier + } + </style> + + !include <C4/C4> + !include <C4/C4_Context> + + Person(User, "User", $descr="", $tags="", $link="") + + SHOW_LEGEND(true) + hide stereotypes + @enduml""", diagram.getDefinition()); + + } + + @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 + title <size:24>System Landscape View</size>\\n<size:24>Description</size> + + set separator none + top to bottom direction + + <style> + root { + BackgroundColor: #ffffff + FontColor: #444444 + } + </style> + + !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()); + + } + + @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 + title <size:24>Component View: Name - Name</size>\\n<size:24>Description</size> + + set separator none + top to bottom direction + + <style> + root { + BackgroundColor: #ffffff + FontColor: #444444 + } + </style> + + !include <C4/C4> + !include <C4/C4_Context> + !include <C4/C4_Component> + + 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) + hide stereotypes + @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 + title <size:24>System Landscape View</size>\\n<size:24>Description</size> + + set separator none + top to bottom direction + + <style> + root { + BackgroundColor: #ffffff + FontColor: #444444 + } + </style> + + !include <C4/C4> + !include <C4/C4_Context> + + 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 + 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 + title <size:24>System Landscape View</size>\\n<size:24>Description</size> + + set separator none + top to bottom direction + + <style> + root { + BackgroundColor: #ffffff + FontColor: #444444 + } + </style> + + !include <C4/C4> + !include <C4/C4_Context> + + System(Name, "Name", $descr="", $tags="", $link="https://example.com") + + SHOW_LEGEND(true) + hide stereotypes + @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 <size:24>System Landscape View</size> + + set separator none + top to bottom direction + + skinparam { + linetype ortho + } + + <style> + root { + BackgroundColor: #ffffff + FontColor: #444444 + } + </style> + + !include <C4/C4> + !include <C4/C4_Context> + + 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/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 new file mode 100644 index 000000000..b781d9a21 --- /dev/null +++ b/structurizr-export/src/test/java/com/structurizr/export/plantuml/StructurizrPlantUMLDiagramExporterTests.java @@ -0,0 +1,4267 @@ +package com.structurizr.export.plantuml; + +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.*; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +import java.io.File; +import java.util.Collection; +import java.util.List; + +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/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<Diagram> diagrams = exporter.export(workspace); + assertEquals(7, diagrams.size()); + + Diagram diagram = diagrams.stream().filter(d -> d.getKey().equals("SystemLandscape")).findFirst().get(); + assertEquals(""" + @startuml + title <size:24>System Landscape View</size> + + set separator none + top to bottom direction + skinparam ranksep 60 + skinparam nodesep 30 + hide stereotype + + <style> + root { + BackgroundColor: #ffffff; + FontColor: #444444; + } + // Element,Person,Bank Staff + .Element-RWxlbWVudCxQZXJzb24sQmFuayBTdGFmZg== { + BackgroundColor: #999999; + LineColor: #6b6b6b; + LineStyle: 0; + LineThickness: 2; + FontColor: #ffffff; + FontSize: 22; + HorizontalAlignment: center; + Shadowing: 0; + MaximumWidth: 450; + } + // Element,Person,Customer + .Element-RWxlbWVudCxQZXJzb24sQ3VzdG9tZXI= { + BackgroundColor: #08427b; + LineColor: #052e56; + LineStyle: 0; + LineThickness: 2; + FontColor: #ffffff; + FontSize: 22; + HorizontalAlignment: center; + Shadowing: 0; + MaximumWidth: 450; + } + // Element,Software System + .Element-RWxlbWVudCxTb2Z0d2FyZSBTeXN0ZW0= { + BackgroundColor: #1168bd; + LineColor: #0b4884; + LineStyle: 0; + LineThickness: 2; + FontColor: #ffffff; + FontSize: 24; + HorizontalAlignment: center; + Shadowing: 0; + MaximumWidth: 450; + } + // Element,Software System,Existing System + .Element-RWxlbWVudCxTb2Z0d2FyZSBTeXN0ZW0sRXhpc3RpbmcgU3lzdGVt { + BackgroundColor: #999999; + LineColor: #6b6b6b; + LineStyle: 0; + LineThickness: 2; + FontColor: #ffffff; + FontSize: 24; + HorizontalAlignment: center; + Shadowing: 0; + MaximumWidth: 450; + } + // Relationship + .Relationship-UmVsYXRpb25zaGlw { + LineThickness: 2; + LineStyle: 10-10; + LineColor: #444444; + FontColor: #444444; + FontSize: 24; + } + // Big Bank plc + .Group-QmlnIEJhbmsgcGxj { + BackgroundColor: #ffffff; + LineColor: #444444; + LineStyle: 2-2; + LineThickness: 2; + FontColor: #444444; + FontSize: 24; + HorizontalAlignment: center; + Shadowing: 0; + } + </style> + + rectangle "Big Bank plc" <<Group-QmlnIEJhbmsgcGxj>> as groupQmlnIEJhbmsgcGxj { + person "==Customer Service Staff\\n<size:15>[Person]</size>\\n\\nCustomer service staff within the bank." <<Element-RWxlbWVudCxQZXJzb24sQmFuayBTdGFmZg==>> as CustomerServiceStaff + person "==Back Office Staff\\n<size:15>[Person]</size>\\n\\nAdministration and support staff within the bank." <<Element-RWxlbWVudCxQZXJzb24sQmFuayBTdGFmZg==>> as BackOfficeStaff + rectangle "==Mainframe Banking System\\n<size:16>[Software System]</size>\\n\\nStores all of the core banking information about customers, accounts, transactions, etc." <<Element-RWxlbWVudCxTb2Z0d2FyZSBTeXN0ZW0sRXhpc3RpbmcgU3lzdGVt>> as MainframeBankingSystem + rectangle "==E-mail System\\n<size:16>[Software System]</size>\\n\\nThe internal Microsoft Exchange e-mail system." <<Element-RWxlbWVudCxTb2Z0d2FyZSBTeXN0ZW0sRXhpc3RpbmcgU3lzdGVt>> as EmailSystem + rectangle "==ATM\\n<size:16>[Software System]</size>\\n\\nAllows customers to withdraw cash." <<Element-RWxlbWVudCxTb2Z0d2FyZSBTeXN0ZW0sRXhpc3RpbmcgU3lzdGVt>> as ATM + rectangle "==Internet Banking System\\n<size:16>[Software System]</size>\\n\\nAllows customers to view information about their bank accounts, and make payments." <<Element-RWxlbWVudCxTb2Z0d2FyZSBTeXN0ZW0=>> as InternetBankingSystem + } + + person "==Personal Banking Customer\\n<size:15>[Person]</size>\\n\\nA customer of the bank, with personal bank accounts." <<Element-RWxlbWVudCxQZXJzb24sQ3VzdG9tZXI=>> as PersonalBankingCustomer + + PersonalBankingCustomer --> InternetBankingSystem <<Relationship-UmVsYXRpb25zaGlw>> : "Views account balances, and makes payments using" + InternetBankingSystem --> MainframeBankingSystem <<Relationship-UmVsYXRpb25zaGlw>> : "Gets account information from, and makes payments using" + InternetBankingSystem --> EmailSystem <<Relationship-UmVsYXRpb25zaGlw>> : "Sends e-mail using" + EmailSystem --> PersonalBankingCustomer <<Relationship-UmVsYXRpb25zaGlw>> : "Sends e-mails to" + PersonalBankingCustomer --> CustomerServiceStaff <<Relationship-UmVsYXRpb25zaGlw>> : "Asks questions to\\n<size:16>[Telephone]</size>" + CustomerServiceStaff --> MainframeBankingSystem <<Relationship-UmVsYXRpb25zaGlw>> : "Uses" + PersonalBankingCustomer --> ATM <<Relationship-UmVsYXRpb25zaGlw>> : "Withdraws cash using" + ATM --> MainframeBankingSystem <<Relationship-UmVsYXRpb25zaGlw>> : "Uses" + BackOfficeStaff --> MainframeBankingSystem <<Relationship-UmVsYXRpb25zaGlw>> : "Uses" + + @enduml""", diagram.getDefinition()); + + diagram = diagrams.stream().filter(d -> d.getKey().equals("SystemContext")).findFirst().get(); + assertEquals(""" + @startuml + title <size:24>System Context View: Internet Banking System</size>\\n<size:24>The system context diagram for the Internet Banking System.</size> + + set separator none + top to bottom direction + skinparam ranksep 60 + skinparam nodesep 30 + hide stereotype + + <style> + root { + BackgroundColor: #ffffff; + FontColor: #444444; + } + // Element,Person,Customer + .Element-RWxlbWVudCxQZXJzb24sQ3VzdG9tZXI= { + BackgroundColor: #08427b; + LineColor: #052e56; + LineStyle: 0; + LineThickness: 2; + FontColor: #ffffff; + FontSize: 22; + HorizontalAlignment: center; + Shadowing: 0; + MaximumWidth: 450; + } + // Element,Software System + .Element-RWxlbWVudCxTb2Z0d2FyZSBTeXN0ZW0= { + BackgroundColor: #1168bd; + LineColor: #0b4884; + LineStyle: 0; + LineThickness: 2; + FontColor: #ffffff; + FontSize: 24; + HorizontalAlignment: center; + Shadowing: 0; + MaximumWidth: 450; + } + // Element,Software System,Existing System + .Element-RWxlbWVudCxTb2Z0d2FyZSBTeXN0ZW0sRXhpc3RpbmcgU3lzdGVt { + BackgroundColor: #999999; + LineColor: #6b6b6b; + LineStyle: 0; + LineThickness: 2; + FontColor: #ffffff; + FontSize: 24; + HorizontalAlignment: center; + Shadowing: 0; + MaximumWidth: 450; + } + // Relationship + .Relationship-UmVsYXRpb25zaGlw { + LineThickness: 2; + LineStyle: 10-10; + LineColor: #444444; + FontColor: #444444; + FontSize: 24; + } + // Big Bank plc + .Group-QmlnIEJhbmsgcGxj { + BackgroundColor: #ffffff; + LineColor: #444444; + LineStyle: 2-2; + LineThickness: 2; + FontColor: #444444; + FontSize: 24; + HorizontalAlignment: center; + Shadowing: 0; + } + </style> + + rectangle "Big Bank plc" <<Group-QmlnIEJhbmsgcGxj>> as groupQmlnIEJhbmsgcGxj { + rectangle "==Mainframe Banking System\\n<size:16>[Software System]</size>\\n\\nStores all of the core banking information about customers, accounts, transactions, etc." <<Element-RWxlbWVudCxTb2Z0d2FyZSBTeXN0ZW0sRXhpc3RpbmcgU3lzdGVt>> as MainframeBankingSystem + rectangle "==E-mail System\\n<size:16>[Software System]</size>\\n\\nThe internal Microsoft Exchange e-mail system." <<Element-RWxlbWVudCxTb2Z0d2FyZSBTeXN0ZW0sRXhpc3RpbmcgU3lzdGVt>> as EmailSystem + rectangle "==Internet Banking System\\n<size:16>[Software System]</size>\\n\\nAllows customers to view information about their bank accounts, and make payments." <<Element-RWxlbWVudCxTb2Z0d2FyZSBTeXN0ZW0=>> as InternetBankingSystem + } + + person "==Personal Banking Customer\\n<size:15>[Person]</size>\\n\\nA customer of the bank, with personal bank accounts." <<Element-RWxlbWVudCxQZXJzb24sQ3VzdG9tZXI=>> as PersonalBankingCustomer + + PersonalBankingCustomer --> InternetBankingSystem <<Relationship-UmVsYXRpb25zaGlw>> : "Views account balances, and makes payments using" + InternetBankingSystem --> MainframeBankingSystem <<Relationship-UmVsYXRpb25zaGlw>> : "Gets account information from, and makes payments using" + InternetBankingSystem --> EmailSystem <<Relationship-UmVsYXRpb25zaGlw>> : "Sends e-mail using" + EmailSystem --> PersonalBankingCustomer <<Relationship-UmVsYXRpb25zaGlw>> : "Sends e-mails to" + + @enduml""", diagram.getDefinition()); + + diagram = diagrams.stream().filter(d -> d.getKey().equals("Containers")).findFirst().get(); + assertEquals(""" + @startuml + title <size:24>Container View: Internet Banking System</size>\\n<size:24>The container diagram for the Internet Banking System.</size> + + set separator none + top to bottom direction + skinparam ranksep 60 + skinparam nodesep 30 + hide stereotype + + <style> + root { + BackgroundColor: #ffffff; + FontColor: #444444; + } + // Element,Container + .Element-RWxlbWVudCxDb250YWluZXI= { + BackgroundColor: #438dd5; + LineColor: #2e6295; + LineStyle: 0; + LineThickness: 2; + FontColor: #ffffff; + FontSize: 24; + HorizontalAlignment: center; + Shadowing: 0; + MaximumWidth: 450; + } + // Element,Container,Database + .Element-RWxlbWVudCxDb250YWluZXIsRGF0YWJhc2U= { + BackgroundColor: #438dd5; + LineColor: #2e6295; + LineStyle: 0; + LineThickness: 2; + FontColor: #ffffff; + FontSize: 24; + HorizontalAlignment: center; + Shadowing: 0; + MaximumWidth: 450; + } + // Element,Container,Mobile App + .Element-RWxlbWVudCxDb250YWluZXIsTW9iaWxlIEFwcA== { + BackgroundColor: #438dd5; + LineColor: #2e6295; + LineStyle: 0; + LineThickness: 2; + FontColor: #ffffff; + FontSize: 24; + HorizontalAlignment: center; + Shadowing: 0; + MaximumWidth: 450; + } + // Element,Container,Web Browser + .Element-RWxlbWVudCxDb250YWluZXIsV2ViIEJyb3dzZXI= { + BackgroundColor: #438dd5; + LineColor: #2e6295; + LineStyle: 0; + LineThickness: 2; + FontColor: #ffffff; + FontSize: 24; + HorizontalAlignment: center; + Shadowing: 0; + MaximumWidth: 450; + } + // Element,Person,Customer + .Element-RWxlbWVudCxQZXJzb24sQ3VzdG9tZXI= { + BackgroundColor: #08427b; + LineColor: #052e56; + LineStyle: 0; + LineThickness: 2; + FontColor: #ffffff; + FontSize: 22; + HorizontalAlignment: center; + Shadowing: 0; + MaximumWidth: 450; + } + // Element,Software System,Existing System + .Element-RWxlbWVudCxTb2Z0d2FyZSBTeXN0ZW0sRXhpc3RpbmcgU3lzdGVt { + BackgroundColor: #999999; + LineColor: #6b6b6b; + LineStyle: 0; + LineThickness: 2; + FontColor: #ffffff; + FontSize: 24; + HorizontalAlignment: center; + Shadowing: 0; + MaximumWidth: 450; + } + // Relationship + .Relationship-UmVsYXRpb25zaGlw { + LineThickness: 2; + LineStyle: 10-10; + LineColor: #444444; + FontColor: #444444; + FontSize: 24; + } + // Internet Banking System + .Boundary-SW50ZXJuZXQgQmFua2luZyBTeXN0ZW0= { + BackgroundColor: #ffffff; + LineColor: #0b4884; + LineStyle: 0; + LineThickness: 2; + FontColor: #0b4884; + FontSize: 24; + HorizontalAlignment: center; + Shadowing: 0; + } + </style> + + person "==Personal Banking Customer\\n<size:15>[Person]</size>\\n\\nA customer of the bank, with personal bank accounts." <<Element-RWxlbWVudCxQZXJzb24sQ3VzdG9tZXI=>> as PersonalBankingCustomer + rectangle "==Mainframe Banking System\\n<size:16>[Software System]</size>\\n\\nStores all of the core banking information about customers, accounts, transactions, etc." <<Element-RWxlbWVudCxTb2Z0d2FyZSBTeXN0ZW0sRXhpc3RpbmcgU3lzdGVt>> as MainframeBankingSystem + rectangle "==E-mail System\\n<size:16>[Software System]</size>\\n\\nThe internal Microsoft Exchange e-mail system." <<Element-RWxlbWVudCxTb2Z0d2FyZSBTeXN0ZW0sRXhpc3RpbmcgU3lzdGVt>> as EmailSystem + + rectangle "Internet Banking System\\n<size:16>[Software System]</size>" <<Boundary-SW50ZXJuZXQgQmFua2luZyBTeXN0ZW0=>> { + rectangle "==Web Application\\n<size:16>[Container: Java and Spring MVC]</size>\\n\\nDelivers the static content and the Internet banking single page application." <<Element-RWxlbWVudCxDb250YWluZXI=>> as InternetBankingSystem.WebApplication + rectangle "==API Application\\n<size:16>[Container: Java and Spring MVC]</size>\\n\\nProvides Internet banking functionality via a JSON/HTTPS API." <<Element-RWxlbWVudCxDb250YWluZXI=>> as InternetBankingSystem.APIApplication + database "==Database\\n<size:16>[Container: Oracle Database Schema]</size>\\n\\nStores user registration information, hashed authentication credentials, access logs, etc." <<Element-RWxlbWVudCxDb250YWluZXIsRGF0YWJhc2U=>> as InternetBankingSystem.Database + rectangle "==Single-Page Application\\n<size:16>[Container: JavaScript and Angular]</size>\\n\\nProvides all of the Internet banking functionality to customers via their web browser." <<Element-RWxlbWVudCxDb250YWluZXIsV2ViIEJyb3dzZXI=>> as InternetBankingSystem.SinglePageApplication + rectangle "==Mobile App\\n<size:16>[Container: Xamarin]</size>\\n\\nProvides a limited subset of the Internet banking functionality to customers via their mobile device." <<Element-RWxlbWVudCxDb250YWluZXIsTW9iaWxlIEFwcA==>> as InternetBankingSystem.MobileApp + } + + EmailSystem --> PersonalBankingCustomer <<Relationship-UmVsYXRpb25zaGlw>> : "Sends e-mails to" + PersonalBankingCustomer --> InternetBankingSystem.WebApplication <<Relationship-UmVsYXRpb25zaGlw>> : "Visits bigbank.com/ib using\\n<size:16>[HTTPS]</size>" + PersonalBankingCustomer --> InternetBankingSystem.SinglePageApplication <<Relationship-UmVsYXRpb25zaGlw>> : "Views account balances, and makes payments using" + PersonalBankingCustomer --> InternetBankingSystem.MobileApp <<Relationship-UmVsYXRpb25zaGlw>> : "Views account balances, and makes payments using" + InternetBankingSystem.WebApplication --> InternetBankingSystem.SinglePageApplication <<Relationship-UmVsYXRpb25zaGlw>> : "Delivers to the customer's web browser" + InternetBankingSystem.SinglePageApplication --> InternetBankingSystem.APIApplication <<Relationship-UmVsYXRpb25zaGlw>> : "Makes API calls to\\n<size:16>[JSON/HTTPS]</size>" + InternetBankingSystem.MobileApp --> InternetBankingSystem.APIApplication <<Relationship-UmVsYXRpb25zaGlw>> : "Makes API calls to\\n<size:16>[JSON/HTTPS]</size>" + InternetBankingSystem.APIApplication --> InternetBankingSystem.Database <<Relationship-UmVsYXRpb25zaGlw>> : "Reads from and writes to\\n<size:16>[SQL/TCP]</size>" + InternetBankingSystem.APIApplication --> MainframeBankingSystem <<Relationship-UmVsYXRpb25zaGlw>> : "Makes API calls to\\n<size:16>[XML/HTTPS]</size>" + InternetBankingSystem.APIApplication --> EmailSystem <<Relationship-UmVsYXRpb25zaGlw>> : "Sends e-mail using" + + @enduml""", diagram.getDefinition()); + + diagram = diagrams.stream().filter(d -> d.getKey().equals("Components")).findFirst().get(); + assertEquals(""" + @startuml + title <size:24>Component View: Internet Banking System - API Application</size>\\n<size:24>The component diagram for the API Application.</size> + + set separator none + top to bottom direction + skinparam ranksep 60 + skinparam nodesep 30 + hide stereotype + + <style> + root { + BackgroundColor: #ffffff; + FontColor: #444444; + } + // Element,Component + .Element-RWxlbWVudCxDb21wb25lbnQ= { + BackgroundColor: #85bbf0; + LineColor: #5d82a8; + LineStyle: 0; + LineThickness: 2; + FontColor: #000000; + FontSize: 24; + HorizontalAlignment: center; + Shadowing: 0; + MaximumWidth: 450; + } + // Element,Container,Database + .Element-RWxlbWVudCxDb250YWluZXIsRGF0YWJhc2U= { + BackgroundColor: #438dd5; + LineColor: #2e6295; + LineStyle: 0; + LineThickness: 2; + FontColor: #ffffff; + FontSize: 24; + HorizontalAlignment: center; + Shadowing: 0; + MaximumWidth: 450; + } + // Element,Container,Mobile App + .Element-RWxlbWVudCxDb250YWluZXIsTW9iaWxlIEFwcA== { + BackgroundColor: #438dd5; + LineColor: #2e6295; + LineStyle: 0; + LineThickness: 2; + FontColor: #ffffff; + FontSize: 24; + HorizontalAlignment: center; + Shadowing: 0; + MaximumWidth: 450; + } + // Element,Container,Web Browser + .Element-RWxlbWVudCxDb250YWluZXIsV2ViIEJyb3dzZXI= { + BackgroundColor: #438dd5; + LineColor: #2e6295; + LineStyle: 0; + LineThickness: 2; + FontColor: #ffffff; + FontSize: 24; + HorizontalAlignment: center; + Shadowing: 0; + MaximumWidth: 450; + } + // Element,Software System,Existing System + .Element-RWxlbWVudCxTb2Z0d2FyZSBTeXN0ZW0sRXhpc3RpbmcgU3lzdGVt { + BackgroundColor: #999999; + LineColor: #6b6b6b; + LineStyle: 0; + LineThickness: 2; + FontColor: #ffffff; + FontSize: 24; + HorizontalAlignment: center; + Shadowing: 0; + MaximumWidth: 450; + } + // Relationship + .Relationship-UmVsYXRpb25zaGlw { + LineThickness: 2; + LineStyle: 10-10; + LineColor: #444444; + FontColor: #444444; + FontSize: 24; + } + // API Application + .Boundary-QVBJIEFwcGxpY2F0aW9u { + BackgroundColor: #ffffff; + LineColor: #2e6295; + LineStyle: 0; + LineThickness: 2; + FontColor: #438dd5; + FontSize: 24; + HorizontalAlignment: center; + Shadowing: 0; + } + // Internet Banking System + .Boundary-SW50ZXJuZXQgQmFua2luZyBTeXN0ZW0= { + BackgroundColor: #ffffff; + LineColor: #0b4884; + LineStyle: 0; + LineThickness: 2; + FontColor: #0b4884; + FontSize: 24; + HorizontalAlignment: center; + Shadowing: 0; + } + </style> + + rectangle "==Mainframe Banking System\\n<size:16>[Software System]</size>\\n\\nStores all of the core banking information about customers, accounts, transactions, etc." <<Element-RWxlbWVudCxTb2Z0d2FyZSBTeXN0ZW0sRXhpc3RpbmcgU3lzdGVt>> as MainframeBankingSystem + rectangle "==E-mail System\\n<size:16>[Software System]</size>\\n\\nThe internal Microsoft Exchange e-mail system." <<Element-RWxlbWVudCxTb2Z0d2FyZSBTeXN0ZW0sRXhpc3RpbmcgU3lzdGVt>> as EmailSystem + + rectangle "Internet Banking System\\n<size:16>[Software System]</size>" <<Boundary-SW50ZXJuZXQgQmFua2luZyBTeXN0ZW0=>> { + rectangle "API Application\\n<size:16>[Container: Java and Spring MVC]</size>" <<Boundary-QVBJIEFwcGxpY2F0aW9u>> { + rectangle "==Sign In Controller\\n<size:16>[Component: Spring MVC Rest Controller]</size>\\n\\nAllows users to sign in to the Internet Banking System." <<Element-RWxlbWVudCxDb21wb25lbnQ=>> as InternetBankingSystem.APIApplication.SignInController + rectangle "==Accounts Summary Controller\\n<size:16>[Component: Spring MVC Rest Controller]</size>\\n\\nProvides customers with a summary of their bank accounts." <<Element-RWxlbWVudCxDb21wb25lbnQ=>> as InternetBankingSystem.APIApplication.AccountsSummaryController + rectangle "==Reset Password Controller\\n<size:16>[Component: Spring MVC Rest Controller]</size>\\n\\nAllows users to reset their passwords with a single use URL." <<Element-RWxlbWVudCxDb21wb25lbnQ=>> as InternetBankingSystem.APIApplication.ResetPasswordController + rectangle "==Security Component\\n<size:16>[Component: Spring Bean]</size>\\n\\nProvides functionality related to signing in, changing passwords, etc." <<Element-RWxlbWVudCxDb21wb25lbnQ=>> as InternetBankingSystem.APIApplication.SecurityComponent + rectangle "==Mainframe Banking System Facade\\n<size:16>[Component: Spring Bean]</size>\\n\\nA facade onto the mainframe banking system." <<Element-RWxlbWVudCxDb21wb25lbnQ=>> as InternetBankingSystem.APIApplication.MainframeBankingSystemFacade + rectangle "==E-mail Component\\n<size:16>[Component: Spring Bean]</size>\\n\\nSends e-mails to users." <<Element-RWxlbWVudCxDb21wb25lbnQ=>> as InternetBankingSystem.APIApplication.EmailComponent + } + + database "==Database\\n<size:16>[Container: Oracle Database Schema]</size>\\n\\nStores user registration information, hashed authentication credentials, access logs, etc." <<Element-RWxlbWVudCxDb250YWluZXIsRGF0YWJhc2U=>> as InternetBankingSystem.Database + rectangle "==Single-Page Application\\n<size:16>[Container: JavaScript and Angular]</size>\\n\\nProvides all of the Internet banking functionality to customers via their web browser." <<Element-RWxlbWVudCxDb250YWluZXIsV2ViIEJyb3dzZXI=>> as InternetBankingSystem.SinglePageApplication + rectangle "==Mobile App\\n<size:16>[Container: Xamarin]</size>\\n\\nProvides a limited subset of the Internet banking functionality to customers via their mobile device." <<Element-RWxlbWVudCxDb250YWluZXIsTW9iaWxlIEFwcA==>> as InternetBankingSystem.MobileApp + } + + InternetBankingSystem.SinglePageApplication --> InternetBankingSystem.APIApplication.SignInController <<Relationship-UmVsYXRpb25zaGlw>> : "Makes API calls to\\n<size:16>[JSON/HTTPS]</size>" + InternetBankingSystem.SinglePageApplication --> InternetBankingSystem.APIApplication.AccountsSummaryController <<Relationship-UmVsYXRpb25zaGlw>> : "Makes API calls to\\n<size:16>[JSON/HTTPS]</size>" + InternetBankingSystem.SinglePageApplication --> InternetBankingSystem.APIApplication.ResetPasswordController <<Relationship-UmVsYXRpb25zaGlw>> : "Makes API calls to\\n<size:16>[JSON/HTTPS]</size>" + InternetBankingSystem.MobileApp --> InternetBankingSystem.APIApplication.SignInController <<Relationship-UmVsYXRpb25zaGlw>> : "Makes API calls to\\n<size:16>[JSON/HTTPS]</size>" + InternetBankingSystem.MobileApp --> InternetBankingSystem.APIApplication.AccountsSummaryController <<Relationship-UmVsYXRpb25zaGlw>> : "Makes API calls to\\n<size:16>[JSON/HTTPS]</size>" + InternetBankingSystem.MobileApp --> InternetBankingSystem.APIApplication.ResetPasswordController <<Relationship-UmVsYXRpb25zaGlw>> : "Makes API calls to\\n<size:16>[JSON/HTTPS]</size>" + InternetBankingSystem.APIApplication.SignInController --> InternetBankingSystem.APIApplication.SecurityComponent <<Relationship-UmVsYXRpb25zaGlw>> : "Uses" + InternetBankingSystem.APIApplication.AccountsSummaryController --> InternetBankingSystem.APIApplication.MainframeBankingSystemFacade <<Relationship-UmVsYXRpb25zaGlw>> : "Uses" + InternetBankingSystem.APIApplication.ResetPasswordController --> InternetBankingSystem.APIApplication.SecurityComponent <<Relationship-UmVsYXRpb25zaGlw>> : "Uses" + InternetBankingSystem.APIApplication.ResetPasswordController --> InternetBankingSystem.APIApplication.EmailComponent <<Relationship-UmVsYXRpb25zaGlw>> : "Uses" + InternetBankingSystem.APIApplication.SecurityComponent --> InternetBankingSystem.Database <<Relationship-UmVsYXRpb25zaGlw>> : "Reads from and writes to\\n<size:16>[SQL/TCP]</size>" + InternetBankingSystem.APIApplication.MainframeBankingSystemFacade --> MainframeBankingSystem <<Relationship-UmVsYXRpb25zaGlw>> : "Makes API calls to\\n<size:16>[XML/HTTPS]</size>" + InternetBankingSystem.APIApplication.EmailComponent --> EmailSystem <<Relationship-UmVsYXRpb25zaGlw>> : "Sends e-mail using" + + @enduml""", diagram.getDefinition()); + + diagram = diagrams.stream().filter(d -> d.getKey().equals("SignIn")).findFirst().get(); + assertEquals(""" + @startuml + title <size:24>Dynamic View: Internet Banking System - API Application</size>\\n<size:24>Summarises how the sign in feature works in the single-page application.</size> + + set separator none + top to bottom direction + skinparam ranksep 60 + skinparam nodesep 30 + hide stereotype + + <style> + root { + BackgroundColor: #ffffff; + FontColor: #444444; + } + // Element,Component + .Element-RWxlbWVudCxDb21wb25lbnQ= { + BackgroundColor: #85bbf0; + LineColor: #5d82a8; + LineStyle: 0; + LineThickness: 2; + FontColor: #000000; + FontSize: 24; + HorizontalAlignment: center; + Shadowing: 0; + MaximumWidth: 450; + } + // Element,Container,Database + .Element-RWxlbWVudCxDb250YWluZXIsRGF0YWJhc2U= { + BackgroundColor: #438dd5; + LineColor: #2e6295; + LineStyle: 0; + LineThickness: 2; + FontColor: #ffffff; + FontSize: 24; + HorizontalAlignment: center; + Shadowing: 0; + MaximumWidth: 450; + } + // Element,Container,Web Browser + .Element-RWxlbWVudCxDb250YWluZXIsV2ViIEJyb3dzZXI= { + BackgroundColor: #438dd5; + LineColor: #2e6295; + LineStyle: 0; + LineThickness: 2; + FontColor: #ffffff; + FontSize: 24; + HorizontalAlignment: center; + Shadowing: 0; + MaximumWidth: 450; + } + // Relationship + .Relationship-UmVsYXRpb25zaGlw { + LineThickness: 2; + LineStyle: 10-10; + LineColor: #444444; + FontColor: #444444; + FontSize: 24; + } + // API Application + .Boundary-QVBJIEFwcGxpY2F0aW9u { + BackgroundColor: #ffffff; + LineColor: #2e6295; + LineStyle: 0; + LineThickness: 2; + FontColor: #438dd5; + FontSize: 24; + HorizontalAlignment: center; + Shadowing: 0; + } + // Internet Banking System + .Boundary-SW50ZXJuZXQgQmFua2luZyBTeXN0ZW0= { + BackgroundColor: #ffffff; + LineColor: #0b4884; + LineStyle: 0; + LineThickness: 2; + FontColor: #0b4884; + FontSize: 24; + HorizontalAlignment: center; + Shadowing: 0; + } + </style> + + rectangle "Internet Banking System\\n<size:16>[Software System]</size>" <<Boundary-SW50ZXJuZXQgQmFua2luZyBTeXN0ZW0=>> { + rectangle "API Application\\n<size:16>[Container: Java and Spring MVC]</size>" <<Boundary-QVBJIEFwcGxpY2F0aW9u>> { + rectangle "==Sign In Controller\\n<size:16>[Component: Spring MVC Rest Controller]</size>\\n\\nAllows users to sign in to the Internet Banking System." <<Element-RWxlbWVudCxDb21wb25lbnQ=>> as InternetBankingSystem.APIApplication.SignInController + rectangle "==Security Component\\n<size:16>[Component: Spring Bean]</size>\\n\\nProvides functionality related to signing in, changing passwords, etc." <<Element-RWxlbWVudCxDb21wb25lbnQ=>> as InternetBankingSystem.APIApplication.SecurityComponent + } + + database "==Database\\n<size:16>[Container: Oracle Database Schema]</size>\\n\\nStores user registration information, hashed authentication credentials, access logs, etc." <<Element-RWxlbWVudCxDb250YWluZXIsRGF0YWJhc2U=>> as InternetBankingSystem.Database + rectangle "==Single-Page Application\\n<size:16>[Container: JavaScript and Angular]</size>\\n\\nProvides all of the Internet banking functionality to customers via their web browser." <<Element-RWxlbWVudCxDb250YWluZXIsV2ViIEJyb3dzZXI=>> as InternetBankingSystem.SinglePageApplication + } + + rectangle "==Single-Page Application\\n<size:16>[Container: JavaScript and Angular]</size>\\n\\nProvides all of the Internet banking functionality to customers via their web browser." <<Element-RWxlbWVudCxDb250YWluZXIsV2ViIEJyb3dzZXI=>> as InternetBankingSystem.SinglePageApplication + database "==Database\\n<size:16>[Container: Oracle Database Schema]</size>\\n\\nStores user registration information, hashed authentication credentials, access logs, etc." <<Element-RWxlbWVudCxDb250YWluZXIsRGF0YWJhc2U=>> as InternetBankingSystem.Database + + InternetBankingSystem.SinglePageApplication --> InternetBankingSystem.APIApplication.SignInController <<Relationship-UmVsYXRpb25zaGlw>> : "1: Submits credentials to\\n<size:16>[JSON/HTTPS]</size>" + InternetBankingSystem.APIApplication.SignInController --> InternetBankingSystem.APIApplication.SecurityComponent <<Relationship-UmVsYXRpb25zaGlw>> : "2: Validates credentials using" + InternetBankingSystem.APIApplication.SecurityComponent --> InternetBankingSystem.Database <<Relationship-UmVsYXRpb25zaGlw>> : "3: select * from users where username = ?\\n<size:16>[SQL/TCP]</size>" + InternetBankingSystem.APIApplication.SecurityComponent <-- InternetBankingSystem.Database <<Relationship-UmVsYXRpb25zaGlw>> : "4: Returns user data to\\n<size:16>[SQL/TCP]</size>" + InternetBankingSystem.APIApplication.SignInController <-- InternetBankingSystem.APIApplication.SecurityComponent <<Relationship-UmVsYXRpb25zaGlw>> : "5: Returns true if the hashed password matches" + InternetBankingSystem.SinglePageApplication <-- InternetBankingSystem.APIApplication.SignInController <<Relationship-UmVsYXRpb25zaGlw>> : "6: Sends back an authentication token to\\n<size:16>[JSON/HTTPS]</size>" + + @enduml""", diagram.getDefinition()); + + diagram = diagrams.stream().filter(md -> md.getKey().equals("DevelopmentDeployment")).findFirst().get(); + assertEquals(""" + @startuml + title <size:24>Deployment View: Internet Banking System - Development</size>\\n<size:24>An example development deployment scenario for the Internet Banking System.</size> + + set separator none + top to bottom direction + skinparam ranksep 60 + skinparam nodesep 30 + hide stereotype + + <style> + root { + BackgroundColor: #ffffff; + FontColor: #444444; + } + // Element,Container + .Element-RWxlbWVudCxDb250YWluZXI= { + BackgroundColor: #438dd5; + LineColor: #2e6295; + LineStyle: 0; + LineThickness: 2; + FontColor: #ffffff; + FontSize: 24; + HorizontalAlignment: center; + Shadowing: 0; + MaximumWidth: 450; + } + // Element,Container,Database + .Element-RWxlbWVudCxDb250YWluZXIsRGF0YWJhc2U= { + BackgroundColor: #438dd5; + LineColor: #2e6295; + LineStyle: 0; + LineThickness: 2; + FontColor: #ffffff; + FontSize: 24; + HorizontalAlignment: center; + Shadowing: 0; + MaximumWidth: 450; + } + // Element,Container,Web Browser + .Element-RWxlbWVudCxDb250YWluZXIsV2ViIEJyb3dzZXI= { + BackgroundColor: #438dd5; + LineColor: #2e6295; + LineStyle: 0; + LineThickness: 2; + FontColor: #ffffff; + FontSize: 24; + HorizontalAlignment: center; + Shadowing: 0; + MaximumWidth: 450; + } + // Element,Software System,Existing System + .Element-RWxlbWVudCxTb2Z0d2FyZSBTeXN0ZW0sRXhpc3RpbmcgU3lzdGVt { + BackgroundColor: #999999; + LineColor: #6b6b6b; + LineStyle: 0; + LineThickness: 2; + FontColor: #ffffff; + FontSize: 24; + HorizontalAlignment: center; + Shadowing: 0; + MaximumWidth: 450; + } + // Relationship + .Relationship-UmVsYXRpb25zaGlw { + LineThickness: 2; + LineStyle: 10-10; + LineColor: #444444; + FontColor: #444444; + FontSize: 24; + } + // Element + .DeploymentNode-RWxlbWVudA== { + BackgroundColor: #ffffff; + LineColor: #444444; + LineStyle: 0; + LineThickness: 2; + FontColor: #444444; + FontSize: 24; + HorizontalAlignment: center; + Shadowing: 0; + } + </style> + + rectangle "Developer Laptop\\n<size:16>[Deployment Node: Microsoft Windows 10 or Apple macOS]</size>" <<DeploymentNode-RWxlbWVudA==>> as Development.DeveloperLaptop { + rectangle "Web Browser\\n<size:16>[Deployment Node: Chrome, Firefox, Safari, or Edge]</size>" <<DeploymentNode-RWxlbWVudA==>> as Development.DeveloperLaptop.WebBrowser { + rectangle "==Single-Page Application\\n<size:16>[Container: JavaScript and Angular]</size>\\n\\nProvides all of the Internet banking functionality to customers via their web browser." <<Element-RWxlbWVudCxDb250YWluZXIsV2ViIEJyb3dzZXI=>> as Development.DeveloperLaptop.WebBrowser.SinglePageApplication_1 + } + + rectangle "Docker Container - Web Server\\n<size:16>[Deployment Node: Docker]</size>" <<DeploymentNode-RWxlbWVudA==>> as Development.DeveloperLaptop.DockerContainerWebServer { + rectangle "Apache Tomcat\\n<size:16>[Deployment Node: Apache Tomcat 8.x]</size>" <<DeploymentNode-RWxlbWVudA==>> as Development.DeveloperLaptop.DockerContainerWebServer.ApacheTomcat { + rectangle "==Web Application\\n<size:16>[Container: Java and Spring MVC]</size>\\n\\nDelivers the static content and the Internet banking single page application." <<Element-RWxlbWVudCxDb250YWluZXI=>> as Development.DeveloperLaptop.DockerContainerWebServer.ApacheTomcat.WebApplication_1 + rectangle "==API Application\\n<size:16>[Container: Java and Spring MVC]</size>\\n\\nProvides Internet banking functionality via a JSON/HTTPS API." <<Element-RWxlbWVudCxDb250YWluZXI=>> as Development.DeveloperLaptop.DockerContainerWebServer.ApacheTomcat.APIApplication_1 + } + + } + + rectangle "Docker Container - Database Server\\n<size:16>[Deployment Node: Docker]</size>" <<DeploymentNode-RWxlbWVudA==>> as Development.DeveloperLaptop.DockerContainerDatabaseServer { + rectangle "Database Server\\n<size:16>[Deployment Node: Oracle 12c]</size>" <<DeploymentNode-RWxlbWVudA==>> as Development.DeveloperLaptop.DockerContainerDatabaseServer.DatabaseServer { + database "==Database\\n<size:16>[Container: Oracle Database Schema]</size>\\n\\nStores user registration information, hashed authentication credentials, access logs, etc." <<Element-RWxlbWVudCxDb250YWluZXIsRGF0YWJhc2U=>> as Development.DeveloperLaptop.DockerContainerDatabaseServer.DatabaseServer.Database_1 + } + + } + + } + + rectangle "Big Bank plc\\n<size:16>[Deployment Node: Big Bank plc data center]</size>" <<DeploymentNode-RWxlbWVudA==>> as Development.BigBankplc { + rectangle "bigbank-dev001\\n<size:16>[Deployment Node]</size>" <<DeploymentNode-RWxlbWVudA==>> as Development.BigBankplc.bigbankdev001 { + rectangle "==Mainframe Banking System\\n<size:16>[Software System]</size>\\n\\nStores all of the core banking information about customers, accounts, transactions, etc." <<Element-RWxlbWVudCxTb2Z0d2FyZSBTeXN0ZW0sRXhpc3RpbmcgU3lzdGVt>> as Development.BigBankplc.bigbankdev001.MainframeBankingSystem_1 + } + + } + + Development.DeveloperLaptop.DockerContainerWebServer.ApacheTomcat.WebApplication_1 --> Development.DeveloperLaptop.WebBrowser.SinglePageApplication_1 <<Relationship-UmVsYXRpb25zaGlw>> : "Delivers to the customer's web browser" + Development.DeveloperLaptop.WebBrowser.SinglePageApplication_1 --> Development.DeveloperLaptop.DockerContainerWebServer.ApacheTomcat.APIApplication_1 <<Relationship-UmVsYXRpb25zaGlw>> : "Makes API calls to\\n<size:16>[JSON/HTTPS]</size>" + Development.DeveloperLaptop.DockerContainerWebServer.ApacheTomcat.APIApplication_1 --> Development.DeveloperLaptop.DockerContainerDatabaseServer.DatabaseServer.Database_1 <<Relationship-UmVsYXRpb25zaGlw>> : "Reads from and writes to\\n<size:16>[SQL/TCP]</size>" + Development.DeveloperLaptop.DockerContainerWebServer.ApacheTomcat.APIApplication_1 --> Development.BigBankplc.bigbankdev001.MainframeBankingSystem_1 <<Relationship-UmVsYXRpb25zaGlw>> : "Makes API calls to\\n<size:16>[XML/HTTPS]</size>" + + @enduml""", diagram.getDefinition()); + + diagram = diagrams.stream().filter(md -> md.getKey().equals("LiveDeployment")).findFirst().get(); + assertEquals(""" + @startuml + title <size:24>Deployment View: Internet Banking System - Live</size>\\n<size:24>An example live deployment scenario for the Internet Banking System.</size> + + set separator none + top to bottom direction + skinparam ranksep 60 + skinparam nodesep 30 + hide stereotype + + <style> + root { + BackgroundColor: #ffffff; + FontColor: #444444; + } + // Element,Container + .Element-RWxlbWVudCxDb250YWluZXI= { + BackgroundColor: #438dd5; + LineColor: #2e6295; + LineStyle: 0; + LineThickness: 2; + FontColor: #ffffff; + FontSize: 24; + HorizontalAlignment: center; + Shadowing: 0; + MaximumWidth: 450; + } + // Element,Container,Database + .Element-RWxlbWVudCxDb250YWluZXIsRGF0YWJhc2U= { + BackgroundColor: #438dd5; + LineColor: #2e6295; + LineStyle: 0; + LineThickness: 2; + FontColor: #ffffff; + FontSize: 24; + HorizontalAlignment: center; + Shadowing: 0; + MaximumWidth: 450; + } + // Element,Container,Mobile App + .Element-RWxlbWVudCxDb250YWluZXIsTW9iaWxlIEFwcA== { + BackgroundColor: #438dd5; + LineColor: #2e6295; + LineStyle: 0; + LineThickness: 2; + FontColor: #ffffff; + FontSize: 24; + HorizontalAlignment: center; + Shadowing: 0; + MaximumWidth: 450; + } + // Element,Container,Web Browser + .Element-RWxlbWVudCxDb250YWluZXIsV2ViIEJyb3dzZXI= { + BackgroundColor: #438dd5; + LineColor: #2e6295; + LineStyle: 0; + LineThickness: 2; + FontColor: #ffffff; + FontSize: 24; + HorizontalAlignment: center; + Shadowing: 0; + MaximumWidth: 450; + } + // Element,Software System,Existing System + .Element-RWxlbWVudCxTb2Z0d2FyZSBTeXN0ZW0sRXhpc3RpbmcgU3lzdGVt { + BackgroundColor: #999999; + LineColor: #6b6b6b; + LineStyle: 0; + LineThickness: 2; + FontColor: #ffffff; + FontSize: 24; + HorizontalAlignment: center; + Shadowing: 0; + MaximumWidth: 450; + } + // Relationship + .Relationship-UmVsYXRpb25zaGlw { + LineThickness: 2; + LineStyle: 10-10; + LineColor: #444444; + 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; + } + </style> + + rectangle "Customer's mobile device\\n<size:16>[Deployment Node: Apple iOS or Android]</size>" <<DeploymentNode-RWxlbWVudA==>> as Live.Customersmobiledevice { + rectangle "==Mobile App\\n<size:16>[Container: Xamarin]</size>\\n\\nProvides a limited subset of the Internet banking functionality to customers via their mobile device." <<Element-RWxlbWVudCxDb250YWluZXIsTW9iaWxlIEFwcA==>> as Live.Customersmobiledevice.MobileApp_1 + } + + rectangle "Customer's computer\\n<size:16>[Deployment Node: Microsoft Windows or Apple macOS]</size>" <<DeploymentNode-RWxlbWVudA==>> as Live.Customerscomputer { + rectangle "Web Browser\\n<size:16>[Deployment Node: Chrome, Firefox, Safari, or Edge]</size>" <<DeploymentNode-RWxlbWVudA==>> as Live.Customerscomputer.WebBrowser { + rectangle "==Single-Page Application\\n<size:16>[Container: JavaScript and Angular]</size>\\n\\nProvides all of the Internet banking functionality to customers via their web browser." <<Element-RWxlbWVudCxDb250YWluZXIsV2ViIEJyb3dzZXI=>> as Live.Customerscomputer.WebBrowser.SinglePageApplication_1 + } + + } + + rectangle "Big Bank plc\\n<size:16>[Deployment Node: Big Bank plc data center]</size>" <<DeploymentNode-RWxlbWVudA==>> as Live.BigBankplc { + rectangle "bigbank-web*** (x4)\\n<size:16>[Deployment Node: Ubuntu 16.04 LTS]</size>" <<DeploymentNode-RWxlbWVudA==>> as Live.BigBankplc.bigbankweb { + rectangle "Apache Tomcat\\n<size:16>[Deployment Node: Apache Tomcat 8.x]</size>" <<DeploymentNode-RWxlbWVudA==>> as Live.BigBankplc.bigbankweb.ApacheTomcat { + rectangle "==Web Application\\n<size:16>[Container: Java and Spring MVC]</size>\\n\\nDelivers the static content and the Internet banking single page application." <<Element-RWxlbWVudCxDb250YWluZXI=>> as Live.BigBankplc.bigbankweb.ApacheTomcat.WebApplication_1 + } + + } + + rectangle "bigbank-api*** (x8)\\n<size:16>[Deployment Node: Ubuntu 16.04 LTS]</size>" <<DeploymentNode-RWxlbWVudA==>> as Live.BigBankplc.bigbankapi { + rectangle "Apache Tomcat\\n<size:16>[Deployment Node: Apache Tomcat 8.x]</size>" <<DeploymentNode-RWxlbWVudA==>> as Live.BigBankplc.bigbankapi.ApacheTomcat { + rectangle "==API Application\\n<size:16>[Container: Java and Spring MVC]</size>\\n\\nProvides Internet banking functionality via a JSON/HTTPS API." <<Element-RWxlbWVudCxDb250YWluZXI=>> as Live.BigBankplc.bigbankapi.ApacheTomcat.APIApplication_1 + } + + } + + rectangle "bigbank-db01\\n<size:16>[Deployment Node: Ubuntu 16.04 LTS]</size>" <<DeploymentNode-RWxlbWVudA==>> as Live.BigBankplc.bigbankdb01 { + rectangle "Oracle - Primary\\n<size:16>[Deployment Node: Oracle 12c]</size>" <<DeploymentNode-RWxlbWVudA==>> as Live.BigBankplc.bigbankdb01.OraclePrimary { + database "==Database\\n<size:16>[Container: Oracle Database Schema]</size>\\n\\nStores user registration information, hashed authentication credentials, access logs, etc." <<Element-RWxlbWVudCxDb250YWluZXIsRGF0YWJhc2U=>> as Live.BigBankplc.bigbankdb01.OraclePrimary.Database_1 + } + + } + + rectangle "bigbank-db02\\n<size:16>[Deployment Node: Ubuntu 16.04 LTS]</size>" <<DeploymentNode-RWxlbWVudCxGYWlsb3Zlcg==>> as Live.BigBankplc.bigbankdb02 { + rectangle "Oracle - Secondary\\n<size:16>[Deployment Node: Oracle 12c]</size>" <<DeploymentNode-RWxlbWVudCxGYWlsb3Zlcg==>> as Live.BigBankplc.bigbankdb02.OracleSecondary { + database "==Database\\n<size:16>[Container: Oracle Database Schema]</size>\\n\\nStores user registration information, hashed authentication credentials, access logs, etc." <<Element-RWxlbWVudCxDb250YWluZXIsRGF0YWJhc2U=>> as Live.BigBankplc.bigbankdb02.OracleSecondary.Database_1 + } + + } + + rectangle "bigbank-prod001\\n<size:16>[Deployment Node]</size>" <<DeploymentNode-RWxlbWVudA==>> as Live.BigBankplc.bigbankprod001 { + rectangle "==Mainframe Banking System\\n<size:16>[Software System]</size>\\n\\nStores all of the core banking information about customers, accounts, transactions, etc." <<Element-RWxlbWVudCxTb2Z0d2FyZSBTeXN0ZW0sRXhpc3RpbmcgU3lzdGVt>> as Live.BigBankplc.bigbankprod001.MainframeBankingSystem_1 + } + + } + + Live.BigBankplc.bigbankweb.ApacheTomcat.WebApplication_1 --> Live.Customerscomputer.WebBrowser.SinglePageApplication_1 <<Relationship-UmVsYXRpb25zaGlw>> : "Delivers to the customer's web browser" + Live.Customersmobiledevice.MobileApp_1 --> Live.BigBankplc.bigbankapi.ApacheTomcat.APIApplication_1 <<Relationship-UmVsYXRpb25zaGlw>> : "Makes API calls to\\n<size:16>[JSON/HTTPS]</size>" + Live.Customerscomputer.WebBrowser.SinglePageApplication_1 --> Live.BigBankplc.bigbankapi.ApacheTomcat.APIApplication_1 <<Relationship-UmVsYXRpb25zaGlw>> : "Makes API calls to\\n<size:16>[JSON/HTTPS]</size>" + Live.BigBankplc.bigbankapi.ApacheTomcat.APIApplication_1 --> Live.BigBankplc.bigbankdb01.OraclePrimary.Database_1 <<Relationship-UmVsYXRpb25zaGlw>> : "Reads from and writes to\\n<size:16>[SQL/TCP]</size>" + Live.BigBankplc.bigbankapi.ApacheTomcat.APIApplication_1 --> Live.BigBankplc.bigbankdb02.OracleSecondary.Database_1 <<Relationship-UmVsYXRpb25zaGlw>> : "Reads from and writes to\\n<size:16>[SQL/TCP]</size>" + Live.BigBankplc.bigbankapi.ApacheTomcat.APIApplication_1 --> Live.BigBankplc.bigbankprod001.MainframeBankingSystem_1 <<Relationship-UmVsYXRpb25zaGlw>> : "Makes API calls to\\n<size:16>[XML/HTTPS]</size>" + Live.BigBankplc.bigbankdb01.OraclePrimary --> Live.BigBankplc.bigbankdb02.OracleSecondary <<Relationship-UmVsYXRpb25zaGlw>> : "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 <size:24>Dynamic View: Internet Banking System - API Application</size>\\n<size:24>Summarises how the sign in feature works in the single-page application.</size> + + set separator none + hide stereotype + + <style> + root { + BackgroundColor: #ffffff; + FontColor: #444444; + } + // Element,Component + .Element-RWxlbWVudCxDb21wb25lbnQ= { + BackgroundColor: #85bbf0; + LineColor: #5d82a8; + LineStyle: 0; + LineThickness: 2; + FontColor: #000000; + FontSize: 24; + HorizontalAlignment: center; + Shadowing: 0; + MaximumWidth: 450; + } + // Element,Container,Database + .Element-RWxlbWVudCxDb250YWluZXIsRGF0YWJhc2U= { + BackgroundColor: #438dd5; + LineColor: #2e6295; + LineStyle: 0; + LineThickness: 2; + FontColor: #2e6295; + FontSize: 24; + HorizontalAlignment: center; + Shadowing: 0; + MaximumWidth: 450; + } + // Element,Container,Web Browser + .Element-RWxlbWVudCxDb250YWluZXIsV2ViIEJyb3dzZXI= { + BackgroundColor: #438dd5; + LineColor: #2e6295; + LineStyle: 0; + LineThickness: 2; + FontColor: #ffffff; + FontSize: 24; + HorizontalAlignment: center; + Shadowing: 0; + MaximumWidth: 450; + } + // Relationship + .Relationship-UmVsYXRpb25zaGlw { + LineThickness: 2; + LineStyle: 10-10; + LineColor: #444444; + FontColor: #444444; + FontSize: 24; + } + </style> + + participant "Single-Page Application\\n<size:16>[Container: JavaScript and Angular]</size>" as InternetBankingSystem.SinglePageApplication <<Element-RWxlbWVudCxDb250YWluZXIsV2ViIEJyb3dzZXI=>> + participant "Sign In Controller\\n<size:16>[Component: Spring MVC Rest Controller]</size>" as InternetBankingSystem.APIApplication.SignInController <<Element-RWxlbWVudCxDb21wb25lbnQ=>> + participant "Security Component\\n<size:16>[Component: Spring Bean]</size>" as InternetBankingSystem.APIApplication.SecurityComponent <<Element-RWxlbWVudCxDb21wb25lbnQ=>> + database "Database\\n<size:16>[Container: Oracle Database Schema]</size>" as InternetBankingSystem.Database <<Element-RWxlbWVudCxDb250YWluZXIsRGF0YWJhc2U=>> + + InternetBankingSystem.SinglePageApplication -> InternetBankingSystem.APIApplication.SignInController <<Relationship-UmVsYXRpb25zaGlw>> : 1: Submits credentials to\\n<size:16>[JSON/HTTPS]</size> + InternetBankingSystem.APIApplication.SignInController -> InternetBankingSystem.APIApplication.SecurityComponent <<Relationship-UmVsYXRpb25zaGlw>> : 2: Validates credentials using + InternetBankingSystem.APIApplication.SecurityComponent -> InternetBankingSystem.Database <<Relationship-UmVsYXRpb25zaGlw>> : 3: select * from users where username = ?\\n<size:16>[SQL/TCP]</size> + InternetBankingSystem.APIApplication.SecurityComponent <-- InternetBankingSystem.Database <<Relationship-UmVsYXRpb25zaGlw>> : 4: Returns user data to\\n<size:16>[SQL/TCP]</size> + InternetBankingSystem.APIApplication.SignInController <-- InternetBankingSystem.APIApplication.SecurityComponent <<Relationship-UmVsYXRpb25zaGlw>> : 5: Returns true if the hashed password matches + InternetBankingSystem.SinglePageApplication <-- InternetBankingSystem.APIApplication.SignInController <<Relationship-UmVsYXRpb25zaGlw>> : 6: Sends back an authentication token to\\n<size:16>[JSON/HTTPS]</size> + + @enduml""", diagram.getDefinition()); + } + + @Test + 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 <size:24>System Landscape View</size>\\n<size:24>Description</size> + + set separator none + top to bottom direction + hide stereotype + + <style> + root { + 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; + } + // Relationship + .Relationship-UmVsYXRpb25zaGlw { + LineThickness: 2; + LineStyle: 10-10; + LineColor: #444444; + FontColor: #444444; + FontSize: 24; + } + </style> + + rectangle "==A\\n<size:16>[Software System]</size>\\n\\nDescription." <<Element-RWxlbWVudA==>> as A + rectangle "==B\\n<size:16>[Software System]</size>\\n\\nDescription." <<Element-RWxlbWVudA==>> as B + + A --> B <<Relationship-UmVsYXRpb25zaGlw>> : "Description\\n<size:16>[Technology]</size>" + + @enduml""", diagram.getDefinition()); + + assertEquals(""" + @startuml + + set separator none + hide stereotype + + <style> + root { + BackgroundColor: #ffffff; + FontColor: #444444; + } + // Element + .Element-RWxlbWVudA== { + BackgroundColor: #ffffff; + LineColor: #444444; + LineStyle: 0; + LineThickness: 2; + FontColor: #444444; + 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; + LineColor: transparent; + FontColor: transparent; + } + </style> + + rectangle "==Element" <<Element-RWxlbWVudA==>> + + rectangle "." <<.Element-Transparent>> as 1 + 1 --> 1 <<Relationship-UmVsYXRpb25zaGlw>> : "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 <size:24>System Landscape View</size>\\n<size:24>Description</size> + + set separator none + top to bottom direction + hide stereotype + + <style> + root { + BackgroundColor: #111111; + FontColor: #cccccc; + } + // Element + .Element-RWxlbWVudA== { + BackgroundColor: #111111; + LineColor: #cccccc; + LineStyle: 0; + LineThickness: 2; + FontColor: #cccccc; + FontSize: 24; + HorizontalAlignment: center; + Shadowing: 0; + MaximumWidth: 450; + } + // Relationship + .Relationship-UmVsYXRpb25zaGlw { + LineThickness: 2; + LineStyle: 10-10; + LineColor: #cccccc; + FontColor: #cccccc; + FontSize: 24; + } + </style> + + rectangle "==A\\n<size:16>[Software System]</size>\\n\\nDescription." <<Element-RWxlbWVudA==>> as A + rectangle "==B\\n<size:16>[Software System]</size>\\n\\nDescription." <<Element-RWxlbWVudA==>> as B + + A --> B <<Relationship-UmVsYXRpb25zaGlw>> : "Description\\n<size:16>[Technology]</size>" + + @enduml""", diagram.getDefinition()); + + assertEquals(""" + @startuml + + set separator none + hide stereotype + + <style> + root { + BackgroundColor: #111111; + FontColor: #cccccc; + } + // Element + .Element-RWxlbWVudA== { + BackgroundColor: #111111; + LineColor: #cccccc; + LineStyle: 0; + LineThickness: 2; + FontColor: #cccccc; + 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; + LineColor: transparent; + FontColor: transparent; + } + </style> + + rectangle "==Element" <<Element-RWxlbWVudA==>> + + rectangle "." <<.Element-Transparent>> as 1 + 1 --> 1 <<Relationship-UmVsYXRpb25zaGlw>> : "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 <size:24>System Context View: A</size>\\n<size:24>Description</size> + + set separator none + top to bottom direction + hide stereotype + + <style> + root { + 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; + } + // Relationship + .Relationship-UmVsYXRpb25zaGlw { + LineThickness: 2; + LineStyle: 10-10; + LineColor: #444444; + FontColor: #444444; + FontSize: 24; + } + </style> + + rectangle "==A\\n<size:16>[Software System]</size>\\n\\nDescription." <<Element-RWxlbWVudA==>> as A + rectangle "==B\\n<size:16>[Software System]</size>\\n\\nDescription." <<Element-RWxlbWVudA==>> as B + + A --> B <<Relationship-UmVsYXRpb25zaGlw>> : "Description\\n<size:16>[Technology]</size>" + + @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 <size:24>Container View: Software System A</size>\\n<size:24>Description</size> + + set separator none + top to bottom direction + hide stereotype + + <style> + root { + 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; + } + // Relationship + .Relationship-UmVsYXRpb25zaGlw { + LineThickness: 2; + LineStyle: 10-10; + LineColor: #444444; + FontColor: #444444; + FontSize: 24; + } + // Software System A + .Boundary-U29mdHdhcmUgU3lzdGVtIEE= { + BackgroundColor: #ffffff; + LineColor: #444444; + LineStyle: 0; + LineThickness: 2; + FontColor: #444444; + FontSize: 24; + HorizontalAlignment: center; + Shadowing: 0; + } + </style> + + rectangle "Software System A\\n<size:16>[Software System]</size>" <<Boundary-U29mdHdhcmUgU3lzdGVtIEE=>> { + rectangle "==Container 1\\n<size:16>[Container: Technology]</size>\\n\\nDescription" <<Element-RWxlbWVudA==>> as SoftwareSystemA.Container1 + rectangle "==Container 2\\n<size:16>[Container: Technology]</size>\\n\\nDescription" <<Element-RWxlbWVudA==>> as SoftwareSystemA.Container2 + } + + SoftwareSystemA.Container1 --> SoftwareSystemA.Container2 <<Relationship-UmVsYXRpb25zaGlw>> : "Description\\n<size:16>[Technology]</size>" + + @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 <size:24>Component View: Software System - Container</size>\\n<size:24>Description</size> + + set separator none + top to bottom direction + hide stereotype + + <style> + root { + 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; + } + // Relationship + .Relationship-UmVsYXRpb25zaGlw { + LineThickness: 2; + LineStyle: 10-10; + LineColor: #444444; + FontColor: #444444; + FontSize: 24; + } + // Container + .Boundary-Q29udGFpbmVy { + BackgroundColor: #ffffff; + LineColor: #444444; + LineStyle: 0; + LineThickness: 2; + FontColor: #444444; + FontSize: 24; + HorizontalAlignment: center; + Shadowing: 0; + } + // Software System + .Boundary-U29mdHdhcmUgU3lzdGVt { + BackgroundColor: #ffffff; + LineColor: #444444; + LineStyle: 0; + LineThickness: 2; + FontColor: #444444; + FontSize: 24; + HorizontalAlignment: center; + Shadowing: 0; + } + </style> + + rectangle "Software System\\n<size:16>[Software System]</size>" <<Boundary-U29mdHdhcmUgU3lzdGVt>> { + rectangle "Container\\n<size:16>[Container: Technology]</size>" <<Boundary-Q29udGFpbmVy>> { + rectangle "==Component 1\\n<size:16>[Component: Technology]</size>\\n\\nDescription" <<Element-RWxlbWVudA==>> as SoftwareSystem.Container.Component1 + rectangle "==Component 2\\n<size:16>[Component: Technology]</size>\\n\\nDescription" <<Element-RWxlbWVudA==>> as SoftwareSystem.Container.Component2 + } + + } + + SoftwareSystem.Container.Component1 --> SoftwareSystem.Container.Component2 <<Relationship-UmVsYXRpb25zaGlw>> : "Description\\n<size:16>[Technology]</size>" + + @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 <size:24>Deployment View: Default</size>\\n<size:24>Default</size> + + set separator none + top to bottom direction + hide stereotype + + <style> + root { + 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 + .DeploymentNode-RWxlbWVudA== { + BackgroundColor: #ffffff; + LineColor: #444444; + LineStyle: 0; + LineThickness: 2; + FontColor: #444444; + FontSize: 24; + HorizontalAlignment: center; + Shadowing: 0; + } + </style> + + rectangle "Node 1\\n<size:16>[Deployment Node]</size>" <<DeploymentNode-RWxlbWVudA==>> as Default.Node1 { + rectangle "Node 2\\n<size:16>[Deployment Node]</size>" <<DeploymentNode-RWxlbWVudA==>> as Default.Node1.Node2 { + rectangle "==A\\n<size:16>[Software System]</size>" <<Element-RWxlbWVudA==>> as Default.Node1.Node2.A_1 + } + + rectangle "Node 3\\n<size:16>[Deployment Node]</size>" <<DeploymentNode-RWxlbWVudA==>> as Default.Node1.Node3 { + rectangle "==Infrastructure Node\\n<size:16>[Infrastructure Node]</size>" <<Element-RWxlbWVudA==>> 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); + + assertEquals(""" + @startuml + title <size:24>Dynamic View</size>\\n<size:24>Description</size> + + set separator none + top to bottom direction + hide stereotype + + <style> + root { + 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; + } + // Relationship + .Relationship-UmVsYXRpb25zaGlw { + LineThickness: 2; + LineStyle: 10-10; + LineColor: #444444; + FontColor: #444444; + FontSize: 24; + } + </style> + + rectangle "==A\\n<size:16>[Software System]</size>" <<Element-RWxlbWVudA==>> as A + rectangle "==B\\n<size:16>[Software System]</size>" <<Element-RWxlbWVudA==>> as B + + A --> B <<Relationship-UmVsYXRpb25zaGlw>> : "1: Uses" + + @enduml""", diagram.getDefinition()); + } + + @Test + 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(); + List<Diagram> frames = exporter.export(view).getFrames(); + assertEquals(2, frames.size()); + + assertEquals(""" + @startuml + title <size:24>Dynamic View</size>\\n<size:24>Description</size> + + set separator none + top to bottom direction + hide stereotype + + <style> + root { + 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; + } + // Relationship + .Relationship-UmVsYXRpb25zaGlw { + LineThickness: 2; + LineStyle: 10-10; + LineColor: #444444; + FontColor: #444444; + FontSize: 24; + } + </style> + + rectangle "==A\\n<size:16>[Software System]</size>" <<Element-RWxlbWVudA==>> as A + rectangle "==B\\n<size:16>[Software System]</size>" <<Element-RWxlbWVudA==>> as B + rectangle "==C\\n<size:16>[Software System]</size>" <<Element-RWxlbWVudA==>> as C + hide C + + A --> B <<Relationship-UmVsYXRpb25zaGlw>> : "1: Uses" + B --> C <<Relationship-UmVsYXRpb25zaGlw>> : "2: Uses" + + @enduml""", frames.get(0).getDefinition()); + + assertEquals(""" + @startuml + title <size:24>Dynamic View</size>\\n<size:24>Description</size> + + set separator none + top to bottom direction + hide stereotype + + <style> + root { + 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; + } + // Relationship + .Relationship-UmVsYXRpb25zaGlw { + LineThickness: 2; + LineStyle: 10-10; + LineColor: #444444; + FontColor: #444444; + FontSize: 24; + } + </style> + + rectangle "==A\\n<size:16>[Software System]</size>" <<Element-RWxlbWVudA==>> as A + hide A + rectangle "==B\\n<size:16>[Software System]</size>" <<Element-RWxlbWVudA==>> as B + rectangle "==C\\n<size:16>[Software System]</size>" <<Element-RWxlbWVudA==>> as C + + A --> B <<Relationship-UmVsYXRpb25zaGlw>> : "1: Uses" + B --> C <<Relationship-UmVsYXRpb25zaGlw>> : "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", "JSON/HTTPS"); + + 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 <size:24>Dynamic View</size>\\n<size:24>Description</size> + + set separator none + hide stereotype + + <style> + root { + 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; + } + // Relationship + .Relationship-UmVsYXRpb25zaGlw { + LineThickness: 2; + LineStyle: 10-10; + LineColor: #444444; + FontColor: #444444; + FontSize: 24; + } + </style> + + participant "A\\n<size:16>[Software System]</size>" as A <<Element-RWxlbWVudA==>> + participant "B\\n<size:16>[Software System]</size>" as B <<Element-RWxlbWVudA==>> + + A -> B <<Relationship-UmVsYXRpb25zaGlw>> : 1: Uses\\n<size:16>[JSON/HTTPS]</size> + + @enduml""", diagram.getDefinition()); + + assertEquals(""" + @startuml + + set separator none + hide stereotype + + <style> + root { + BackgroundColor: #ffffff; + FontColor: #444444; + } + // Element + .Element-RWxlbWVudA== { + BackgroundColor: #ffffff; + LineColor: #444444; + LineStyle: 0; + LineThickness: 2; + FontColor: #444444; + 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; + LineColor: transparent; + FontColor: transparent; + } + </style> + + rectangle "==Element" <<Element-RWxlbWVudA==>> + + rectangle "." <<.Element-Transparent>> as 1 + 1 --> 1 <<Relationship-UmVsYXRpb25zaGlw>> : "Relationship" + + @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 <size:24>Dynamic View</size>\\n<size:24>Description</size> + + set separator none + hide stereotype + + <style> + root { + BackgroundColor: #ffffff; + FontColor: #444444; + } + // Element,A + .Element-RWxlbWVudCxB { + BackgroundColor: #ffffff; + LineColor: #444444; + LineStyle: 0; + LineThickness: 2; + FontColor: #444444; + FontSize: 24; + HorizontalAlignment: center; + Shadowing: 0; + MaximumWidth: 450; + } + // Element,B + .Element-RWxlbWVudCxC { + BackgroundColor: #ffffff; + LineColor: #444444; + LineStyle: 0; + LineThickness: 2; + FontColor: #444444; + FontSize: 24; + HorizontalAlignment: center; + Shadowing: 0; + MaximumWidth: 450; + } + // Relationship + .Relationship-UmVsYXRpb25zaGlw { + LineThickness: 2; + LineStyle: 10-10; + LineColor: #444444; + FontColor: #444444; + FontSize: 24; + } + </style> + + actor "A\\n<size:16>[Software System]</size>" as A <<Element-RWxlbWVudCxB>> + database "B\\n<size:16>[Software System]</size>" as B <<Element-RWxlbWVudCxC>> + + A -> B <<Relationship-UmVsYXRpb25zaGlw>> : 1: Uses\\n<size:16>[JSON/HTTPS]</size> + + @enduml""", diagram.getDefinition()); + + assertEquals(""" + @startuml + + set separator none + hide stereotype + + <style> + root { + BackgroundColor: #ffffff; + FontColor: #444444; + } + // Element,A + .Element-RWxlbWVudCxB { + BackgroundColor: #ffffff; + LineColor: #444444; + LineStyle: 0; + LineThickness: 2; + FontColor: #444444; + FontSize: 24; + HorizontalAlignment: center; + Shadowing: 0; + MaximumWidth: 200; + } + // Element,B + .Element-RWxlbWVudCxC { + BackgroundColor: #ffffff; + LineColor: #444444; + LineStyle: 0; + LineThickness: 2; + FontColor: #444444; + 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; + LineColor: transparent; + FontColor: transparent; + } + </style> + + person "==A" <<Element-RWxlbWVudCxB>> + + database "==B" <<Element-RWxlbWVudCxC>> + + rectangle "." <<.Element-Transparent>> as 1 + 1 --> 1 <<Relationship-UmVsYXRpb25zaGlw>> : "Relationship" + + @enduml""", diagram.getLegend().getDefinition()); + } + + @Test + public void groups() throws Exception { + Workspace workspace = WorkspaceUtils.loadWorkspaceFromJson(new File("./src/test/resources/groups.json")); + + 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(); + assertEquals(""" + @startuml + title <size:24>System Landscape View</size> + + set separator none + top to bottom direction + skinparam ranksep 60 + skinparam nodesep 30 + hide stereotype + + <style> + root { + 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; + } + // Relationship + .Relationship-UmVsYXRpb25zaGlw { + LineThickness: 2; + LineStyle: 10-10; + LineColor: #444444; + FontColor: #444444; + FontSize: 24; + } + // Group 1 + .Group-R3JvdXAgMQ== { + BackgroundColor: #ffffff; + LineColor: #444444; + LineStyle: 2-2; + LineThickness: 2; + FontColor: #444444; + FontSize: 24; + HorizontalAlignment: center; + Shadowing: 0; + } + // Group 2 + .Group-R3JvdXAgMg== { + BackgroundColor: #ffffff; + LineColor: #444444; + LineStyle: 2-2; + LineThickness: 2; + FontColor: #444444; + FontSize: 24; + HorizontalAlignment: center; + Shadowing: 0; + } + // Group 2/Group 3 + .Group-R3JvdXAgMi9Hcm91cCAz { + BackgroundColor: #ffffff; + LineColor: #444444; + LineStyle: 2-2; + LineThickness: 2; + FontColor: #444444; + FontSize: 24; + HorizontalAlignment: center; + Shadowing: 0; + } + </style> + + rectangle "Group 1" <<Group-R3JvdXAgMQ==>> as groupR3JvdXAgMQ== { + rectangle "==B\\n<size:16>[Software System]</size>" <<Element-RWxlbWVudA==>> as B + } + + rectangle "Group 2" <<Group-R3JvdXAgMg==>> as groupR3JvdXAgMg== { + rectangle "==C\\n<size:16>[Software System]</size>" <<Element-RWxlbWVudA==>> as C + rectangle "Group 3" <<Group-R3JvdXAgMi9Hcm91cCAz>> as groupR3JvdXAgMi9Hcm91cCAz { + rectangle "==D\\n<size:16>[Software System]</size>" <<Element-RWxlbWVudA==>> as D + } + + } + + rectangle "==A\\n<size:16>[Software System]</size>" <<Element-RWxlbWVudA==>> as A + + B --> C <<Relationship-UmVsYXRpb25zaGlw>> : "" + C --> D <<Relationship-UmVsYXRpb25zaGlw>> : "" + A --> B <<Relationship-UmVsYXRpb25zaGlw>> : "" + + @enduml""", diagram.getDefinition()); + + diagram = diagrams.stream().filter(md -> md.getKey().equals("Containers")).findFirst().get(); + assertEquals(""" + @startuml + title <size:24>Container View: D</size> + + set separator none + top to bottom direction + skinparam ranksep 60 + skinparam nodesep 30 + hide stereotype + + <style> + root { + 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; + } + // Relationship + .Relationship-UmVsYXRpb25zaGlw { + LineThickness: 2; + LineStyle: 10-10; + LineColor: #444444; + FontColor: #444444; + FontSize: 24; + } + // D + .Boundary-RA== { + BackgroundColor: #ffffff; + LineColor: #444444; + LineStyle: 0; + LineThickness: 2; + FontColor: #444444; + FontSize: 24; + HorizontalAlignment: center; + Shadowing: 0; + } + // Group 4 + .Group-R3JvdXAgNA== { + BackgroundColor: #ffffff; + LineColor: #444444; + LineStyle: 2-2; + LineThickness: 2; + FontColor: #444444; + FontSize: 24; + HorizontalAlignment: center; + Shadowing: 0; + } + </style> + + rectangle "==C\\n<size:16>[Software System]</size>" <<Element-RWxlbWVudA==>> as C + + rectangle "D\\n<size:16>[Software System]</size>" <<Boundary-RA==>> { + rectangle "Group 4" <<Group-R3JvdXAgNA==>> as groupR3JvdXAgNA== { + rectangle "==F\\n<size:16>[Container]</size>" <<Element-RWxlbWVudA==>> as D.F + } + + rectangle "==E\\n<size:16>[Container]</size>" <<Element-RWxlbWVudA==>> as D.E + } + + C --> D.E <<Relationship-UmVsYXRpb25zaGlw>> : "" + C --> D.F <<Relationship-UmVsYXRpb25zaGlw>> : "" + + @enduml""", diagram.getDefinition()); + + diagram = diagrams.stream().filter(md -> md.getKey().equals("Components")).findFirst().get(); + assertEquals(""" + @startuml + title <size:24>Component View: D - F</size> + + set separator none + top to bottom direction + skinparam ranksep 60 + skinparam nodesep 30 + hide stereotype + + <style> + root { + 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; + } + // Relationship + .Relationship-UmVsYXRpb25zaGlw { + LineThickness: 2; + LineStyle: 10-10; + LineColor: #444444; + 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; + LineColor: #444444; + LineStyle: 0; + LineThickness: 2; + FontColor: #444444; + FontSize: 24; + HorizontalAlignment: center; + Shadowing: 0; + } + // Group 5 + .Group-R3JvdXAgNQ== { + BackgroundColor: #ffffff; + LineColor: #444444; + LineStyle: 2-2; + LineThickness: 2; + FontColor: #444444; + FontSize: 24; + HorizontalAlignment: center; + Shadowing: 0; + } + </style> + + rectangle "==C\\n<size:16>[Software System]</size>" <<Element-RWxlbWVudA==>> as C + + rectangle "D\\n<size:16>[Software System]</size>" <<Boundary-RA==>> { + rectangle "F\\n<size:16>[Container]</size>" <<Boundary-Rg==>> { + rectangle "Group 5" <<Group-R3JvdXAgNQ==>> as groupR3JvdXAgNQ== { + rectangle "==H\\n<size:16>[Component]</size>" <<Element-RWxlbWVudA==>> as D.F.H + } + + rectangle "==G\\n<size:16>[Component]</size>" <<Element-RWxlbWVudA==>> as D.F.G + } + + } + + C --> D.F.G <<Relationship-UmVsYXRpb25zaGlw>> : "" + C --> D.F.H <<Relationship-UmVsYXRpb25zaGlw>> : "" + + @enduml""", diagram.getDefinition()); + } + + @Test + public void nestedGroups() { + 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"); + 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(); + assertEquals(""" + @startuml + title <size:24>System Landscape View</size> + + set separator none + top to bottom direction + hide stereotype + + <style> + root { + 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; + } + // Organisation 1 + .Group-T3JnYW5pc2F0aW9uIDE= { + BackgroundColor: #ffffff; + LineColor: #444444; + LineStyle: 2-2; + LineThickness: 2; + FontColor: #444444; + FontSize: 24; + HorizontalAlignment: center; + Shadowing: 0; + } + // Organisation 1/Department 1 + .Group-T3JnYW5pc2F0aW9uIDEvRGVwYXJ0bWVudCAx { + BackgroundColor: #ffffff; + LineColor: #444444; + LineStyle: 2-2; + LineThickness: 2; + FontColor: #444444; + FontSize: 24; + HorizontalAlignment: center; + Shadowing: 0; + } + // Organisation 1/Department 1/Team 1 + .Group-T3JnYW5pc2F0aW9uIDEvRGVwYXJ0bWVudCAxL1RlYW0gMQ== { + BackgroundColor: #ffffff; + LineColor: #444444; + LineStyle: 2-2; + LineThickness: 2; + FontColor: #ff0000; + FontSize: 24; + HorizontalAlignment: center; + Shadowing: 0; + } + // Organisation 1/Department 1/Team 2 + .Group-T3JnYW5pc2F0aW9uIDEvRGVwYXJ0bWVudCAxL1RlYW0gMg== { + BackgroundColor: #ffffff; + LineColor: #444444; + LineStyle: 2-2; + LineThickness: 2; + FontColor: #0000ff; + FontSize: 24; + HorizontalAlignment: center; + Shadowing: 0; + } + // Organisation 2 + .Group-T3JnYW5pc2F0aW9uIDI= { + BackgroundColor: #ffffff; + LineColor: #444444; + LineStyle: 2-2; + LineThickness: 2; + FontColor: #444444; + FontSize: 24; + HorizontalAlignment: center; + Shadowing: 0; + } + </style> + + rectangle "Organisation 1" <<Group-T3JnYW5pc2F0aW9uIDE=>> as groupT3JnYW5pc2F0aW9uIDE= { + rectangle "==Organisation 1\\n<size:16>[Software System]</size>" <<Element-RWxlbWVudA==>> as Organisation1 + rectangle "Department 1" <<Group-T3JnYW5pc2F0aW9uIDEvRGVwYXJ0bWVudCAx>> as groupT3JnYW5pc2F0aW9uIDEvRGVwYXJ0bWVudCAx { + rectangle "==Department 1\\n<size:16>[Software System]</size>" <<Element-RWxlbWVudA==>> as Department1 + rectangle "Team 1" <<Group-T3JnYW5pc2F0aW9uIDEvRGVwYXJ0bWVudCAxL1RlYW0gMQ==>> as groupT3JnYW5pc2F0aW9uIDEvRGVwYXJ0bWVudCAxL1RlYW0gMQ== { + rectangle "==Team 1\\n<size:16>[Software System]</size>" <<Element-RWxlbWVudA==>> as Team1 + } + + rectangle "Team 2" <<Group-T3JnYW5pc2F0aW9uIDEvRGVwYXJ0bWVudCAxL1RlYW0gMg==>> as groupT3JnYW5pc2F0aW9uIDEvRGVwYXJ0bWVudCAxL1RlYW0gMg== { + rectangle "==Team 2\\n<size:16>[Software System]</size>" <<Element-RWxlbWVudA==>> as Team2 + } + + } + + } + + rectangle "Organisation 2" <<Group-T3JnYW5pc2F0aW9uIDI=>> as groupT3JnYW5pc2F0aW9uIDI= { + rectangle "==Organisation 2\\n<size:16>[Software System]</size>" <<Element-RWxlbWVudA==>> as Organisation2 + } + + + @enduml""", diagram.getDefinition()); + } + + @Test + 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"); + 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 + title <size:24>Container View: Software System 1</size> + + set separator none + top to bottom direction + hide stereotype + + <style> + root { + 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; + } + // Relationship + .Relationship-UmVsYXRpb25zaGlw { + LineThickness: 2; + LineStyle: 10-10; + LineColor: #444444; + FontColor: #444444; + FontSize: 24; + } + // Software System 1 + .Boundary-U29mdHdhcmUgU3lzdGVtIDE= { + BackgroundColor: #ffffff; + LineColor: #444444; + LineStyle: 0; + LineThickness: 2; + FontColor: #444444; + FontSize: 24; + HorizontalAlignment: center; + Shadowing: 0; + } + // Software System 2 + .Boundary-U29mdHdhcmUgU3lzdGVtIDI= { + BackgroundColor: #ffffff; + LineColor: #444444; + LineStyle: 0; + LineThickness: 2; + FontColor: #444444; + FontSize: 24; + HorizontalAlignment: center; + Shadowing: 0; + } + </style> + + rectangle "Software System 1\\n<size:16>[Software System]</size>" <<Boundary-U29mdHdhcmUgU3lzdGVtIDE=>> { + rectangle "==Container 1\\n<size:16>[Container]</size>" <<Element-RWxlbWVudA==>> as SoftwareSystem1.Container1 + } + + rectangle "Software System 2\\n<size:16>[Software System]</size>" <<Boundary-U29mdHdhcmUgU3lzdGVtIDI=>> { + rectangle "==Container 2\\n<size:16>[Container]</size>" <<Element-RWxlbWVudA==>> as SoftwareSystem2.Container2 + } + + SoftwareSystem1.Container1 --> SoftwareSystem2.Container2 <<Relationship-UmVsYXRpb25zaGlw>> : "Uses" + + @enduml""", diagram.getDefinition()); + } + + @Test + 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"); + Container container2 = softwareSystem1.addContainer("Container 2"); + + component1.uses(component2, "Uses"); + component2.uses(container2, "Uses"); + + ComponentView componentView = workspace.getViews().createComponentView(container1, "Components", ""); + componentView.addDefaultElements(); + + Diagram diagram = new StructurizrPlantUMLExporter().export(componentView); + assertEquals(""" + @startuml + title <size:24>Component View: Software System 1 - Container 1</size> + + set separator none + top to bottom direction + hide stereotype + + <style> + root { + 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; + } + // Relationship + .Relationship-UmVsYXRpb25zaGlw { + LineThickness: 2; + LineStyle: 10-10; + LineColor: #444444; + FontColor: #444444; + FontSize: 24; + } + // Container 1 + .Boundary-Q29udGFpbmVyIDE= { + BackgroundColor: #ffffff; + LineColor: #444444; + LineStyle: 0; + LineThickness: 2; + FontColor: #444444; + FontSize: 24; + HorizontalAlignment: center; + Shadowing: 0; + } + // Software System 1 + .Boundary-U29mdHdhcmUgU3lzdGVtIDE= { + BackgroundColor: #ffffff; + LineColor: #444444; + LineStyle: 0; + LineThickness: 2; + FontColor: #444444; + FontSize: 24; + HorizontalAlignment: center; + Shadowing: 0; + } + </style> + + rectangle "Software System 1\\n<size:16>[Software System]</size>" <<Boundary-U29mdHdhcmUgU3lzdGVtIDE=>> { + rectangle "Container 1\\n<size:16>[Container]</size>" <<Boundary-Q29udGFpbmVyIDE=>> { + rectangle "==Component 1\\n<size:16>[Component]</size>" <<Element-RWxlbWVudA==>> as SoftwareSystem1.Container1.Component1 + rectangle "==Component 2\\n<size:16>[Component]</size>" <<Element-RWxlbWVudA==>> as SoftwareSystem1.Container1.Component2 + } + + rectangle "==Container 2\\n<size:16>[Container]</size>" <<Element-RWxlbWVudA==>> as SoftwareSystem1.Container2 + } + + SoftwareSystem1.Container1.Component1 --> SoftwareSystem1.Container1.Component2 <<Relationship-UmVsYXRpb25zaGlw>> : "Uses" + SoftwareSystem1.Container1.Component2 --> SoftwareSystem1.Container2 <<Relationship-UmVsYXRpb25zaGlw>> : "Uses" + + @enduml""", diagram.getDefinition()); + } + + @Test + public void componentDiagramWithExternalComponents() { + 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 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.add(container4); + + Diagram diagram = new StructurizrPlantUMLExporter().export(componentView); + assertEquals(""" + @startuml + title <size:24>Component View: Software System 1 - Container 1</size> + + set separator none + top to bottom direction + hide stereotype + + <style> + root { + 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; + } + // Relationship + .Relationship-UmVsYXRpb25zaGlw { + LineThickness: 2; + LineStyle: 10-10; + LineColor: #444444; + FontColor: #444444; + FontSize: 24; + } + // Container 1 + .Boundary-Q29udGFpbmVyIDE= { + BackgroundColor: #ffffff; + LineColor: #444444; + LineStyle: 0; + LineThickness: 2; + FontColor: #444444; + FontSize: 24; + HorizontalAlignment: center; + Shadowing: 0; + } + // Container 2 + .Boundary-Q29udGFpbmVyIDI= { + BackgroundColor: #ffffff; + LineColor: #444444; + LineStyle: 0; + LineThickness: 2; + FontColor: #444444; + FontSize: 24; + HorizontalAlignment: center; + Shadowing: 0; + } + // Software System 1 + .Boundary-U29mdHdhcmUgU3lzdGVtIDE= { + BackgroundColor: #ffffff; + LineColor: #444444; + LineStyle: 0; + LineThickness: 2; + FontColor: #444444; + FontSize: 24; + HorizontalAlignment: center; + Shadowing: 0; + } + // Software System 2 + .Boundary-U29mdHdhcmUgU3lzdGVtIDI= { + BackgroundColor: #ffffff; + LineColor: #444444; + LineStyle: 0; + LineThickness: 2; + FontColor: #444444; + FontSize: 24; + HorizontalAlignment: center; + Shadowing: 0; + } + </style> + + rectangle "Software System 1\\n<size:16>[Software System]</size>" <<Boundary-U29mdHdhcmUgU3lzdGVtIDE=>> { + rectangle "Container 1\\n<size:16>[Container]</size>" <<Boundary-Q29udGFpbmVyIDE=>> { + rectangle "==Component 1\\n<size:16>[Component]</size>" <<Element-RWxlbWVudA==>> as SoftwareSystem1.Container1.Component1 + rectangle "==Component 2\\n<size:16>[Component]</size>" <<Element-RWxlbWVudA==>> as SoftwareSystem1.Container1.Component2 + } + + } + + rectangle "Software System 2\\n<size:16>[Software System]</size>" <<Boundary-U29mdHdhcmUgU3lzdGVtIDI=>> { + rectangle "Container 2\\n<size:16>[Container]</size>" <<Boundary-Q29udGFpbmVyIDI=>> { + rectangle "==Component 3\\n<size:16>[Component]</size>" <<Element-RWxlbWVudA==>> as SoftwareSystem2.Container2.Component3 + } + + rectangle "==Container 4\\n<size:16>[Container]</size>" <<Element-RWxlbWVudA==>> as SoftwareSystem2.Container4 + } + + SoftwareSystem1.Container1.Component2 --> SoftwareSystem2.Container2.Component3 <<Relationship-UmVsYXRpb25zaGlw>> : "Uses" + SoftwareSystem2.Container2.Component3 --> SoftwareSystem2.Container4 <<Relationship-UmVsYXRpb25zaGlw>> : "Uses" + SoftwareSystem1.Container1.Component1 --> SoftwareSystem1.Container1.Component2 <<Relationship-UmVsYXRpb25zaGlw>> : "Uses" + + @enduml""", diagram.getDefinition()); + } + + @Test + 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"); + 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 + title <size:24>Dynamic View: Software System 1</size> + + set separator none + top to bottom direction + hide stereotype + + <style> + root { + 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; + } + // Relationship + .Relationship-UmVsYXRpb25zaGlw { + LineThickness: 2; + LineStyle: 10-10; + LineColor: #444444; + FontColor: #444444; + FontSize: 24; + } + // Software System 1 + .Boundary-U29mdHdhcmUgU3lzdGVtIDE= { + BackgroundColor: #ffffff; + LineColor: #444444; + LineStyle: 0; + LineThickness: 2; + FontColor: #444444; + FontSize: 24; + HorizontalAlignment: center; + Shadowing: 0; + } + // Software System 2 + .Boundary-U29mdHdhcmUgU3lzdGVtIDI= { + BackgroundColor: #ffffff; + LineColor: #444444; + LineStyle: 0; + LineThickness: 2; + FontColor: #444444; + FontSize: 24; + HorizontalAlignment: center; + Shadowing: 0; + } + </style> + + rectangle "Software System 1\\n<size:16>[Software System]</size>" <<Boundary-U29mdHdhcmUgU3lzdGVtIDE=>> { + rectangle "==Container 1\\n<size:16>[Container]</size>" <<Element-RWxlbWVudA==>> as SoftwareSystem1.Container1 + } + + rectangle "Software System 2\\n<size:16>[Software System]</size>" <<Boundary-U29mdHdhcmUgU3lzdGVtIDI=>> { + rectangle "==Container 2\\n<size:16>[Container]</size>" <<Element-RWxlbWVudA==>> as SoftwareSystem2.Container2 + } + + SoftwareSystem1.Container1 --> SoftwareSystem2.Container2 <<Relationship-UmVsYXRpb25zaGlw>> : "1: Uses" + + @enduml""", diagram.getDefinition()); + } + + @Test + public void dynamicView_ExternalComponents() { + 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 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); + dynamicView.add(component3, container4); + + Diagram diagram = new StructurizrPlantUMLExporter().export(dynamicView); + assertEquals(""" + @startuml + title <size:24>Dynamic View: Software System 1 - Container 1</size> + + set separator none + top to bottom direction + hide stereotype + + <style> + root { + 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; + } + // Relationship + .Relationship-UmVsYXRpb25zaGlw { + LineThickness: 2; + LineStyle: 10-10; + LineColor: #444444; + FontColor: #444444; + FontSize: 24; + } + // Container 1 + .Boundary-Q29udGFpbmVyIDE= { + BackgroundColor: #ffffff; + LineColor: #444444; + LineStyle: 0; + LineThickness: 2; + FontColor: #444444; + FontSize: 24; + HorizontalAlignment: center; + Shadowing: 0; + } + // Container 2 + .Boundary-Q29udGFpbmVyIDI= { + BackgroundColor: #ffffff; + LineColor: #444444; + LineStyle: 0; + LineThickness: 2; + FontColor: #444444; + FontSize: 24; + HorizontalAlignment: center; + Shadowing: 0; + } + // Software System 1 + .Boundary-U29mdHdhcmUgU3lzdGVtIDE= { + BackgroundColor: #ffffff; + LineColor: #444444; + LineStyle: 0; + LineThickness: 2; + FontColor: #444444; + FontSize: 24; + HorizontalAlignment: center; + Shadowing: 0; + } + // Software System 2 + .Boundary-U29mdHdhcmUgU3lzdGVtIDI= { + BackgroundColor: #ffffff; + LineColor: #444444; + LineStyle: 0; + LineThickness: 2; + FontColor: #444444; + FontSize: 24; + HorizontalAlignment: center; + Shadowing: 0; + } + </style> + + rectangle "Software System 1\\n<size:16>[Software System]</size>" <<Boundary-U29mdHdhcmUgU3lzdGVtIDE=>> { + rectangle "Container 1\\n<size:16>[Container]</size>" <<Boundary-Q29udGFpbmVyIDE=>> { + rectangle "==Component 1\\n<size:16>[Component]</size>" <<Element-RWxlbWVudA==>> as SoftwareSystem1.Container1.Component1 + rectangle "==Component 2\\n<size:16>[Component]</size>" <<Element-RWxlbWVudA==>> as SoftwareSystem1.Container1.Component2 + } + + } + + rectangle "Software System 2\\n<size:16>[Software System]</size>" <<Boundary-U29mdHdhcmUgU3lzdGVtIDI=>> { + rectangle "Container 2\\n<size:16>[Container]</size>" <<Boundary-Q29udGFpbmVyIDI=>> { + rectangle "==Component 3\\n<size:16>[Component]</size>" <<Element-RWxlbWVudA==>> as SoftwareSystem2.Container2.Component3 + } + + rectangle "==Container 4\\n<size:16>[Container]</size>" <<Element-RWxlbWVudA==>> as SoftwareSystem2.Container4 + } + + rectangle "==Container 4\\n<size:16>[Container]</size>" <<Element-RWxlbWVudA==>> as SoftwareSystem2.Container4 + + SoftwareSystem1.Container1.Component1 --> SoftwareSystem1.Container1.Component2 <<Relationship-UmVsYXRpb25zaGlw>> : "1: Uses" + SoftwareSystem1.Container1.Component2 --> SoftwareSystem2.Container2.Component3 <<Relationship-UmVsYXRpb25zaGlw>> : "2: Uses" + SoftwareSystem2.Container2.Component3 --> SoftwareSystem2.Container4 <<Relationship-UmVsYXRpb25zaGlw>> : "3: Uses" + + @enduml""", diagram.getDefinition()); + } + + @Test + 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"); + view.addDefaultElements(); + + Diagram diagram = new StructurizrPlantUMLExporter().export(view); + assertTrue(diagram.getDefinition().contains("as SoftwareSystem [[https://structurizr.com]]")); + } + + @Test + 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); + + DeploymentView view = workspace.getViews().createDeploymentView("deployment", "Default"); + view.add(aInstance); + + assertTrue(new StructurizrPlantUMLExporter().export(view).getDefinition().contains("as Default.Node.A_1 [[https://example.com/url1]]")); + + 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 newLineCharacterInElementName() { + Workspace workspace = new Workspace("Name"); + workspace.getModel().addSoftwareSystem("Software\nSystem"); + + SystemLandscapeView view = workspace.getViews().createSystemLandscapeView("key"); + view.addDefaultElements(); + + Diagram diagram = new StructurizrPlantUMLExporter().export(view); + assertEquals(""" + @startuml + title <size:24>System Landscape View</size> + + set separator none + top to bottom direction + hide stereotype + + <style> + root { + 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; + } + </style> + + rectangle "==Software\\nSystem\\n<size:16>[Software System]</size>" <<Element-RWxlbWVudA==>> as SoftwareSystem + + @enduml""", diagram.getDefinition()); + } + + @Test + 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"); + view.addDefaultElements(); + + Diagram diagram = new StructurizrPlantUMLExporter().export(view); + assertEquals(""" + @startuml + title <size:24>Title</size> + + set separator none + top to bottom direction + hide stereotype + + <style> + root { + 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; + } + // Relationship + .Relationship-UmVsYXRpb25zaGlw { + LineThickness: 2; + LineStyle: 10-10; + LineColor: #444444; + FontColor: #444444; + FontSize: 24; + } + </style> + + rectangle "==A" <<Element-RWxlbWVudA==>> as 1 + rectangle "==B\\n<size:16>[Custom]</size>\\n\\nDescription" <<Element-RWxlbWVudA==>> as 2 + + 1 --> 2 <<Relationship-UmVsYXRpb25zaGlw>> : "Uses" + + @enduml""", diagram.getDefinition()); + } + + @Test + void renderWorkspaceWithUnicodeElementName() { + Workspace workspace = new Workspace("Name"); + workspace.getModel().addPerson("Пользователь"); + workspace.getViews().createSystemLandscapeView("key", "Description").addDefaultElements(); + + String diagramDefinition = new StructurizrPlantUMLExporter().export(workspace).stream().findFirst().get().getDefinition(); + + assertEquals(""" + @startuml + title <size:24>System Landscape View</size>\\n<size:24>Description</size> + + set separator none + top to bottom direction + hide stereotype + + <style> + root { + 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; + } + </style> + + rectangle "==Пользователь\\n<size:16>[Person]</size>" <<Element-RWxlbWVudA==>> as Пользователь + + @enduml""", diagramDefinition); + } + + @Test + public void font() { + Workspace workspace = new Workspace("Name"); + workspace.getModel().addPerson("User"); + SystemLandscapeView view = workspace.getViews().createSystemLandscapeView("key"); + view.addAllElements(); + workspace.getViews().getConfiguration().getBranding().setFont(new Font("Courier")); + + Diagram diagram = new StructurizrPlantUMLExporter().export(view); + assertTrue(diagram.getDefinition().contains(""" + <style> + root { + BackgroundColor: #ffffff; + FontColor: #444444; + FontName: Courier; + }""")); + + assertTrue(diagram.getLegend().getDefinition().contains(""" + <style> + root { + BackgroundColor: #ffffff; + FontColor: #444444; + FontName: Courier; + }""")); + } + + @Test + public void include() { + Workspace workspace = new Workspace("Name"); + workspace.getModel().addSoftwareSystem("A"); + + SystemLandscapeView view = workspace.getViews().createSystemLandscapeView("key"); + view.addDefaultElements(); + + view.getViewSet().getConfiguration().addProperty(StructurizrPlantUMLExporter.PLANTUML_INCLUDES_PROPERTY, "styles.puml"); + + Diagram diagram = new StructurizrPlantUMLExporter().export(view); + System.out.println(diagram.getDefinition()); + assertTrue(diagram.getDefinition().contains("!include styles.puml\n")); + } + + @Test + public void dynamicView_UnscopedWithGroups() { + Workspace workspace = new Workspace("Name"); + 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"); + view.add(softwareSystemA, softwareSystemB); + + StructurizrPlantUMLExporter exporter = new StructurizrPlantUMLExporter(); + Diagram diagram = exporter.export(view); + assertEquals(""" + @startuml + title <size:24>Dynamic View</size> + + set separator none + top to bottom direction + hide stereotype + + <style> + root { + 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; + } + // Relationship + .Relationship-UmVsYXRpb25zaGlw { + LineThickness: 2; + LineStyle: 10-10; + LineColor: #444444; + FontColor: #444444; + FontSize: 24; + } + // Group 1 + .Group-R3JvdXAgMQ== { + BackgroundColor: #ffffff; + LineColor: #444444; + LineStyle: 2-2; + LineThickness: 2; + FontColor: #444444; + FontSize: 24; + HorizontalAlignment: center; + Shadowing: 0; + } + // Group 2 + .Group-R3JvdXAgMg== { + BackgroundColor: #ffffff; + LineColor: #444444; + LineStyle: 2-2; + LineThickness: 2; + FontColor: #444444; + FontSize: 24; + HorizontalAlignment: center; + Shadowing: 0; + } + </style> + + rectangle "Group 1" <<Group-R3JvdXAgMQ==>> as groupR3JvdXAgMQ== { + rectangle "==A\\n<size:16>[Software System]</size>" <<Element-RWxlbWVudA==>> as A + } + + rectangle "Group 2" <<Group-R3JvdXAgMg==>> as groupR3JvdXAgMg== { + rectangle "==B\\n<size:16>[Software System]</size>" <<Element-RWxlbWVudA==>> as B + } + + + A --> B <<Relationship-UmVsYXRpb25zaGlw>> : "1: Uses" + + @enduml""", diagram.getDefinition()); + } + + @Test + public void dynamicView_SoftwareSystemScopedWithGroups() { + Workspace workspace = new Workspace("Name"); + 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"); + view.add(containerA, containerB); + + StructurizrPlantUMLExporter exporter = new StructurizrPlantUMLExporter(); + Diagram diagram = exporter.export(view); + assertEquals(""" + @startuml + title <size:24>Dynamic View: A</size> + + set separator none + top to bottom direction + hide stereotype + + <style> + root { + 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; + } + // Relationship + .Relationship-UmVsYXRpb25zaGlw { + LineThickness: 2; + LineStyle: 10-10; + LineColor: #444444; + FontColor: #444444; + FontSize: 24; + } + // A + .Boundary-QQ== { + BackgroundColor: #ffffff; + LineColor: #444444; + LineStyle: 0; + LineThickness: 2; + FontColor: #444444; + FontSize: 24; + HorizontalAlignment: center; + Shadowing: 0; + } + // B + .Boundary-Qg== { + BackgroundColor: #ffffff; + LineColor: #444444; + LineStyle: 0; + LineThickness: 2; + FontColor: #444444; + FontSize: 24; + HorizontalAlignment: center; + Shadowing: 0; + } + // Group 1 + .Group-R3JvdXAgMQ== { + BackgroundColor: #ffffff; + LineColor: #444444; + LineStyle: 2-2; + LineThickness: 2; + FontColor: #444444; + FontSize: 24; + HorizontalAlignment: center; + Shadowing: 0; + } + // Group 2 + .Group-R3JvdXAgMg== { + BackgroundColor: #ffffff; + LineColor: #444444; + LineStyle: 2-2; + LineThickness: 2; + FontColor: #444444; + FontSize: 24; + HorizontalAlignment: center; + Shadowing: 0; + } + </style> + + rectangle "A\\n<size:16>[Software System]</size>" <<Boundary-QQ==>> { + rectangle "Group 1" <<Group-R3JvdXAgMQ==>> as groupR3JvdXAgMQ== { + rectangle "==A\\n<size:16>[Container]</size>" <<Element-RWxlbWVudA==>> as A.A + } + + } + + rectangle "B\\n<size:16>[Software System]</size>" <<Boundary-Qg==>> { + rectangle "Group 2" <<Group-R3JvdXAgMg==>> as groupR3JvdXAgMg== { + rectangle "==B\\n<size:16>[Container]</size>" <<Element-RWxlbWVudA==>> as B.B + } + + } + + A.A --> B.B <<Relationship-UmVsYXRpb25zaGlw>> : "1: Uses" + + @enduml""", diagram.getDefinition()); + } + + @Test + 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"); + 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"); + view.add(componentA, componentB); + + StructurizrPlantUMLExporter exporter = new StructurizrPlantUMLExporter(); + Diagram diagram = exporter.export(view); + assertEquals(""" + @startuml + title <size:24>Dynamic View: A - A</size> + + set separator none + top to bottom direction + hide stereotype + + <style> + root { + 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; + } + // Relationship + .Relationship-UmVsYXRpb25zaGlw { + LineThickness: 2; + LineStyle: 10-10; + LineColor: #444444; + FontColor: #444444; + FontSize: 24; + } + // A + .Boundary-QQ== { + BackgroundColor: #ffffff; + LineColor: #444444; + LineStyle: 0; + LineThickness: 2; + FontColor: #444444; + FontSize: 24; + HorizontalAlignment: center; + Shadowing: 0; + } + // B + .Boundary-Qg== { + BackgroundColor: #ffffff; + LineColor: #444444; + LineStyle: 0; + LineThickness: 2; + FontColor: #444444; + FontSize: 24; + HorizontalAlignment: center; + Shadowing: 0; + } + // Group 1 + .Group-R3JvdXAgMQ== { + BackgroundColor: #ffffff; + LineColor: #444444; + LineStyle: 2-2; + LineThickness: 2; + FontColor: #444444; + FontSize: 24; + HorizontalAlignment: center; + Shadowing: 0; + } + // Group 2 + .Group-R3JvdXAgMg== { + BackgroundColor: #ffffff; + LineColor: #444444; + LineStyle: 2-2; + LineThickness: 2; + FontColor: #444444; + FontSize: 24; + HorizontalAlignment: center; + Shadowing: 0; + } + </style> + + rectangle "A\\n<size:16>[Software System]</size>" <<Boundary-QQ==>> { + rectangle "A\\n<size:16>[Container]</size>" <<Boundary-QQ==>> { + rectangle "Group 1" <<Group-R3JvdXAgMQ==>> as groupR3JvdXAgMQ== { + rectangle "==A\\n<size:16>[Component]</size>" <<Element-RWxlbWVudA==>> as A.A.A + } + + } + + } + + rectangle "B\\n<size:16>[Software System]</size>" <<Boundary-Qg==>> { + rectangle "B\\n<size:16>[Container]</size>" <<Boundary-Qg==>> { + rectangle "Group 2" <<Group-R3JvdXAgMg==>> as groupR3JvdXAgMg== { + rectangle "==B\\n<size:16>[Component]</size>" <<Element-RWxlbWVudA==>> as B.B.B + } + + } + + } + + A.A.A --> B.B.B <<Relationship-UmVsYXRpb25zaGlw>> : "1: Uses" + + @enduml""", 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"); + view.add(infrastructureNode1); + view.add(infrastructureNode2); + view.add(softwareSystemInstance); + + StructurizrPlantUMLExporter exporter = new StructurizrPlantUMLExporter(); + Diagram diagram = exporter.export(view); + assertEquals(""" + @startuml + title <size:24>Deployment View: Default</size> + + set separator none + top to bottom direction + hide stereotype + + <style> + root { + 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 + .DeploymentNode-RWxlbWVudA== { + BackgroundColor: #ffffff; + LineColor: #444444; + LineStyle: 0; + LineThickness: 2; + FontColor: #444444; + FontSize: 24; + HorizontalAlignment: center; + Shadowing: 0; + } + // Group 1 + .Group-R3JvdXAgMQ== { + BackgroundColor: #ffffff; + LineColor: #444444; + LineStyle: 2-2; + LineThickness: 2; + FontColor: #444444; + FontSize: 24; + HorizontalAlignment: center; + Shadowing: 0; + } + // Group 2 + .Group-R3JvdXAgMg== { + BackgroundColor: #ffffff; + LineColor: #444444; + LineStyle: 2-2; + LineThickness: 2; + FontColor: #444444; + FontSize: 24; + HorizontalAlignment: center; + Shadowing: 0; + } + </style> + + rectangle "Group 1" <<Group-R3JvdXAgMQ==>> as groupR3JvdXAgMQ== { + rectangle "Server 1\\n<size:16>[Deployment Node]</size>" <<DeploymentNode-RWxlbWVudA==>> as Default.Server1 { + rectangle "Group 2" <<Group-R3JvdXAgMg==>> as groupR3JvdXAgMg== { + rectangle "==Infrastructure Node 2\\n<size:16>[Infrastructure Node]</size>" <<Element-RWxlbWVudA==>> as Default.Server1.InfrastructureNode2 + rectangle "==Software System\\n<size:16>[Software System]</size>" <<Element-RWxlbWVudA==>> as Default.Server1.SoftwareSystem_1 + } + + rectangle "==Infrastructure Node 1\\n<size:16>[Infrastructure Node]</size>" <<Element-RWxlbWVudA==>> as Default.Server1.InfrastructureNode1 + } + + } + + @enduml""", diagram.getDefinition()); + } + + @Test + void light_group() { + Workspace workspace = new Workspace("Name"); + SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Name"); + softwareSystem.setGroup("Name"); + + SystemLandscapeView view = workspace.getViews().createSystemLandscapeView("key"); + view.add(softwareSystem); + + StructurizrPlantUMLExporter exporter = new StructurizrPlantUMLExporter(); + Diagram diagram = exporter.export(view); + assertEquals(""" + @startuml + title <size:24>System Landscape View</size> + + set separator none + top to bottom direction + hide stereotype + + <style> + root { + 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; + } + // Name + .Group-TmFtZQ== { + BackgroundColor: #ffffff; + LineColor: #444444; + LineStyle: 2-2; + LineThickness: 2; + FontColor: #444444; + FontSize: 24; + HorizontalAlignment: center; + Shadowing: 0; + } + </style> + + rectangle "Name" <<Group-TmFtZQ==>> as groupTmFtZQ== { + rectangle "==Name\\n<size:16>[Software System]</size>" <<Element-RWxlbWVudA==>> as Name + } + + + @enduml""", diagram.getDefinition()); + } + + @Test + void dark_group() { + Workspace workspace = new Workspace("Name"); + SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Name"); + softwareSystem.setGroup("Name"); + + SystemLandscapeView view = workspace.getViews().createSystemLandscapeView("key"); + view.add(softwareSystem); + + StructurizrPlantUMLExporter exporter = new StructurizrPlantUMLExporter(ColorScheme.Dark); + Diagram diagram = exporter.export(view); + assertEquals(""" + @startuml + title <size:24>System Landscape View</size> + + set separator none + top to bottom direction + hide stereotype + + <style> + root { + BackgroundColor: #111111; + FontColor: #cccccc; + } + // Element + .Element-RWxlbWVudA== { + BackgroundColor: #111111; + LineColor: #cccccc; + LineStyle: 0; + LineThickness: 2; + FontColor: #cccccc; + FontSize: 24; + HorizontalAlignment: center; + Shadowing: 0; + MaximumWidth: 450; + } + // Name + .Group-TmFtZQ== { + BackgroundColor: #111111; + LineColor: #cccccc; + LineStyle: 2-2; + LineThickness: 2; + FontColor: #cccccc; + FontSize: 24; + HorizontalAlignment: center; + Shadowing: 0; + } + </style> + + rectangle "Name" <<Group-TmFtZQ==>> as groupTmFtZQ== { + rectangle "==Name\\n<size:16>[Software System]</size>" <<Element-RWxlbWVudA==>> 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")); + HttpClient httpClient = new HttpClient(); + httpClient.allow(".*"); + ThemeUtils.loadThemes(workspace, httpClient); + + StructurizrPlantUMLExporter exporter = new StructurizrPlantUMLExporter(ColorScheme.Light); + Collection<Diagram> diagrams = exporter.export(workspace); + assertEquals(1, diagrams.size()); + + Diagram diagram = diagrams.stream().findFirst().get(); + assertEquals(""" + @startuml + title <size:24>Deployment View: X - Live</size> + + set separator none + left to right direction + skinparam ranksep 60 + skinparam nodesep 30 + hide stereotype + + <style> + root { + BackgroundColor: #ffffff; + FontColor: #444444; + } + // Element,Amazon Web Services - Elastic Load Balancing + .Element-RWxlbWVudCxBbWF6b24gV2ViIFNlcnZpY2VzIC0gRWxhc3RpYyBMb2FkIEJhbGFuY2luZw== { + BackgroundColor: #ffffff; + LineColor: #693cc5; + LineStyle: 0; + LineThickness: 2; + FontColor: #693cc5; + FontSize: 24; + HorizontalAlignment: center; + Shadowing: 0; + MaximumWidth: 450; + } + // Element,Amazon Web Services - Route 53 + .Element-RWxlbWVudCxBbWF6b24gV2ViIFNlcnZpY2VzIC0gUm91dGUgNTM= { + BackgroundColor: #ffffff; + LineColor: #693cc5; + LineStyle: 0; + LineThickness: 2; + FontColor: #693cc5; + FontSize: 24; + HorizontalAlignment: center; + Shadowing: 0; + MaximumWidth: 450; + } + // Element,Application + .Element-RWxlbWVudCxBcHBsaWNhdGlvbg== { + BackgroundColor: #ffffff; + LineColor: #444444; + LineStyle: 0; + LineThickness: 2; + RoundCorner: 20; + FontColor: #444444; + FontSize: 24; + HorizontalAlignment: center; + Shadowing: 0; + MaximumWidth: 450; + } + // Element,Database + .Element-RWxlbWVudCxEYXRhYmFzZQ== { + BackgroundColor: #ffffff; + LineColor: #444444; + LineStyle: 0; + LineThickness: 2; + FontColor: #444444; + FontSize: 24; + HorizontalAlignment: center; + Shadowing: 0; + MaximumWidth: 450; + } + // 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: #cc2264; + LineStyle: 0; + LineThickness: 2; + FontColor: #cc2264; + FontSize: 24; + HorizontalAlignment: center; + Shadowing: 0; + } + // Element,Amazon Web Services - Cloud + .DeploymentNode-RWxlbWVudCxBbWF6b24gV2ViIFNlcnZpY2VzIC0gQ2xvdWQ= { + BackgroundColor: #ffffff; + LineColor: #232f3e; + LineStyle: 0; + LineThickness: 2; + FontColor: #232f3e; + FontSize: 24; + HorizontalAlignment: center; + Shadowing: 0; + } + // Element,Amazon Web Services - EC2 + .DeploymentNode-RWxlbWVudCxBbWF6b24gV2ViIFNlcnZpY2VzIC0gRUMy { + BackgroundColor: #ffffff; + LineColor: #d86613; + LineStyle: 0; + LineThickness: 2; + FontColor: #d86613; + FontSize: 24; + HorizontalAlignment: center; + Shadowing: 0; + } + // Element,Amazon Web Services - RDS + .DeploymentNode-RWxlbWVudCxBbWF6b24gV2ViIFNlcnZpY2VzIC0gUkRT { + BackgroundColor: #ffffff; + LineColor: #3b48cc; + LineStyle: 0; + LineThickness: 2; + FontColor: #3b48cc; + FontSize: 24; + HorizontalAlignment: center; + Shadowing: 0; + } + // Element,Amazon Web Services - RDS MySQL instance + .DeploymentNode-RWxlbWVudCxBbWF6b24gV2ViIFNlcnZpY2VzIC0gUkRTIE15U1FMIGluc3RhbmNl { + BackgroundColor: #ffffff; + LineColor: #3b48cc; + LineStyle: 0; + LineThickness: 2; + FontColor: #3b48cc; + FontSize: 24; + HorizontalAlignment: center; + Shadowing: 0; + } + // Element,Amazon Web Services - Region + .DeploymentNode-RWxlbWVudCxBbWF6b24gV2ViIFNlcnZpY2VzIC0gUmVnaW9u { + BackgroundColor: #ffffff; + LineColor: #147eba; + LineStyle: 0; + LineThickness: 2; + FontColor: #147eba; + FontSize: 24; + HorizontalAlignment: center; + Shadowing: 0; + } + </style> + + rectangle "Amazon Web Services\\n<size:16>[Deployment Node]</size>\\n\\n<img:https://static.structurizr.com/themes/amazon-web-services-2020.04.30/aws-cloud.png{scale=0.5142857142857142}>" <<DeploymentNode-RWxlbWVudCxBbWF6b24gV2ViIFNlcnZpY2VzIC0gQ2xvdWQ=>> as Live.AmazonWebServices { + rectangle "US-East-1\\n<size:16>[Deployment Node]</size>\\n\\n<img:https://static.structurizr.com/themes/amazon-web-services-2020.04.30/region.png{scale=0.5142857142857142}>" <<DeploymentNode-RWxlbWVudCxBbWF6b24gV2ViIFNlcnZpY2VzIC0gUmVnaW9u>> as Live.AmazonWebServices.USEast1 { + rectangle "Autoscaling group\\n<size:16>[Deployment Node]</size>\\n\\n<img:https://static.structurizr.com/themes/amazon-web-services-2020.04.30/aws-auto-scaling.png{scale=0.24}>" <<DeploymentNode-RWxlbWVudCxBbWF6b24gV2ViIFNlcnZpY2VzIC0gQXV0byBTY2FsaW5n>> as Live.AmazonWebServices.USEast1.Autoscalinggroup { + rectangle "Amazon EC2 - Ubuntu server\\n<size:16>[Deployment Node]</size>\\n\\n<img:https://static.structurizr.com/themes/amazon-web-services-2020.04.30/amazon-ec2.png{scale=0.24}>" <<DeploymentNode-RWxlbWVudCxBbWF6b24gV2ViIFNlcnZpY2VzIC0gRUMy>> as Live.AmazonWebServices.USEast1.Autoscalinggroup.AmazonEC2Ubuntuserver { + rectangle "==Web Application\\n<size:16>[Container: Java and Spring Boot]</size>" <<Element-RWxlbWVudCxBcHBsaWNhdGlvbg==>> as Live.AmazonWebServices.USEast1.Autoscalinggroup.AmazonEC2Ubuntuserver.WebApplication_1 + } + + } + + rectangle "Amazon RDS\\n<size:16>[Deployment Node]</size>\\n\\n<img:https://static.structurizr.com/themes/amazon-web-services-2020.04.30/amazon-rds.png{scale=0.24}>" <<DeploymentNode-RWxlbWVudCxBbWF6b24gV2ViIFNlcnZpY2VzIC0gUkRT>> as Live.AmazonWebServices.USEast1.AmazonRDS { + rectangle "MySQL\\n<size:16>[Deployment Node]</size>\\n\\n<img:https://static.structurizr.com/themes/amazon-web-services-2020.04.30/amazon-rds-mysql-instance.png{scale=0.36}>" <<DeploymentNode-RWxlbWVudCxBbWF6b24gV2ViIFNlcnZpY2VzIC0gUkRTIE15U1FMIGluc3RhbmNl>> as Live.AmazonWebServices.USEast1.AmazonRDS.MySQL { + database "==Database Schema\\n<size:16>[Container]</size>" <<Element-RWxlbWVudCxEYXRhYmFzZQ==>> as Live.AmazonWebServices.USEast1.AmazonRDS.MySQL.DatabaseSchema_1 + } + + } + + rectangle "==DNS router\\n<size:16>[Infrastructure Node: Route 53]</size>\\n\\n<img:https://static.structurizr.com/themes/amazon-web-services-2020.04.30/amazon-route-53.png{scale=0.24}>\\n\\nRoutes incoming requests based upon domain name." <<Element-RWxlbWVudCxBbWF6b24gV2ViIFNlcnZpY2VzIC0gUm91dGUgNTM=>> as Live.AmazonWebServices.USEast1.DNSrouter + rectangle "==Load Balancer\\n<size:16>[Infrastructure Node: Elastic Load Balancer]</size>\\n\\n<img:https://static.structurizr.com/themes/amazon-web-services-2020.04.30/elastic-load-balancing.png{scale=0.24}>\\n\\nAutomatically distributes incoming application traffic." <<Element-RWxlbWVudCxBbWF6b24gV2ViIFNlcnZpY2VzIC0gRWxhc3RpYyBMb2FkIEJhbGFuY2luZw==>> as Live.AmazonWebServices.USEast1.LoadBalancer + } + + } + + Live.AmazonWebServices.USEast1.LoadBalancer --> Live.AmazonWebServices.USEast1.Autoscalinggroup.AmazonEC2Ubuntuserver.WebApplication_1 <<Relationship-UmVsYXRpb25zaGlw>> : "Forwards requests to\\n<size:16>[HTTPS]</size>" + Live.AmazonWebServices.USEast1.Autoscalinggroup.AmazonEC2Ubuntuserver.WebApplication_1 --> Live.AmazonWebServices.USEast1.AmazonRDS.MySQL.DatabaseSchema_1 <<Relationship-UmVsYXRpb25zaGlw>> : "Reads from and writes to\\n<size:16>[MySQL Protocol/SSL]</size>" + Live.AmazonWebServices.USEast1.DNSrouter --> Live.AmazonWebServices.USEast1.LoadBalancer <<Relationship-UmVsYXRpb25zaGlw>> : "Forwards requests to\\n<size:16>[HTTPS]</size>" + + @enduml""", diagram.getDefinition()); + + assertEquals(""" + @startuml + + set separator none + hide stereotype + + <style> + root { + BackgroundColor: #ffffff; + FontColor: #444444; + } + // Element,Amazon Web Services - Elastic Load Balancing + .Element-RWxlbWVudCxBbWF6b24gV2ViIFNlcnZpY2VzIC0gRWxhc3RpYyBMb2FkIEJhbGFuY2luZw== { + BackgroundColor: #ffffff; + LineColor: #693cc5; + LineStyle: 0; + LineThickness: 2; + FontColor: #693cc5; + FontSize: 24; + HorizontalAlignment: center; + Shadowing: 0; + MaximumWidth: 200; + } + // Element,Amazon Web Services - Route 53 + .Element-RWxlbWVudCxBbWF6b24gV2ViIFNlcnZpY2VzIC0gUm91dGUgNTM= { + BackgroundColor: #ffffff; + LineColor: #693cc5; + LineStyle: 0; + LineThickness: 2; + FontColor: #693cc5; + FontSize: 24; + HorizontalAlignment: center; + Shadowing: 0; + MaximumWidth: 200; + } + // Element,Application + .Element-RWxlbWVudCxBcHBsaWNhdGlvbg== { + BackgroundColor: #ffffff; + LineColor: #444444; + LineStyle: 0; + LineThickness: 2; + RoundCorner: 20; + FontColor: #444444; + FontSize: 24; + HorizontalAlignment: center; + Shadowing: 0; + MaximumWidth: 200; + } + // Element,Database + .Element-RWxlbWVudCxEYXRhYmFzZQ== { + BackgroundColor: #ffffff; + LineColor: #444444; + LineStyle: 0; + LineThickness: 2; + FontColor: #444444; + FontSize: 24; + HorizontalAlignment: center; + Shadowing: 0; + MaximumWidth: 200; + } + // 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: #cc2264; + LineStyle: 0; + LineThickness: 2; + FontColor: #cc2264; + FontSize: 24; + HorizontalAlignment: center; + Shadowing: 0; + MaximumWidth: 200; + } + // Element,Amazon Web Services - Cloud + .DeploymentNode-RWxlbWVudCxBbWF6b24gV2ViIFNlcnZpY2VzIC0gQ2xvdWQ= { + BackgroundColor: #ffffff; + LineColor: #232f3e; + LineStyle: 0; + LineThickness: 2; + FontColor: #232f3e; + FontSize: 24; + HorizontalAlignment: center; + Shadowing: 0; + MaximumWidth: 200; + } + // Element,Amazon Web Services - EC2 + .DeploymentNode-RWxlbWVudCxBbWF6b24gV2ViIFNlcnZpY2VzIC0gRUMy { + BackgroundColor: #ffffff; + LineColor: #d86613; + LineStyle: 0; + LineThickness: 2; + FontColor: #d86613; + FontSize: 24; + HorizontalAlignment: center; + Shadowing: 0; + MaximumWidth: 200; + } + // Element,Amazon Web Services - RDS + .DeploymentNode-RWxlbWVudCxBbWF6b24gV2ViIFNlcnZpY2VzIC0gUkRT { + BackgroundColor: #ffffff; + LineColor: #3b48cc; + LineStyle: 0; + LineThickness: 2; + FontColor: #3b48cc; + FontSize: 24; + HorizontalAlignment: center; + Shadowing: 0; + MaximumWidth: 200; + } + // Element,Amazon Web Services - RDS MySQL instance + .DeploymentNode-RWxlbWVudCxBbWF6b24gV2ViIFNlcnZpY2VzIC0gUkRTIE15U1FMIGluc3RhbmNl { + BackgroundColor: #ffffff; + LineColor: #3b48cc; + LineStyle: 0; + LineThickness: 2; + FontColor: #3b48cc; + FontSize: 24; + HorizontalAlignment: center; + Shadowing: 0; + MaximumWidth: 200; + } + // Element,Amazon Web Services - Region + .DeploymentNode-RWxlbWVudCxBbWF6b24gV2ViIFNlcnZpY2VzIC0gUmVnaW9u { + BackgroundColor: #ffffff; + LineColor: #147eba; + LineStyle: 0; + LineThickness: 2; + FontColor: #147eba; + FontSize: 24; + HorizontalAlignment: center; + Shadowing: 0; + MaximumWidth: 200; + } + // transparent element for relationships in legend + .Element-Transparent { + BackgroundColor: transparent; + LineColor: transparent; + FontColor: transparent; + } + </style> + + rectangle "==Amazon Web Services - Auto Scaling\\n\\n<img:https://static.structurizr.com/themes/amazon-web-services-2020.04.30/aws-auto-scaling.png{scale=0.24}>" <<DeploymentNode-RWxlbWVudCxBbWF6b24gV2ViIFNlcnZpY2VzIC0gQXV0byBTY2FsaW5n>> + + rectangle "==Amazon Web Services - Cloud\\n\\n<img:https://static.structurizr.com/themes/amazon-web-services-2020.04.30/aws-cloud.png{scale=0.5142857142857142}>" <<DeploymentNode-RWxlbWVudCxBbWF6b24gV2ViIFNlcnZpY2VzIC0gQ2xvdWQ=>> + + rectangle "==Amazon Web Services - EC2\\n\\n<img:https://static.structurizr.com/themes/amazon-web-services-2020.04.30/amazon-ec2.png{scale=0.24}>" <<DeploymentNode-RWxlbWVudCxBbWF6b24gV2ViIFNlcnZpY2VzIC0gRUMy>> + + rectangle "==Amazon Web Services - RDS\\n\\n<img:https://static.structurizr.com/themes/amazon-web-services-2020.04.30/amazon-rds.png{scale=0.24}>" <<DeploymentNode-RWxlbWVudCxBbWF6b24gV2ViIFNlcnZpY2VzIC0gUkRT>> + + 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.png{scale=0.36}>" <<DeploymentNode-RWxlbWVudCxBbWF6b24gV2ViIFNlcnZpY2VzIC0gUkRTIE15U1FMIGluc3RhbmNl>> + + rectangle "==Amazon Web Services - Region\\n\\n<img:https://static.structurizr.com/themes/amazon-web-services-2020.04.30/region.png{scale=0.5142857142857142}>" <<DeploymentNode-RWxlbWVudCxBbWF6b24gV2ViIFNlcnZpY2VzIC0gUmVnaW9u>> + + rectangle "==Amazon Web Services - Elastic Load Balancing\\n\\n<img:https://static.structurizr.com/themes/amazon-web-services-2020.04.30/elastic-load-balancing.png{scale=0.24}>" <<Element-RWxlbWVudCxBbWF6b24gV2ViIFNlcnZpY2VzIC0gRWxhc3RpYyBMb2FkIEJhbGFuY2luZw==>> + + rectangle "==Amazon Web Services - Route 53\\n\\n<img:https://static.structurizr.com/themes/amazon-web-services-2020.04.30/amazon-route-53.png{scale=0.24}>" <<Element-RWxlbWVudCxBbWF6b24gV2ViIFNlcnZpY2VzIC0gUm91dGUgNTM=>> + + rectangle "==Application" <<Element-RWxlbWVudCxBcHBsaWNhdGlvbg==>> + + database "==Database" <<Element-RWxlbWVudCxEYXRhYmFzZQ==>> + + rectangle "." <<.Element-Transparent>> as 1 + 1 --> 1 <<Relationship-UmVsYXRpb25zaGlw>> : "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")); + HttpClient httpClient = new HttpClient(); + httpClient.allow(".*"); + ThemeUtils.loadThemes(workspace, httpClient); + + StructurizrPlantUMLExporter exporter = new StructurizrPlantUMLExporter(ColorScheme.Dark); + Collection<Diagram> diagrams = exporter.export(workspace); + assertEquals(1, diagrams.size()); + + Diagram diagram = diagrams.stream().findFirst().get(); + assertEquals(""" + @startuml + title <size:24>Deployment View: X - Live</size> + + set separator none + left to right direction + skinparam ranksep 60 + skinparam nodesep 30 + hide stereotype + + <style> + root { + BackgroundColor: #111111; + FontColor: #cccccc; + } + // Element,Amazon Web Services - Elastic Load Balancing + .Element-RWxlbWVudCxBbWF6b24gV2ViIFNlcnZpY2VzIC0gRWxhc3RpYyBMb2FkIEJhbGFuY2luZw== { + BackgroundColor: #111111; + LineColor: #693cc5; + LineStyle: 0; + LineThickness: 2; + FontColor: #693cc5; + FontSize: 24; + HorizontalAlignment: center; + Shadowing: 0; + MaximumWidth: 450; + } + // Element,Amazon Web Services - Route 53 + .Element-RWxlbWVudCxBbWF6b24gV2ViIFNlcnZpY2VzIC0gUm91dGUgNTM= { + BackgroundColor: #111111; + LineColor: #693cc5; + LineStyle: 0; + LineThickness: 2; + FontColor: #693cc5; + FontSize: 24; + HorizontalAlignment: center; + Shadowing: 0; + MaximumWidth: 450; + } + // Element,Application + .Element-RWxlbWVudCxBcHBsaWNhdGlvbg== { + BackgroundColor: #111111; + LineColor: #cccccc; + LineStyle: 0; + LineThickness: 2; + RoundCorner: 20; + FontColor: #cccccc; + FontSize: 24; + HorizontalAlignment: center; + Shadowing: 0; + MaximumWidth: 450; + } + // Element,Database + .Element-RWxlbWVudCxEYXRhYmFzZQ== { + BackgroundColor: #111111; + LineColor: #cccccc; + LineStyle: 0; + LineThickness: 2; + FontColor: #cccccc; + FontSize: 24; + HorizontalAlignment: center; + Shadowing: 0; + MaximumWidth: 450; + } + // 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: #cc2264; + LineStyle: 0; + LineThickness: 2; + FontColor: #cc2264; + FontSize: 24; + HorizontalAlignment: center; + Shadowing: 0; + } + // Element,Amazon Web Services - Cloud + .DeploymentNode-RWxlbWVudCxBbWF6b24gV2ViIFNlcnZpY2VzIC0gQ2xvdWQ= { + BackgroundColor: #111111; + LineColor: #232f3e; + LineStyle: 0; + LineThickness: 2; + FontColor: #232f3e; + FontSize: 24; + HorizontalAlignment: center; + Shadowing: 0; + } + // Element,Amazon Web Services - EC2 + .DeploymentNode-RWxlbWVudCxBbWF6b24gV2ViIFNlcnZpY2VzIC0gRUMy { + BackgroundColor: #111111; + LineColor: #d86613; + LineStyle: 0; + LineThickness: 2; + FontColor: #d86613; + FontSize: 24; + HorizontalAlignment: center; + Shadowing: 0; + } + // Element,Amazon Web Services - RDS + .DeploymentNode-RWxlbWVudCxBbWF6b24gV2ViIFNlcnZpY2VzIC0gUkRT { + BackgroundColor: #111111; + LineColor: #3b48cc; + LineStyle: 0; + LineThickness: 2; + FontColor: #3b48cc; + FontSize: 24; + HorizontalAlignment: center; + Shadowing: 0; + } + // Element,Amazon Web Services - RDS MySQL instance + .DeploymentNode-RWxlbWVudCxBbWF6b24gV2ViIFNlcnZpY2VzIC0gUkRTIE15U1FMIGluc3RhbmNl { + BackgroundColor: #111111; + LineColor: #3b48cc; + LineStyle: 0; + LineThickness: 2; + FontColor: #3b48cc; + FontSize: 24; + HorizontalAlignment: center; + Shadowing: 0; + } + // Element,Amazon Web Services - Region + .DeploymentNode-RWxlbWVudCxBbWF6b24gV2ViIFNlcnZpY2VzIC0gUmVnaW9u { + BackgroundColor: #111111; + LineColor: #147eba; + LineStyle: 0; + LineThickness: 2; + FontColor: #147eba; + FontSize: 24; + HorizontalAlignment: center; + Shadowing: 0; + } + </style> + + rectangle "Amazon Web Services\\n<size:16>[Deployment Node]</size>\\n\\n<img:https://static.structurizr.com/themes/amazon-web-services-2020.04.30/aws-cloud.png{scale=0.5142857142857142}>" <<DeploymentNode-RWxlbWVudCxBbWF6b24gV2ViIFNlcnZpY2VzIC0gQ2xvdWQ=>> as Live.AmazonWebServices { + rectangle "US-East-1\\n<size:16>[Deployment Node]</size>\\n\\n<img:https://static.structurizr.com/themes/amazon-web-services-2020.04.30/region.png{scale=0.5142857142857142}>" <<DeploymentNode-RWxlbWVudCxBbWF6b24gV2ViIFNlcnZpY2VzIC0gUmVnaW9u>> as Live.AmazonWebServices.USEast1 { + rectangle "Autoscaling group\\n<size:16>[Deployment Node]</size>\\n\\n<img:https://static.structurizr.com/themes/amazon-web-services-2020.04.30/aws-auto-scaling.png{scale=0.24}>" <<DeploymentNode-RWxlbWVudCxBbWF6b24gV2ViIFNlcnZpY2VzIC0gQXV0byBTY2FsaW5n>> as Live.AmazonWebServices.USEast1.Autoscalinggroup { + rectangle "Amazon EC2 - Ubuntu server\\n<size:16>[Deployment Node]</size>\\n\\n<img:https://static.structurizr.com/themes/amazon-web-services-2020.04.30/amazon-ec2.png{scale=0.24}>" <<DeploymentNode-RWxlbWVudCxBbWF6b24gV2ViIFNlcnZpY2VzIC0gRUMy>> as Live.AmazonWebServices.USEast1.Autoscalinggroup.AmazonEC2Ubuntuserver { + rectangle "==Web Application\\n<size:16>[Container: Java and Spring Boot]</size>" <<Element-RWxlbWVudCxBcHBsaWNhdGlvbg==>> as Live.AmazonWebServices.USEast1.Autoscalinggroup.AmazonEC2Ubuntuserver.WebApplication_1 + } + + } + + rectangle "Amazon RDS\\n<size:16>[Deployment Node]</size>\\n\\n<img:https://static.structurizr.com/themes/amazon-web-services-2020.04.30/amazon-rds.png{scale=0.24}>" <<DeploymentNode-RWxlbWVudCxBbWF6b24gV2ViIFNlcnZpY2VzIC0gUkRT>> as Live.AmazonWebServices.USEast1.AmazonRDS { + rectangle "MySQL\\n<size:16>[Deployment Node]</size>\\n\\n<img:https://static.structurizr.com/themes/amazon-web-services-2020.04.30/amazon-rds-mysql-instance.png{scale=0.36}>" <<DeploymentNode-RWxlbWVudCxBbWF6b24gV2ViIFNlcnZpY2VzIC0gUkRTIE15U1FMIGluc3RhbmNl>> as Live.AmazonWebServices.USEast1.AmazonRDS.MySQL { + database "==Database Schema\\n<size:16>[Container]</size>" <<Element-RWxlbWVudCxEYXRhYmFzZQ==>> as Live.AmazonWebServices.USEast1.AmazonRDS.MySQL.DatabaseSchema_1 + } + + } + + rectangle "==DNS router\\n<size:16>[Infrastructure Node: Route 53]</size>\\n\\n<img:https://static.structurizr.com/themes/amazon-web-services-2020.04.30/amazon-route-53.png{scale=0.24}>\\n\\nRoutes incoming requests based upon domain name." <<Element-RWxlbWVudCxBbWF6b24gV2ViIFNlcnZpY2VzIC0gUm91dGUgNTM=>> as Live.AmazonWebServices.USEast1.DNSrouter + rectangle "==Load Balancer\\n<size:16>[Infrastructure Node: Elastic Load Balancer]</size>\\n\\n<img:https://static.structurizr.com/themes/amazon-web-services-2020.04.30/elastic-load-balancing.png{scale=0.24}>\\n\\nAutomatically distributes incoming application traffic." <<Element-RWxlbWVudCxBbWF6b24gV2ViIFNlcnZpY2VzIC0gRWxhc3RpYyBMb2FkIEJhbGFuY2luZw==>> as Live.AmazonWebServices.USEast1.LoadBalancer + } + + } + + Live.AmazonWebServices.USEast1.LoadBalancer --> Live.AmazonWebServices.USEast1.Autoscalinggroup.AmazonEC2Ubuntuserver.WebApplication_1 <<Relationship-UmVsYXRpb25zaGlw>> : "Forwards requests to\\n<size:16>[HTTPS]</size>" + Live.AmazonWebServices.USEast1.Autoscalinggroup.AmazonEC2Ubuntuserver.WebApplication_1 --> Live.AmazonWebServices.USEast1.AmazonRDS.MySQL.DatabaseSchema_1 <<Relationship-UmVsYXRpb25zaGlw>> : "Reads from and writes to\\n<size:16>[MySQL Protocol/SSL]</size>" + Live.AmazonWebServices.USEast1.DNSrouter --> Live.AmazonWebServices.USEast1.LoadBalancer <<Relationship-UmVsYXRpb25zaGlw>> : "Forwards requests to\\n<size:16>[HTTPS]</size>" + + @enduml""", diagram.getDefinition()); + + assertEquals(""" + @startuml + + set separator none + hide stereotype + + <style> + root { + BackgroundColor: #111111; + FontColor: #cccccc; + } + // Element,Amazon Web Services - Elastic Load Balancing + .Element-RWxlbWVudCxBbWF6b24gV2ViIFNlcnZpY2VzIC0gRWxhc3RpYyBMb2FkIEJhbGFuY2luZw== { + BackgroundColor: #111111; + LineColor: #693cc5; + LineStyle: 0; + LineThickness: 2; + FontColor: #693cc5; + FontSize: 24; + HorizontalAlignment: center; + Shadowing: 0; + MaximumWidth: 200; + } + // Element,Amazon Web Services - Route 53 + .Element-RWxlbWVudCxBbWF6b24gV2ViIFNlcnZpY2VzIC0gUm91dGUgNTM= { + BackgroundColor: #111111; + LineColor: #693cc5; + LineStyle: 0; + LineThickness: 2; + FontColor: #693cc5; + FontSize: 24; + HorizontalAlignment: center; + Shadowing: 0; + MaximumWidth: 200; + } + // Element,Application + .Element-RWxlbWVudCxBcHBsaWNhdGlvbg== { + BackgroundColor: #111111; + LineColor: #cccccc; + LineStyle: 0; + LineThickness: 2; + RoundCorner: 20; + FontColor: #cccccc; + FontSize: 24; + HorizontalAlignment: center; + Shadowing: 0; + MaximumWidth: 200; + } + // Element,Database + .Element-RWxlbWVudCxEYXRhYmFzZQ== { + BackgroundColor: #111111; + LineColor: #cccccc; + LineStyle: 0; + LineThickness: 2; + FontColor: #cccccc; + FontSize: 24; + HorizontalAlignment: center; + Shadowing: 0; + MaximumWidth: 200; + } + // 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: #cc2264; + LineStyle: 0; + LineThickness: 2; + FontColor: #cc2264; + FontSize: 24; + HorizontalAlignment: center; + Shadowing: 0; + MaximumWidth: 200; + } + // Element,Amazon Web Services - Cloud + .DeploymentNode-RWxlbWVudCxBbWF6b24gV2ViIFNlcnZpY2VzIC0gQ2xvdWQ= { + BackgroundColor: #111111; + LineColor: #232f3e; + LineStyle: 0; + LineThickness: 2; + FontColor: #232f3e; + FontSize: 24; + HorizontalAlignment: center; + Shadowing: 0; + MaximumWidth: 200; + } + // Element,Amazon Web Services - EC2 + .DeploymentNode-RWxlbWVudCxBbWF6b24gV2ViIFNlcnZpY2VzIC0gRUMy { + BackgroundColor: #111111; + LineColor: #d86613; + LineStyle: 0; + LineThickness: 2; + FontColor: #d86613; + FontSize: 24; + HorizontalAlignment: center; + Shadowing: 0; + MaximumWidth: 200; + } + // Element,Amazon Web Services - RDS + .DeploymentNode-RWxlbWVudCxBbWF6b24gV2ViIFNlcnZpY2VzIC0gUkRT { + BackgroundColor: #111111; + LineColor: #3b48cc; + LineStyle: 0; + LineThickness: 2; + FontColor: #3b48cc; + FontSize: 24; + HorizontalAlignment: center; + Shadowing: 0; + MaximumWidth: 200; + } + // Element,Amazon Web Services - RDS MySQL instance + .DeploymentNode-RWxlbWVudCxBbWF6b24gV2ViIFNlcnZpY2VzIC0gUkRTIE15U1FMIGluc3RhbmNl { + BackgroundColor: #111111; + LineColor: #3b48cc; + LineStyle: 0; + LineThickness: 2; + FontColor: #3b48cc; + FontSize: 24; + HorizontalAlignment: center; + Shadowing: 0; + MaximumWidth: 200; + } + // Element,Amazon Web Services - Region + .DeploymentNode-RWxlbWVudCxBbWF6b24gV2ViIFNlcnZpY2VzIC0gUmVnaW9u { + BackgroundColor: #111111; + LineColor: #147eba; + LineStyle: 0; + LineThickness: 2; + FontColor: #147eba; + FontSize: 24; + HorizontalAlignment: center; + Shadowing: 0; + MaximumWidth: 200; + } + // transparent element for relationships in legend + .Element-Transparent { + BackgroundColor: transparent; + LineColor: transparent; + FontColor: transparent; + } + </style> + + rectangle "==Amazon Web Services - Auto Scaling\\n\\n<img:https://static.structurizr.com/themes/amazon-web-services-2020.04.30/aws-auto-scaling.png{scale=0.24}>" <<DeploymentNode-RWxlbWVudCxBbWF6b24gV2ViIFNlcnZpY2VzIC0gQXV0byBTY2FsaW5n>> + + rectangle "==Amazon Web Services - Cloud\\n\\n<img:https://static.structurizr.com/themes/amazon-web-services-2020.04.30/aws-cloud.png{scale=0.5142857142857142}>" <<DeploymentNode-RWxlbWVudCxBbWF6b24gV2ViIFNlcnZpY2VzIC0gQ2xvdWQ=>> + + rectangle "==Amazon Web Services - EC2\\n\\n<img:https://static.structurizr.com/themes/amazon-web-services-2020.04.30/amazon-ec2.png{scale=0.24}>" <<DeploymentNode-RWxlbWVudCxBbWF6b24gV2ViIFNlcnZpY2VzIC0gRUMy>> + + rectangle "==Amazon Web Services - RDS\\n\\n<img:https://static.structurizr.com/themes/amazon-web-services-2020.04.30/amazon-rds.png{scale=0.24}>" <<DeploymentNode-RWxlbWVudCxBbWF6b24gV2ViIFNlcnZpY2VzIC0gUkRT>> + + 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.png{scale=0.36}>" <<DeploymentNode-RWxlbWVudCxBbWF6b24gV2ViIFNlcnZpY2VzIC0gUkRTIE15U1FMIGluc3RhbmNl>> + + rectangle "==Amazon Web Services - Region\\n\\n<img:https://static.structurizr.com/themes/amazon-web-services-2020.04.30/region.png{scale=0.5142857142857142}>" <<DeploymentNode-RWxlbWVudCxBbWF6b24gV2ViIFNlcnZpY2VzIC0gUmVnaW9u>> + + rectangle "==Amazon Web Services - Elastic Load Balancing\\n\\n<img:https://static.structurizr.com/themes/amazon-web-services-2020.04.30/elastic-load-balancing.png{scale=0.24}>" <<Element-RWxlbWVudCxBbWF6b24gV2ViIFNlcnZpY2VzIC0gRWxhc3RpYyBMb2FkIEJhbGFuY2luZw==>> + + rectangle "==Amazon Web Services - Route 53\\n\\n<img:https://static.structurizr.com/themes/amazon-web-services-2020.04.30/amazon-route-53.png{scale=0.24}>" <<Element-RWxlbWVudCxBbWF6b24gV2ViIFNlcnZpY2VzIC0gUm91dGUgNTM=>> + + rectangle "==Application" <<Element-RWxlbWVudCxBcHBsaWNhdGlvbg==>> + + database "==Database" <<Element-RWxlbWVudCxEYXRhYmFzZQ==>> + + rectangle "." <<.Element-Transparent>> as 1 + 1 --> 1 <<Relationship-UmVsYXRpb25zaGlw>> : "Relationship" + + @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 <size:24>System Landscape View</size> + + set separator none + top to bottom direction + hide stereotype + + skinparam { + linetype ortho + } + + <style> + root { + 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; + } + </style> + + rectangle "==A\\n<size:16>[Software System]</size>" <<Element-RWxlbWVudA==>> as A + + @enduml""", 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 new file mode 100644 index 000000000..9e9d6b8a3 --- /dev/null +++ b/structurizr-export/src/test/java/com/structurizr/export/websequencediagrams/WebSequenceDiagramsExporterTests.java @@ -0,0 +1,68 @@ +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/big-bank-plc.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(); + assertEquals(""" + title Dynamic View: Internet Banking System - API Application\\nSummarises how the sign in feature works in the single-page application. + + 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 + """, 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 View\\nDescription + + participant <<Software System>>\\nA as A + participant <<Software System>>\\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/big-bank-plc.json b/structurizr-export/src/test/resources/big-bank-plc.json new file mode 100644 index 000000000..d0162591f --- /dev/null +++ b/structurizr-export/src/test/resources/big-bank-plc.json @@ -0,0 +1,1590 @@ +{ + "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" + }, + "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" + }, + "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" + }, + "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" + }, + "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" + }, + "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" + }, + "sourceId" : "14", + "destinationId" : "15", + "description" : "Uses" + }, { + "id" : "43", + "tags" : "Relationship", + "properties" : { + "structurizr.dsl.identifier" : "aa43b36b-ddc2-4aad-a021-d54f79abd36b" + }, + "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" + }, + "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" + }, + "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" + }, + "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" + }, + "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" + }, + "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" + }, + "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" + }, + "environment" : "Live", + "deploymentGroups" : [ "Default" ], + "instanceId" : 1, + "containerId" : "18" + } ] + } ] + }, { + "id" : "77", + "tags" : "Element,Deployment Node,", + "properties" : { + "structurizr.dsl.identifier" : "291edaeb-88f1-4c8e-9a26-54d0c113b1ae" + }, + "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" + }, + "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" + }, + "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-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 new file mode 100644 index 000000000..9ffc544f4 --- /dev/null +++ b/structurizr-export/src/test/resources/groups.json @@ -0,0 +1,255 @@ +{ + "id" : 0, + "name" : "Name", + "description" : "Description", + "properties" : { + "structurizr.dsl" : "d29ya3NwYWNlIHsKCiAgICBtb2RlbCB7CiAgICAgICAgcHJvcGVydGllcyB7CiAgICAgICAgICAgIHN0cnVjdHVyaXpyLmdyb3VwU2VwYXJhdG9yIC8KICAgICAgICB9CgogICAgICAgIGEgPSBzb2Z0d2FyZVN5c3RlbSAiQSIKCiAgICAgICAgZ3JvdXAgIkdyb3VwIDEiIHsKICAgICAgICAgICAgYiA9IHNvZnR3YXJlU3lzdGVtICJCIgogICAgICAgIH0KCiAgICAgICAgZ3JvdXAgIkdyb3VwIDIiIHsKICAgICAgICAgICAgYyA9IHNvZnR3YXJlU3lzdGVtICJDIgoKICAgICAgICAgICAgZ3JvdXAgIkdyb3VwIDMiIHsKICAgICAgICAgICAgICAgIGQgPSBzb2Z0d2FyZVN5c3RlbSAiRCIgewogICAgICAgICAgICAgICAgICAgIGUgPSBjb250YWluZXIgIkUiCgogICAgICAgICAgICAgICAgICAgIGdyb3VwICJHcm91cCA0IiB7CiAgICAgICAgICAgICAgICAgICAgICAgIGYgPSBjb250YWluZXIgIkYiIHsKICAgICAgICAgICAgICAgICAgICAgICAgICAgIGcgPSBjb21wb25lbnQgIkciCgogICAgICAgICAgICAgICAgICAgICAgICAgICAgZ3JvdXAgIkdyb3VwIDUiIHsKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBoID0gY29tcG9uZW50ICJIIgogICAgICAgICAgICAgICAgICAgICAgICAgICAgfQogICAgICAgICAgICAgICAgICAgICAgICB9CiAgICAgICAgICAgICAgICAgICAgfQogICAgICAgICAgICAgICAgfQogICAgICAgICAgICB9CiAgICAgICAgfQoKICAgICAgICBhIC0+IGIKICAgICAgICBiIC0+IGMKICAgICAgICBjIC0+IGUKICAgICAgICBjIC0+IGcKICAgICAgICBjIC0+IGgKCiAgICB9CgogICAgdmlld3MgewogICAgICAgIHN5c3RlbWxhbmRzY2FwZSAiU3lzdGVtTGFuZHNjYXBlIiB7CiAgICAgICAgICAgIGluY2x1ZGUgKgogICAgICAgICAgICBhdXRvbGF5b3V0CiAgICAgICAgfQoKICAgICAgICBjb250YWluZXIgZCAiQ29udGFpbmVycyIgewogICAgICAgICAgICBpbmNsdWRlICoKICAgICAgICAgICAgYXV0b2xheW91dAogICAgICAgIH0KCiAgICAgICAgY29tcG9uZW50IGYgIkNvbXBvbmVudHMiIHsKICAgICAgICAgICAgaW5jbHVkZSAqCiAgICAgICAgICAgIGF1dG9sYXlvdXQKICAgICAgICB9CiAgICB9Cgp9Cg==" + }, + "configuration" : { }, + "model" : { + "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" : "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" : "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", + "sourceId" : "3", + "destinationId" : "4", + "linkedRelationshipId" : "11" + }, { + "id" : "13", + "tags" : "Relationship", + "properties" : { + "structurizr.dsl.identifier" : "0f23b6e5-9276-4468-b3fb-9381f53c8cd7" + }, + "sourceId" : "3", + "destinationId" : "7" + }, { + "id" : "14", + "sourceId" : "3", + "destinationId" : "6", + "linkedRelationshipId" : "13" + }, { + "id" : "15", + "tags" : "Relationship", + "properties" : { + "structurizr.dsl.identifier" : "b0b99a2e-129b-4f75-81c0-de9c67916229" + }, + "sourceId" : "3", + "destinationId" : "8" + } ], + "group" : "Group 2", + "location" : "Unspecified", + "documentation" : { } + }, { + "id" : "4", + "tags" : "Element,Software System", + "properties" : { + "structurizr.dsl.identifier" : "d" + }, + "name" : "D", + "group" : "Group 2/Group 3", + "location" : "Unspecified", + "containers" : [ { + "id" : "6", + "tags" : "Element,Container", + "properties" : { + "structurizr.dsl.identifier" : "f" + }, + "name" : "F", + "group" : "Group 4", + "components" : [ { + "id" : "8", + "tags" : "Element,Component", + "properties" : { + "structurizr.dsl.identifier" : "h" + }, + "name" : "H", + "group" : "Group 5", + "documentation" : { } + }, { + "id" : "7", + "tags" : "Element,Component", + "properties" : { + "structurizr.dsl.identifier" : "g" + }, + "name" : "G", + "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, + "applied" : false + }, + "enterpriseBoundaryVisible" : true, + "relationships" : [ { + "id" : "12" + }, { + "id" : "9" + }, { + "id" : "10" + } ], + "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 + } ] + } ], + "containerViews" : [ { + "key" : "Containers", + "order" : 2, + "softwareSystemId" : "4", + "automaticLayout" : { + "implementation" : "Graphviz", + "rankDirection" : "TopBottom", + "rankSeparation" : 300, + "nodeSeparation" : 300, + "edgeSeparation" : 0, + "vertices" : false, + "applied" : false + }, + "externalSoftwareSystemBoundariesVisible" : true, + "relationships" : [ { + "id" : "14" + }, { + "id" : "11" + } ], + "elements" : [ { + "id" : "3", + "x" : 0, + "y" : 0 + }, { + "id" : "5", + "x" : 0, + "y" : 0 + }, { + "id" : "6", + "x" : 0, + "y" : 0 + } ] + } ], + "componentViews" : [ { + "key" : "Components", + "order" : 3, + "automaticLayout" : { + "implementation" : "Graphviz", + "rankDirection" : "TopBottom", + "rankSeparation" : 300, + "nodeSeparation" : 300, + "edgeSeparation" : 0, + "vertices" : false, + "applied" : false + }, + "containerId" : "6", + "externalContainerBoundariesVisible" : true, + "relationships" : [ { + "id" : "15" + }, { + "id" : "13" + } ], + "elements" : [ { + "id" : "3", + "x" : 0, + "y" : 0 + }, { + "id" : "7", + "x" : 0, + "y" : 0 + }, { + "id" : "8", + "x" : 0, + "y" : 0 + } ] + } ], + "configuration" : { + "branding" : { }, + "styles" : { }, + "terminology" : { } + } + } +} \ 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..53caa555b --- /dev/null +++ b/structurizr-import/build.gradle @@ -0,0 +1,7 @@ +dependencies { + + api project(':structurizr-client') + +} + +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..16c951e31 --- /dev/null +++ b/structurizr-import/src/main/java/com/structurizr/importer/diagrams/AbstractDiagramImporter.java @@ -0,0 +1,44 @@ +package com.structurizr.importer.diagrams; + +import com.structurizr.http.HttpClient; +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<String, String> 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 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(); + + return + view.getProperties().getOrDefault(name, + views.getConfiguration().getProperties().get(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 new file mode 100644 index 000000000..600f10d21 --- /dev/null +++ b/structurizr-import/src/main/java/com/structurizr/importer/diagrams/image/ImageImporter.java @@ -0,0 +1,64 @@ +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; + +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); + } + + 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 { + RemoteContent remoteContent = httpClient.get(url, false); + + String inline = getViewOrViewSetProperty(view, IMAGE_INLINE_PROPERTY); + if ("true".equals(inline)) { + + 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 (contentType.equals(CONTENT_TYPE_IMAGE_SVG)) { + view.setContent(ImageUtils.getSvgAsDataUri(remoteContent.getContentAsString()), colorScheme); + } else { + view.setContent(ImageUtils.getPngAsDataUri(remoteContent.getContentAsBytes()), colorScheme); + } + + view.setContentType(contentType); + } else { + view.setContent(url, colorScheme); + view.setContentType(remoteContent.getContentType()); + } + + view.setTitle(url.substring(url.lastIndexOf("/")+1)); + } + +} \ No newline at end of file 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..fd06aa1c3 --- /dev/null +++ b/structurizr-import/src/main/java/com/structurizr/importer/diagrams/kroki/KrokiImporter.java @@ -0,0 +1,76 @@ +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; +import com.structurizr.view.ColorScheme; +import com.structurizr.view.ImageView; + +import java.io.File; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; + +public class KrokiImporter extends AbstractDiagramImporter { + + 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 KrokiImporter() { + } + + public KrokiImporter(HttpClient httpClient) { + super(httpClient); + } + + 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, 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"); + } + + 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); + + 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(remoteContent.getContentAsString()), colorScheme); + } else { + view.setContent(ImageUtils.getPngAsDataUri(remoteContent.getContentAsBytes()), colorScheme); + } + } else { + view.setContent(url, colorScheme); + } + 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..553d33d78 --- /dev/null +++ b/structurizr-import/src/main/java/com/structurizr/importer/diagrams/mermaid/MermaidEncoder.java @@ -0,0 +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 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 new file mode 100644 index 000000000..190b2b2d5 --- /dev/null +++ b/structurizr-import/src/main/java/com/structurizr/importer/diagrams/mermaid/MermaidImporter.java @@ -0,0 +1,87 @@ +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; +import com.structurizr.view.ColorScheme; +import com.structurizr.view.ImageView; + +import java.io.File; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; + +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 MermaidImporter() { + } + + public MermaidImporter(HttpClient httpClient) { + super(httpClient); + } + + 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, 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"); + } + + String format = getViewOrViewSetProperty(view, MERMAID_FORMAT_PROPERTY); + if (StringUtils.isNullOrEmpty(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 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); + } else { + url = String.format("%s/svg/%s", mermaidServer, encodedMermaid); + } + + 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(remoteContent.getContentAsString()), colorScheme); + } else { + view.setContent(ImageUtils.getPngAsDataUri(remoteContent.getContentAsBytes()), colorScheme); + } + } else { + view.setContent(url, colorScheme); + } + 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..96088b150 --- /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. + */ +public final 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/plantuml/PlantUMLImporter.java b/structurizr-import/src/main/java/com/structurizr/importer/diagrams/plantuml/PlantUMLImporter.java new file mode 100644 index 000000000..0e52bcad6 --- /dev/null +++ b/structurizr-import/src/main/java/com/structurizr/importer/diagrams/plantuml/PlantUMLImporter.java @@ -0,0 +1,103 @@ +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.view.ColorScheme; +import com.structurizr.view.ImageView; + +import java.io.File; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; + +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"; + + public PlantUMLImporter() { + } + + public PlantUMLImporter(HttpClient httpClient) { + super(httpClient); + } + + 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, 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"); + } + + String format = getViewOrViewSetProperty(view, PLANTUML_FORMAT_PROPERTY); + if (StringUtils.isNullOrEmpty(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 encodedPlantUML = new PlantUMLEncoder().encode(content); + String url = String.format("%s/%s/%s", plantUMLServer, format, encodedPlantUML); + + 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(remoteContent.getContentAsString()), colorScheme); + } else { + view.setContent(ImageUtils.getPngAsDataUri(remoteContent.getContentAsBytes()), colorScheme); + } + } else { + view.setContent(url, colorScheme); + } + view.setContentType(CONTENT_TYPES_BY_FORMAT.get(format)); + + String[] lines = content.split(NEWLINE); + for (String line : lines) { + if (line.startsWith(TITLE_STRING)) { + 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("<size:")) { + title = title.substring(title.indexOf(">") + 1); + + if (title.endsWith("</size>")) { + title = title.substring(0, title.indexOf("</size>")); + } + } + + return title; + } + +} \ No newline at end of file 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 new file mode 100644 index 000000000..817aa306f --- /dev/null +++ b/structurizr-import/src/main/java/com/structurizr/importer/documentation/AdrToolsDecisionImporter.java @@ -0,0 +1,219 @@ +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 class AdrToolsDecisionImporter extends AbstractDecisionImporter { + + 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"; + + /** + * 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; + } + + /** + * Imports Markdown files from the specified path, one per decision. + * + * @param documentable the item that decisions should be associated with + * @param path the path to import decisions 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<String, Decision> decisionsById = new LinkedHashMap<>(); + + File[] markdownFiles = path.listFiles((dir, name) -> name.endsWith(".md")); + if (markdownFiles != null) { + Map<String,Decision> 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 = Files.readString(file.toPath(), characterEncoding); + 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<String,Decision> 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 target = matcher.group(2); + + Decision targetDecision = decisionsByFilename.get(target); + 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..18c6a093e --- /dev/null +++ b/structurizr-import/src/main/java/com/structurizr/importer/documentation/FormatFinder.java @@ -0,0 +1,42 @@ +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<String> MARKDOWN_EXTENSIONS = new HashSet<>(Arrays.asList(".md", ".markdown", ".text")); + private static final Set<String> ASCIIDOC_EXTENSIONS = new HashSet<>(Arrays.asList(".asciidoc", ".adoc", ".asc")); + private static final String DOT = "."; + + public static boolean isMarkdownOrAsciiDoc(File file) { + if (file.getName().contains(DOT)) { + String extension = file.getName().substring(file.getName().lastIndexOf(DOT)); + + return MARKDOWN_EXTENSIONS.contains(extension) || ASCIIDOC_EXTENSIONS.contains(extension); + } + + return false; + } + + 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/Log4brainsDecisionImporter.java b/structurizr-import/src/main/java/com/structurizr/importer/documentation/Log4brainsDecisionImporter.java new file mode 100644 index 000000000..eb029e385 --- /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 decisions should be associated with + * @param path the path to import decisions 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<String, Decision> decisionsById = new LinkedHashMap<>(); + + File[] markdownFiles = path.listFiles((dir, name) -> name.matches("\\d{4}\\d{2}\\d{2}-.+?.md")); + if (markdownFiles != null) { + Map<String,Decision> 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<String,Decision> 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 target = matcher.group(2); + + Decision targetDecision = decisionsByFilename.get(target); + 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/MadrDecisionImporter.java b/structurizr-import/src/main/java/com/structurizr/importer/documentation/MadrDecisionImporter.java new file mode 100644 index 000000000..864642b8f --- /dev/null +++ b/structurizr-import/src/main/java/com/structurizr/importer/documentation/MadrDecisionImporter.java @@ -0,0 +1,206 @@ +package com.structurizr.importer.documentation; + +import com.structurizr.documentation.Decision; +import com.structurizr.documentation.Documentable; +import com.structurizr.documentation.Format; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +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 in MADR format (see https://adr.github.io/madr). + */ +public class MadrDecisionImporter extends AbstractDecisionImporter { + + private static final Log log = LogFactory.getLog(MadrDecisionImporter.class); + + private static final String STATUS_PREFIX = "status: "; + private static final String DEFAULT_STATUS = "accepted"; + + private static final String DATE_PREFIX = "date: "; + private static final String DATE_FORMAT = "yyyy-MM-dd"; + + private static final Pattern LINK_REGEX = Pattern.compile("\\[.*]\\((.*)\\)"); + private static final String FRONT_MATTER_SEPARATOR = "---"; + private static final String DEFAULT_LINK_DESCRIPTION = "Links to"; + + /** + * Imports Markdown files from the specified path, one per decision. + * + * @param documentable the item that decisions should be associated with + * @param path the path to import decisions 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."); + } + + Map<String, Decision> decisionsById = new LinkedHashMap<>(); + + File[] markdownFiles = path.listFiles((dir, name) -> name.matches("\\d{4}-.+?.md")); + if (markdownFiles != null) { + Map<String, Decision> decisionsByFilename = new HashMap<>(); + + Arrays.sort(markdownFiles, Comparator.comparing(File::getName)); + + for (File file : markdownFiles) { + try { + Decision decision = importDecision(file); + documentable.getDocumentation().addDecision(decision); + + decisionsById.put(decision.getId(), decision); + decisionsByFilename.put(file.getName(), decision); + } catch (Exception e) { + throw new DocumentationImportException("Error importing decision from + " + file.getAbsolutePath(), e); + } + } + + for (Decision decision : decisionsById.values()) { + try { + 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) { + log.warn("Error extracting links from decision with ID " + decision.getId()); + } + } + } + } + + protected Decision importDecision(File file) throws Exception { + String id = extractIntegerIDFromFileName(file); + Decision decision = new Decision(id); + + String content = Files.readString(file.toPath(), characterEncoding); + content = content.replace("\r", ""); + + String contentWithoutFrontMatter = content.replaceFirst("(?m)^---\n(^.*\n)*?---\n", ""); + decision.setContent(contentWithoutFrontMatter); + + String[] linesWithFrontMatter = content.split("\\n"); + String[] linesWithoutFrontMatter = contentWithoutFrontMatter.split("\\n"); + + decision.setTitle(extractTitle(linesWithoutFrontMatter)); + + decision.setDate(extractDate(file)); + Date dateFromFrontMatter = extractDate(linesWithFrontMatter); + if (dateFromFrontMatter != null) { + decision.setDate(dateFromFrontMatter); + } + + decision.setStatus(extractStatus(linesWithFrontMatter)); + 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_TITLE} + for (String line : lines) { + if (line.startsWith("# ")) { + return line.substring(2); + } + } + + return "Title"; + } + + protected Date extractDate(File file) throws Exception { + return new Date(file.lastModified()); + } + + protected Date extractDate(String[] lines) throws Exception { + // the date is on a line of its own, in the front matter, in the format: + // date: {YYYY-MM-DD} + if (lines[0].startsWith(FRONT_MATTER_SEPARATOR)) { + for (int i = 1; i < lines.length; i++) { + String line = lines[i]; + + if (line.startsWith(FRONT_MATTER_SEPARATOR)) { + // we've hit the end of the front matter + return null; + } else if (line.startsWith(DATE_PREFIX)) { + SimpleDateFormat sdf = new SimpleDateFormat(DATE_FORMAT); + sdf.setTimeZone(timeZone); + + return sdf.parse(line.substring(DATE_PREFIX.length())); + } + } + } + + return null; + } + + protected String extractStatus(String[] lines) { + // the status is on a line of its own, in the front matter, in the format: + // status: {DECISION_STATUS} + if (lines[0].startsWith(FRONT_MATTER_SEPARATOR)) { + for (int i = 1; i < lines.length; i++) { + String line = lines[i]; + + if (line.startsWith(FRONT_MATTER_SEPARATOR)) { + // we've hit the end of the front matter + return DEFAULT_STATUS; + } else if (line.startsWith(STATUS_PREFIX)) { + return line.substring(STATUS_PREFIX.length()); + } + } + } + + return DEFAULT_STATUS; + } + + protected void extractLinks(Decision decision, Map<String,Decision> decisionsByFilename) { + // extracts standard Markdown links from the content + String[] lines = decision.getContent().split("\\n"); + for (String line : lines) { + Matcher matcher = LINK_REGEX.matcher(line); + while (matcher.find()) { + String target = matcher.group(1); + + Decision targetDecision = decisionsByFilename.get(target); + if (targetDecision != null) { + decision.addLink(targetDecision, DEFAULT_LINK_DESCRIPTION); + } + } + } + } + + 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/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/image/ImageImporterTests.java b/structurizr-import/src/test/java/com/structurizr/importer/diagrams/image/ImageImporterTests.java new file mode 100644 index 000000000..722ccc141 --- /dev/null +++ b/structurizr-import/src/test/java/com/structurizr/importer/diagrams/image/ImageImporterTests.java @@ -0,0 +1,45 @@ +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; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +public class ImageImporterTests { + + @Test + @Tag("IntegrationTest") + public void importDiagram_Url() throws Exception { + Workspace workspace = new Workspace("Name", "Description"); + ImageView view = workspace.getViews().createImageView("key"); + + 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()); + } + + @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"); + + 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()); + } + +} \ 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..9fac27fd2 --- /dev/null +++ b/structurizr-import/src/test/java/com/structurizr/importer/diagrams/kroki/KrokiImporterTests.java @@ -0,0 +1,132 @@ +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; + +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(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")); + 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(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")); + 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 + @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"); + + 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()); + 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(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")); + 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 + @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"); + + 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()); + 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"); + 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(KrokiImporter.KROKI_URL_PROPERTY, "https://kroki.io"); + workspace.getViews().getConfiguration().addProperty(KrokiImporter.KROKI_FORMAT_PROPERTY, "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..b4b69190b --- /dev/null +++ b/structurizr-import/src/test/java/com/structurizr/importer/diagrams/mermaid/MermaidEncoderTests.java @@ -0,0 +1,35 @@ +package com.structurizr.importer.diagrams.mermaid; + +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 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()); + 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 + 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..ca4cd4452 --- /dev/null +++ b/structurizr-import/src/test/java/com/structurizr/importer/diagrams/mermaid/MermaidImporterTests.java @@ -0,0 +1,137 @@ +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; + +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(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")); + 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_AsPNG() 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, "false"); + 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 + @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"); + + 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()); + assertEquals("flowchart.mmd", view.getTitle()); + 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()); + } + + @Test + public void importDiagram_AsSVG() 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"); + 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 + @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"); + + 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()); + assertEquals("flowchart.mmd", view.getTitle()); + 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()); + } + + @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(MermaidImporter.MERMAID_URL_PROPERTY, "https://mermaid.ink"); + workspace.getViews().getConfiguration().addProperty(MermaidImporter.MERMAID_FORMAT_PROPERTY, "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..e13f848ee --- /dev/null +++ b/structurizr-import/src/test/java/com/structurizr/importer/diagrams/plantuml/PlantUMLEncoderTests.java @@ -0,0 +1,21 @@ +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 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 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..a9c270024 --- /dev/null +++ b/structurizr-import/src/test/java/com/structurizr/importer/diagrams/plantuml/PlantUMLImporterTests.java @@ -0,0 +1,132 @@ +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; + +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(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")); + assertEquals("key", view.getKey()); + assertNull(view.getElement()); + assertNull(view.getElementId()); + assertEquals("Sequence diagram example", view.getTitle()); + 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(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")); + assertEquals("key", view.getKey()); + assertNull(view.getElement()); + assertNull(view.getElementId()); + assertEquals("without-title.puml", view.getTitle()); + 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(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")); + assertEquals("https://plantuml.com/plantuml/png/SoWkIImgAStDuIh9BCb9LGXEBInDpKjELKZ9J4mlIinLIAr8p2t8IULooazIqBLJSCp914fQAMIavkJaSpcavgK0zG80", view.getContent()); + 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"); + + 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("image/png", view.getContentType()); + } + + @Test + public void importDiagram_AsSVG() 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"); + 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 + @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"); + + 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()); + } + + @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(PlantUMLImporter.PLANTUML_URL_PROPERTY, "https://plantuml.com/plantuml"); + workspace.getViews().getConfiguration().addProperty(PlantUMLImporter.PLANTUML_FORMAT_PROPERTY, "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..7634ea81a --- /dev/null +++ b/structurizr-import/src/test/java/com/structurizr/importer/documentation/AdrToolsDecisionImporterTests.java @@ -0,0 +1,129 @@ +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.Locale; + +import static org.junit.jupiter.api.Assertions.*; + + +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; + + @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, DECISIONS_FOLDER); + + 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", Locale.US); + assertEquals("12-Feb-2016", 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, DECISIONS_FOLDER); + + 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, 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)")); + } + +} \ 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<Section> 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<Image> 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/Log4brainsDecisionImporterTests.java b/structurizr-import/src/test/java/com/structurizr/importer/documentation/Log4brainsDecisionImporterTests.java new file mode 100644 index 000000000..e2314c0b9 --- /dev/null +++ b/structurizr-import/src/test/java/com/structurizr/importer/documentation/Log4brainsDecisionImporterTests.java @@ -0,0 +1,230 @@ +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.Locale; + +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", Locale.US); + + 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()); + 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 <https://github.com/joelparkerhenderson/architecture_decision_record> +- 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/java/com/structurizr/importer/documentation/MadrDecisionImporterTests.java b/structurizr-import/src/test/java/com/structurizr/importer/documentation/MadrDecisionImporterTests.java new file mode 100644 index 000000000..6fa7989fc --- /dev/null +++ b/structurizr-import/src/test/java/com/structurizr/importer/documentation/MadrDecisionImporterTests.java @@ -0,0 +1,235 @@ +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.Locale; + +import static org.junit.jupiter.api.Assertions.*; + + +public class MadrDecisionImporterTests { + + private static final File DECISIONS_FOLDER = new File("./src/test/resources/decisions/madr"); + + private MadrDecisionImporter decisionImporter; + private Workspace workspace; + private Documentation documentation; + + @BeforeEach + public void setUp() { + decisionImporter = new MadrDecisionImporter(); + 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", Locale.US); + + assertEquals(19, documentation.getDecisions().size()); + + Decision decision0 = documentation.getDecisions().stream().filter(d -> d.getId().equals("0")).findFirst().get(); + assertEquals("0", decision0.getId()); + assertEquals("Use Markdown Any Decision Records", decision0.getTitle()); + assertEquals("11-Jan-2024", sdf.format(decision0.getDate())); + assertEquals("accepted", decision0.getStatus()); + Assertions.assertEquals(Format.Markdown, decision0.getFormat()); + assertEquals(""" +# 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 <https://github.com/joelparkerhenderson/architecture_decision_record> +* 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. +""", decision0.getContent()); + + Decision decision3 = documentation.getDecisions().stream().filter(d -> d.getId().equals("3")).findFirst().get(); + assertEquals("on hold", decision3.getStatus()); + + Decision decision8 = documentation.getDecisions().stream().filter(d -> d.getId().equals("8")).findFirst().get(); + assertEquals(""" +# Add Status Field + +## Context and Problem Statement + +Technical Story: <https://github.com/adr/madr/issues/2> + +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 <https://shields.io> 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](#13) for more reasoning on using YAML front matter. + """, decision8.getContent()); + + Decision decision18 = documentation.getDecisions().stream().filter(d -> d.getId().equals("18")).findFirst().get(); + assertEquals("Use \"Confirmation\" as Heading", decision18.getTitle()); + } + + @Test + public void test_importDocumentation_CapturesLinksBetweenDecisions() { + decisionImporter.importDocumentation(workspace, DECISIONS_FOLDER); + + Decision decision8 = documentation.getDecisions().stream().filter(d -> d.getId().equals("8")).findFirst().get(); + assertEquals(1, decision8.getLinks().size()); + Decision.Link link = decision8.getLinks().iterator().next(); + assertEquals("13", link.getId()); + assertEquals("Links to", link.getDescription()); + } + +} \ No newline at end of file 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<Section> 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/decisions/adrtools/0001-record-architecture-decisions.md b/structurizr-import/src/test/resources/decisions/adrtools/0001-record-architecture-decisions.md new file mode 100644 index 000000000..f30860000 --- /dev/null +++ b/structurizr-import/src/test/resources/decisions/adrtools/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/decisions/adrtools/0002-implement-as-shell-scripts.md b/structurizr-import/src/test/resources/decisions/adrtools/0002-implement-as-shell-scripts.md new file mode 100644 index 000000000..8e6ea15e6 --- /dev/null +++ b/structurizr-import/src/test/resources/decisions/adrtools/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/decisions/adrtools/0003-single-command-with-subcommands.md b/structurizr-import/src/test/resources/decisions/adrtools/0003-single-command-with-subcommands.md new file mode 100644 index 000000000..f64db8da1 --- /dev/null +++ b/structurizr-import/src/test/resources/decisions/adrtools/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/decisions/adrtools/0004-markdown-format.md b/structurizr-import/src/test/resources/decisions/adrtools/0004-markdown-format.md new file mode 100644 index 000000000..160d1689f --- /dev/null +++ b/structurizr-import/src/test/resources/decisions/adrtools/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/decisions/adrtools/0005-help-comments.md b/structurizr-import/src/test/resources/decisions/adrtools/0005-help-comments.md new file mode 100644 index 000000000..b19bf0fb1 --- /dev/null +++ b/structurizr-import/src/test/resources/decisions/adrtools/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/decisions/adrtools/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 new file mode 100644 index 000000000..4a3485f79 --- /dev/null +++ b/structurizr-import/src/test/resources/decisions/adrtools/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/decisions/adrtools/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 new file mode 100644 index 000000000..a649b2356 --- /dev/null +++ b/structurizr-import/src/test/resources/decisions/adrtools/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/decisions/adrtools/0008-use-iso-8601-format-for-dates.md b/structurizr-import/src/test/resources/decisions/adrtools/0008-use-iso-8601-format-for-dates.md new file mode 100644 index 000000000..4146f11df --- /dev/null +++ b/structurizr-import/src/test/resources/decisions/adrtools/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/decisions/adrtools/0009-help-scripts.md b/structurizr-import/src/test/resources/decisions/adrtools/0009-help-scripts.md new file mode 100644 index 000000000..0146d127c --- /dev/null +++ b/structurizr-import/src/test/resources/decisions/adrtools/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-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 <https://github.com/joelparkerhenderson/architecture_decision_record> +- 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) diff --git a/structurizr-import/src/test/resources/decisions/madr/0000-use-markdown-any-decision-records.md b/structurizr-import/src/test/resources/decisions/madr/0000-use-markdown-any-decision-records.md new file mode 100644 index 000000000..b8e778b3f --- /dev/null +++ b/structurizr-import/src/test/resources/decisions/madr/0000-use-markdown-any-decision-records.md @@ -0,0 +1,31 @@ +--- +parent: Decisions +nav_order: 0 +date: 2024-01-11 +--- +# 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 <https://github.com/joelparkerhenderson/architecture_decision_record> +* 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-import/src/test/resources/decisions/madr/0001-use-CC0-or-MIT-as-license.md b/structurizr-import/src/test/resources/decisions/madr/0001-use-CC0-or-MIT-as-license.md new file mode 100644 index 000000000..09ec7aaf0 --- /dev/null +++ b/structurizr-import/src/test/resources/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 <https://help.github.com/articles/licensing-a-repository/>. + +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-import/src/test/resources/decisions/madr/0002-do-not-use-numbers-in-headings.md b/structurizr-import/src/test/resources/decisions/madr/0002-do-not-use-numbers-in-headings.md new file mode 100644 index 000000000..9f3197305 --- /dev/null +++ b/structurizr-import/src/test/resources/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-import/src/test/resources/decisions/madr/0003-provide-own-madr-tools.md b/structurizr-import/src/test/resources/decisions/madr/0003-provide-own-madr-tools.md new file mode 100644 index 000000000..00750fd68 --- /dev/null +++ b/structurizr-import/src/test/resources/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 <https://adr.github.io/madr/tooling.html>. diff --git a/structurizr-import/src/test/resources/decisions/madr/0004-write-own-toc-tool.md b/structurizr-import/src/test/resources/decisions/madr/0004-write-own-toc-tool.md new file mode 100644 index 000000000..038204407 --- /dev/null +++ b/structurizr-import/src/test/resources/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: <https://github.com/npryce/adr-tools/blob/master/tests/generate-contents-with-header-and-footer.sh>. + +### 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-import/src/test/resources/decisions/madr/0005-use-dashes-in-filenames.md b/structurizr-import/src/test/resources/decisions/madr/0005-use-dashes-in-filenames.md new file mode 100644 index 000000000..4c94869e8 --- /dev/null +++ b/structurizr-import/src/test/resources/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 <https://github.com/joelparkerhenderson/architecture_decision_record#adr-file-name-conventions> + +## 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-import/src/test/resources/decisions/madr/0006-use-names-as-identifier.md b/structurizr-import/src/test/resources/decisions/madr/0006-use-names-as-identifier.md new file mode 100644 index 000000000..1734742b8 --- /dev/null +++ b/structurizr-import/src/test/resources/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-import/src/test/resources/decisions/madr/0007-do-not-emphasize-line-headings.md b/structurizr-import/src/test/resources/decisions/madr/0007-do-not-emphasize-line-headings.md new file mode 100644 index 000000000..c12abeab3 --- /dev/null +++ b/structurizr-import/src/test/resources/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-import/src/test/resources/decisions/madr/0008-add-status-field.md b/structurizr-import/src/test/resources/decisions/madr/0008-add-status-field.md new file mode 100644 index 000000000..bae2bb479 --- /dev/null +++ b/structurizr-import/src/test/resources/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: <https://github.com/adr/madr/issues/2> + +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 <https://shields.io> 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-import/src/test/resources/decisions/madr/0008-example-badge.png b/structurizr-import/src/test/resources/decisions/madr/0008-example-badge.png new file mode 100644 index 000000000..20ced2835 Binary files /dev/null and b/structurizr-import/src/test/resources/decisions/madr/0008-example-badge.png differ diff --git a/structurizr-import/src/test/resources/decisions/madr/0008-example-separate-heading.png b/structurizr-import/src/test/resources/decisions/madr/0008-example-separate-heading.png new file mode 100644 index 000000000..8a898033e Binary files /dev/null and b/structurizr-import/src/test/resources/decisions/madr/0008-example-separate-heading.png differ diff --git a/structurizr-import/src/test/resources/decisions/madr/0008-example-table.png b/structurizr-import/src/test/resources/decisions/madr/0008-example-table.png new file mode 100644 index 000000000..768c2d145 Binary files /dev/null and b/structurizr-import/src/test/resources/decisions/madr/0008-example-table.png differ diff --git a/structurizr-import/src/test/resources/decisions/madr/0009-support-links-between-adrs-inside-an-adrs.md b/structurizr-import/src/test/resources/decisions/madr/0009-support-links-between-adrs-inside-an-adrs.md new file mode 100644 index 000000000..f08efe328 --- /dev/null +++ b/structurizr-import/src/test/resources/decisions/madr/0009-support-links-between-adrs-inside-an-adrs.md @@ -0,0 +1,79 @@ +--- +parent: Decisions +nav_order: 9 +--- +# Support Links To Other ADRs Inside an ADR + +## Context and Problem Statement + +A decision might point to another decision. +For instance, if a decision is a follow-up to another decision. +This should be supported by MADR, too. + +Technical Story: <https://github.com/adr/madr/issues/9> + +## 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 <https://github.com/adr/madr/issues/2>) + +### 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-import/src/test/resources/decisions/madr/0010-support-categories.md b/structurizr-import/src/test/resources/decisions/madr/0010-support-categories.md new file mode 100644 index 000000000..a9160f5d9 --- /dev/null +++ b/structurizr-import/src/test/resources/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 <https://gist.github.com/FaKeller/2f9c63b6e1d436abb7358b68bf396f57>) +* 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-import/src/test/resources/decisions/madr/0011-use-asterisk-as-list-marker.md b/structurizr-import/src/test/resources/decisions/madr/0011-use-asterisk-as-list-marker.md new file mode 100644 index 000000000..60956c008 --- /dev/null +++ b/structurizr-import/src/test/resources/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-import/src/test/resources/decisions/madr/0012-use-curly-braces-to-denote-placeholder.md b/structurizr-import/src/test/resources/decisions/madr/0012-use-curly-braces-to-denote-placeholder.md new file mode 100644 index 000000000..c73921b26 --- /dev/null +++ b/structurizr-import/src/test/resources/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: `<option 1>` + +Idea taken from <https://github.com/schubmat/DecisionCapture/blob/master/templates/captureTemplate_full.md> + +* Good, because kept in Markdown as is +* Bad, because could be mixed up with an HTML element diff --git a/structurizr-import/src/test/resources/decisions/madr/0013-example.png b/structurizr-import/src/test/resources/decisions/madr/0013-example.png new file mode 100644 index 000000000..9facf8226 Binary files /dev/null and b/structurizr-import/src/test/resources/decisions/madr/0013-example.png differ diff --git a/structurizr-import/src/test/resources/decisions/madr/0013-use-yaml-front-matter-for-meta-data.md b/structurizr-import/src/test/resources/decisions/madr/0013-use-yaml-front-matter-for-meta-data.md new file mode 100644 index 000000000..2788effc9 --- /dev/null +++ b/structurizr-import/src/test/resources/decisions/madr/0013-use-yaml-front-matter-for-meta-data.md @@ -0,0 +1,65 @@ +--- +parent: Decisions +nav_order: 13 +--- +# Use YAML front matter for metadata + +## Context and Problem Statement + +MADR offers the fields "Status", "Deciders", and "Date". +These are a kind of metadata fields. +Should this data be included in the ADR directly, or should it be separated somehow? + +## Decision Drivers + +* Easy to read +* Easy to write + +## Considered Options + +* Use YAML front matter +* Use plain Markdown everywhere + +## 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 +--- +status: accepted +deciders: +date: +--- + +## Context and problem statement + +We want to record architectural decisions made in this project. +Which format and structure should these records follow? +``` + +Rendered output: + +![adr-013 rendered output](0013-example.png) + +* Good, because it shortens the body (essence of the ADR) +* Good, because tools can handle it more easily +* Good, because indicates the lower importance of the data +* Bad, because pretends to be more accurate than it can be (e.g., possible status values) +* Bad, because rendering not standardized +* Bad, because not all Markdown parsers can parse it + +## Use plain Markdown everywhere + +* Good, because all parsers can handle it +* Bad, because special markdown parsing tooling is needed +* Bad, because metadata is handled the same way as the content + +## More Information + +[ADR-0008](0008-add-status-field.md) reasons on adding metadata (such as status). diff --git a/structurizr-import/src/test/resources/decisions/madr/0014-allow-neutral-arguments.md b/structurizr-import/src/test/resources/decisions/madr/0014-allow-neutral-arguments.md new file mode 100644 index 000000000..bfd221988 --- /dev/null +++ b/structurizr-import/src/test/resources/decisions/madr/0014-allow-neutral-arguments.md @@ -0,0 +1,62 @@ +--- +parent: Decisions +nav_order: 14 +--- +# Allow "neutral" arguments + +## Context and Problem Statement + +Sometimes, one wants to write down an argument, which is neither pro nor con. + +## Considered Options + +* Neutral, because … +* OK, because … +* Good/bad, because … +* Interesting, because … +* Indifferent, because … + +## Decision Outcome + +Chosen option: "Neutral, because …", because + +* it fits best. +* it is consistent to patterns: +/0/- + +## Pros and Cons of the Options + +### Neutral, because … + +In the following, a full pro/con list is given: + +Example: + +* The proposed solution is good, because it resolves the force 1. +* The proposed solution is good, because it addresses decision driver 2. +* The proposed solution is good, because it mitigates the technical risk / dept with respect to decision driver 4. +* The proposed solution is good, because it removes architectural smell … +* The proposed solution is good, because it addresses stakeholder concern … +* The proposed solution is good, because it has a positive effect on the performance (quality property). +* The proposed solution is neutral, because it is indifferent to decision driver 2. +* The proposed solution is bad, because it does not address decision driver 3. +* The proposed solution is bad, because it has a negative effect on the maintainability (quality property). + +Shorter example for pros and cons: + +* The proposed solution is good with respect to resolving the force 1. +* The considered option is a good solution, because it resolves force1 and the non-resolving of force 1 is OK, … + +### OK, because … + +Real world example: <https://github.com/island-is/island.is/blob/main/handbook/technical-overview/adr/0005-error-tracking-and-monitoring.md> + +```markdown +### Bugsnag + +- Good, because it offers a Slack integration for faster feedback. +- Good, because it offers a Github integration to link to possible commits and PRs. +- Good, because it offers bot front-side/server-side/serverless error tracking. +- OK, because it was ranked the **\#5** as the best Javascript (client-side) error logging service in a community survey. +- Bad, because it's expensive. (**\$199/mo** for **450k events** and **15 collaborators**) +- Bad, because it's pricing includes a fixed set of collaborators. +``` diff --git a/structurizr-import/src/test/resources/decisions/madr/0015-include-consulting-informed-of-raci.md b/structurizr-import/src/test/resources/decisions/madr/0015-include-consulting-informed-of-raci.md new file mode 100644 index 000000000..b028fbb7c --- /dev/null +++ b/structurizr-import/src/test/resources/decisions/madr/0015-include-consulting-informed-of-raci.md @@ -0,0 +1,44 @@ +--- +parent: Decisions +nav_order: 15 +--- +# Include "Consulted" and "Informed" of RACI + +## Context and Problem Statement + +We noticed an intersection between MADR and [RACI](https://en.wikipedia.org/wiki/Responsibility_assignment_matrix), and felt the need to add a "consulted" and "informed" field in addition to "deciders". +Would it be beneficial to "upstream" these fields to MADR? + +MADR Issue: [#62](https://github.com/adr/madr/issues/62). + +## Decision Drivers + +* MADR should contain fields important to the ADR decision process +* MADR template should be easy to understand +* MADR should be lightweight + +## Considered Options + +* Include "Consulted" and "Informed" of RACI +* Include all fields of RACI +* Do not include anything of RACI + +## Decision Outcome + +Chosen option: "Include 'Consulted' and 'Informed' of RACI", because comes out best (see below). + +## Pros and Cons of the Options + +### Include "Consulted" and "Informed" of RACI + +* Good, because these two roles of RACI are well understood. +* Good, because we make these fields optional, thus it keeps MADR still lightweight. +* Bad, because it adds two additional fields + +### Include all fields of RACI + +This would add "Responsible", "Accountable", "Consulted", and "Informed" + +* Good, because complete RACI would be included +* Bad, because get confused about who is "accountable" and who is "responsible". +* Bad, because if decisions are mostly taken by consensus in small committees, then there might not be an "accountable" person. diff --git a/structurizr-import/src/test/resources/decisions/madr/0016-outcome-before-detailed-pros-cons.md b/structurizr-import/src/test/resources/decisions/madr/0016-outcome-before-detailed-pros-cons.md new file mode 100644 index 000000000..9caa315e8 --- /dev/null +++ b/structurizr-import/src/test/resources/decisions/madr/0016-outcome-before-detailed-pros-cons.md @@ -0,0 +1,74 @@ +--- +parent: Decisions +nav_order: 16 +--- +# Outcome before Detailed Pros and Cons + +## Context and Problem Statement + +MADR aims to list pros and cons of each option. +Where should that list be placed? + +## Decision Drivers + +* Most important information should be above the fold. +* MADR should be easy to write. +* MADR should be easy to read. + +## Considered Options + +* Section "Pros and Cons of the Options" after "Decision Outcome" +* Section "Pros and Cons of the Options" before "Decision Outcome" + +## Decision Outcome + +Chosen option: "Section 'Pros and Cons of the Options' after 'Decision Outcome'", because this keeps the available options and the decision outcome close together. +One gets the decision outcome above the fold and preserve a nice logical flow. +Hence, one can see the "Pros and Cons" section as a sort of "appendix" to the considered options section. +It is almost like the "Considered Options" section implies the following sentence: "For a more detailed analysis of these options, refer to pros and cons". + +## Pros and Cons of the Options + +### Section "Pros and Cons of the Options" after "Decision Outcome" + +Illustration: + +```markdown +## Considered Options + +... + +## Decision Outcome + +... + +## Pros and Cons of the Options + +... +``` + +* Good, because this keeps the available options and the decision outcome close together. +* Bad, because readers might be confused, because the logical flow broken: First comes the result, then the detailed arguments. + +### Section "Pros and Cons of the Options" before "Decision Outcome" + +Illustration: + +```markdown +## Considered Options + +... + +## Pros and Cons of the Options + +... + +## Decision Outcome + +... +``` + +* Good, because the logical flow is kept +* Bad, because the decision outcome is not close to the options +* Bad, because "small" MADRs with the list of options and the decision outcome (and not containing the section "Pros and Cons of the Options") +* Bad, because "small" MADRs with the list of options and the decision outcome (and not containing the section "Pros and Cons of the Options") diff --git a/structurizr-import/src/test/resources/decisions/madr/0017-use-same-format-for-outcomes-and-options.md b/structurizr-import/src/test/resources/decisions/madr/0017-use-same-format-for-outcomes-and-options.md new file mode 100644 index 000000000..bbf876996 --- /dev/null +++ b/structurizr-import/src/test/resources/decisions/madr/0017-use-same-format-for-outcomes-and-options.md @@ -0,0 +1,26 @@ +--- +parent: Decisions +nav_order: 17 +--- +# Use Same Format for Outcomes and Options + +## Context and Problem Statement + +"Outcome" has "Positive Consequences" and "Negative Consequences" sections. "Options" just have a single list with "Good" and "Bad" prefixes. + +Ticket: [issue#75](https://github.com/adr/madr/issues/75) + +## Decision Drivers + +* Consistent design of MADR +* Allow easy copy and paste + +## Considered Options + +* Section "Consequences" listing positive and negative consequences as "Good, because" and "Bad, because" +* Section "Consequences" listing positive and negative consequences as "Positive, because" and "Negative, because" +* No sections "Consequences", "Positive Consequences", and "Negative Consequences" + +## Decision Outcome + +Chosen option: 'Section "Consequences" listing positive and negative consequences as "Good, because" and "Bad, because"', because resolves all forces. diff --git a/structurizr-import/src/test/resources/decisions/madr/0018-use-confirmation-as-heading.md b/structurizr-import/src/test/resources/decisions/madr/0018-use-confirmation-as-heading.md new file mode 100644 index 000000000..230f4f54a --- /dev/null +++ b/structurizr-import/src/test/resources/decisions/madr/0018-use-confirmation-as-heading.md @@ -0,0 +1,30 @@ +--- +parent: Decisions +nav_order: 18 +--- +<!-- we need to disable MD025, because we use the different heading "ADR Template" in the homepage (see above) than it is foreseen in the template --> +<!-- markdownlint-disable-next-line MD025 --> +# Use "Confirmation" as Heading + +## Context and Problem Statement + +In MADR, we want to include some sort of check that the decision was implemented. +How to name the heading for the explanation? + +## Decision Drivers + +* Consistent with terms used in IT +* Common word + +## Considered Options + +* "Confirmation" +* "Validation" +* "Verification" + +## Decision Outcome + +Chosen option: "Confirmation", because "validation" is out of scope of the template. +There is a process leading to a "valid" ADR. +The other term "Verification" is often bound to a formal tool or formal procedure. +We wanted to enable also less formal checks. 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-core/test/unit/com/structurizr/documentation/automatic/01-section-1.md b/structurizr-import/src/test/resources/docs/docs/01-section-1.md similarity index 100% rename from structurizr-core/test/unit/com/structurizr/documentation/automatic/01-section-1.md rename to structurizr-import/src/test/resources/docs/docs/01-section-1.md diff --git a/structurizr-core/test/unit/com/structurizr/documentation/automatic/02-section-2.markdown b/structurizr-import/src/test/resources/docs/docs/02-section-2.markdown similarity index 100% rename from structurizr-core/test/unit/com/structurizr/documentation/automatic/02-section-2.markdown rename to structurizr-import/src/test/resources/docs/docs/02-section-2.markdown diff --git a/structurizr-core/test/unit/com/structurizr/documentation/automatic/03-section-3.text b/structurizr-import/src/test/resources/docs/docs/03-section-3.text similarity index 100% rename from structurizr-core/test/unit/com/structurizr/documentation/automatic/03-section-3.text rename to structurizr-import/src/test/resources/docs/docs/03-section-3.text diff --git a/structurizr-core/test/unit/com/structurizr/documentation/automatic/04-section-4.adoc b/structurizr-import/src/test/resources/docs/docs/04-section-4.adoc similarity index 100% rename from structurizr-core/test/unit/com/structurizr/documentation/automatic/04-section-4.adoc rename to structurizr-import/src/test/resources/docs/docs/04-section-4.adoc diff --git a/structurizr-core/test/unit/com/structurizr/documentation/automatic/05-section-5.asciidoc b/structurizr-import/src/test/resources/docs/docs/05-section-5.asciidoc similarity index 100% rename from structurizr-core/test/unit/com/structurizr/documentation/automatic/05-section-5.asciidoc rename to structurizr-import/src/test/resources/docs/docs/05-section-5.asciidoc diff --git a/structurizr-core/test/unit/com/structurizr/documentation/automatic/06-section-6.asc b/structurizr-import/src/test/resources/docs/docs/06-section-6.asc similarity index 100% rename from structurizr-core/test/unit/com/structurizr/documentation/automatic/06-section-6.asc rename to structurizr-import/src/test/resources/docs/docs/06-section-6.asc 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/file_without_extension b/structurizr-import/src/test/resources/docs/docs/file_without_extension new file mode 100644 index 000000000..d7df09c19 --- /dev/null +++ b/structurizr-import/src/test/resources/docs/docs/file_without_extension @@ -0,0 +1 @@ +This file should be ignored. \ No newline at end of file diff --git a/structurizr-core/test/unit/com/structurizr/documentation/image.gif b/structurizr-import/src/test/resources/docs/docs/images/image.gif similarity index 100% rename from structurizr-core/test/unit/com/structurizr/documentation/image.gif rename to structurizr-import/src/test/resources/docs/docs/images/image.gif diff --git a/structurizr-core/test/unit/com/structurizr/documentation/image.jpeg b/structurizr-import/src/test/resources/docs/docs/images/image.jpeg similarity index 100% rename from structurizr-core/test/unit/com/structurizr/documentation/image.jpeg rename to structurizr-import/src/test/resources/docs/docs/images/image.jpeg diff --git a/structurizr-core/test/unit/com/structurizr/documentation/image.jpg b/structurizr-import/src/test/resources/docs/docs/images/image.jpg similarity index 100% rename from structurizr-core/test/unit/com/structurizr/documentation/image.jpg rename to structurizr-import/src/test/resources/docs/docs/images/image.jpg diff --git a/structurizr-core/test/unit/com/structurizr/documentation/image.png b/structurizr-import/src/test/resources/docs/docs/images/image.png similarity index 100% rename from structurizr-core/test/unit/com/structurizr/documentation/image.png rename to structurizr-import/src/test/resources/docs/docs/images/image.png diff --git a/structurizr-core/test/unit/com/structurizr/documentation/images/image.gif b/structurizr-import/src/test/resources/docs/images/image.gif similarity index 100% rename from structurizr-core/test/unit/com/structurizr/documentation/images/image.gif rename to structurizr-import/src/test/resources/docs/images/image.gif diff --git a/structurizr-core/test/unit/com/structurizr/documentation/images/image.jpeg b/structurizr-import/src/test/resources/docs/images/image.jpeg similarity index 100% rename from structurizr-core/test/unit/com/structurizr/documentation/images/image.jpeg rename to structurizr-import/src/test/resources/docs/images/image.jpeg diff --git a/structurizr-core/test/unit/com/structurizr/documentation/images/image.jpg b/structurizr-import/src/test/resources/docs/images/image.jpg similarity index 100% rename from structurizr-core/test/unit/com/structurizr/documentation/images/image.jpg rename to structurizr-import/src/test/resources/docs/images/image.jpg diff --git a/structurizr-core/test/unit/com/structurizr/documentation/images/image.png b/structurizr-import/src/test/resources/docs/images/image.png similarity index 100% rename from structurizr-core/test/unit/com/structurizr/documentation/images/image.png rename to structurizr-import/src/test/resources/docs/images/image.png 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 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100"> + <circle cx="50" cy="50" r="40" stroke="black" stroke-width="3" fill="red" /> +</svg> \ 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 000000000..c06542ada Binary files /dev/null and b/structurizr-import/src/test/resources/docs/images/images/image.gif differ diff --git a/structurizr-import/src/test/resources/docs/images/images/image.jpeg b/structurizr-import/src/test/resources/docs/images/images/image.jpeg new file mode 100644 index 000000000..0b9e9c39d Binary files /dev/null and b/structurizr-import/src/test/resources/docs/images/images/image.jpeg differ 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 000000000..0b9e9c39d Binary files /dev/null and b/structurizr-import/src/test/resources/docs/images/images/image.jpg differ 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 000000000..644aef77b Binary files /dev/null and b/structurizr-import/src/test/resources/docs/images/images/image.png differ diff --git a/structurizr-core/test/unit/com/structurizr/documentation/noimages/readme.md b/structurizr-import/src/test/resources/docs/images/noimages/readme.md similarity index 100% rename from structurizr-core/test/unit/com/structurizr/documentation/noimages/readme.md rename to structurizr-import/src/test/resources/docs/images/noimages/readme.md diff --git a/structurizr-inspection/README.md b/structurizr-inspection/README.md new file mode 100644 index 000000000..ed2285468 --- /dev/null +++ b/structurizr-inspection/README.md @@ -0,0 +1,7 @@ +# structurizr-inspection + +[![Maven Central](https://img.shields.io/maven-central/v/com.structurizr/structurizr-inspection.svg?label=Maven%20Central)](https://search.maven.org/artifact/com.structurizr/structurizr-inspection) + +This library provides utilities to inspect a Structurizr workspace for warnings. + +- [Documentation](https://docs.structurizr.com/workspaces) \ No newline at end of file diff --git a/structurizr-inspection/build.gradle b/structurizr-inspection/build.gradle new file mode 100644 index 000000000..72a7ec6ba --- /dev/null +++ b/structurizr-inspection/build.gradle @@ -0,0 +1,7 @@ +dependencies { + + api project(':structurizr-core') + + testImplementation project(':structurizr-dsl') + +} \ No newline at end of file diff --git a/structurizr-inspection/src/main/java/com/structurizr/inspection/DefaultInspector.java b/structurizr-inspection/src/main/java/com/structurizr/inspection/DefaultInspector.java new file mode 100644 index 000000000..e691825ab --- /dev/null +++ b/structurizr-inspection/src/main/java/com/structurizr/inspection/DefaultInspector.java @@ -0,0 +1,163 @@ +package com.structurizr.inspection; + +import com.structurizr.Workspace; +import com.structurizr.inspection.documentation.EmbeddedViewMissingInspection; +import com.structurizr.inspection.documentation.EmbeddedViewWithGeneratedKeyInspection; +import com.structurizr.inspection.model.*; +import com.structurizr.inspection.view.*; +import com.structurizr.inspection.workspace.WorkspaceScopeInspection; +import com.structurizr.inspection.workspace.WorkspaceToolingInspection; +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); + + if (!"false".equalsIgnoreCase(workspace.getProperties().get("structurizr.inspection"))) { + runWorkspaceInspections(); + runModelInspections(); + runViewInspections(); + + List<Violation> 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()); + } + } + + private void runWorkspaceInspections() { + add(new WorkspaceToolingInspection(this).run()); + add(new WorkspaceScopeInspection(this).run()); + add(new EmbeddedViewMissingInspection(this).run(getWorkspace())); + add(new EmbeddedViewWithGeneratedKeyInspection(this).run(getWorkspace())); + } + + private void runModelInspections() { + add(new EmptyModelInspection(this).run()); + add(new MultipleSoftwareSystemsDetailedInspection(this).run()); + ElementNotIncludedInAnyViewsInspection elementNotIncludedInAnyViewsCheck = new ElementNotIncludedInAnyViewsInspection(this); + DisconnectedElementInspection disconnectedElementCheck = new DisconnectedElementInspection(this); + for (Element element : getWorkspace().getModel().getElements()) { + if (element instanceof Person) { + add(new PersonDescriptionInspection(this).run(element)); + } + + if (element instanceof SoftwareSystem) { + add(new SoftwareSystemDescriptionInspection(this).run(element)); + add(new SoftwareSystemDocumentationInspection(this).run(element)); + add(new SoftwareSystemDecisionsInspection(this).run(element)); + add(new EmbeddedViewMissingInspection(this).run((SoftwareSystem)element)); + add(new EmbeddedViewWithGeneratedKeyInspection(this).run((SoftwareSystem)element)); + } + + if (element instanceof Container) { + add(new ContainerDescriptionInspection(this).run(element)); + add(new ContainerTechnologyInspection(this).run(element)); + add(new EmbeddedViewMissingInspection(this).run((Container)element)); + add(new EmbeddedViewWithGeneratedKeyInspection(this).run((Container)element)); + } + + if (element instanceof Component) { + add(new ComponentDescriptionInspection(this).run(element)); + add(new ComponentTechnologyInspection(this).run(element)); + add(new EmbeddedViewMissingInspection(this).run((Component)element)); + add(new EmbeddedViewWithGeneratedKeyInspection(this).run((Component)element)); + } + + if (element instanceof DeploymentNode) { + add(new DeploymentNodeDescriptionInspection(this).run(element)); + add(new DeploymentNodeTechnologyInspection(this).run(element)); + add(new EmptyDeploymentNodeInspection(this).run(element)); + } + + if (element instanceof InfrastructureNode) { + add(new InfrastructureNodeDescriptionInspection(this).run(element)); + add(new InfrastructureNodeTechnologyInspection(this).run(element)); + } + + add(disconnectedElementCheck.run(element)); + add(elementNotIncludedInAnyViewsCheck.run(element)); + + for (Relationship relationship : element.getRelationships()) { + add(new RelationshipDescriptionInspection(this).run(relationship)); + add(new RelationshipTechnologyInspection(this).run(relationship)); + } + } + } + + private void runViewInspections() { + add(new EmptyViewsInspection(this).run()); + + for (CustomView view : getWorkspace().getViews().getCustomViews()) { + add(new GeneratedKeyInspection(this).run(view)); + add(new EmptyViewInspection(this).run(view)); + add(new ManualLayoutInspection(this).run(view)); + } + + for (SystemLandscapeView view : getWorkspace().getViews().getSystemLandscapeViews()) { + add(new GeneratedKeyInspection(this).run(view)); + add(new EmptyViewInspection(this).run(view)); + add(new ManualLayoutInspection(this).run(view)); + } + + add(new SystemContextViewsForMultipleSoftwareSystemsInspection(this).run()); + for (SystemContextView view : getWorkspace().getViews().getSystemContextViews()) { + add(new GeneratedKeyInspection(this).run(view)); + add(new EmptyViewInspection(this).run(view)); + add(new ManualLayoutInspection(this).run(view)); + } + + add(new ContainerViewsForMultipleSoftwareSystemsInspection(this).run()); + for (ContainerView view : getWorkspace().getViews().getContainerViews()) { + add(new GeneratedKeyInspection(this).run(view)); + add(new EmptyViewInspection(this).run(view)); + add(new ManualLayoutInspection(this).run(view)); + } + + for (ComponentView view : getWorkspace().getViews().getComponentViews()) { + add(new GeneratedKeyInspection(this).run(view)); + add(new EmptyViewInspection(this).run(view)); + add(new ManualLayoutInspection(this).run(view)); + } + + for (DynamicView view : getWorkspace().getViews().getDynamicViews()) { + add(new GeneratedKeyInspection(this).run(view)); + add(new EmptyViewInspection(this).run(view)); + add(new ManualLayoutInspection(this).run(view)); + } + + for (DeploymentView view : getWorkspace().getViews().getDeploymentViews()) { + add(new GeneratedKeyInspection(this).run(view)); + add(new EmptyViewInspection(this).run(view)); + add(new ManualLayoutInspection(this).run(view)); + } + + for (FilteredView view : getWorkspace().getViews().getFilteredViews()) { + add(new GeneratedKeyInspection(this).run(view)); + } + + for (ImageView view : getWorkspace().getViews().getImageViews()) { + add(new GeneratedKeyInspection(this).run(view)); + } + + for (ElementStyle elementStyle : getWorkspace().getViews().getConfiguration().getStyles().getElements()) { + add(new ElementStyleMetadataInspection(this).run(elementStyle)); + } + } + + @Override + public SeverityStrategy getSeverityStrategy() { + return new PropertyBasedSeverityStrategy(); + } + +} \ No newline at end of file diff --git a/structurizr-inspection/src/main/java/com/structurizr/inspection/FixedSeverityStrategy.java b/structurizr-inspection/src/main/java/com/structurizr/inspection/FixedSeverityStrategy.java new file mode 100644 index 000000000..81e9901b4 --- /dev/null +++ b/structurizr-inspection/src/main/java/com/structurizr/inspection/FixedSeverityStrategy.java @@ -0,0 +1,54 @@ +package com.structurizr.inspection; + +import com.structurizr.Workspace; +import com.structurizr.model.Element; +import com.structurizr.model.Model; +import com.structurizr.model.Relationship; +import com.structurizr.view.ElementStyle; +import com.structurizr.view.View; +import com.structurizr.view.ViewSet; + +public class FixedSeverityStrategy implements SeverityStrategy { + + private final Severity severity; + + public FixedSeverityStrategy(Severity severity) { + this.severity = severity; + } + + @Override + public Severity getSeverity(Inspection inspection, Workspace workspace) { + return severity; + } + + @Override + public Severity getSeverity(Inspection inspection, ViewSet viewSet) { + return severity; + } + + @Override + public Severity getSeverity(Inspection inspection, View view) { + return severity; + } + + @Override + public Severity getSeverity(Inspection inspection, ElementStyle elementStyle) { + return severity; + } + + @Override + public Severity getSeverity(Inspection inspection, Model model) { + return severity; + } + + @Override + public Severity getSeverity(Inspection inspection, Element element) { + return severity; + } + + @Override + public Severity getSeverity(Inspection inspection, Relationship relationship) { + return severity; + } + +} \ No newline at end of file diff --git a/structurizr-inspection/src/main/java/com/structurizr/inspection/Inspection.java b/structurizr-inspection/src/main/java/com/structurizr/inspection/Inspection.java new file mode 100644 index 000000000..4b4c66409 --- /dev/null +++ b/structurizr-inspection/src/main/java/com/structurizr/inspection/Inspection.java @@ -0,0 +1,31 @@ +package com.structurizr.inspection; + +import com.structurizr.Workspace; + +public abstract class Inspection { + + private final Inspector inspector; + + protected Inspection(Inspector inspector) { + this.inspector = inspector; + } + + protected abstract String getType(); + + public Inspector getInspector() { + return inspector; + } + + protected Workspace getWorkspace() { + return inspector.getWorkspace(); + } + + protected Violation noViolation() { + return null; + } + + protected Violation violation(String description) { + return new Violation(this, description); + } + +} \ No newline at end of file diff --git a/structurizr-inspection/src/main/java/com/structurizr/inspection/Inspector.java b/structurizr-inspection/src/main/java/com/structurizr/inspection/Inspector.java new file mode 100644 index 000000000..5df5fea46 --- /dev/null +++ b/structurizr-inspection/src/main/java/com/structurizr/inspection/Inspector.java @@ -0,0 +1,41 @@ +package com.structurizr.inspection; + +import com.structurizr.Workspace; + +import java.util.ArrayList; +import java.util.List; + +public abstract class Inspector { + + private final Workspace workspace; + + private final List<Violation> violations = new ArrayList<>(); + private int numberOfInspections = 0; + + protected Inspector(Workspace workspace) { + this.workspace = workspace; + } + + public Workspace getWorkspace() { + return workspace; + } + + public List<Violation> getViolations() { + return new ArrayList<>(violations); + } + + public int getNumberOfInspections() { + return numberOfInspections; + } + + protected void add(Violation violation) { + numberOfInspections++; + + if (violation != null) { + violations.add(violation); + } + } + + public abstract SeverityStrategy getSeverityStrategy(); + +} \ No newline at end of file diff --git a/structurizr-inspection/src/main/java/com/structurizr/inspection/PropertyBasedSeverityStrategy.java b/structurizr-inspection/src/main/java/com/structurizr/inspection/PropertyBasedSeverityStrategy.java new file mode 100644 index 000000000..0483cba69 --- /dev/null +++ b/structurizr-inspection/src/main/java/com/structurizr/inspection/PropertyBasedSeverityStrategy.java @@ -0,0 +1,150 @@ +package com.structurizr.inspection; + +import com.structurizr.PropertyHolder; +import com.structurizr.Workspace; +import com.structurizr.model.Element; +import com.structurizr.model.Model; +import com.structurizr.model.Relationship; +import com.structurizr.util.StringUtils; +import com.structurizr.view.ElementStyle; +import com.structurizr.view.View; +import com.structurizr.view.ViewSet; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +public class PropertyBasedSeverityStrategy implements SeverityStrategy { + + private static final String STRUCTURIZR_INSPECTION_PREFIX = "structurizr.inspection."; + + public PropertyBasedSeverityStrategy() { + } + + @Override + public Severity getSeverity(Inspection inspection, Workspace workspace) { + Severity severity = getSeverityFromProperties(inspection.getType(), workspace); + return severity != null ? severity : Severity.ERROR; + } + + @Override + public Severity getSeverity(Inspection inspection, ViewSet viewSet) { + Severity severity = getSeverityFromProperties(inspection.getType(), inspection.getWorkspace(), viewSet.getConfiguration()); + return severity != null ? severity : Severity.ERROR; + } + + @Override + public Severity getSeverity(Inspection inspection, View view) { + Severity severity = getSeverityFromProperties(inspection.getType(), inspection.getWorkspace(), inspection.getWorkspace().getViews().getConfiguration(), view); + return severity != null ? severity : Severity.ERROR; + } + + @Override + public Severity getSeverity(Inspection inspection, ElementStyle elementStyle) { + Severity severity = getSeverityFromProperties(inspection.getType(), inspection.getWorkspace(), inspection.getWorkspace().getViews().getConfiguration(), elementStyle); + return severity != null ? severity : Severity.ERROR; + } + + @Override + public Severity getSeverity(Inspection inspection, Model model) { + Severity severity = getSeverityFromProperties(inspection.getType(), inspection.getWorkspace(), model); + return severity != null ? severity : Severity.ERROR; + } + + @Override + public Severity getSeverity(Inspection inspection, Element element) { + Element parentElement = element.getParent(); + Element grandParentElement = null; + if (parentElement != null) { + grandParentElement = parentElement.getParent(); + } + + Severity severity = getSeverityFromProperties(inspection.getType(), inspection.getWorkspace(), inspection.getWorkspace().getModel(), grandParentElement, parentElement, element); + return severity != null ? severity : Severity.ERROR; + } + + @Override + public Severity getSeverity(Inspection inspection, Relationship relationship) { + Element source = relationship.getSource(); + Relationship linkedRelationship = null; + if (!StringUtils.isNullOrEmpty(relationship.getLinkedRelationshipId())) { + linkedRelationship = inspection.getWorkspace().getModel().getRelationship(relationship.getLinkedRelationshipId()); + } + + String allRelationshipsType = inspection.getType(); + String specificRelationshipType = inspection.getType(); + + // convert model.relationship.description to model.relationship[sourceType->destinationType].description + String sourceType = relationship.getSource().getClass().getSimpleName().toLowerCase(); + String destinationType = relationship.getDestination().getClass().getSimpleName().toLowerCase(); + specificRelationshipType = allRelationshipsType.replaceFirst( + "\\.relationship\\.", + String.format(".relationship[%s->%s].", sourceType, destinationType) + ); + + Severity severity = getSeverityFromProperties(specificRelationshipType, inspection.getWorkspace(), inspection.getWorkspace().getModel(), source.getParent(), source, linkedRelationship, relationship); + if (severity == null) { + severity = getSeverityFromProperties(allRelationshipsType, inspection.getWorkspace(), inspection.getWorkspace().getModel(), source.getParent(), source, linkedRelationship, relationship); + } + + return severity != null ? severity : Severity.ERROR; + } + + protected Severity getSeverityFromProperties(String type, PropertyHolder... propertyHolders) { + List<String> types = generatePropertyNames(type); + List<PropertyHolder> reversedPropertyHolders = Arrays.asList(propertyHolders); + Collections.reverse(reversedPropertyHolders); + + for (PropertyHolder propertyHolder : reversedPropertyHolders) { + if (propertyHolder != null) { + for (String t : types) { + if (propertyHolder.getProperties().containsKey(t)) { + return Severity.valueOf(propertyHolder.getProperties().get(t).toUpperCase()); + } + } + } + } + + return null; + } + + protected List<String> generatePropertyNames(String type) { + // example input: + // model.component.description + // + // example output: + // structurizr.inspection.model.component.description + // structurizr.inspection.model.component.* + // structurizr.inspection.model.* + // structurizr.inspection.* + + // example input: + // model.relationship[component->component].technology + // + // example output: + // structurizr.inspection.model.relationship[component->component].technology + // structurizr.inspection.model.relationship[component->component].* + + type = type.toLowerCase(); + List<String> types = new ArrayList<>(); + + String[] parts = type.split("\\."); + String buf = STRUCTURIZR_INSPECTION_PREFIX; + types.add(buf + "*"); + for (int i = 0; i < parts.length-1; i++) { + buf = buf + parts[i] + "."; + types.add(buf + "*"); + } + + types.add(STRUCTURIZR_INSPECTION_PREFIX + type); + Collections.reverse(types); + + if (type.contains(".relationship[")) { + return types.subList(0, types.size()-2); + } + + return types; + } + +} diff --git a/structurizr-inspection/src/main/java/com/structurizr/inspection/Severity.java b/structurizr-inspection/src/main/java/com/structurizr/inspection/Severity.java new file mode 100644 index 000000000..3b86141b5 --- /dev/null +++ b/structurizr-inspection/src/main/java/com/structurizr/inspection/Severity.java @@ -0,0 +1,10 @@ +package com.structurizr.inspection; + +public enum Severity { + + ERROR, + WARNING, + INFO, + IGNORE + +} \ No newline at end of file diff --git a/structurizr-inspection/src/main/java/com/structurizr/inspection/SeverityStrategy.java b/structurizr-inspection/src/main/java/com/structurizr/inspection/SeverityStrategy.java new file mode 100644 index 000000000..4a037054c --- /dev/null +++ b/structurizr-inspection/src/main/java/com/structurizr/inspection/SeverityStrategy.java @@ -0,0 +1,27 @@ +package com.structurizr.inspection; + +import com.structurizr.Workspace; +import com.structurizr.model.Element; +import com.structurizr.model.Model; +import com.structurizr.model.Relationship; +import com.structurizr.view.ElementStyle; +import com.structurizr.view.View; +import com.structurizr.view.ViewSet; + +public interface SeverityStrategy { + + Severity getSeverity(Inspection inspection, Workspace workspace); + + Severity getSeverity(Inspection inspection, ViewSet viewSet); + + Severity getSeverity(Inspection inspection, View view); + + Severity getSeverity(Inspection inspection, ElementStyle elementStyle); + + Severity getSeverity(Inspection inspection, Model model); + + Severity getSeverity(Inspection inspection, Element element); + + Severity getSeverity(Inspection inspection, Relationship relationship); + +} diff --git a/structurizr-inspection/src/main/java/com/structurizr/inspection/Violation.java b/structurizr-inspection/src/main/java/com/structurizr/inspection/Violation.java new file mode 100644 index 000000000..082577689 --- /dev/null +++ b/structurizr-inspection/src/main/java/com/structurizr/inspection/Violation.java @@ -0,0 +1,37 @@ +package com.structurizr.inspection; + +public final class Violation { + + private Inspection inspection; + private Severity severity; + private final String message; + + Violation(Inspection inspection, String message) { + this.inspection = inspection; + this.message = message; + } + + public String getType() { + return inspection.getType(); + } + + public Severity getSeverity() { + return severity; + } + + public String getMessage() { + return message; + } + + @Override + public String toString() { + return inspection.getType() + " | " + severity + " | " + message; + } + + public Violation withSeverity(Severity severity) { + this.severity = severity; + + return this; + } + +} \ No newline at end of file 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 new file mode 100644 index 000000000..7b2eb945d --- /dev/null +++ b/structurizr-inspection/src/main/java/com/structurizr/inspection/documentation/AbstractDocumentableInspection.java @@ -0,0 +1,90 @@ +package com.structurizr.inspection.documentation; + +import com.structurizr.Workspace; +import com.structurizr.documentation.*; +import com.structurizr.inspection.Inspection; +import com.structurizr.inspection.Inspector; +import com.structurizr.inspection.Severity; +import com.structurizr.inspection.Violation; +import com.structurizr.model.Element; + +import java.util.LinkedHashSet; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public abstract class AbstractDocumentableInspection extends Inspection { + + private static final Pattern MARKDOWN_EMBED = Pattern.compile("!\\[.*?]\\(embed:(.+?)\\)"); + private static final Pattern ASCIIDOC_EMBED = Pattern.compile("image::embed:(.+?)\\[]"); + + public AbstractDocumentableInspection(Inspector inspector) { + super(inspector); + } + + public final Violation run(Documentable documentable) { + Severity severity; + if (documentable instanceof Workspace) { + severity = getInspector().getSeverityStrategy().getSeverity(this, (Workspace)documentable); + } else { + Element element = (Element)documentable; + severity = getInspector().getSeverityStrategy().getSeverity(this, element); + } + Violation violation = inspect(documentable); + + return violation == null ? null : violation.withSeverity(severity); + } + + protected abstract Violation inspect(Documentable documentable); + + protected Set<String> findEmbeddedViewKeys(Documentable documentable) { + Set<String> keys = new LinkedHashSet<>(); + + if (documentable.getDocumentation() != null) { + for (Section section : documentable.getDocumentation().getSections()) { + keys.addAll(findEmbeddedViewKeys(section)); + } + + for (Decision decision : documentable.getDocumentation().getDecisions()) { + keys.addAll(findEmbeddedViewKeys(decision)); + } + } + + return keys; + } + + private Set<String> findEmbeddedViewKeys(DocumentationContent content) { + Set<String> keys = new LinkedHashSet<>(); + + String[] lines = content.getContent().split("\n"); + for (String line : lines) { + if (content.getFormat() == Format.Markdown) { + // ![](embed:MyDiagramKey) + Matcher matcher = MARKDOWN_EMBED.matcher(line); + if (matcher.matches()) { + String key = matcher.group(1); + keys.add(key); + } + } else if (content.getFormat() == Format.AsciiDoc) { + // image::embed:MyDiagramKey[] + Matcher matcher = ASCIIDOC_EMBED.matcher(line); + if (matcher.matches()) { + String key = matcher.group(1); + keys.add(key); + } + } + } + + return keys; + } + + protected String terminologyFor(Element element) { + return getWorkspace().getViews().getConfiguration().getTerminology().findTerminology(element).toLowerCase(); + } + + protected String nameOf(Element element) { + String canonicalName = element.getCanonicalName(); + return canonicalName.substring(canonicalName.indexOf("://") + 3); + } + +} \ No newline at end of file diff --git a/structurizr-inspection/src/main/java/com/structurizr/inspection/documentation/EmbeddedViewMissingInspection.java b/structurizr-inspection/src/main/java/com/structurizr/inspection/documentation/EmbeddedViewMissingInspection.java new file mode 100644 index 000000000..de1259aed --- /dev/null +++ b/structurizr-inspection/src/main/java/com/structurizr/inspection/documentation/EmbeddedViewMissingInspection.java @@ -0,0 +1,52 @@ +package com.structurizr.inspection.documentation; + +import com.structurizr.Workspace; +import com.structurizr.documentation.Documentable; +import com.structurizr.documentation.Format; +import com.structurizr.documentation.Section; +import com.structurizr.inspection.Inspector; +import com.structurizr.inspection.Violation; +import com.structurizr.model.Element; +import com.structurizr.view.View; + +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class EmbeddedViewMissingInspection extends AbstractDocumentableInspection { + + public EmbeddedViewMissingInspection(Inspector inspector) { + super(inspector); + } + + protected Violation inspect(Documentable documentable) { + Set<String> keys = findEmbeddedViewKeys(documentable); + Set<String> missingViews = new LinkedHashSet<>(); + + for (String key : keys) { + View view = getWorkspace().getViews().getViewWithKey(key); + if (view == null) { + missingViews.add(key); + } + } + + if (!missingViews.isEmpty()) { + if (documentable instanceof Workspace) { + return violation("The following views are embedded into documentation for the workspace but do not exist in the workspace: " + String.join(", ", missingViews)); + } else if (documentable instanceof Element) { + Element element = (Element)documentable; + return violation("The following views are embedded into documentation for the " + terminologyFor(element).toLowerCase() + " \"" + nameOf(element) + "\" but do not exist in the workspace: " + String.join(", ", missingViews)); + } + } + + return noViolation(); + } + + @Override + protected String getType() { + return "documentation.embeddedview"; + } + +} \ No newline at end of file diff --git a/structurizr-inspection/src/main/java/com/structurizr/inspection/documentation/EmbeddedViewWithGeneratedKeyInspection.java b/structurizr-inspection/src/main/java/com/structurizr/inspection/documentation/EmbeddedViewWithGeneratedKeyInspection.java new file mode 100644 index 000000000..363f8efe3 --- /dev/null +++ b/structurizr-inspection/src/main/java/com/structurizr/inspection/documentation/EmbeddedViewWithGeneratedKeyInspection.java @@ -0,0 +1,49 @@ +package com.structurizr.inspection.documentation; + +import com.structurizr.Workspace; +import com.structurizr.documentation.Documentable; +import com.structurizr.inspection.Inspector; +import com.structurizr.inspection.Violation; +import com.structurizr.model.Element; +import com.structurizr.view.View; + +import java.util.LinkedHashSet; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class EmbeddedViewWithGeneratedKeyInspection extends AbstractDocumentableInspection { + + public EmbeddedViewWithGeneratedKeyInspection(Inspector inspector) { + super(inspector); + } + + protected Violation inspect(Documentable documentable) { + Set<String> keys = findEmbeddedViewKeys(documentable); + Set<String> viewsWithGeneratedKeys = new LinkedHashSet<>(); + + for (String key : keys) { + View view = getWorkspace().getViews().getViewWithKey(key); + if (view != null && view.isGeneratedKey()) { + viewsWithGeneratedKeys.add(key); + } + } + + if (!viewsWithGeneratedKeys.isEmpty()) { + if (documentable instanceof Workspace) { + return violation("The following views are embedded into documentation for the workspace via an automatically generated view key: " + String.join(", ", viewsWithGeneratedKeys)); + } else if (documentable instanceof Element) { + Element element = (Element)documentable; + return violation("The following views are embedded into documentation for the " + terminologyFor(element).toLowerCase() + " \"" + nameOf(element) + "\" via an automatically generated view key: " + String.join(", ", viewsWithGeneratedKeys)); + } + } + + return noViolation(); + } + + @Override + protected String getType() { + return "documentation.embeddedview"; + } + +} \ No newline at end of file diff --git a/structurizr-inspection/src/main/java/com/structurizr/inspection/model/AbstractComponentInspection.java b/structurizr-inspection/src/main/java/com/structurizr/inspection/model/AbstractComponentInspection.java new file mode 100644 index 000000000..08edc0863 --- /dev/null +++ b/structurizr-inspection/src/main/java/com/structurizr/inspection/model/AbstractComponentInspection.java @@ -0,0 +1,21 @@ +package com.structurizr.inspection.model; + +import com.structurizr.inspection.Inspector; +import com.structurizr.inspection.Violation; +import com.structurizr.model.Component; +import com.structurizr.model.Element; + +abstract class AbstractComponentInspection extends AbstractElementInspection { + + public AbstractComponentInspection(Inspector inspector) { + super(inspector); + } + + @Override + protected final Violation inspect(Element element) { + return inspect((Component)element); + } + + protected abstract Violation inspect(Component component); + +} \ No newline at end of file diff --git a/structurizr-inspection/src/main/java/com/structurizr/inspection/model/AbstractContainerInspection.java b/structurizr-inspection/src/main/java/com/structurizr/inspection/model/AbstractContainerInspection.java new file mode 100644 index 000000000..7ec4e579a --- /dev/null +++ b/structurizr-inspection/src/main/java/com/structurizr/inspection/model/AbstractContainerInspection.java @@ -0,0 +1,21 @@ +package com.structurizr.inspection.model; + +import com.structurizr.inspection.Inspector; +import com.structurizr.inspection.Violation; +import com.structurizr.model.Container; +import com.structurizr.model.Element; + +abstract class AbstractContainerInspection extends AbstractElementInspection { + + public AbstractContainerInspection(Inspector inspector) { + super(inspector); + } + + @Override + protected final Violation inspect(Element element) { + return inspect((Container)element); + } + + protected abstract Violation inspect(Container container); + +} \ No newline at end of file diff --git a/structurizr-inspection/src/main/java/com/structurizr/inspection/model/AbstractDeploymentNodeInspection.java b/structurizr-inspection/src/main/java/com/structurizr/inspection/model/AbstractDeploymentNodeInspection.java new file mode 100644 index 000000000..74fa9958c --- /dev/null +++ b/structurizr-inspection/src/main/java/com/structurizr/inspection/model/AbstractDeploymentNodeInspection.java @@ -0,0 +1,21 @@ +package com.structurizr.inspection.model; + +import com.structurizr.inspection.Inspector; +import com.structurizr.inspection.Violation; +import com.structurizr.model.DeploymentNode; +import com.structurizr.model.Element; + +abstract class AbstractDeploymentNodeInspection extends AbstractElementInspection { + + public AbstractDeploymentNodeInspection(Inspector inspector) { + super(inspector); + } + + @Override + protected Violation inspect(Element element) { + return inspect((DeploymentNode)element); + } + + protected abstract Violation inspect(DeploymentNode deploymentNode); + +} \ No newline at end of file diff --git a/structurizr-inspection/src/main/java/com/structurizr/inspection/model/AbstractElementInspection.java b/structurizr-inspection/src/main/java/com/structurizr/inspection/model/AbstractElementInspection.java new file mode 100644 index 000000000..7a895d79d --- /dev/null +++ b/structurizr-inspection/src/main/java/com/structurizr/inspection/model/AbstractElementInspection.java @@ -0,0 +1,33 @@ +package com.structurizr.inspection.model; + +import com.structurizr.inspection.Inspection; +import com.structurizr.inspection.Inspector; +import com.structurizr.inspection.Severity; +import com.structurizr.inspection.Violation; +import com.structurizr.model.Element; + +abstract class AbstractElementInspection extends Inspection { + + public AbstractElementInspection(Inspector inspector) { + super(inspector); + } + + public final Violation run(Element element) { + Severity severity = getInspector().getSeverityStrategy().getSeverity(this, element); + Violation violation = inspect(element); + + return violation == null ? null : violation.withSeverity(severity); + } + + protected String terminologyFor(Element element) { + return getWorkspace().getViews().getConfiguration().getTerminology().findTerminology(element).toLowerCase(); + } + + protected String nameOf(Element element) { + String canonicalName = element.getCanonicalName(); + return canonicalName.substring(canonicalName.indexOf("://") + 3); + } + + protected abstract Violation inspect(Element element); + +} \ No newline at end of file diff --git a/structurizr-inspection/src/main/java/com/structurizr/inspection/model/AbstractInfrastructureNodeInspection.java b/structurizr-inspection/src/main/java/com/structurizr/inspection/model/AbstractInfrastructureNodeInspection.java new file mode 100644 index 000000000..51f08cd61 --- /dev/null +++ b/structurizr-inspection/src/main/java/com/structurizr/inspection/model/AbstractInfrastructureNodeInspection.java @@ -0,0 +1,21 @@ +package com.structurizr.inspection.model; + +import com.structurizr.inspection.Inspector; +import com.structurizr.inspection.Violation; +import com.structurizr.model.Element; +import com.structurizr.model.InfrastructureNode; + +abstract class AbstractInfrastructureNodeInspection extends AbstractElementInspection { + + public AbstractInfrastructureNodeInspection(Inspector inspector) { + super(inspector); + } + + @Override + protected Violation inspect(Element element) { + return inspect((InfrastructureNode)element); + } + + protected abstract Violation inspect(InfrastructureNode infrastructureNode); + +} \ No newline at end of file diff --git a/structurizr-inspection/src/main/java/com/structurizr/inspection/model/AbstractModelInspection.java b/structurizr-inspection/src/main/java/com/structurizr/inspection/model/AbstractModelInspection.java new file mode 100644 index 000000000..e5a28ea25 --- /dev/null +++ b/structurizr-inspection/src/main/java/com/structurizr/inspection/model/AbstractModelInspection.java @@ -0,0 +1,24 @@ +package com.structurizr.inspection.model; + +import com.structurizr.Workspace; +import com.structurizr.inspection.Inspection; +import com.structurizr.inspection.Inspector; +import com.structurizr.inspection.Severity; +import com.structurizr.inspection.Violation; + +abstract class AbstractModelInspection extends Inspection { + + public AbstractModelInspection(Inspector inspector) { + super(inspector); + } + + public final Violation run() { + Severity severity = getInspector().getSeverityStrategy().getSeverity(this, getWorkspace().getModel()); + Violation violation = inspect(getWorkspace()); + + return violation == null ? null : violation.withSeverity(severity); + } + + protected abstract Violation inspect(Workspace workspace); + +} \ No newline at end of file diff --git a/structurizr-inspection/src/main/java/com/structurizr/inspection/model/AbstractRelationshipInspection.java b/structurizr-inspection/src/main/java/com/structurizr/inspection/model/AbstractRelationshipInspection.java new file mode 100644 index 000000000..d84da68f8 --- /dev/null +++ b/structurizr-inspection/src/main/java/com/structurizr/inspection/model/AbstractRelationshipInspection.java @@ -0,0 +1,34 @@ +package com.structurizr.inspection.model; + +import com.structurizr.inspection.Inspection; +import com.structurizr.inspection.Inspector; +import com.structurizr.inspection.Severity; +import com.structurizr.inspection.Violation; +import com.structurizr.model.Element; +import com.structurizr.model.Relationship; + +public abstract class AbstractRelationshipInspection extends Inspection { + + public AbstractRelationshipInspection(Inspector inspector) { + super(inspector); + } + + public final Violation run(Relationship relationship) { + Severity severity = getInspector().getSeverityStrategy().getSeverity(this, relationship); + Violation violation = inspect(relationship); + + return violation == null ? null : violation.withSeverity(severity); + } + + protected String terminologyFor(Element element) { + return getWorkspace().getViews().getConfiguration().getTerminology().findTerminology(element).toLowerCase(); + } + + protected String nameOf(Element element) { + String canonicalName = element.getCanonicalName(); + return canonicalName.substring(canonicalName.indexOf("://") + 3); + } + + protected abstract Violation inspect(Relationship relationship); + +} \ No newline at end of file diff --git a/structurizr-inspection/src/main/java/com/structurizr/inspection/model/AbstractSoftwareSystemInspection.java b/structurizr-inspection/src/main/java/com/structurizr/inspection/model/AbstractSoftwareSystemInspection.java new file mode 100644 index 000000000..188349479 --- /dev/null +++ b/structurizr-inspection/src/main/java/com/structurizr/inspection/model/AbstractSoftwareSystemInspection.java @@ -0,0 +1,21 @@ +package com.structurizr.inspection.model; + +import com.structurizr.inspection.Inspector; +import com.structurizr.inspection.Violation; +import com.structurizr.model.Element; +import com.structurizr.model.SoftwareSystem; + +abstract class AbstractSoftwareSystemInspection extends AbstractElementInspection { + + public AbstractSoftwareSystemInspection(Inspector inspector) { + super(inspector); + } + + @Override + protected final Violation inspect(Element element) { + return inspect((SoftwareSystem)element); + } + + protected abstract Violation inspect(SoftwareSystem softwareSystem); + +} \ No newline at end of file diff --git a/structurizr-inspection/src/main/java/com/structurizr/inspection/model/ComponentDescriptionInspection.java b/structurizr-inspection/src/main/java/com/structurizr/inspection/model/ComponentDescriptionInspection.java new file mode 100644 index 000000000..cb1eb32d6 --- /dev/null +++ b/structurizr-inspection/src/main/java/com/structurizr/inspection/model/ComponentDescriptionInspection.java @@ -0,0 +1,16 @@ +package com.structurizr.inspection.model; + +import com.structurizr.inspection.Inspector; + +public class ComponentDescriptionInspection extends ElementDescriptionInspection { + + public ComponentDescriptionInspection(Inspector inspector) { + super(inspector); + } + + @Override + protected String getType() { + return "model.component.description"; + } + +} \ No newline at end of file diff --git a/structurizr-inspection/src/main/java/com/structurizr/inspection/model/ComponentTechnologyInspection.java b/structurizr-inspection/src/main/java/com/structurizr/inspection/model/ComponentTechnologyInspection.java new file mode 100644 index 000000000..04cd65312 --- /dev/null +++ b/structurizr-inspection/src/main/java/com/structurizr/inspection/model/ComponentTechnologyInspection.java @@ -0,0 +1,28 @@ +package com.structurizr.inspection.model; + +import com.structurizr.inspection.Inspector; +import com.structurizr.inspection.Violation; +import com.structurizr.model.Component; +import com.structurizr.util.StringUtils; + +public class ComponentTechnologyInspection extends AbstractComponentInspection { + + public ComponentTechnologyInspection(Inspector inspector) { + super(inspector); + } + + @Override + protected Violation inspect(Component component) { + if (StringUtils.isNullOrEmpty(component.getDescription())) { + return violation("The " + terminologyFor(component).toLowerCase() + " \"" + nameOf(component) + "\" is missing a technology."); + } + + return noViolation(); + } + + @Override + protected String getType() { + return "model.component.technology"; + } + +} \ No newline at end of file diff --git a/structurizr-inspection/src/main/java/com/structurizr/inspection/model/ContainerDescriptionInspection.java b/structurizr-inspection/src/main/java/com/structurizr/inspection/model/ContainerDescriptionInspection.java new file mode 100644 index 000000000..0efcecc23 --- /dev/null +++ b/structurizr-inspection/src/main/java/com/structurizr/inspection/model/ContainerDescriptionInspection.java @@ -0,0 +1,16 @@ +package com.structurizr.inspection.model; + +import com.structurizr.inspection.Inspector; + +public class ContainerDescriptionInspection extends ElementDescriptionInspection { + + public ContainerDescriptionInspection(Inspector inspector) { + super(inspector); + } + + @Override + protected String getType() { + return "model.container.description"; + } + +} \ No newline at end of file diff --git a/structurizr-inspection/src/main/java/com/structurizr/inspection/model/ContainerTechnologyInspection.java b/structurizr-inspection/src/main/java/com/structurizr/inspection/model/ContainerTechnologyInspection.java new file mode 100644 index 000000000..2c8efd2a2 --- /dev/null +++ b/structurizr-inspection/src/main/java/com/structurizr/inspection/model/ContainerTechnologyInspection.java @@ -0,0 +1,28 @@ +package com.structurizr.inspection.model; + +import com.structurizr.inspection.Inspector; +import com.structurizr.inspection.Violation; +import com.structurizr.model.Container; +import com.structurizr.util.StringUtils; + +public class ContainerTechnologyInspection extends AbstractContainerInspection { + + public ContainerTechnologyInspection(Inspector inspector) { + super(inspector); + } + + @Override + protected Violation inspect(Container container) { + if (StringUtils.isNullOrEmpty(container.getTechnology())) { + return violation("The " + terminologyFor(container).toLowerCase() + " \"" + nameOf(container) + "\" is missing a technology."); + } + + return noViolation(); + } + + @Override + protected String getType() { + return "model.container.technology"; + } + +} \ No newline at end of file diff --git a/structurizr-inspection/src/main/java/com/structurizr/inspection/model/DeploymentNodeDescriptionInspection.java b/structurizr-inspection/src/main/java/com/structurizr/inspection/model/DeploymentNodeDescriptionInspection.java new file mode 100644 index 000000000..c94c03567 --- /dev/null +++ b/structurizr-inspection/src/main/java/com/structurizr/inspection/model/DeploymentNodeDescriptionInspection.java @@ -0,0 +1,16 @@ +package com.structurizr.inspection.model; + +import com.structurizr.inspection.Inspector; + +public class DeploymentNodeDescriptionInspection extends ElementDescriptionInspection { + + public DeploymentNodeDescriptionInspection(Inspector inspector) { + super(inspector); + } + + @Override + protected String getType() { + return "model.deploymentnode.description"; + } + +} \ No newline at end of file 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 new file mode 100644 index 000000000..8919c90df --- /dev/null +++ b/structurizr-inspection/src/main/java/com/structurizr/inspection/model/DeploymentNodeTechnologyInspection.java @@ -0,0 +1,28 @@ +package com.structurizr.inspection.model; + +import com.structurizr.inspection.Inspector; +import com.structurizr.inspection.Violation; +import com.structurizr.model.DeploymentNode; +import com.structurizr.util.StringUtils; + +public class DeploymentNodeTechnologyInspection extends AbstractDeploymentNodeInspection { + + public DeploymentNodeTechnologyInspection(Inspector inspector) { + super(inspector); + } + + @Override + protected Violation inspect(DeploymentNode deploymentNode) { + if (StringUtils.isNullOrEmpty(deploymentNode.getTechnology())) { + return violation("The " + terminologyFor(deploymentNode).toLowerCase() + " \"" + nameOf(deploymentNode) + "\" is missing a technology."); + } + + return noViolation(); + } + + @Override + protected String getType() { + return "model.deploymentnode.technology"; + } + +} \ No newline at end of file diff --git a/structurizr-inspection/src/main/java/com/structurizr/inspection/model/DisconnectedElementInspection.java b/structurizr-inspection/src/main/java/com/structurizr/inspection/model/DisconnectedElementInspection.java new file mode 100644 index 000000000..328c2deb0 --- /dev/null +++ b/structurizr-inspection/src/main/java/com/structurizr/inspection/model/DisconnectedElementInspection.java @@ -0,0 +1,44 @@ +package com.structurizr.inspection.model; + +import com.structurizr.inspection.Inspector; +import com.structurizr.inspection.Violation; +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 DisconnectedElementInspection extends AbstractElementInspection { + + private final Set<String> elementsWithRelationships = new HashSet<>(); + + public DisconnectedElementInspection(Inspector inspector) { + super(inspector); + + for (Relationship relationship : getWorkspace().getModel().getRelationships()) { + elementsWithRelationships.add(relationship.getSourceId()); + elementsWithRelationships.add(relationship.getDestinationId()); + } + } + + @Override + protected Violation inspect(Element element) { + if (element instanceof DeploymentNode) { + // deployment nodes typically won't have relationships to/from them + return noViolation(); + } + + if (!elementsWithRelationships.contains(element.getId())) { + return violation("The " + terminologyFor(element).toLowerCase() + " \"" + nameOf(element) + "\" is disconnected - add a relationship to/from it, or consider removing it from the model."); + } + + return noViolation(); + } + + @Override + protected String getType() { + return "model.element.disconnected"; + } + +} \ No newline at end of file diff --git a/structurizr-inspection/src/main/java/com/structurizr/inspection/model/ElementDescriptionInspection.java b/structurizr-inspection/src/main/java/com/structurizr/inspection/model/ElementDescriptionInspection.java new file mode 100644 index 000000000..b981ab01b --- /dev/null +++ b/structurizr-inspection/src/main/java/com/structurizr/inspection/model/ElementDescriptionInspection.java @@ -0,0 +1,23 @@ +package com.structurizr.inspection.model; + +import com.structurizr.inspection.Inspector; +import com.structurizr.inspection.Violation; +import com.structurizr.model.Element; +import com.structurizr.util.StringUtils; + +abstract class ElementDescriptionInspection extends AbstractElementInspection { + + public ElementDescriptionInspection(Inspector inspector) { + super(inspector); + } + + @Override + protected Violation inspect(Element element) { + if (StringUtils.isNullOrEmpty(element.getDescription())) { + return violation("The " + terminologyFor(element).toLowerCase() + " \"" + nameOf(element) + "\" is missing a description."); + } + + return noViolation(); + } + +} \ No newline at end of file diff --git a/structurizr-inspection/src/main/java/com/structurizr/inspection/model/ElementNotIncludedInAnyViewsInspection.java b/structurizr-inspection/src/main/java/com/structurizr/inspection/model/ElementNotIncludedInAnyViewsInspection.java new file mode 100644 index 000000000..a53857c1c --- /dev/null +++ b/structurizr-inspection/src/main/java/com/structurizr/inspection/model/ElementNotIncludedInAnyViewsInspection.java @@ -0,0 +1,44 @@ +package com.structurizr.inspection.model; + +import com.structurizr.inspection.Inspector; +import com.structurizr.inspection.Violation; +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 AbstractElementInspection { + + private final Set<String> elementsInViews = new HashSet<>(); + + public ElementNotIncludedInAnyViewsInspection(Inspector inspector) { + super(inspector); + + for (View view : getWorkspace().getViews().getViews()) { + if (view instanceof ModelView) { + ModelView modelView = (ModelView)view; + for (ElementView elementView : modelView.getElements()) { + elementsInViews.add(elementView.getId()); + } + } + } + } + + @Override + protected Violation inspect(Element element) { + if (!elementsInViews.contains(element.getId())) { + return violation("The " + terminologyFor(element) + " named \"" + element.getName() + "\" is not included on any views - add it to a view."); + } + + return noViolation(); + } + + @Override + protected String getType() { + return "model.element.noview"; + } + +} \ No newline at end of file diff --git a/structurizr-inspection/src/main/java/com/structurizr/inspection/model/EmptyDeploymentNodeInspection.java b/structurizr-inspection/src/main/java/com/structurizr/inspection/model/EmptyDeploymentNodeInspection.java new file mode 100644 index 000000000..207d6e48e --- /dev/null +++ b/structurizr-inspection/src/main/java/com/structurizr/inspection/model/EmptyDeploymentNodeInspection.java @@ -0,0 +1,27 @@ +package com.structurizr.inspection.model; + +import com.structurizr.inspection.Inspector; +import com.structurizr.inspection.Violation; +import com.structurizr.model.DeploymentNode; + +public class EmptyDeploymentNodeInspection extends AbstractDeploymentNodeInspection { + + public EmptyDeploymentNodeInspection(Inspector inspector) { + super(inspector); + } + + @Override + protected Violation inspect(DeploymentNode deploymentNode) { + if (!deploymentNode.hasChildren() && !deploymentNode.hasSoftwareSystemInstances() && !deploymentNode.hasContainerInstances() && !deploymentNode.hasInfrastructureNodes()) { + return violation("The " + terminologyFor(deploymentNode).toLowerCase() + " \"" + nameOf(deploymentNode) + "\" is empty."); + } + + return noViolation(); + } + + @Override + protected String getType() { + return "model.deploymentnode.empty"; + } + +} \ No newline at end of file diff --git a/structurizr-inspection/src/main/java/com/structurizr/inspection/model/EmptyModelInspection.java b/structurizr-inspection/src/main/java/com/structurizr/inspection/model/EmptyModelInspection.java new file mode 100644 index 000000000..4031f44b9 --- /dev/null +++ b/structurizr-inspection/src/main/java/com/structurizr/inspection/model/EmptyModelInspection.java @@ -0,0 +1,27 @@ +package com.structurizr.inspection.model; + +import com.structurizr.Workspace; +import com.structurizr.inspection.Inspector; +import com.structurizr.inspection.Violation; + +public class EmptyModelInspection extends AbstractModelInspection { + + public EmptyModelInspection(Inspector inspector) { + super(inspector); + } + + @Override + protected Violation inspect(Workspace workspace) { + if (workspace.getModel().isEmpty()) { + return violation("The model is empty."); + } + + return noViolation(); + } + + @Override + protected String getType() { + return "model.empty"; + } + +} \ No newline at end of file diff --git a/structurizr-inspection/src/main/java/com/structurizr/inspection/model/InfrastructureNodeDescriptionInspection.java b/structurizr-inspection/src/main/java/com/structurizr/inspection/model/InfrastructureNodeDescriptionInspection.java new file mode 100644 index 000000000..91ffca161 --- /dev/null +++ b/structurizr-inspection/src/main/java/com/structurizr/inspection/model/InfrastructureNodeDescriptionInspection.java @@ -0,0 +1,15 @@ +package com.structurizr.inspection.model; + +import com.structurizr.inspection.Inspector; + +public class InfrastructureNodeDescriptionInspection extends ElementDescriptionInspection { + + public InfrastructureNodeDescriptionInspection(Inspector inspector) { + super(inspector); + } + + protected String getType() { + return "model.infrastructurenode.description"; + } + +} \ No newline at end of file diff --git a/structurizr-inspection/src/main/java/com/structurizr/inspection/model/InfrastructureNodeTechnologyInspection.java b/structurizr-inspection/src/main/java/com/structurizr/inspection/model/InfrastructureNodeTechnologyInspection.java new file mode 100644 index 000000000..1365d2c50 --- /dev/null +++ b/structurizr-inspection/src/main/java/com/structurizr/inspection/model/InfrastructureNodeTechnologyInspection.java @@ -0,0 +1,28 @@ +package com.structurizr.inspection.model; + +import com.structurizr.inspection.Inspector; +import com.structurizr.inspection.Violation; +import com.structurizr.model.InfrastructureNode; +import com.structurizr.util.StringUtils; + +public class InfrastructureNodeTechnologyInspection extends AbstractInfrastructureNodeInspection { + + public InfrastructureNodeTechnologyInspection(Inspector inspector) { + super(inspector); + } + + @Override + protected Violation inspect(InfrastructureNode infrastructureNode) { + if (StringUtils.isNullOrEmpty(infrastructureNode.getTechnology())) { + return violation("The " + terminologyFor(infrastructureNode).toLowerCase() + " \"" + nameOf(infrastructureNode) + "\" is missing a technology."); + } + + return noViolation(); + } + + @Override + protected String getType() { + return "model.infrastructurenode.technology"; + } + +} \ No newline at end of file diff --git a/structurizr-inspection/src/main/java/com/structurizr/inspection/model/MultipleSoftwareSystemsDetailedInspection.java b/structurizr-inspection/src/main/java/com/structurizr/inspection/model/MultipleSoftwareSystemsDetailedInspection.java new file mode 100644 index 000000000..a3220a769 --- /dev/null +++ b/structurizr-inspection/src/main/java/com/structurizr/inspection/model/MultipleSoftwareSystemsDetailedInspection.java @@ -0,0 +1,35 @@ +package com.structurizr.inspection.model; + +import com.structurizr.Workspace; +import com.structurizr.inspection.Inspector; +import com.structurizr.inspection.Violation; +import com.structurizr.model.SoftwareSystem; + +public class MultipleSoftwareSystemsDetailedInspection extends AbstractModelInspection { + + public MultipleSoftwareSystemsDetailedInspection(Inspector inspector) { + super(inspector); + } + + @Override + protected Violation inspect(Workspace workspace) { + int softwareSystemsWithDetails = 0; + for (SoftwareSystem softwareSystem : workspace.getModel().getSoftwareSystems()) { + if (softwareSystem.hasContainers() || !softwareSystem.getDocumentation().isEmpty()) { + softwareSystemsWithDetails++; + } + } + + if (softwareSystemsWithDetails > 1) { + return violation("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 noViolation(); + } + + @Override + protected String getType() { + return "workspace.scope"; + } + +} \ No newline at end of file diff --git a/structurizr-inspection/src/main/java/com/structurizr/inspection/model/PersonDescriptionInspection.java b/structurizr-inspection/src/main/java/com/structurizr/inspection/model/PersonDescriptionInspection.java new file mode 100644 index 000000000..ed9ec4753 --- /dev/null +++ b/structurizr-inspection/src/main/java/com/structurizr/inspection/model/PersonDescriptionInspection.java @@ -0,0 +1,16 @@ +package com.structurizr.inspection.model; + +import com.structurizr.inspection.Inspector; + +public class PersonDescriptionInspection extends ElementDescriptionInspection { + + public PersonDescriptionInspection(Inspector inspector) { + super(inspector); + } + + @Override + protected String getType() { + return "model.person.description"; + } + +} \ No newline at end of file diff --git a/structurizr-inspection/src/main/java/com/structurizr/inspection/model/RelationshipDescriptionInspection.java b/structurizr-inspection/src/main/java/com/structurizr/inspection/model/RelationshipDescriptionInspection.java new file mode 100644 index 000000000..9159e29d9 --- /dev/null +++ b/structurizr-inspection/src/main/java/com/structurizr/inspection/model/RelationshipDescriptionInspection.java @@ -0,0 +1,33 @@ +package com.structurizr.inspection.model; + +import com.structurizr.inspection.Inspector; +import com.structurizr.inspection.Violation; +import com.structurizr.model.Component; +import com.structurizr.model.Element; +import com.structurizr.model.Relationship; +import com.structurizr.util.StringUtils; + +public class RelationshipDescriptionInspection extends AbstractRelationshipInspection { + + public RelationshipDescriptionInspection(Inspector inspector) { + super(inspector); + } + + @Override + protected Violation inspect(Relationship relationship) { + Element source = relationship.getSource(); + Element destination = relationship.getDestination(); + + if (StringUtils.isNullOrEmpty(relationship.getDescription())) { + return violation("The relationship between the " + terminologyFor(source) + " \"" + nameOf(source) + "\" and the " + terminologyFor(destination) + " \"" + nameOf(destination) + "\" is missing a description."); + } + + return noViolation(); + } + + @Override + protected String getType() { + return "model.relationship.description"; + } + +} \ No newline at end of file diff --git a/structurizr-inspection/src/main/java/com/structurizr/inspection/model/RelationshipTechnologyInspection.java b/structurizr-inspection/src/main/java/com/structurizr/inspection/model/RelationshipTechnologyInspection.java new file mode 100644 index 000000000..48a0bf2f7 --- /dev/null +++ b/structurizr-inspection/src/main/java/com/structurizr/inspection/model/RelationshipTechnologyInspection.java @@ -0,0 +1,33 @@ +package com.structurizr.inspection.model; + +import com.structurizr.inspection.Inspector; +import com.structurizr.inspection.Violation; +import com.structurizr.model.Container; +import com.structurizr.model.Element; +import com.structurizr.model.Relationship; +import com.structurizr.util.StringUtils; + +public class RelationshipTechnologyInspection extends AbstractRelationshipInspection { + + public RelationshipTechnologyInspection(Inspector inspector) { + super(inspector); + } + + @Override + protected Violation inspect(Relationship relationship) { + Element source = relationship.getSource(); + Element destination = relationship.getDestination(); + + if (StringUtils.isNullOrEmpty(relationship.getTechnology())) { + return violation("The relationship between the " + terminologyFor(source) + " \"" + nameOf(source) + "\" and the " + terminologyFor(destination) + " \"" + nameOf(destination) + "\" is missing a technology."); + } + + return noViolation(); + } + + @Override + protected String getType() { + return "model.relationship.technology"; + } + +} \ No newline at end of file diff --git a/structurizr-inspection/src/main/java/com/structurizr/inspection/model/SoftwareSystemDecisionsInspection.java b/structurizr-inspection/src/main/java/com/structurizr/inspection/model/SoftwareSystemDecisionsInspection.java new file mode 100644 index 000000000..890c579f4 --- /dev/null +++ b/structurizr-inspection/src/main/java/com/structurizr/inspection/model/SoftwareSystemDecisionsInspection.java @@ -0,0 +1,27 @@ +package com.structurizr.inspection.model; + +import com.structurizr.inspection.Inspector; +import com.structurizr.inspection.Violation; +import com.structurizr.model.SoftwareSystem; + +public class SoftwareSystemDecisionsInspection extends AbstractSoftwareSystemInspection { + + public SoftwareSystemDecisionsInspection(Inspector inspector) { + super(inspector); + } + + @Override + protected Violation inspect(SoftwareSystem softwareSystem) { + if (softwareSystem.hasContainers() && softwareSystem.getDocumentation().getDecisions().isEmpty()) { + return violation("The " + terminologyFor(softwareSystem).toLowerCase() + " \"" + nameOf(softwareSystem) + "\" has containers, but is missing decisions."); + } + + return noViolation(); + } + + @Override + protected String getType() { + return "model.softwaresystem.decisions"; + } + +} \ No newline at end of file diff --git a/structurizr-inspection/src/main/java/com/structurizr/inspection/model/SoftwareSystemDescriptionInspection.java b/structurizr-inspection/src/main/java/com/structurizr/inspection/model/SoftwareSystemDescriptionInspection.java new file mode 100644 index 000000000..0ea96e233 --- /dev/null +++ b/structurizr-inspection/src/main/java/com/structurizr/inspection/model/SoftwareSystemDescriptionInspection.java @@ -0,0 +1,16 @@ +package com.structurizr.inspection.model; + +import com.structurizr.inspection.Inspector; + +public class SoftwareSystemDescriptionInspection extends ElementDescriptionInspection { + + public SoftwareSystemDescriptionInspection(Inspector inspector) { + super(inspector); + } + + @Override + protected String getType() { + return "model.softwaresystem.description"; + } + +} \ No newline at end of file diff --git a/structurizr-inspection/src/main/java/com/structurizr/inspection/model/SoftwareSystemDocumentationInspection.java b/structurizr-inspection/src/main/java/com/structurizr/inspection/model/SoftwareSystemDocumentationInspection.java new file mode 100644 index 000000000..8496d310d --- /dev/null +++ b/structurizr-inspection/src/main/java/com/structurizr/inspection/model/SoftwareSystemDocumentationInspection.java @@ -0,0 +1,27 @@ +package com.structurizr.inspection.model; + +import com.structurizr.inspection.Inspector; +import com.structurizr.inspection.Violation; +import com.structurizr.model.SoftwareSystem; + +public class SoftwareSystemDocumentationInspection extends AbstractSoftwareSystemInspection { + + public SoftwareSystemDocumentationInspection(Inspector inspector) { + super(inspector); + } + + @Override + protected Violation inspect(SoftwareSystem softwareSystem) { + if (softwareSystem.hasContainers() && softwareSystem.getDocumentation().getSections().isEmpty()) { + return violation("The " + terminologyFor(softwareSystem).toLowerCase() + " \"" + nameOf(softwareSystem) + "\" has containers, but is missing documentation."); + } + + return noViolation(); + } + + @Override + protected String getType() { + return "model.softwaresystem.documentation"; + } + +} \ No newline at end of file diff --git a/structurizr-inspection/src/main/java/com/structurizr/inspection/view/AbstractModelViewInspection.java b/structurizr-inspection/src/main/java/com/structurizr/inspection/view/AbstractModelViewInspection.java new file mode 100644 index 000000000..157ccc0e5 --- /dev/null +++ b/structurizr-inspection/src/main/java/com/structurizr/inspection/view/AbstractModelViewInspection.java @@ -0,0 +1,24 @@ +package com.structurizr.inspection.view; + +import com.structurizr.inspection.Inspection; +import com.structurizr.inspection.Inspector; +import com.structurizr.inspection.Severity; +import com.structurizr.inspection.Violation; +import com.structurizr.view.ModelView; + +abstract class AbstractModelViewInspection extends Inspection { + + public AbstractModelViewInspection(Inspector inspector) { + super(inspector); + } + + public final Violation run(ModelView view) { + Severity severity = getInspector().getSeverityStrategy().getSeverity(this, view); + Violation violation = inspect(view); + + return violation == null ? null : violation.withSeverity(severity); + } + + protected abstract Violation inspect(ModelView view); + +} \ No newline at end of file diff --git a/structurizr-inspection/src/main/java/com/structurizr/inspection/view/AbstractViewInspection.java b/structurizr-inspection/src/main/java/com/structurizr/inspection/view/AbstractViewInspection.java new file mode 100644 index 000000000..a4a44ad84 --- /dev/null +++ b/structurizr-inspection/src/main/java/com/structurizr/inspection/view/AbstractViewInspection.java @@ -0,0 +1,24 @@ +package com.structurizr.inspection.view; + +import com.structurizr.inspection.Inspection; +import com.structurizr.inspection.Inspector; +import com.structurizr.inspection.Severity; +import com.structurizr.inspection.Violation; +import com.structurizr.view.View; + +abstract class AbstractViewInspection extends Inspection { + + public AbstractViewInspection(Inspector inspector) { + super(inspector); + } + + public final Violation run(View view) { + Severity severity = getInspector().getSeverityStrategy().getSeverity(this, view); + Violation violation = inspect(view); + + return violation == null ? null : violation.withSeverity(severity); + } + + protected abstract Violation inspect(View view); + +} \ No newline at end of file diff --git a/structurizr-inspection/src/main/java/com/structurizr/inspection/view/AbstractViewsInspection.java b/structurizr-inspection/src/main/java/com/structurizr/inspection/view/AbstractViewsInspection.java new file mode 100644 index 000000000..a2c64a831 --- /dev/null +++ b/structurizr-inspection/src/main/java/com/structurizr/inspection/view/AbstractViewsInspection.java @@ -0,0 +1,24 @@ +package com.structurizr.inspection.view; + +import com.structurizr.Workspace; +import com.structurizr.inspection.Inspection; +import com.structurizr.inspection.Inspector; +import com.structurizr.inspection.Severity; +import com.structurizr.inspection.Violation; + +abstract class AbstractViewsInspection extends Inspection { + + public AbstractViewsInspection(Inspector inspector) { + super(inspector); + } + + public final Violation run() { + Severity severity = getInspector().getSeverityStrategy().getSeverity(this, getWorkspace().getViews()); + Violation violation = inspect(getWorkspace()); + + return violation == null ? null : violation.withSeverity(severity); + } + + protected abstract Violation inspect(Workspace workspace); + +} \ No newline at end of file diff --git a/structurizr-inspection/src/main/java/com/structurizr/inspection/view/ContainerViewsForMultipleSoftwareSystemsInspection.java b/structurizr-inspection/src/main/java/com/structurizr/inspection/view/ContainerViewsForMultipleSoftwareSystemsInspection.java new file mode 100644 index 000000000..beb8d048d --- /dev/null +++ b/structurizr-inspection/src/main/java/com/structurizr/inspection/view/ContainerViewsForMultipleSoftwareSystemsInspection.java @@ -0,0 +1,35 @@ +package com.structurizr.inspection.view; + +import com.structurizr.Workspace; +import com.structurizr.inspection.Inspector; +import com.structurizr.inspection.Violation; +import com.structurizr.view.ContainerView; + +import java.util.HashSet; +import java.util.Set; + +public class ContainerViewsForMultipleSoftwareSystemsInspection extends AbstractViewsInspection { + + public ContainerViewsForMultipleSoftwareSystemsInspection(Inspector inspector) { + super(inspector); + } + + @Override + protected Violation inspect(Workspace workspace) { + Set<String> softwareSystemsWithContainerViews = new HashSet<>(); + for (ContainerView view : workspace.getViews().getContainerViews()) { + softwareSystemsWithContainerViews.add(view.getSoftwareSystemId()); + } + if (softwareSystemsWithContainerViews.size() > 1) { + return violation("Container views exist for " + softwareSystemsWithContainerViews.size() + " software systems. It is recommended that a workspace includes container views for a single software system only."); + } + + return noViolation(); + } + + @Override + protected String getType() { + return "workspace.scope"; + } + +} \ No newline at end of file diff --git a/structurizr-inspection/src/main/java/com/structurizr/inspection/view/ElementStyleMetadataInspection.java b/structurizr-inspection/src/main/java/com/structurizr/inspection/view/ElementStyleMetadataInspection.java new file mode 100644 index 000000000..027d94b16 --- /dev/null +++ b/structurizr-inspection/src/main/java/com/structurizr/inspection/view/ElementStyleMetadataInspection.java @@ -0,0 +1,35 @@ +package com.structurizr.inspection.view; + +import com.structurizr.inspection.Inspection; +import com.structurizr.inspection.Inspector; +import com.structurizr.inspection.Severity; +import com.structurizr.inspection.Violation; +import com.structurizr.view.ElementStyle; + +public class ElementStyleMetadataInspection extends Inspection { + + public ElementStyleMetadataInspection(Inspector inspector) { + super(inspector); + } + + public final Violation run(ElementStyle elementStyle) { + Severity severity = getInspector().getSeverityStrategy().getSeverity(this, elementStyle); + Violation violation = inspect(elementStyle); + + return violation == null ? null : violation.withSeverity(severity); + } + + protected Violation inspect(ElementStyle elementStyle) { + if (elementStyle.getMetadata() != null && !elementStyle.getMetadata()) { + return violation("The element style for tag \"" + elementStyle.getTag() + "\" has metadata hidden, which may introduce ambiguity on rendered diagrams."); + } + + return noViolation(); + } + + @Override + protected String getType() { + return "views.styles.element.metadata"; + } + +} \ No newline at end of file diff --git a/structurizr-inspection/src/main/java/com/structurizr/inspection/view/EmptyViewInspection.java b/structurizr-inspection/src/main/java/com/structurizr/inspection/view/EmptyViewInspection.java new file mode 100644 index 000000000..af9763c1d --- /dev/null +++ b/structurizr-inspection/src/main/java/com/structurizr/inspection/view/EmptyViewInspection.java @@ -0,0 +1,27 @@ +package com.structurizr.inspection.view; + +import com.structurizr.inspection.Inspector; +import com.structurizr.inspection.Violation; +import com.structurizr.view.ModelView; + +public class EmptyViewInspection extends AbstractModelViewInspection { + + public EmptyViewInspection(Inspector inspector) { + super(inspector); + } + + @Override + protected Violation inspect(ModelView view) { + if (view.getElements().isEmpty()) { + return violation("The view with key \"" + view.getKey() + "\" is empty."); + } + + return noViolation(); + } + + @Override + protected String getType() { + return "views.view.empty"; + } + +} \ No newline at end of file diff --git a/structurizr-inspection/src/main/java/com/structurizr/inspection/view/EmptyViewsInspection.java b/structurizr-inspection/src/main/java/com/structurizr/inspection/view/EmptyViewsInspection.java new file mode 100644 index 000000000..9f5b1e61c --- /dev/null +++ b/structurizr-inspection/src/main/java/com/structurizr/inspection/view/EmptyViewsInspection.java @@ -0,0 +1,27 @@ +package com.structurizr.inspection.view; + +import com.structurizr.Workspace; +import com.structurizr.inspection.Inspector; +import com.structurizr.inspection.Violation; + +public class EmptyViewsInspection extends AbstractViewsInspection { + + public EmptyViewsInspection(Inspector inspector) { + super(inspector); + } + + @Override + protected Violation inspect(Workspace workspace) { + if (workspace.getViews().isEmpty()) { + return violation("This workspace has no views."); + } + + return noViolation(); + } + + @Override + protected String getType() { + return "views.empty"; + } + +} \ No newline at end of file diff --git a/structurizr-inspection/src/main/java/com/structurizr/inspection/view/GeneratedKeyInspection.java b/structurizr-inspection/src/main/java/com/structurizr/inspection/view/GeneratedKeyInspection.java new file mode 100644 index 000000000..9ea19512d --- /dev/null +++ b/structurizr-inspection/src/main/java/com/structurizr/inspection/view/GeneratedKeyInspection.java @@ -0,0 +1,28 @@ +package com.structurizr.inspection.view; + +import com.structurizr.inspection.Inspector; +import com.structurizr.inspection.Violation; +import com.structurizr.view.ModelView; +import com.structurizr.view.View; + +public class GeneratedKeyInspection extends AbstractViewInspection { + + public GeneratedKeyInspection(Inspector inspector) { + super(inspector); + } + + @Override + protected Violation inspect(View view) { + if (view.isGeneratedKey()) { + return violation("The view with key \"" + view.getKey() + "\" has an automatically generated view key and this is not guaranteed to be stable over time."); + } + + return noViolation(); + } + + @Override + protected String getType() { + return "views.view.key"; + } + +} \ No newline at end of file diff --git a/structurizr-inspection/src/main/java/com/structurizr/inspection/view/ManualLayoutInspection.java b/structurizr-inspection/src/main/java/com/structurizr/inspection/view/ManualLayoutInspection.java new file mode 100644 index 000000000..69bbe4c0d --- /dev/null +++ b/structurizr-inspection/src/main/java/com/structurizr/inspection/view/ManualLayoutInspection.java @@ -0,0 +1,27 @@ +package com.structurizr.inspection.view; + +import com.structurizr.inspection.Inspector; +import com.structurizr.inspection.Violation; +import com.structurizr.view.ModelView; + +public class ManualLayoutInspection extends AbstractModelViewInspection { + + public ManualLayoutInspection(Inspector inspector) { + super(inspector); + } + + @Override + protected Violation inspect(ModelView view) { + if (view.isGeneratedKey() && view.getAutomaticLayout() == null) { + return violation("The view with key \"" + view.getKey() + "\" has an automatically generated view key and this may cause manual layout information to be lost in the future."); + } + + return noViolation(); + } + + @Override + protected String getType() { + return "views.view.layout"; + } + +} \ No newline at end of file diff --git a/structurizr-inspection/src/main/java/com/structurizr/inspection/view/SystemContextViewsForMultipleSoftwareSystemsInspection.java b/structurizr-inspection/src/main/java/com/structurizr/inspection/view/SystemContextViewsForMultipleSoftwareSystemsInspection.java new file mode 100644 index 000000000..6314f6931 --- /dev/null +++ b/structurizr-inspection/src/main/java/com/structurizr/inspection/view/SystemContextViewsForMultipleSoftwareSystemsInspection.java @@ -0,0 +1,35 @@ +package com.structurizr.inspection.view; + +import com.structurizr.Workspace; +import com.structurizr.inspection.Inspector; +import com.structurizr.inspection.Violation; +import com.structurizr.view.SystemContextView; + +import java.util.HashSet; +import java.util.Set; + +public class SystemContextViewsForMultipleSoftwareSystemsInspection extends AbstractViewsInspection { + + public SystemContextViewsForMultipleSoftwareSystemsInspection(Inspector inspector) { + super(inspector); + } + + @Override + protected Violation inspect(Workspace workspace) { + Set<String> softwareSystemsWithSystemContextViews = new HashSet<>(); + for (SystemContextView view : workspace.getViews().getSystemContextViews()) { + softwareSystemsWithSystemContextViews.add(view.getSoftwareSystemId()); + } + if (softwareSystemsWithSystemContextViews.size() > 1) { + return violation("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 noViolation(); + } + + @Override + protected String getType() { + return "workspace.scope"; + } + +} \ No newline at end of file diff --git a/structurizr-inspection/src/main/java/com/structurizr/inspection/workspace/AbstractWorkspaceInspection.java b/structurizr-inspection/src/main/java/com/structurizr/inspection/workspace/AbstractWorkspaceInspection.java new file mode 100644 index 000000000..a74a52dca --- /dev/null +++ b/structurizr-inspection/src/main/java/com/structurizr/inspection/workspace/AbstractWorkspaceInspection.java @@ -0,0 +1,24 @@ +package com.structurizr.inspection.workspace; + +import com.structurizr.Workspace; +import com.structurizr.inspection.Inspection; +import com.structurizr.inspection.Inspector; +import com.structurizr.inspection.Severity; +import com.structurizr.inspection.Violation; + +abstract class AbstractWorkspaceInspection extends Inspection { + + public AbstractWorkspaceInspection(Inspector inspector) { + super(inspector); + } + + public final Violation run() { + Severity severity = getInspector().getSeverityStrategy().getSeverity(this, getWorkspace()); + Violation violation = inspect(getWorkspace()); + + return violation == null ? null : violation.withSeverity(severity); + } + + protected abstract Violation inspect(Workspace workspace); + +} \ No newline at end of file diff --git a/structurizr-inspection/src/main/java/com/structurizr/inspection/workspace/WorkspaceScopeInspection.java b/structurizr-inspection/src/main/java/com/structurizr/inspection/workspace/WorkspaceScopeInspection.java new file mode 100644 index 000000000..5619abafc --- /dev/null +++ b/structurizr-inspection/src/main/java/com/structurizr/inspection/workspace/WorkspaceScopeInspection.java @@ -0,0 +1,27 @@ +package com.structurizr.inspection.workspace; + +import com.structurizr.Workspace; +import com.structurizr.inspection.Inspector; +import com.structurizr.inspection.Violation; + +public class WorkspaceScopeInspection extends AbstractWorkspaceInspection { + + public WorkspaceScopeInspection(Inspector inspector) { + super(inspector); + } + + @Override + protected Violation inspect(Workspace workspace) { + if (workspace.getConfiguration().getScope() == null) { + return violation("This workspace has no defined scope. It is recommended that the workspace scope is set to \"Landscape\" or \"SoftwareSystem\"."); + } + + return noViolation(); + } + + @Override + protected String getType() { + return "workspace.scope"; + } + +} \ No newline at end of file diff --git a/structurizr-inspection/src/main/java/com/structurizr/inspection/workspace/WorkspaceToolingInspection.java b/structurizr-inspection/src/main/java/com/structurizr/inspection/workspace/WorkspaceToolingInspection.java new file mode 100644 index 000000000..19b9c2e7d --- /dev/null +++ b/structurizr-inspection/src/main/java/com/structurizr/inspection/workspace/WorkspaceToolingInspection.java @@ -0,0 +1,33 @@ +package com.structurizr.inspection.workspace; + +import com.structurizr.Workspace; +import com.structurizr.inspection.Inspector; +import com.structurizr.inspection.Violation; +import com.structurizr.util.StringUtils; + +public class WorkspaceToolingInspection extends AbstractWorkspaceInspection { + + private static final String CLOUD_SERVICE_DSL_EDITOR = "structurizr-cloud/dsl-editor"; + private static final String ONPREMISES_DSL_EDITOR = "structurizr-onpremises/dsl-editor"; + + public WorkspaceToolingInspection(Inspector inspector) { + super(inspector); + } + + @Override + protected Violation inspect(Workspace workspace) { + if (!StringUtils.isNullOrEmpty(workspace.getLastModifiedAgent())) { + if (workspace.getLastModifiedAgent().startsWith(CLOUD_SERVICE_DSL_EDITOR) || workspace.getLastModifiedAgent().startsWith(ONPREMISES_DSL_EDITOR)) { + return violation("The browser-based DSL editor is the easiest way to get started without installing any tooling, but it does not provide access to the full feature set of the Structurizr DSL. It is recommended that you use the Structurizr DSL in conjunction with the Structurizr CLI's \"push\" command."); + } + } + + return noViolation(); + } + + @Override + protected String getType() { + return "workspace.tooling"; + } + +} \ No newline at end of file diff --git a/structurizr-inspection/src/test/java/com/structurizr/inspection/DefaultInspectorTests.java b/structurizr-inspection/src/test/java/com/structurizr/inspection/DefaultInspectorTests.java new file mode 100644 index 000000000..619f12d22 --- /dev/null +++ b/structurizr-inspection/src/test/java/com/structurizr/inspection/DefaultInspectorTests.java @@ -0,0 +1,103 @@ +package com.structurizr.inspection; + +import com.structurizr.Workspace; +import com.structurizr.dsl.StructurizrDslParser; +import org.junit.jupiter.api.Test; + +import java.io.File; +import java.util.Collection; +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() { + Workspace workspace = new Workspace("Name", "Description"); + DefaultInspector inspector = new DefaultInspector(workspace); + List<Violation> violations = inspector.getViolations(); + + assertEquals(9, inspector.getNumberOfInspections()); + assertEquals(3, violations.size()); + + Violation violation = violations.get(0); + assertEquals(Severity.ERROR, violation.getSeverity()); + assertEquals("workspace.scope", violation.getType()); + assertEquals("This workspace has no defined scope. It is recommended that the workspace scope is set to \"Landscape\" or \"SoftwareSystem\".", violation.getMessage()); + + violation = violations.get(1); + assertEquals(Severity.ERROR, violation.getSeverity()); + assertEquals("model.empty", violation.getType()); + assertEquals("The model is empty.", violation.getMessage()); + + violation = violations.get(2); + 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 + void test_EmptyWorkspace_WhenInspectionsAreDisabled() { + Workspace workspace = new Workspace("Name", "Description"); + workspace.addProperty("structurizr.inspection", "false"); + + DefaultInspector inspector = new DefaultInspector(workspace); + List<Violation> violations = inspector.getViolations(); + + 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 + void test() throws Exception { + StructurizrDslParser parser = new StructurizrDslParser(); + parser.parse(new File("src/test/resources/workspace.dsl")); + Workspace workspace = parser.getWorkspace(); + + DefaultInspector inspector = new DefaultInspector(workspace); + Collection<Violation> violations = inspector.getViolations(); + + for (Violation violation : violations) { + System.out.println(violation); + } + + assertEquals(28, violations.size()); + assertEquals(0, violations.stream().filter(v -> v.getSeverity() == Severity.ERROR).count()); + assertEquals(27, violations.stream().filter(v -> v.getSeverity() == Severity.WARNING).count()); + assertEquals(1, violations.stream().filter(v -> v.getSeverity() == Severity.INFO).count()); + } + + @Test + void test_WithAllViolationsAsError() throws Exception { + StructurizrDslParser parser = new StructurizrDslParser(); + parser.parse(new File("src/test/resources/workspace.dsl")); + Workspace workspace = parser.getWorkspace(); + + DefaultInspector inspector = new DefaultInspector(workspace) { + @Override + public SeverityStrategy getSeverityStrategy() { + return new FixedSeverityStrategy(Severity.ERROR); + } + }; + Collection<Violation> violations = inspector.getViolations(); + + for (Violation violation : violations) { + System.out.println(violation); + } + + assertEquals(28, violations.size()); + assertEquals(28, violations.stream().filter(v -> v.getSeverity() == Severity.ERROR).count()); + } + +} diff --git a/structurizr-inspection/src/test/java/com/structurizr/inspection/PropertyBasedSeverityStrategyTests.java b/structurizr-inspection/src/test/java/com/structurizr/inspection/PropertyBasedSeverityStrategyTests.java new file mode 100644 index 000000000..e6bf91594 --- /dev/null +++ b/structurizr-inspection/src/test/java/com/structurizr/inspection/PropertyBasedSeverityStrategyTests.java @@ -0,0 +1,271 @@ +package com.structurizr.inspection; + +import com.structurizr.Workspace; +import com.structurizr.inspection.model.ComponentDescriptionInspection; +import com.structurizr.inspection.model.RelationshipDescriptionInspection; +import com.structurizr.inspection.model.RelationshipTechnologyInspection; +import com.structurizr.inspection.workspace.WorkspaceScopeInspection; +import com.structurizr.model.*; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class PropertyBasedSeverityStrategyTests { + + private Workspace workspace; + private Inspector inspector; + private Inspection inspection; + private PropertyBasedSeverityStrategy severityStrategy; + + @BeforeEach + void setUp() { + workspace = new Workspace("Name", "Description"); + inspector = new Inspector(workspace) { + @Override + public SeverityStrategy getSeverityStrategy() { + return null; + } + }; + severityStrategy = new PropertyBasedSeverityStrategy(); + } + + @Test + void getSeverityForWorkspace() { + inspection = new WorkspaceScopeInspection(inspector); + + // default is error + assertEquals(Severity.ERROR, severityStrategy.getSeverity(inspection, workspace)); + } + + @Test + void getSeverityForWorkspace_WhenInspectionSpecifiedByName() { + inspection = new WorkspaceScopeInspection(inspector); + + // specify by name at workspace level + workspace.addProperty("structurizr.inspection." + inspection.getType(), "warning"); + assertEquals(Severity.WARNING, severityStrategy.getSeverity(inspection, workspace)); + } + + @Test + void getSeverityForWorkspace_WhenInspectionSpecifiedByWildcard() { + inspection = new WorkspaceScopeInspection(inspector); + + // specify by wildcard at workspace level + workspace.addProperty("structurizr.inspection.*", "warning"); + assertEquals(Severity.WARNING, severityStrategy.getSeverity(inspection, workspace)); + } + + @Test + void getSeverityForComponent() { + inspection = new ComponentDescriptionInspection(inspector); + SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Software System"); + Container container = softwareSystem.addContainer("Container"); + Component component = container.addComponent("Component"); + + // default is error + assertEquals(Severity.ERROR, severityStrategy.getSeverity(inspection, component)); + } + + @Test + void getSeverityForComponent_WhenInspectionSpecifiedByNameInComponent() { + inspection = new ComponentDescriptionInspection(inspector); + SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Software System"); + Container container = softwareSystem.addContainer("Container"); + Component component = container.addComponent("Component"); + + // specify by name at component level + component.addProperty("structurizr.inspection." + inspection.getType(), "info"); + assertEquals(Severity.INFO, severityStrategy.getSeverity(inspection, component)); + } + + @Test + void getSeverityForComponent_WhenInspectionSpecifiedByWildcardInComponent() { + inspection = new ComponentDescriptionInspection(inspector); + SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Software System"); + Container container = softwareSystem.addContainer("Container"); + Component component = container.addComponent("Component"); + + // specify by wildcard at component level + workspace.addProperty("structurizr.inspection.model.component.*", "ignore"); + component.addProperty("structurizr.inspection.model.component.*", "info"); + assertEquals(Severity.INFO, severityStrategy.getSeverity(inspection, component)); + } + + @Test + void getSeverityForComponent_WhenInspectionSpecifiedByNameInContainer() { + inspection = new ComponentDescriptionInspection(inspector); + SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Software System"); + Container container = softwareSystem.addContainer("Container"); + Component component = container.addComponent("Component"); + + // specify by name in parent container + container.addProperty("structurizr.inspection." + inspection.getType(), "info"); + assertEquals(Severity.INFO, severityStrategy.getSeverity(inspection, component)); + } + + @Test + void getSeverityForComponent_WhenInspectionSpecifiedByWildcardInContainer() { + inspection = new ComponentDescriptionInspection(inspector); + SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Software System"); + Container container = softwareSystem.addContainer("Container"); + Component component = container.addComponent("Component"); + + // specify by wildcard in parent container + container.addProperty("structurizr.inspection.model.component.*", "info"); + assertEquals(Severity.INFO, severityStrategy.getSeverity(inspection, component)); + } + + @Test + void getSeverityForComponent_WhenSpecifiedByNameInSoftwareSystem() { + inspection = new ComponentDescriptionInspection(inspector); + SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Software System"); + Container container = softwareSystem.addContainer("Container"); + Component component = container.addComponent("Component"); + + // specify by name in parent software system + softwareSystem.addProperty("structurizr.inspection." + inspection.getType(), "info"); + assertEquals(Severity.INFO, severityStrategy.getSeverity(inspection, component)); + } + + @Test + void getSeverityForComponent_WhenSpecifiedByWildcardInSoftwareSystem() { + inspection = new ComponentDescriptionInspection(inspector); + SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Software System"); + Container container = softwareSystem.addContainer("Container"); + Component component = container.addComponent("Component"); + + // specify by wildcard in parent software system + softwareSystem.addProperty("structurizr.inspection.model.component.*", "info"); + assertEquals(Severity.INFO, severityStrategy.getSeverity(inspection, component)); + } + + @Test + void getSeverityForComponent_WhenSpecifiedByNameInModel() { + inspection = new ComponentDescriptionInspection(inspector); + SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Software System"); + Container container = softwareSystem.addContainer("Container"); + Component component = container.addComponent("Component"); + + // specify by name in model + workspace.getModel().addProperty("structurizr.inspection." + inspection.getType(), "info"); + assertEquals(Severity.INFO, severityStrategy.getSeverity(inspection, component)); + } + + @Test + void getSeverityForComponent_WhenSpecifiedByWildcardInModel() { + inspection = new ComponentDescriptionInspection(inspector); + SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Software System"); + Container container = softwareSystem.addContainer("Container"); + Component component = container.addComponent("Component"); + + // specify by wildcard in model + workspace.getModel().addProperty("structurizr.inspection.model.component.*", "info"); + assertEquals(Severity.INFO, severityStrategy.getSeverity(inspection, component)); + } + + @Test + void getSeverityForComponent_WhenSpecifiedByNameInWorkspace() { + inspection = new ComponentDescriptionInspection(inspector); + SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Software System"); + Container container = softwareSystem.addContainer("Container"); + Component component = container.addComponent("Component"); + + // specify by name in workspace + workspace.addProperty("structurizr.inspection." + inspection.getType(), "info"); + assertEquals(Severity.INFO, severityStrategy.getSeverity(inspection, component)); + } + + @Test + void getSeverityForComponent_WhenSpecifiedByComponentWildcardInWorkspace() { + inspection = new ComponentDescriptionInspection(inspector); + SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Software System"); + Container container = softwareSystem.addContainer("Container"); + Component component = container.addComponent("Component"); + + // specify by wildcard in workspace + workspace.addProperty("structurizr.inspection.model.component.*", "info"); + assertEquals(Severity.INFO, severityStrategy.getSeverity(inspection, component)); + } + + @Test + void getSeverityForComponent_WhenSpecifiedByModelWildcardInWorkspace() { + inspection = new ComponentDescriptionInspection(inspector); + SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Software System"); + Container container = softwareSystem.addContainer("Container"); + Component component = container.addComponent("Component"); + + // specify by model wildcard in workspace + workspace.addProperty("structurizr.inspection.model.*", "info"); + assertEquals(Severity.INFO, severityStrategy.getSeverity(inspection, component)); + } + + @Test + void generateTypes_ForElement() { + PropertyBasedSeverityStrategy strategy = new PropertyBasedSeverityStrategy(); + List<String> types = strategy.generatePropertyNames("model.component.description"); + + assertEquals(4, types.size()); + assertEquals("structurizr.inspection.model.component.description", types.get(0)); + assertEquals("structurizr.inspection.model.component.*", types.get(1)); + assertEquals("structurizr.inspection.model.*", types.get(2)); + assertEquals("structurizr.inspection.*", types.get(3)); + } + + @Test + void generateTypes_ForRelationship() { + PropertyBasedSeverityStrategy strategy = new PropertyBasedSeverityStrategy(); + List<String> types = strategy.generatePropertyNames("model.relationship[component->component].technology"); + + assertEquals(2, types.size()); + assertEquals("structurizr.inspection.model.relationship[component->component].technology", types.get(0)); + assertEquals("structurizr.inspection.model.relationship[component->component].*", types.get(1)); + } + + @Test + void getSeverityForRelationship_BetweenComponents() { + inspection = new RelationshipDescriptionInspection(inspector); + SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Software System"); + Container container = softwareSystem.addContainer("Container"); + Component component1 = container.addComponent("Component 1"); + Component component2 = container.addComponent("Component 2"); + Relationship relationship = component1.uses(component2, ""); + + // default is error + assertEquals(Severity.ERROR, severityStrategy.getSeverity(inspection, relationship)); + } + + @Test + void getSeverityForRelationship_BetweenComponents_WhenSpecifiedByRelationshipTypeInWorkspace() { + inspection = new RelationshipTechnologyInspection(inspector); + SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Software System"); + Container container = softwareSystem.addContainer("Container"); + Component component1 = container.addComponent("Component 1"); + Component component2 = container.addComponent("Component 2"); + Relationship relationship = component1.uses(component2, ""); + + // specify by relationship type in workspace + workspace.addProperty("structurizr.inspection.model.relationship[component->component].technology", "info"); + 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 diff --git a/structurizr-inspection/src/test/java/com/structurizr/inspection/documentation/EmbeddedViewMissingInspectionTests.java b/structurizr-inspection/src/test/java/com/structurizr/inspection/documentation/EmbeddedViewMissingInspectionTests.java new file mode 100644 index 000000000..9cf023b41 --- /dev/null +++ b/structurizr-inspection/src/test/java/com/structurizr/inspection/documentation/EmbeddedViewMissingInspectionTests.java @@ -0,0 +1,91 @@ +package com.structurizr.inspection.documentation; + +import com.structurizr.Workspace; +import com.structurizr.documentation.Decision; +import com.structurizr.documentation.Format; +import com.structurizr.documentation.Section; +import com.structurizr.inspection.DefaultInspector; +import com.structurizr.inspection.Severity; +import com.structurizr.inspection.Violation; +import com.structurizr.inspection.model.SoftwareSystemDocumentationInspection; +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 EmbeddedViewMissingInspectionTests { + + @Test + public void run_WithMissingView() { + Workspace workspace = new Workspace("Name", "Description"); + SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Software System"); + + Section section = new Section(); + section.setFormat(Format.Markdown); + section.setContent(""" + ## Context + + ![](embed:SystemContext) + """); + softwareSystem.getDocumentation().addSection(section); + + Decision decision = new Decision("1"); + decision.setTitle("Decision 1"); + decision.setStatus("Accepted"); + decision.setFormat(Format.AsciiDoc); + decision.setContent(""" + ## Containers + + image::embed:Containers[] + """); + softwareSystem.getDocumentation().addDecision(decision); + + Violation violation = new EmbeddedViewMissingInspection(new DefaultInspector(workspace)).run(softwareSystem); + Assertions.assertEquals(Severity.ERROR, violation.getSeverity()); + assertEquals("documentation.embeddedview", violation.getType()); + assertEquals("The following views are embedded into documentation for the software system \"Software System\" but do not exist in the workspace: SystemContext, Containers", violation.getMessage()); + + workspace.getViews().createSystemContextView(softwareSystem, "SystemContext", "Description"); + + violation = new EmbeddedViewMissingInspection(new DefaultInspector(workspace)).run(softwareSystem); + Assertions.assertEquals(Severity.ERROR, violation.getSeverity()); + assertEquals("documentation.embeddedview", violation.getType()); + assertEquals("The following views are embedded into documentation for the software system \"Software System\" but do not exist in the workspace: Containers", violation.getMessage()); + } + + @Test + public void run_WithoutMissingView() { + Workspace workspace = new Workspace("Name", "Description"); + SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Software System"); + + Section section = new Section(); + section.setFormat(Format.Markdown); + section.setContent(""" + ## Context + + ![](embed:SystemContext) + """); + softwareSystem.getDocumentation().addSection(section); + + Decision decision = new Decision("1"); + decision.setTitle("Decision 1"); + decision.setStatus("Accepted"); + decision.setFormat(Format.AsciiDoc); + decision.setContent(""" + ## Containers + + image::embed:Containers[] + """); + softwareSystem.getDocumentation().addDecision(decision); + + workspace.getViews().createSystemContextView(softwareSystem, "SystemContext", "Description"); + workspace.getViews().createContainerView(softwareSystem, "Containers", "Description"); + + Violation violation = new EmbeddedViewMissingInspection(new DefaultInspector(workspace)).run(softwareSystem); + assertNull(violation); + } + +} diff --git a/structurizr-inspection/src/test/java/com/structurizr/inspection/documentation/EmbeddedViewWithGeneratedKeyInspectionTests.java b/structurizr-inspection/src/test/java/com/structurizr/inspection/documentation/EmbeddedViewWithGeneratedKeyInspectionTests.java new file mode 100644 index 000000000..d602d65f8 --- /dev/null +++ b/structurizr-inspection/src/test/java/com/structurizr/inspection/documentation/EmbeddedViewWithGeneratedKeyInspectionTests.java @@ -0,0 +1,80 @@ +package com.structurizr.inspection.documentation; + +import com.structurizr.Workspace; +import com.structurizr.documentation.Format; +import com.structurizr.documentation.Section; +import com.structurizr.inspection.DefaultInspector; +import com.structurizr.inspection.Severity; +import com.structurizr.inspection.Violation; +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 EmbeddedViewWithGeneratedKeyInspectionTests { + + @Test + public void run_WithGeneratedKey() { + Workspace workspace = new Workspace("Name", "Description"); + SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Software System"); + + Section section = new Section(); + section.setFormat(Format.Markdown); + section.setContent(""" + ## Context + + ![](embed:SystemContext-001) + """); + softwareSystem.getDocumentation().addSection(section); + + section = new Section(); + section.setFormat(Format.AsciiDoc); + section.setContent(""" + ## Containers + + image::embed:Container-001[] + """); + softwareSystem.getDocumentation().addSection(section); + + workspace.getViews().createSystemContextView(softwareSystem, "", "Description"); + workspace.getViews().createContainerView(softwareSystem, "", "Description"); + + Violation violation = new EmbeddedViewWithGeneratedKeyInspection(new DefaultInspector(workspace)).run(softwareSystem); + Assertions.assertEquals(Severity.ERROR, violation.getSeverity()); + assertEquals("documentation.embeddedview", violation.getType()); + assertEquals("The following views are embedded into documentation for the software system \"Software System\" via an automatically generated view key: SystemContext-001, Container-001", violation.getMessage()); + } + + @Test + public void run_WithoutGeneratedKey() { + Workspace workspace = new Workspace("Name", "Description"); + SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Software System"); + + Section section = new Section(); + section.setFormat(Format.Markdown); + section.setContent(""" + ## Context + + ![](embed:SystemContext) + """); + softwareSystem.getDocumentation().addSection(section); + + section = new Section(); + section.setFormat(Format.AsciiDoc); + section.setContent(""" + ## Containers + + image::embed:Containers[] + """); + softwareSystem.getDocumentation().addSection(section); + + workspace.getViews().createSystemContextView(softwareSystem, "SystemContext", "Description"); + workspace.getViews().createContainerView(softwareSystem, "Containers", "Description"); + + Violation violation = new EmbeddedViewWithGeneratedKeyInspection(new DefaultInspector(workspace)).run(softwareSystem); + assertNull(violation); + } + +} diff --git a/structurizr-inspection/src/test/java/com/structurizr/inspection/model/ComponentDescriptionInspectionTests.java b/structurizr-inspection/src/test/java/com/structurizr/inspection/model/ComponentDescriptionInspectionTests.java new file mode 100644 index 000000000..8417ac25b --- /dev/null +++ b/structurizr-inspection/src/test/java/com/structurizr/inspection/model/ComponentDescriptionInspectionTests.java @@ -0,0 +1,43 @@ +package com.structurizr.inspection.model; + +import com.structurizr.Workspace; +import com.structurizr.inspection.DefaultInspector; +import com.structurizr.inspection.Severity; +import com.structurizr.inspection.Violation; +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"); + + Violation violation = new ComponentDescriptionInspection(new DefaultInspector(workspace)).run(component); + Assertions.assertEquals(Severity.ERROR, violation.getSeverity()); + assertEquals("model.component.description", violation.getType()); + assertEquals("The component \"Software System.Container.Name\" is missing a description.", violation.getMessage()); + } + + @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"); + + Violation violation = new ComponentDescriptionInspection(new DefaultInspector(workspace)).run(component); + assertNull(violation); + } + +} diff --git a/structurizr-inspection/src/test/java/com/structurizr/inspection/model/ComponentTechnologyInspectionTests.java b/structurizr-inspection/src/test/java/com/structurizr/inspection/model/ComponentTechnologyInspectionTests.java new file mode 100644 index 000000000..f81612bdd --- /dev/null +++ b/structurizr-inspection/src/test/java/com/structurizr/inspection/model/ComponentTechnologyInspectionTests.java @@ -0,0 +1,43 @@ +package com.structurizr.inspection.model; + +import com.structurizr.Workspace; +import com.structurizr.inspection.DefaultInspector; +import com.structurizr.inspection.Severity; +import com.structurizr.inspection.Violation; +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"); + + Violation violation = new ComponentTechnologyInspection(new DefaultInspector(workspace)).run(component); + Assertions.assertEquals(Severity.ERROR, violation.getSeverity()); + assertEquals("model.component.technology", violation.getType()); + assertEquals("The component \"Software System.Container.Name\" is missing a technology.", violation.getMessage()); + } + + @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"); + + Violation violation = new ComponentTechnologyInspection(new DefaultInspector(workspace)).run(component); + assertNull(violation); + } + +} diff --git a/structurizr-inspection/src/test/java/com/structurizr/inspection/model/ContainerDescriptionInspectionTests.java b/structurizr-inspection/src/test/java/com/structurizr/inspection/model/ContainerDescriptionInspectionTests.java new file mode 100644 index 000000000..423aca5fb --- /dev/null +++ b/structurizr-inspection/src/test/java/com/structurizr/inspection/model/ContainerDescriptionInspectionTests.java @@ -0,0 +1,40 @@ +package com.structurizr.inspection.model; + +import com.structurizr.Workspace; +import com.structurizr.inspection.DefaultInspector; +import com.structurizr.inspection.Severity; +import com.structurizr.inspection.Violation; +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"); + + Violation violation = new ContainerDescriptionInspection(new DefaultInspector(workspace)).run(container); + Assertions.assertEquals(Severity.ERROR, violation.getSeverity()); + assertEquals("model.container.description", violation.getType()); + assertEquals("The container \"Software System.Name\" is missing a description.", violation.getMessage()); + } + + @Test + public void run_WithDescription() { + Workspace workspace = new Workspace("Name", "Description"); + SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Software System"); + Container container = softwareSystem.addContainer("Name", "Description"); + + Violation violation = new ContainerDescriptionInspection(new DefaultInspector(workspace)).run(container); + assertNull(violation); + } + +} diff --git a/structurizr-inspection/src/test/java/com/structurizr/inspection/model/ContainerTechnologyInspectionTests.java b/structurizr-inspection/src/test/java/com/structurizr/inspection/model/ContainerTechnologyInspectionTests.java new file mode 100644 index 000000000..c2f7d681c --- /dev/null +++ b/structurizr-inspection/src/test/java/com/structurizr/inspection/model/ContainerTechnologyInspectionTests.java @@ -0,0 +1,40 @@ +package com.structurizr.inspection.model; + +import com.structurizr.Workspace; +import com.structurizr.inspection.DefaultInspector; +import com.structurizr.inspection.Severity; +import com.structurizr.inspection.Violation; +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"); + + Violation violation = new ContainerTechnologyInspection(new DefaultInspector(workspace)).run(container); + Assertions.assertEquals(Severity.ERROR, violation.getSeverity()); + assertEquals("model.container.technology", violation.getType()); + assertEquals("The container \"Software System.Name\" is missing a technology.", violation.getMessage()); + } + + @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"); + + Violation violation = new ContainerTechnologyInspection(new DefaultInspector(workspace)).run(container); + assertNull(violation); + } + +} diff --git a/structurizr-inspection/src/test/java/com/structurizr/inspection/model/DeploymentNodeDescriptionInspectionTests.java b/structurizr-inspection/src/test/java/com/structurizr/inspection/model/DeploymentNodeDescriptionInspectionTests.java new file mode 100644 index 000000000..96adc01dd --- /dev/null +++ b/structurizr-inspection/src/test/java/com/structurizr/inspection/model/DeploymentNodeDescriptionInspectionTests.java @@ -0,0 +1,37 @@ +package com.structurizr.inspection.model; + +import com.structurizr.Workspace; +import com.structurizr.inspection.DefaultInspector; +import com.structurizr.inspection.Severity; +import com.structurizr.inspection.Violation; +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"); + + Violation violation = new DeploymentNodeDescriptionInspection(new DefaultInspector(workspace)).run(deploymentNode); + Assertions.assertEquals(Severity.ERROR, violation.getSeverity()); + assertEquals("model.deploymentnode.description", violation.getType()); + assertEquals("The deployment node \"Default/Name\" is missing a description.", violation.getMessage()); + } + + @Test + public void run_WithDescription() { + Workspace workspace = new Workspace("Name", "Description"); + DeploymentNode deploymentNode = workspace.getModel().addDeploymentNode("Name", "Description", "Technology"); + + Violation violation = new DeploymentNodeDescriptionInspection(new DefaultInspector(workspace)).run(deploymentNode); + assertNull(violation); + } + +} 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 new file mode 100644 index 000000000..dc7844b8e --- /dev/null +++ b/structurizr-inspection/src/test/java/com/structurizr/inspection/model/DeploymentNodeTechnologyInspectionTests.java @@ -0,0 +1,37 @@ +package com.structurizr.inspection.model; + +import com.structurizr.Workspace; +import com.structurizr.inspection.DefaultInspector; +import com.structurizr.inspection.Severity; +import com.structurizr.inspection.Violation; +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", "Description", ""); + + Violation violation = new DeploymentNodeTechnologyInspection(new DefaultInspector(workspace)).run(deploymentNode); + Assertions.assertEquals(Severity.ERROR, violation.getSeverity()); + assertEquals("model.deploymentnode.technology", violation.getType()); + assertEquals("The deployment node \"Default/Name\" is missing a technology.", violation.getMessage()); + } + + @Test + public void run_WithDescription() { + Workspace workspace = new Workspace("Name", "Description"); + DeploymentNode deploymentNode = workspace.getModel().addDeploymentNode("Name", "Description", "Technology"); + + Violation violation = new DeploymentNodeTechnologyInspection(new DefaultInspector(workspace)).run(deploymentNode); + assertNull(violation); + } + +} diff --git a/structurizr-inspection/src/test/java/com/structurizr/inspection/model/DisconnectedElementInspectionTests.java b/structurizr-inspection/src/test/java/com/structurizr/inspection/model/DisconnectedElementInspectionTests.java new file mode 100644 index 000000000..c944d8e38 --- /dev/null +++ b/structurizr-inspection/src/test/java/com/structurizr/inspection/model/DisconnectedElementInspectionTests.java @@ -0,0 +1,42 @@ +package com.structurizr.inspection.model; + +import com.structurizr.Workspace; +import com.structurizr.inspection.DefaultInspector; +import com.structurizr.inspection.Severity; +import com.structurizr.inspection.Violation; +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 +DisconnectedElementInspectionTests { + + @Test + public void run_WithDisconnectedElement() { + Workspace workspace = new Workspace("Name", "Description"); + SoftwareSystem a = workspace.getModel().addSoftwareSystem("A"); + + Violation violation = new DisconnectedElementInspection(new DefaultInspector(workspace)).run(a); + Assertions.assertEquals(Severity.ERROR, violation.getSeverity()); + assertEquals("model.element.disconnected", violation.getType()); + assertEquals("The software system \"A\" is disconnected - add a relationship to/from it, or consider removing it from the model.", violation.getMessage()); + } + + @Test + public void run_WithoutDisconnectedElement() { + Workspace workspace = new Workspace("Name", "Description"); + SoftwareSystem a = workspace.getModel().addSoftwareSystem("A"); + SoftwareSystem b = workspace.getModel().addSoftwareSystem("B"); + a.uses(b, "Uses"); + + Violation violation = new DisconnectedElementInspection(new DefaultInspector(workspace)).run(a); + assertNull(violation); + + violation = new DisconnectedElementInspection(new DefaultInspector(workspace)).run(b); + assertNull(violation); + } + +} diff --git a/structurizr-inspection/src/test/java/com/structurizr/inspection/model/ElementNotIncludedInAnyViewsInspectionTests.java b/structurizr-inspection/src/test/java/com/structurizr/inspection/model/ElementNotIncludedInAnyViewsInspectionTests.java new file mode 100644 index 000000000..a6a9232f3 --- /dev/null +++ b/structurizr-inspection/src/test/java/com/structurizr/inspection/model/ElementNotIncludedInAnyViewsInspectionTests.java @@ -0,0 +1,38 @@ +package com.structurizr.inspection.model; + +import com.structurizr.Workspace; +import com.structurizr.inspection.DefaultInspector; +import com.structurizr.inspection.Severity; +import com.structurizr.inspection.Violation; +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"); + + Violation violation = new ElementNotIncludedInAnyViewsInspection(new DefaultInspector(workspace)).run(softwareSystem); + Assertions.assertEquals(Severity.ERROR, violation.getSeverity()); + assertEquals("model.element.noview", violation.getType()); + assertEquals("The software system named \"Name\" is not included on any views - add it to a view.", violation.getMessage()); + } + + @Test + public void run_InViews() { + Workspace workspace = new Workspace("Name", "Description"); + SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Name"); + workspace.getViews().createSystemLandscapeView("key", "Description").addAllElements(); + + Violation violation = new ElementNotIncludedInAnyViewsInspection(new DefaultInspector(workspace)).run(softwareSystem); + assertNull(violation); + } + +} diff --git a/structurizr-inspection/src/test/java/com/structurizr/inspection/model/EmptyDeploymentNodeCheckTests.java b/structurizr-inspection/src/test/java/com/structurizr/inspection/model/EmptyDeploymentNodeCheckTests.java new file mode 100644 index 000000000..100b254be --- /dev/null +++ b/structurizr-inspection/src/test/java/com/structurizr/inspection/model/EmptyDeploymentNodeCheckTests.java @@ -0,0 +1,73 @@ +package com.structurizr.inspection.model; + +import com.structurizr.Workspace; +import com.structurizr.inspection.DefaultInspector; +import com.structurizr.inspection.Severity; +import com.structurizr.inspection.Violation; +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"); + + Violation violation = new EmptyDeploymentNodeInspection(new DefaultInspector(workspace)).run(deploymentNode); + Assertions.assertEquals(Severity.ERROR, violation.getSeverity()); + assertEquals("model.deploymentnode.empty", violation.getType()); + assertEquals("The deployment node \"Default/Name\" is empty.", violation.getMessage()); + } + + @Test + public void run_WithDeploymentNode() { + Workspace workspace = new Workspace("Name", "Description"); + DeploymentNode deploymentNode = workspace.getModel().addDeploymentNode("Deployment Node", "Description", "Technology"); + deploymentNode.addDeploymentNode("Deployment Node"); + + Violation violation = new EmptyDeploymentNodeInspection(new DefaultInspector(workspace)).run(deploymentNode); + assertNull(violation); + } + + @Test + public void run_WithInfrastructureNode() { + Workspace workspace = new Workspace("Name", "Description"); + DeploymentNode deploymentNode = workspace.getModel().addDeploymentNode("Deployment Node", "Description", "Technology"); + deploymentNode.addInfrastructureNode("Infrastructure Node"); + + Violation violation = new EmptyDeploymentNodeInspection(new DefaultInspector(workspace)).run(deploymentNode); + assertNull(violation); + } + + @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); + + Violation violation = new EmptyDeploymentNodeInspection(new DefaultInspector(workspace)).run(deploymentNode); + assertNull(violation); + } + + @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); + + Violation violation = new EmptyDeploymentNodeInspection(new DefaultInspector(workspace)).run(deploymentNode); + assertNull(violation); + } + +} diff --git a/structurizr-inspection/src/test/java/com/structurizr/inspection/model/EmptyModelInspectionTests.java b/structurizr-inspection/src/test/java/com/structurizr/inspection/model/EmptyModelInspectionTests.java new file mode 100644 index 000000000..7d3c4b0e2 --- /dev/null +++ b/structurizr-inspection/src/test/java/com/structurizr/inspection/model/EmptyModelInspectionTests.java @@ -0,0 +1,35 @@ +package com.structurizr.inspection.model; + +import com.structurizr.Workspace; +import com.structurizr.inspection.DefaultInspector; +import com.structurizr.inspection.Severity; +import com.structurizr.inspection.Violation; +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"); + + Violation violation = new EmptyModelInspection(new DefaultInspector(workspace)).run(); + Assertions.assertEquals(Severity.ERROR, violation.getSeverity()); + assertEquals("model.empty", violation.getType()); + assertEquals("The model is empty.", violation.getMessage()); + } + + @Test + public void run_WhenThereAreElements() { + Workspace workspace = new Workspace("Name", "Description"); + workspace.getModel().addSoftwareSystem("Name"); + + Violation violation = new EmptyModelInspection(new DefaultInspector(workspace)).run(); + assertNull(violation); + } + +} diff --git a/structurizr-inspection/src/test/java/com/structurizr/inspection/model/InfrastructureNodeDescriptionInspectionTests.java b/structurizr-inspection/src/test/java/com/structurizr/inspection/model/InfrastructureNodeDescriptionInspectionTests.java new file mode 100644 index 000000000..99be5e896 --- /dev/null +++ b/structurizr-inspection/src/test/java/com/structurizr/inspection/model/InfrastructureNodeDescriptionInspectionTests.java @@ -0,0 +1,39 @@ +package com.structurizr.inspection.model; + +import com.structurizr.Workspace; +import com.structurizr.inspection.DefaultInspector; +import com.structurizr.inspection.Severity; +import com.structurizr.inspection.Violation; +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"); + + Violation violation = new InfrastructureNodeDescriptionInspection(new DefaultInspector(workspace)).run(infrastructureNode); + Assertions.assertEquals(Severity.ERROR, violation.getSeverity()); + assertEquals("model.infrastructurenode.description", violation.getType()); + assertEquals("The infrastructure node \"Default/Deployment Node/Name\" is missing a description.", violation.getMessage()); + } + + @Test + public void run_WithDescription() { + Workspace workspace = new Workspace("Name", "Description"); + InfrastructureNode infrastructureNode = workspace.getModel().addDeploymentNode("Deployment Node") + .addInfrastructureNode("Name", "Description", "Technology"); + + Violation violation = new InfrastructureNodeDescriptionInspection(new DefaultInspector(workspace)).run(infrastructureNode); + assertNull(violation); + } + +} diff --git a/structurizr-inspection/src/test/java/com/structurizr/inspection/model/InfrastructureNodeTechnologyInspectionTests.java b/structurizr-inspection/src/test/java/com/structurizr/inspection/model/InfrastructureNodeTechnologyInspectionTests.java new file mode 100644 index 000000000..852bdeac8 --- /dev/null +++ b/structurizr-inspection/src/test/java/com/structurizr/inspection/model/InfrastructureNodeTechnologyInspectionTests.java @@ -0,0 +1,39 @@ +package com.structurizr.inspection.model; + +import com.structurizr.Workspace; +import com.structurizr.inspection.DefaultInspector; +import com.structurizr.inspection.Severity; +import com.structurizr.inspection.Violation; +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"); + + Violation violation = new InfrastructureNodeTechnologyInspection(new DefaultInspector(workspace)).run(infrastructureNode); + Assertions.assertEquals(Severity.ERROR, violation.getSeverity()); + assertEquals("model.infrastructurenode.technology", violation.getType()); + assertEquals("The infrastructure node \"Default/Deployment Node/Name\" is missing a technology.", violation.getMessage()); + } + + @Test + public void run_WithDescription() { + Workspace workspace = new Workspace("Name", "Description"); + InfrastructureNode infrastructureNode = workspace.getModel().addDeploymentNode("Deployment Node") + .addInfrastructureNode("Name", "Description", "Technology"); + + Violation violation = new InfrastructureNodeTechnologyInspection(new DefaultInspector(workspace)).run(infrastructureNode); + assertNull(violation); + } + +} diff --git a/structurizr-inspection/src/test/java/com/structurizr/inspection/model/MultipleSoftwareSystemsDetailedInspectionTests.java b/structurizr-inspection/src/test/java/com/structurizr/inspection/model/MultipleSoftwareSystemsDetailedInspectionTests.java new file mode 100644 index 000000000..6f3fe9023 --- /dev/null +++ b/structurizr-inspection/src/test/java/com/structurizr/inspection/model/MultipleSoftwareSystemsDetailedInspectionTests.java @@ -0,0 +1,81 @@ +package com.structurizr.inspection.model; + +import com.structurizr.Workspace; +import com.structurizr.documentation.Decision; +import com.structurizr.documentation.Format; +import com.structurizr.documentation.Section; +import com.structurizr.inspection.DefaultInspector; +import com.structurizr.inspection.Severity; +import com.structurizr.inspection.Violation; +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"); + + Violation violation = new MultipleSoftwareSystemsDetailedInspection(new DefaultInspector(workspace)).run(); + Assertions.assertEquals(Severity.ERROR, violation.getSeverity()); + assertEquals("workspace.scope", violation.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.", violation.getMessage()); + } + + @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")); + + Violation violation = new MultipleSoftwareSystemsDetailedInspection(new DefaultInspector(workspace)).run(); + Assertions.assertEquals(Severity.ERROR, violation.getSeverity()); + assertEquals("workspace.scope", violation.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.", violation.getMessage()); + } + + @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); + + Violation violation = new MultipleSoftwareSystemsDetailedInspection(new DefaultInspector(workspace)).run(); + Assertions.assertEquals(Severity.ERROR, violation.getSeverity()); + assertEquals("workspace.scope", violation.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.", violation.getMessage()); + } + + @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); + + Violation violation = new MultipleSoftwareSystemsDetailedInspection(new DefaultInspector(workspace)).run(); + assertNull(violation); + } + +} diff --git a/structurizr-inspection/src/test/java/com/structurizr/inspection/model/PersonDescriptionInspectionTests.java b/structurizr-inspection/src/test/java/com/structurizr/inspection/model/PersonDescriptionInspectionTests.java new file mode 100644 index 000000000..02fc6c914 --- /dev/null +++ b/structurizr-inspection/src/test/java/com/structurizr/inspection/model/PersonDescriptionInspectionTests.java @@ -0,0 +1,37 @@ +package com.structurizr.inspection.model; + +import com.structurizr.Workspace; +import com.structurizr.inspection.DefaultInspector; +import com.structurizr.inspection.Severity; +import com.structurizr.inspection.Violation; +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"); + + Violation violation = new PersonDescriptionInspection(new DefaultInspector(workspace)).run(person); + Assertions.assertEquals(Severity.ERROR, violation.getSeverity()); + assertEquals("model.person.description", violation.getType()); + assertEquals("The person \"Name\" is missing a description.", violation.getMessage()); + } + + @Test + public void run_WithDescription() { + Workspace workspace = new Workspace("Name", "Description"); + Person person = workspace.getModel().addPerson("Name", "Description"); + + Violation violation = new PersonDescriptionInspection(new DefaultInspector(workspace)).run(person); + assertNull(violation); + } + +} diff --git a/structurizr-inspection/src/test/java/com/structurizr/inspection/model/RelationshipDescriptionInspectionTests.java b/structurizr-inspection/src/test/java/com/structurizr/inspection/model/RelationshipDescriptionInspectionTests.java new file mode 100644 index 000000000..df3bae94e --- /dev/null +++ b/structurizr-inspection/src/test/java/com/structurizr/inspection/model/RelationshipDescriptionInspectionTests.java @@ -0,0 +1,44 @@ +package com.structurizr.inspection.model; + +import com.structurizr.Workspace; +import com.structurizr.inspection.DefaultInspector; +import com.structurizr.inspection.Severity; +import com.structurizr.inspection.Violation; +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() { + Workspace workspace = new Workspace("Name", "Description"); + SoftwareSystem a = workspace.getModel().addSoftwareSystem("A"); + SoftwareSystem b = workspace.getModel().addSoftwareSystem("B"); + Relationship relationship = a.uses(b, ""); + + Violation violation = new RelationshipDescriptionInspection(new DefaultInspector(workspace)).run(relationship); + Assertions.assertEquals(Severity.ERROR, violation.getSeverity()); + assertEquals("model.relationship.description", violation.getType()); + assertEquals("The relationship between the software system \"A\" and the software system \"B\" is missing a description.", violation.getMessage()); + } + + @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"); + + Violation violation = new RelationshipDescriptionInspection(new DefaultInspector(workspace)).run(relationship); + assertNull(violation); + } + +} diff --git a/structurizr-inspection/src/test/java/com/structurizr/inspection/model/RelationshipTechnologyInspectionTests.java b/structurizr-inspection/src/test/java/com/structurizr/inspection/model/RelationshipTechnologyInspectionTests.java new file mode 100644 index 000000000..ab022e14c --- /dev/null +++ b/structurizr-inspection/src/test/java/com/structurizr/inspection/model/RelationshipTechnologyInspectionTests.java @@ -0,0 +1,43 @@ +package com.structurizr.inspection.model; + +import com.structurizr.Workspace; +import com.structurizr.inspection.DefaultInspector; +import com.structurizr.inspection.Severity; +import com.structurizr.inspection.Violation; +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() { + Workspace workspace = new Workspace("Name", "Description"); + SoftwareSystem a = workspace.getModel().addSoftwareSystem("A"); + SoftwareSystem b = workspace.getModel().addSoftwareSystem("B"); + Relationship relationship = a.uses(b, "Description"); + + Violation violation = new RelationshipTechnologyInspection(new DefaultInspector(workspace)).run(relationship); + Assertions.assertEquals(Severity.ERROR, violation.getSeverity()); + assertEquals("model.relationship.technology", violation.getType()); + assertEquals("The relationship between the software system \"A\" and the software system \"B\" is missing a technology.", violation.getMessage()); + } + + @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"); + + Violation violation = new RelationshipTechnologyInspection(new DefaultInspector(workspace)).run(relationship); + assertNull(violation); + } + +} diff --git a/structurizr-inspection/src/test/java/com/structurizr/inspection/model/SoftwareSystemDecisionInspectionTests.java b/structurizr-inspection/src/test/java/com/structurizr/inspection/model/SoftwareSystemDecisionInspectionTests.java new file mode 100644 index 000000000..7508f3646 --- /dev/null +++ b/structurizr-inspection/src/test/java/com/structurizr/inspection/model/SoftwareSystemDecisionInspectionTests.java @@ -0,0 +1,49 @@ +package com.structurizr.inspection.model; + +import com.structurizr.Workspace; +import com.structurizr.documentation.Decision; +import com.structurizr.documentation.Format; +import com.structurizr.inspection.DefaultInspector; +import com.structurizr.inspection.Severity; +import com.structurizr.inspection.Violation; +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"); + + Violation violation = new SoftwareSystemDecisionsInspection(new DefaultInspector(workspace)).run(softwareSystem); + Assertions.assertEquals(Severity.ERROR, violation.getSeverity()); + assertEquals("model.softwaresystem.decisions", violation.getType()); + assertEquals("The software system \"Software System\" has containers, but is missing decisions.", violation.getMessage()); + } + + @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); + + Violation violation = new SoftwareSystemDecisionsInspection(new DefaultInspector(workspace)).run(softwareSystem); + assertNull(violation); + } + +} diff --git a/structurizr-inspection/src/test/java/com/structurizr/inspection/model/SoftwareSystemDescriptionInspectionTests.java b/structurizr-inspection/src/test/java/com/structurizr/inspection/model/SoftwareSystemDescriptionInspectionTests.java new file mode 100644 index 000000000..2b9405415 --- /dev/null +++ b/structurizr-inspection/src/test/java/com/structurizr/inspection/model/SoftwareSystemDescriptionInspectionTests.java @@ -0,0 +1,37 @@ +package com.structurizr.inspection.model; + +import com.structurizr.Workspace; +import com.structurizr.inspection.DefaultInspector; +import com.structurizr.inspection.Severity; +import com.structurizr.inspection.Violation; +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"); + + Violation violation = new SoftwareSystemDescriptionInspection(new DefaultInspector(workspace)).run(softwareSystem); + Assertions.assertEquals(Severity.ERROR, violation.getSeverity()); + assertEquals("model.softwaresystem.description", violation.getType()); + assertEquals("The software system \"Name\" is missing a description.", violation.getMessage()); + } + + @Test + public void run_WithDescription() { + Workspace workspace = new Workspace("Name", "Description"); + SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Name", "Description"); + + Violation violation = new SoftwareSystemDescriptionInspection(new DefaultInspector(workspace)).run(softwareSystem); + assertNull(violation); + } + +} diff --git a/structurizr-inspection/src/test/java/com/structurizr/inspection/model/SoftwareSystemDocumentationInspectionTests.java b/structurizr-inspection/src/test/java/com/structurizr/inspection/model/SoftwareSystemDocumentationInspectionTests.java new file mode 100644 index 000000000..e6d443477 --- /dev/null +++ b/structurizr-inspection/src/test/java/com/structurizr/inspection/model/SoftwareSystemDocumentationInspectionTests.java @@ -0,0 +1,43 @@ +package com.structurizr.inspection.model; + +import com.structurizr.Workspace; +import com.structurizr.documentation.Format; +import com.structurizr.documentation.Section; +import com.structurizr.inspection.DefaultInspector; +import com.structurizr.inspection.Severity; +import com.structurizr.inspection.Violation; +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"); + + Violation violation = new SoftwareSystemDocumentationInspection(new DefaultInspector(workspace)).run(softwareSystem); + Assertions.assertEquals(Severity.ERROR, violation.getSeverity()); + assertEquals("model.softwaresystem.documentation", violation.getType()); + assertEquals("The software system \"Software System\" has containers, but is missing documentation.", violation.getMessage()); + } + + @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")); + + Violation violation = new SoftwareSystemDocumentationInspection(new DefaultInspector(workspace)).run(softwareSystem); + assertNull(violation); + } + +} diff --git a/structurizr-inspection/src/test/java/com/structurizr/inspection/view/ContainerViewsForMultipleSoftwareSystemsInspectionTests.java b/structurizr-inspection/src/test/java/com/structurizr/inspection/view/ContainerViewsForMultipleSoftwareSystemsInspectionTests.java new file mode 100644 index 000000000..3c387d9df --- /dev/null +++ b/structurizr-inspection/src/test/java/com/structurizr/inspection/view/ContainerViewsForMultipleSoftwareSystemsInspectionTests.java @@ -0,0 +1,41 @@ +package com.structurizr.inspection.view; + +import com.structurizr.Workspace; +import com.structurizr.inspection.DefaultInspector; +import com.structurizr.inspection.Severity; +import com.structurizr.inspection.Violation; +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"); + + Violation violation = new ContainerViewsForMultipleSoftwareSystemsInspection(new DefaultInspector(workspace)).run(); + Assertions.assertEquals(Severity.ERROR, violation.getSeverity()); + assertEquals("workspace.scope", violation.getType()); + assertEquals("Container views exist for 2 software systems. It is recommended that a workspace includes container views for a single software system only.", violation.getMessage()); + } + + @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"); + + Violation violation = new ContainerViewsForMultipleSoftwareSystemsInspection(new DefaultInspector(workspace)).run(); + assertNull(violation); + } + +} diff --git a/structurizr-inspection/src/test/java/com/structurizr/inspection/view/ElementStyleMetadataInspectionTests.java b/structurizr-inspection/src/test/java/com/structurizr/inspection/view/ElementStyleMetadataInspectionTests.java new file mode 100644 index 000000000..8b55a903c --- /dev/null +++ b/structurizr-inspection/src/test/java/com/structurizr/inspection/view/ElementStyleMetadataInspectionTests.java @@ -0,0 +1,47 @@ +package com.structurizr.inspection.view; + +import com.structurizr.Workspace; +import com.structurizr.inspection.DefaultInspector; +import com.structurizr.inspection.Severity; +import com.structurizr.inspection.Violation; +import com.structurizr.view.ElementStyle; +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 + +ElementStyleMetadataInspectionTests { + + @Test + public void run_WithMetadataFalse() { + Workspace workspace = new Workspace("Name", "Description"); + ElementStyle elementStyle = workspace.getViews().getConfiguration().getStyles().addElementStyle("Tag").metadata(false); + + Violation violation = new ElementStyleMetadataInspection(new DefaultInspector(workspace)).run(elementStyle); + Assertions.assertEquals(Severity.ERROR, violation.getSeverity()); + assertEquals("views.styles.element.metadata", violation.getType()); + assertEquals("The element style for tag \"Tag\" has metadata hidden, which may introduce ambiguity on rendered diagrams.", violation.getMessage()); + } + + @Test + public void run_WithMetadataTrue() { + Workspace workspace = new Workspace("Name", "Description"); + ElementStyle elementStyle = workspace.getViews().getConfiguration().getStyles().addElementStyle("Tag").metadata(true); + + Violation violation = new ElementStyleMetadataInspection(new DefaultInspector(workspace)).run(elementStyle); + assertNull(violation); + } + + @Test + public void run_WithMetadataUnset() { + Workspace workspace = new Workspace("Name", "Description"); + ElementStyle elementStyle = workspace.getViews().getConfiguration().getStyles().addElementStyle("Tag"); + + Violation violation = new ElementStyleMetadataInspection(new DefaultInspector(workspace)).run(elementStyle); + assertNull(violation); + } + +} diff --git a/structurizr-inspection/src/test/java/com/structurizr/inspection/view/EmptyViewInspectionTests.java b/structurizr-inspection/src/test/java/com/structurizr/inspection/view/EmptyViewInspectionTests.java new file mode 100644 index 000000000..8a46cae9d --- /dev/null +++ b/structurizr-inspection/src/test/java/com/structurizr/inspection/view/EmptyViewInspectionTests.java @@ -0,0 +1,39 @@ +package com.structurizr.inspection.view; + +import com.structurizr.Workspace; +import com.structurizr.inspection.DefaultInspector; +import com.structurizr.inspection.Severity; +import com.structurizr.inspection.Violation; +import com.structurizr.view.SystemLandscapeView; +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 +EmptyViewInspectionTests { + + @Test + public void run_EmptyView() { + Workspace workspace = new Workspace("Name", "Description"); + SystemLandscapeView view = workspace.getViews().createSystemLandscapeView("key", "Description"); + + Violation violation = new EmptyViewInspection(new DefaultInspector(workspace)).run(view); + Assertions.assertEquals(Severity.ERROR, violation.getSeverity()); + assertEquals("views.view.empty", violation.getType()); + assertEquals("The view with key \"key\" is empty.", violation.getMessage()); + } + + @Test + public void run_NonEmptyView() { + Workspace workspace = new Workspace("Name", "Description"); + workspace.getModel().addPerson("User"); + SystemLandscapeView view = workspace.getViews().createSystemLandscapeView("key", "Description"); + view.addAllElements(); + + Violation violation = new EmptyViewInspection(new DefaultInspector(workspace)).run(view); + assertNull(violation); + } + +} diff --git a/structurizr-inspection/src/test/java/com/structurizr/inspection/view/EmptyViewsInspectionTests.java b/structurizr-inspection/src/test/java/com/structurizr/inspection/view/EmptyViewsInspectionTests.java new file mode 100644 index 000000000..93e06d247 --- /dev/null +++ b/structurizr-inspection/src/test/java/com/structurizr/inspection/view/EmptyViewsInspectionTests.java @@ -0,0 +1,36 @@ +package com.structurizr.inspection.view; + +import com.structurizr.Workspace; +import com.structurizr.inspection.DefaultInspector; +import com.structurizr.inspection.Severity; +import com.structurizr.inspection.Violation; +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"); + + Violation violation = new EmptyViewsInspection(new DefaultInspector(workspace)).run(); + Assertions.assertEquals(Severity.ERROR, violation.getSeverity()); + assertEquals("views.empty", violation.getType()); + assertEquals("This workspace has no views.", violation.getMessage()); + } + + @Test + public void run_WhenThereAreViews() { + Workspace workspace = new Workspace("Name", "Description"); + workspace.getViews().createSystemLandscapeView("key", "Description"); + + Violation violation = new EmptyViewsInspection(new DefaultInspector(workspace)).run(); + assertNull(violation); + } + +} diff --git a/structurizr-inspection/src/test/java/com/structurizr/inspection/view/GeneratedKeyInspectionTests.java b/structurizr-inspection/src/test/java/com/structurizr/inspection/view/GeneratedKeyInspectionTests.java new file mode 100644 index 000000000..82833a1f9 --- /dev/null +++ b/structurizr-inspection/src/test/java/com/structurizr/inspection/view/GeneratedKeyInspectionTests.java @@ -0,0 +1,36 @@ +package com.structurizr.inspection.view; + +import com.structurizr.Workspace; +import com.structurizr.inspection.DefaultInspector; +import com.structurizr.inspection.Severity; +import com.structurizr.inspection.Violation; +import com.structurizr.view.SystemLandscapeView; +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 GeneratedKeyInspectionTests { + + @Test + public void run_GeneratedKey() { + Workspace workspace = new Workspace("Name", "Description"); + SystemLandscapeView view = workspace.getViews().createSystemLandscapeView("", "Description"); + + Violation violation = new GeneratedKeyInspection(new DefaultInspector(workspace)).run(view); + Assertions.assertEquals(Severity.ERROR, violation.getSeverity()); + assertEquals("views.view.key", violation.getType()); + assertEquals("The view with key \"SystemLandscape-001\" has an automatically generated view key and this is not guaranteed to be stable over time.", violation.getMessage()); + } + + @Test + public void run_NonGeneratedKey() { + Workspace workspace = new Workspace("Name", "Description"); + SystemLandscapeView view = workspace.getViews().createSystemLandscapeView("key", "Description"); + + Violation violation = new GeneratedKeyInspection(new DefaultInspector(workspace)).run(view); + assertNull(violation); + } + +} diff --git a/structurizr-inspection/src/test/java/com/structurizr/inspection/view/ManualLayoutInspectionTests.java b/structurizr-inspection/src/test/java/com/structurizr/inspection/view/ManualLayoutInspectionTests.java new file mode 100644 index 000000000..95e6cacb4 --- /dev/null +++ b/structurizr-inspection/src/test/java/com/structurizr/inspection/view/ManualLayoutInspectionTests.java @@ -0,0 +1,56 @@ +package com.structurizr.inspection.view; + +import com.structurizr.Workspace; +import com.structurizr.inspection.DefaultInspector; +import com.structurizr.inspection.Severity; +import com.structurizr.inspection.Violation; +import com.structurizr.view.SystemLandscapeView; +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 ManualLayoutInspectionTests { + + @Test + public void run_GeneratedKeyAndManualLayout() { + Workspace workspace = new Workspace("Name", "Description"); + SystemLandscapeView view = workspace.getViews().createSystemLandscapeView("", "Description"); + + Violation violation = new ManualLayoutInspection(new DefaultInspector(workspace)).run(view); + Assertions.assertEquals(Severity.ERROR, violation.getSeverity()); + assertEquals("views.view.layout", violation.getType()); + assertEquals("The view with key \"SystemLandscape-001\" has an automatically generated view key and this may cause manual layout information to be lost in the future.", violation.getMessage()); + } + + @Test + public void run_GeneratedKeyAndAutomaticLayout() { + Workspace workspace = new Workspace("Name", "Description"); + SystemLandscapeView view = workspace.getViews().createSystemLandscapeView("", "Description"); + view.enableAutomaticLayout(); + + Violation violation = new ManualLayoutInspection(new DefaultInspector(workspace)).run(view); + assertNull(violation); + } + + @Test + public void run_NonGeneratedKeyAndManualLayout() { + Workspace workspace = new Workspace("Name", "Description"); + SystemLandscapeView view = workspace.getViews().createSystemLandscapeView("key", "Description"); + + Violation violation = new ManualLayoutInspection(new DefaultInspector(workspace)).run(view); + assertNull(violation); + } + + @Test + public void run_NonGeneratedKeyAndAutomaticLayout() { + Workspace workspace = new Workspace("Name", "Description"); + SystemLandscapeView view = workspace.getViews().createSystemLandscapeView("key", "Description"); + view.enableAutomaticLayout(); + + Violation violation = new ManualLayoutInspection(new DefaultInspector(workspace)).run(view); + assertNull(violation); + } + +} diff --git a/structurizr-inspection/src/test/java/com/structurizr/inspection/view/SystemContextViewsForMultipleSoftwareSystemsInspectionTests.java b/structurizr-inspection/src/test/java/com/structurizr/inspection/view/SystemContextViewsForMultipleSoftwareSystemsInspectionTests.java new file mode 100644 index 000000000..b65a5f914 --- /dev/null +++ b/structurizr-inspection/src/test/java/com/structurizr/inspection/view/SystemContextViewsForMultipleSoftwareSystemsInspectionTests.java @@ -0,0 +1,42 @@ +package com.structurizr.inspection.view; + +import com.structurizr.Workspace; +import com.structurizr.inspection.DefaultInspector; +import com.structurizr.inspection.Severity; +import com.structurizr.inspection.Violation; +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"); + + Violation violation = new SystemContextViewsForMultipleSoftwareSystemsInspection(new DefaultInspector(workspace)).run(); + Assertions.assertEquals(Severity.ERROR, violation.getSeverity()); + assertEquals("workspace.scope", violation.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.", violation.getMessage()); + } + + @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"); + + Violation violation = new SystemContextViewsForMultipleSoftwareSystemsInspection(new DefaultInspector(workspace)).run(); + assertNull(violation); + } + +} diff --git a/structurizr-inspection/src/test/java/com/structurizr/inspection/workspace/WorkspaceScopeInspectionTests.java b/structurizr-inspection/src/test/java/com/structurizr/inspection/workspace/WorkspaceScopeInspectionTests.java new file mode 100644 index 000000000..294666aef --- /dev/null +++ b/structurizr-inspection/src/test/java/com/structurizr/inspection/workspace/WorkspaceScopeInspectionTests.java @@ -0,0 +1,35 @@ +package com.structurizr.inspection.workspace; + +import com.structurizr.Workspace; +import com.structurizr.configuration.WorkspaceScope; +import com.structurizr.inspection.DefaultInspector; +import com.structurizr.inspection.Severity; +import com.structurizr.inspection.Violation; +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"); + + Violation violation = new WorkspaceScopeInspection(new DefaultInspector(workspace)).run(); + Assertions.assertEquals(Severity.ERROR, violation.getSeverity()); + assertEquals("workspace.scope", violation.getType()); + assertEquals("This workspace has no defined scope. It is recommended that the workspace scope is set to \"Landscape\" or \"SoftwareSystem\".", violation.getMessage()); + } + + @Test + public void run_WithScopedWorkspace() { + Workspace workspace = new Workspace("Name", "Description"); + workspace.getConfiguration().setScope(WorkspaceScope.SoftwareSystem); + + Violation violation = new WorkspaceScopeInspection(new DefaultInspector(workspace)).run(); + assertNull(violation); + } + +} diff --git a/structurizr-inspection/src/test/java/com/structurizr/inspection/workspace/WorkspaceToolingInspectionTests.java b/structurizr-inspection/src/test/java/com/structurizr/inspection/workspace/WorkspaceToolingInspectionTests.java new file mode 100644 index 000000000..425c68932 --- /dev/null +++ b/structurizr-inspection/src/test/java/com/structurizr/inspection/workspace/WorkspaceToolingInspectionTests.java @@ -0,0 +1,46 @@ +package com.structurizr.inspection.workspace; + +import com.structurizr.Workspace; +import com.structurizr.inspection.DefaultInspector; +import com.structurizr.inspection.Severity; +import com.structurizr.inspection.Violation; +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 WorkspaceToolingInspectionTests { + + @Test + public void run_WithLastModifiedAgentAsCloudServiceDslEditor() { + Workspace workspace = new Workspace("Name", "Description"); + workspace.setLastModifiedAgent("structurizr-cloud/dsl-editor/1234567890"); + + Violation violation = new WorkspaceToolingInspection(new DefaultInspector(workspace)).run(); + Assertions.assertEquals(Severity.ERROR, violation.getSeverity()); + assertEquals("workspace.tooling", violation.getType()); + assertEquals("The browser-based DSL editor is the easiest way to get started without installing any tooling, but it does not provide access to the full feature set of the Structurizr DSL. It is recommended that you use the Structurizr DSL in conjunction with the Structurizr CLI's \"push\" command.", violation.getMessage()); + } + + @Test + public void run_WithLastModifiedAgentAsOnpremisesInstallationDslEditor() { + Workspace workspace = new Workspace("Name", "Description"); + workspace.setLastModifiedAgent("structurizr-onpremises/dsl-editor/1234567890"); + + Violation violation = new WorkspaceToolingInspection(new DefaultInspector(workspace)).run(); + Assertions.assertEquals(Severity.ERROR, violation.getSeverity()); + assertEquals("workspace.tooling", violation.getType()); + assertEquals("The browser-based DSL editor is the easiest way to get started without installing any tooling, but it does not provide access to the full feature set of the Structurizr DSL. It is recommended that you use the Structurizr DSL in conjunction with the Structurizr CLI's \"push\" command.", violation.getMessage()); + } + + @Test + public void run_WithLastModifiedAgentAsStructurizrCli() { + Workspace workspace = new Workspace("Name", "Description"); + workspace.setLastModifiedAgent("structurizr-cli/2.0.0"); + + Violation violation = new WorkspaceToolingInspection(new DefaultInspector(workspace)).run(); + assertNull(violation); + } + +} diff --git a/structurizr-inspection/src/test/resources/workspace.dsl b/structurizr-inspection/src/test/resources/workspace.dsl new file mode 100644 index 000000000..8110ea302 --- /dev/null +++ b/structurizr-inspection/src/test/resources/workspace.dsl @@ -0,0 +1,30 @@ +workspace { + + properties { + structurizr.inspection.* INFO + structurizr.inspection.model.* WARNING + } + + model { + user = person "User" + softwareSystem1 = softwareSystem "Software System 1" { + container1 = container "Container 1" { + component1 = component "Component 1" + } + } + + user -> component1 + + deploymentEnvironment "dev" { + deploymentNode "Server 1" { + infrastructureNode "Load Balancer" + containerInstance container1 + } + } + } + + configuration { + scope softwaresystem + } + +} \ No newline at end of file diff --git a/structurizr-javaee/build.gradle b/structurizr-javaee/build.gradle deleted file mode 100644 index 6cf72e7e5..000000000 --- a/structurizr-javaee/build.gradle +++ /dev/null @@ -1,5 +0,0 @@ -dependencies { - compile project(':structurizr-core') - - compile 'javax:javaee-api:7.0' -} diff --git a/structurizr-javaee/src/com/structurizr/analysis/JavaEEComponentFinderStrategy.java b/structurizr-javaee/src/com/structurizr/analysis/JavaEEComponentFinderStrategy.java deleted file mode 100644 index a78673e49..000000000 --- a/structurizr-javaee/src/com/structurizr/analysis/JavaEEComponentFinderStrategy.java +++ /dev/null @@ -1,38 +0,0 @@ -package com.structurizr.analysis; - -import com.structurizr.model.Component; - -import javax.ejb.Singleton; -import javax.ejb.Stateful; -import javax.ejb.Stateless; -import javax.inject.Named; -import javax.websocket.server.ServerEndpoint; -import javax.ws.rs.Path; -import java.util.HashSet; -import java.util.Set; - -public class JavaEEComponentFinderStrategy extends AbstractComponentFinderStrategy { - - public JavaEEComponentFinderStrategy() { - super(new FirstImplementationOfInterfaceSupportingTypesStrategy()); - } - - public JavaEEComponentFinderStrategy(SupportingTypesStrategy... strategies) { - super(strategies); - } - - @Override - protected Set<Component> doFindComponents() { - Set<Component> components = new HashSet<>(); - - components.addAll(findClassesWithAnnotation(Path.class, "JAX-RS web service")); - components.addAll(findClassesWithAnnotation(ServerEndpoint.class, "Websocket endpoint")); - components.addAll(findClassesWithAnnotation(Stateless.class, "Stateless session bean")); - components.addAll(findClassesWithAnnotation(Stateful.class, "Stateful session bean")); - components.addAll(findClassesWithAnnotation(Singleton.class, "Singleton session bean")); - components.addAll(findClassesWithAnnotation(Named.class, "Named bean")); - - return components; - } - -} diff --git a/structurizr-spring/build.gradle b/structurizr-spring/build.gradle deleted file mode 100644 index b2cecf3d9..000000000 --- a/structurizr-spring/build.gradle +++ /dev/null @@ -1,9 +0,0 @@ -dependencies { - compile project(':structurizr-core') - - compile 'org.springframework:spring-web:4.2.5.RELEASE' - compile 'org.springframework.data:spring-data-jpa:1.9.4.RELEASE' - - testCompile 'junit:junit:4.12' - -} \ No newline at end of file diff --git a/structurizr-spring/src/com/structurizr/analysis/AbstractSpringComponentFinderStrategy.java b/structurizr-spring/src/com/structurizr/analysis/AbstractSpringComponentFinderStrategy.java deleted file mode 100644 index 3411cc582..000000000 --- a/structurizr-spring/src/com/structurizr/analysis/AbstractSpringComponentFinderStrategy.java +++ /dev/null @@ -1,82 +0,0 @@ -package com.structurizr.analysis; - -import com.structurizr.model.Component; - -import java.lang.annotation.Annotation; -import java.lang.reflect.Modifier; -import java.util.HashSet; -import java.util.Set; - -public abstract class AbstractSpringComponentFinderStrategy extends AbstractComponentFinderStrategy { - - public static final String SPRING_MVC_CONTROLLER = "Spring MVC Controller"; - public static final String SPRING_SERVICE = "Spring Service"; - public static final String SPRING_REPOSITORY = "Spring Repository"; - public static final String SPRING_COMPONENT = "Spring Component"; - public static final String SPRING_REST_CONTROLLER = "Spring REST Controller"; - - protected boolean includePublicTypesOnly = true; - - public AbstractSpringComponentFinderStrategy(SupportingTypesStrategy... strategies) { - super(strategies); - } - - protected Set<Component> findInterfacesForImplementationClassesWithAnnotation(Class<? extends Annotation> type, String technology) { - Set<Component> components = new HashSet<>(); - - Set<Class<?>> annotatedTypes = findTypesAnnotatedWith(type); - for (Class<?> annotatedType : annotatedTypes) { - if (annotatedType.isInterface()) { - // the annotated type is an interface, so we're done - components.add(getComponentFinder().getContainer().addComponent( - annotatedType.getSimpleName(), annotatedType.getCanonicalName(), "", technology)); - } else { - // The Spring @Component, @Service and @Repository annotations are typically used to annotate implementation - // classes, but we really want to find the interface type and use that to represent the component. Why? - // Well, for example, a Spring MVC controller may have a dependency on a "SomeRepository" interface, but - // it's the "JdbcSomeRepositoryImpl" implementation class that gets annotated with @Repository. - // - // This next bit of code tries to find the "SomeRepository" interface... - String componentName = annotatedType.getSimpleName(); // e.g. JdbcSomeRepositoryImpl - Class<?> componentType = annotatedType; - boolean foundInterface = false; - - if (annotatedType.getInterfaces().length > 0) { - for (Class interfaceType : annotatedType.getInterfaces()) { - String interfaceName = interfaceType.getSimpleName(); - if (componentName.startsWith(interfaceName) || // <InterfaceName><***> - componentName.endsWith(interfaceName) || // <***><InterfaceName> - componentName.contains(interfaceName)) { // <***><InterfaceName><***> - componentName = interfaceName; - componentType = interfaceType; - foundInterface = true; - break; - } - } - } - - if (!includePublicTypesOnly || Modifier.isPublic(componentType.getModifiers())) { - Component component = getComponentFinder().getContainer().addComponent(componentName, componentType, "", technology); - components.add(component); - - if (foundInterface) { - // the primary component type is now an interface, so add the type we originally found as a supporting type - component.addSupportingType(annotatedType.getCanonicalName()); - } - } - } - } - - return components; - } - - /** - * Sets whether this component finder strategy only finds components that are based upon public types. - * - * @param includePublicTypesOnly true for public types only, false otherwise - */ - public void setIncludePublicTypesOnly(boolean includePublicTypesOnly) { - this.includePublicTypesOnly = includePublicTypesOnly; - } - -} \ No newline at end of file diff --git a/structurizr-spring/src/com/structurizr/analysis/SpringComponentComponentFinderStrategy.java b/structurizr-spring/src/com/structurizr/analysis/SpringComponentComponentFinderStrategy.java deleted file mode 100644 index 2d0b5a8ac..000000000 --- a/structurizr-spring/src/com/structurizr/analysis/SpringComponentComponentFinderStrategy.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.structurizr.analysis; - -import com.structurizr.model.Component; - -import java.util.Set; - -/** - * A component finder strategy that finds Spring components (classes annotated @Component). - */ -public final class SpringComponentComponentFinderStrategy extends AbstractSpringComponentFinderStrategy { - - public SpringComponentComponentFinderStrategy(SupportingTypesStrategy... strategies) { - super(strategies); - } - - @Override - protected Set<Component> doFindComponents() { - return findInterfacesForImplementationClassesWithAnnotation( - org.springframework.stereotype.Component.class, - SPRING_COMPONENT - ); - } - -} \ No newline at end of file diff --git a/structurizr-spring/src/com/structurizr/analysis/SpringComponentFinderStrategy.java b/structurizr-spring/src/com/structurizr/analysis/SpringComponentFinderStrategy.java deleted file mode 100644 index 26fceb3a0..000000000 --- a/structurizr-spring/src/com/structurizr/analysis/SpringComponentFinderStrategy.java +++ /dev/null @@ -1,67 +0,0 @@ -package com.structurizr.analysis; - -import com.structurizr.model.Component; - -import java.util.HashSet; -import java.util.LinkedList; -import java.util.List; -import java.util.Set; - -/** - * <p> - * This component finder strategy knows how to find the following Spring components: - * </p> - * - * <ul> - * <li>Spring MVC controllers (classes annotated @Controller)</li> - * <li>Spring REST controllers (classes annotated @RestController)</li> - * <li>Spring services (classes annotated @Service)</li> - * <li>Spring components (classes annotated @Component)</li> - * <li>Spring repositories (classes annotated @Repository, plus those that extend JpaRepository or CrudRepository)</li> - * </ul> - * - * <p> - * By default, non-public types will be ignored so that, for example, you can - * hide repository implementations behind services, as described at - * <a href="http://olivergierke.de/2013/01/whoops-where-did-my-architecture-go/">Whoops! Where did my architecture go</a>. - * You can change this behaviour by passing false to {@link #setIncludePublicTypesOnly(boolean)}. - * </p> - */ -public class SpringComponentFinderStrategy extends AbstractSpringComponentFinderStrategy { - - private List<AbstractSpringComponentFinderStrategy> componentFinderStrategies = new LinkedList<>(); - - public SpringComponentFinderStrategy(SupportingTypesStrategy... strategies) { - super(strategies); - } - - @Override - public void beforeFindComponents() { - super.beforeFindComponents(); - - componentFinderStrategies.add(new SpringRestControllerComponentFinderStrategy()); - componentFinderStrategies.add(new SpringMvcControllerComponentFinderStrategy()); - componentFinderStrategies.add(new SpringServiceComponentFinderStrategy()); - componentFinderStrategies.add(new SpringComponentComponentFinderStrategy()); - componentFinderStrategies.add(new SpringRepositoryComponentFinderStrategy()); - - for (AbstractSpringComponentFinderStrategy componentFinderStrategy : componentFinderStrategies) { - componentFinderStrategy.setIncludePublicTypesOnly(includePublicTypesOnly); - componentFinderStrategy.setComponentFinder(getComponentFinder()); - supportingTypesStrategies.forEach(componentFinderStrategy::addSupportingTypesStrategy); - componentFinderStrategy.beforeFindComponents(); - } - } - - @Override - protected Set<Component> doFindComponents() { - Set<Component> components = new HashSet<>(); - - for (AbstractComponentFinderStrategy componentFinderStrategy : componentFinderStrategies) { - components.addAll(componentFinderStrategy.findComponents()); - } - - return components; - } - -} \ No newline at end of file diff --git a/structurizr-spring/src/com/structurizr/analysis/SpringMvcControllerComponentFinderStrategy.java b/structurizr-spring/src/com/structurizr/analysis/SpringMvcControllerComponentFinderStrategy.java deleted file mode 100644 index 6ad9ea99f..000000000 --- a/structurizr-spring/src/com/structurizr/analysis/SpringMvcControllerComponentFinderStrategy.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.structurizr.analysis; - -import com.structurizr.model.Component; - -import java.util.Set; - -/** - * A component finder strategy that finds Spring MVC controllers (classes annotated @Controller). - */ -public final class SpringMvcControllerComponentFinderStrategy extends AbstractSpringComponentFinderStrategy { - - public SpringMvcControllerComponentFinderStrategy(SupportingTypesStrategy... strategies) { - super(strategies); - } - - @Override - protected Set<Component> doFindComponents() { - return findClassesWithAnnotation( - org.springframework.stereotype.Controller.class, - SPRING_MVC_CONTROLLER, - includePublicTypesOnly - ); - } - -} \ No newline at end of file diff --git a/structurizr-spring/src/com/structurizr/analysis/SpringRepositoryComponentFinderStrategy.java b/structurizr-spring/src/com/structurizr/analysis/SpringRepositoryComponentFinderStrategy.java deleted file mode 100644 index c6fceffbb..000000000 --- a/structurizr-spring/src/com/structurizr/analysis/SpringRepositoryComponentFinderStrategy.java +++ /dev/null @@ -1,67 +0,0 @@ -package com.structurizr.analysis; - -import com.structurizr.model.Component; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.repository.CrudRepository; -import org.springframework.data.repository.Repository; - -import java.lang.reflect.Modifier; -import java.util.HashSet; -import java.util.Set; - -/** - * A component finder strategy for Spring repositories (classes annotated @Repository, - * plus those that extend JpaRepository or CrudRepository). - */ -public final class SpringRepositoryComponentFinderStrategy extends AbstractSpringComponentFinderStrategy { - - public SpringRepositoryComponentFinderStrategy(SupportingTypesStrategy... strategies) { - super(strategies); - } - - @Override - protected Set<Component> doFindComponents() { - Set<Component> components = new HashSet<>(); - - components.addAll(findAnnotatedSpringRepositories()); - components.addAll(findSpringRepositoryInterfaces()); - - return components; - } - - private Set<Component> findSpringRepositoryInterfaces() { - Set<Component> componentsFound = new HashSet<>(); - Set<Class<?>> componentTypes = new HashSet<>(); - - Set<Class<?>> types = getTypeRepository().getAllTypes(); - for (Class<?> type : types) { - if (type.isInterface()) { - if ( - Repository.class.isAssignableFrom(type) || - JpaRepository.class.isAssignableFrom(type) || - CrudRepository.class.isAssignableFrom(type) - ) { - componentTypes.add(type); - } - } - } - - for (Class<?> componentType : componentTypes) { - if (!includePublicTypesOnly || Modifier.isPublic(componentType.getModifiers())) { - componentsFound.add(getComponentFinder().getContainer().addComponent( - componentType.getSimpleName(), - componentType.getCanonicalName(), - "", - SPRING_REPOSITORY)); - } - } - - return componentsFound; - } - - private Set<Component> findAnnotatedSpringRepositories() { - return findInterfacesForImplementationClassesWithAnnotation( - org.springframework.stereotype.Repository.class, SPRING_REPOSITORY); - } - -} \ No newline at end of file diff --git a/structurizr-spring/src/com/structurizr/analysis/SpringRestControllerComponentFinderStrategy.java b/structurizr-spring/src/com/structurizr/analysis/SpringRestControllerComponentFinderStrategy.java deleted file mode 100644 index 7459ceb4a..000000000 --- a/structurizr-spring/src/com/structurizr/analysis/SpringRestControllerComponentFinderStrategy.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.structurizr.analysis; - -import com.structurizr.model.Component; - -import java.util.Set; - -/** - * A component finder strategy that finds Spring REST controllers (classes annotated @RestController). - */ -public final class SpringRestControllerComponentFinderStrategy extends AbstractSpringComponentFinderStrategy { - - public SpringRestControllerComponentFinderStrategy(SupportingTypesStrategy... strategies) { - super(strategies); - } - - @Override - protected Set<Component> doFindComponents() { - return findClassesWithAnnotation( - org.springframework.web.bind.annotation.RestController.class, - SPRING_REST_CONTROLLER, - includePublicTypesOnly - ); - } - -} \ No newline at end of file diff --git a/structurizr-spring/src/com/structurizr/analysis/SpringServiceComponentFinderStrategy.java b/structurizr-spring/src/com/structurizr/analysis/SpringServiceComponentFinderStrategy.java deleted file mode 100644 index 48211cc02..000000000 --- a/structurizr-spring/src/com/structurizr/analysis/SpringServiceComponentFinderStrategy.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.structurizr.analysis; - -import com.structurizr.model.Component; - -import java.util.Set; - -/** - * A component finder strategy that finds Spring Services (classes annotated @Service). - */ -public final class SpringServiceComponentFinderStrategy extends AbstractSpringComponentFinderStrategy { - - public SpringServiceComponentFinderStrategy(SupportingTypesStrategy... strategies) { - super(strategies); - } - - @Override - protected Set<Component> doFindComponents() { - return findInterfacesForImplementationClassesWithAnnotation( - org.springframework.stereotype.Service.class, - SPRING_SERVICE - ); - } - -} \ No newline at end of file diff --git a/structurizr-spring/test/unit/com/structurizr/analysis/AbstractSpringComponentFinderStrategyTests.java b/structurizr-spring/test/unit/com/structurizr/analysis/AbstractSpringComponentFinderStrategyTests.java deleted file mode 100644 index a22188da4..000000000 --- a/structurizr-spring/test/unit/com/structurizr/analysis/AbstractSpringComponentFinderStrategyTests.java +++ /dev/null @@ -1,65 +0,0 @@ -package com.structurizr.analysis; - -import com.structurizr.Workspace; -import com.structurizr.model.Component; -import com.structurizr.model.Container; -import com.structurizr.model.Model; -import com.structurizr.model.SoftwareSystem; -import org.junit.Test; - -import static org.junit.Assert.assertEquals; - -public class AbstractSpringComponentFinderStrategyTests { - - @Test - public void test_findComponents_IgnoresNonPublicTypesByDefault() throws Exception { - Workspace workspace = new Workspace("Name", "Description"); - Model model = workspace.getModel(); - SoftwareSystem softwareSystem = model.addSoftwareSystem("Name", "Description"); - Container container = softwareSystem.addContainer("Name", "Description", "Technology"); - - ComponentFinder componentFinder = new ComponentFinder( - container, - "test.AbstractSpringComponentFinderStrategy", - new SpringComponentFinderStrategy() - ); - componentFinder.findComponents(); - - assertEquals(2, container.getComponents().size()); - - Component component = container.getComponentWithName("SomeController"); - assertEquals("test.AbstractSpringComponentFinderStrategy.SomeController", component.getType()); - - component = container.getComponentWithName("SomePublicRepository"); - assertEquals("test.AbstractSpringComponentFinderStrategy.SomePublicRepository", component.getType()); - } - - @Test - public void test_findComponents_DoesNotIgnoreNonPublicTypes_WhenConfiguredToIncludeNonPublicTypes() throws Exception { - Workspace workspace = new Workspace("Name", "Description"); - Model model = workspace.getModel(); - SoftwareSystem softwareSystem = model.addSoftwareSystem("Name", "Description"); - Container container = softwareSystem.addContainer("Name", "Description", "Technology"); - - SpringComponentFinderStrategy springComponentFinderStrategy = new SpringComponentFinderStrategy(); - springComponentFinderStrategy.setIncludePublicTypesOnly(false); - ComponentFinder componentFinder = new ComponentFinder( - container, - "test.AbstractSpringComponentFinderStrategy", - springComponentFinderStrategy - ); - componentFinder.findComponents(); - - assertEquals(3, container.getComponents().size()); - - Component component = container.getComponentWithName("SomeController"); - assertEquals("test.AbstractSpringComponentFinderStrategy.SomeController", component.getType()); - - component = container.getComponentWithName("SomePublicRepository"); - assertEquals("test.AbstractSpringComponentFinderStrategy.SomePublicRepository", component.getType()); - - component = container.getComponentWithName("SomeNonPublicRepository"); - assertEquals("test.AbstractSpringComponentFinderStrategy.SomeNonPublicRepository", component.getType()); - } - -} diff --git a/structurizr-spring/test/unit/com/structurizr/analysis/SpringComponentComponentFinderStrategyTests.java b/structurizr-spring/test/unit/com/structurizr/analysis/SpringComponentComponentFinderStrategyTests.java deleted file mode 100644 index 9e45a6a8f..000000000 --- a/structurizr-spring/test/unit/com/structurizr/analysis/SpringComponentComponentFinderStrategyTests.java +++ /dev/null @@ -1,35 +0,0 @@ -package com.structurizr.analysis; - -import com.structurizr.Workspace; -import com.structurizr.model.Component; -import com.structurizr.model.Container; -import com.structurizr.model.Model; -import com.structurizr.model.SoftwareSystem; -import org.junit.Test; - -import static org.junit.Assert.assertEquals; - -public class SpringComponentComponentFinderStrategyTests { - - @Test - public void test_findComponents_FindsSpringComponents() throws Exception { - Workspace workspace = new Workspace("Name", "Description"); - Model model = workspace.getModel(); - SoftwareSystem softwareSystem = model.addSoftwareSystem("Name", "Description"); - Container container = softwareSystem.addContainer("Name", "Description", "Technology"); - - ComponentFinder componentFinder = new ComponentFinder( - container, - "test.SpringComponentComponentFinderStrategy", - new SpringComponentComponentFinderStrategy() - ); - componentFinder.findComponents(); - - assertEquals(1, container.getComponents().size()); - Component component = container.getComponentWithName("SomeComponent"); - assertEquals("test.SpringComponentComponentFinderStrategy.SomeComponent", component.getType()); - assertEquals("", component.getDescription()); - assertEquals("Spring Component", component.getTechnology()); - } - -} diff --git a/structurizr-spring/test/unit/com/structurizr/analysis/SpringComponentFinderStrategyTests.java b/structurizr-spring/test/unit/com/structurizr/analysis/SpringComponentFinderStrategyTests.java deleted file mode 100644 index b45f15207..000000000 --- a/structurizr-spring/test/unit/com/structurizr/analysis/SpringComponentFinderStrategyTests.java +++ /dev/null @@ -1,98 +0,0 @@ -package com.structurizr.analysis; - -import com.structurizr.Workspace; -import com.structurizr.model.*; -import org.junit.Before; -import org.junit.Test; - -import java.util.Set; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; - -public class SpringComponentFinderStrategyTests { - - private Container webApplication; - - @Before - public void setUp() { - Workspace workspace = new Workspace("Name", "Description"); - Model model = workspace.getModel(); - SoftwareSystem softwareSystem = model.addSoftwareSystem("Name", "Description"); - webApplication = softwareSystem.addContainer("Name", "Description", "Technology"); - } - - @Test - public void test_findComponents() throws Exception { - ComponentFinder componentFinder = new ComponentFinder( - webApplication, - "com.structurizr.analysis.myapp", - new SpringComponentFinderStrategy() - ); - componentFinder.findComponents(); - - assertEquals(5, webApplication.getComponents().size()); - assertEquals(0, webApplication.getComponents().stream().filter(c -> "SomeNonPublicRepository".equals(c.getName())).count()); - - Component someMvcController = webApplication.getComponentWithName("SomeController"); - assertNotNull(someMvcController); - assertEquals("SomeController", someMvcController.getName()); - assertEquals("com.structurizr.analysis.myapp.web.SomeController", someMvcController.getType()); - assertEquals(1, someMvcController.getCode().size()); - - Component someRestController = webApplication.getComponentWithName("SomeApiController"); - assertNotNull(someRestController); - assertEquals("SomeApiController", someRestController.getName()); - assertEquals("com.structurizr.analysis.myapp.api.SomeApiController", someRestController.getType()); - assertEquals(1, someRestController.getCode().size()); - - Component someService = webApplication.getComponentWithName("SomeService"); - assertNotNull(someService); - assertEquals("SomeService", someService.getName()); - assertEquals("com.structurizr.analysis.myapp.service.SomeService", someService.getType()); - assertEquals(2, someService.getCode().size()); - assertCodeElementInComponent(someService, "com.structurizr.analysis.myapp.service.SomeService", CodeElementRole.Primary); - assertCodeElementInComponent(someService, "com.structurizr.analysis.myapp.service.SomeServiceImpl", CodeElementRole.Supporting); - - Component someRepository = webApplication.getComponentWithName("SomeRepository"); - assertNotNull(someRepository); - assertEquals("SomeRepository", someRepository.getName()); - assertEquals("com.structurizr.analysis.myapp.data.SomeRepository", someRepository.getType()); - assertEquals(2, someRepository.getCode().size()); - assertCodeElementInComponent(someService, "com.structurizr.analysis.myapp.data.SomeRepository", CodeElementRole.Primary); - assertCodeElementInComponent(someService, "com.structurizr.analysis.myapp.data.JdbcSomeRepository", CodeElementRole.Supporting); - - - Component someOtherRepository = webApplication.getComponentWithName("SomeOtherRepository"); - assertNotNull(someOtherRepository); - assertEquals("SomeOtherRepository", someOtherRepository.getName()); - assertEquals("com.structurizr.analysis.myapp.data.SomeOtherRepository", someOtherRepository.getType()); - - assertEquals(1, someMvcController.getRelationships().size()); - Relationship relationship = someMvcController.getRelationships().iterator().next(); - assertEquals(someMvcController, relationship.getSource()); - assertEquals(someService, relationship.getDestination()); - - assertEquals(1, someRestController.getRelationships().size()); - relationship = someRestController.getRelationships().iterator().next(); - assertEquals(someRestController, relationship.getSource()); - assertEquals(someService, relationship.getDestination()); - - assertEquals(2, someService.getRelationships().size()); - - Set<Relationship> relationships = someService.getRelationships(); - assertNotNull(relationships.stream().filter(r -> r.getDestination() == someRepository).findFirst().get()); - assertNotNull(relationships.stream().filter(r -> r.getDestination() == someOtherRepository).findFirst().get()); - } - - private boolean assertCodeElementInComponent(Component component, String type, CodeElementRole role) { - for (CodeElement codeElement : component.getCode()) { - if (codeElement.getType().equals(type)) { - return codeElement.getRole() == role; - } - } - - return false; - } - -} diff --git a/structurizr-spring/test/unit/com/structurizr/analysis/SpringMvcControllerComponentFinderStrategyTests.java b/structurizr-spring/test/unit/com/structurizr/analysis/SpringMvcControllerComponentFinderStrategyTests.java deleted file mode 100644 index ac80644d6..000000000 --- a/structurizr-spring/test/unit/com/structurizr/analysis/SpringMvcControllerComponentFinderStrategyTests.java +++ /dev/null @@ -1,35 +0,0 @@ -package com.structurizr.analysis; - -import com.structurizr.Workspace; -import com.structurizr.model.Component; -import com.structurizr.model.Container; -import com.structurizr.model.Model; -import com.structurizr.model.SoftwareSystem; -import org.junit.Test; - -import static org.junit.Assert.assertEquals; - -public class SpringMvcControllerComponentFinderStrategyTests { - - @Test - public void test_findComponents_FindsSpringMvcControllers() throws Exception { - Workspace workspace = new Workspace("Name", "Description"); - Model model = workspace.getModel(); - SoftwareSystem softwareSystem = model.addSoftwareSystem("Name", "Description"); - Container container = softwareSystem.addContainer("Name", "Description", "Technology"); - - ComponentFinder componentFinder = new ComponentFinder( - container, - "test.SpringMvcControllerComponentFinderStrategy", - new SpringMvcControllerComponentFinderStrategy() - ); - componentFinder.findComponents(); - - assertEquals(1, container.getComponents().size()); - Component component = container.getComponentWithName("SomeController"); - assertEquals("test.SpringMvcControllerComponentFinderStrategy.SomeController", component.getType()); - assertEquals("", component.getDescription()); - assertEquals("Spring MVC Controller", component.getTechnology()); - } - -} diff --git a/structurizr-spring/test/unit/com/structurizr/analysis/SpringRepositoryComponentFinderStrategyTests.java b/structurizr-spring/test/unit/com/structurizr/analysis/SpringRepositoryComponentFinderStrategyTests.java deleted file mode 100644 index 0c7875a90..000000000 --- a/structurizr-spring/test/unit/com/structurizr/analysis/SpringRepositoryComponentFinderStrategyTests.java +++ /dev/null @@ -1,77 +0,0 @@ -package com.structurizr.analysis; - -import com.structurizr.Workspace; -import com.structurizr.model.Component; -import com.structurizr.model.Container; -import com.structurizr.model.Model; -import com.structurizr.model.SoftwareSystem; -import org.junit.Test; - -import static org.junit.Assert.assertEquals; - -public class SpringRepositoryComponentFinderStrategyTests { - - @Test - public void test_findComponents_FindsSpringRepositoriesDefinedWithAnAnnotation() throws Exception { - Workspace workspace = new Workspace("Name", "Description"); - Model model = workspace.getModel(); - SoftwareSystem softwareSystem = model.addSoftwareSystem("Name", "Description"); - Container container = softwareSystem.addContainer("Name", "Description", "Technology"); - - ComponentFinder componentFinder = new ComponentFinder( - container, - "test.SpringRepositoryComponentFinderStrategy.annotation", - new SpringRepositoryComponentFinderStrategy() - ); - componentFinder.findComponents(); - - assertEquals(1, container.getComponents().size()); - Component component = container.getComponentWithName("SomeRepository"); - assertEquals("test.SpringRepositoryComponentFinderStrategy.annotation.SomeRepository", component.getType()); - assertEquals("", component.getDescription()); - assertEquals("Spring Repository", component.getTechnology()); - } - - @Test - public void test_findComponents_FindsSpringRepositoriesThatExtendJpaRepository() throws Exception { - Workspace workspace = new Workspace("Name", "Description"); - Model model = workspace.getModel(); - SoftwareSystem softwareSystem = model.addSoftwareSystem("Name", "Description"); - Container container = softwareSystem.addContainer("Name", "Description", "Technology"); - - ComponentFinder componentFinder = new ComponentFinder( - container, - "test.SpringRepositoryComponentFinderStrategy.jpaRepository", - new SpringRepositoryComponentFinderStrategy() - ); - componentFinder.findComponents(); - - assertEquals(1, container.getComponents().size()); - Component component = container.getComponentWithName("SomeJpaRepository"); - assertEquals("test.SpringRepositoryComponentFinderStrategy.jpaRepository.SomeJpaRepository", component.getType()); - assertEquals("", component.getDescription()); - assertEquals("Spring Repository", component.getTechnology()); - } - - @Test - public void test_findComponents_FindsSpringRepositoriesThatExtendCrudRepository() throws Exception { - Workspace workspace = new Workspace("Name", "Description"); - Model model = workspace.getModel(); - SoftwareSystem softwareSystem = model.addSoftwareSystem("Name", "Description"); - Container container = softwareSystem.addContainer("Name", "Description", "Technology"); - - ComponentFinder componentFinder = new ComponentFinder( - container, - "test.SpringRepositoryComponentFinderStrategy.crudRepository", - new SpringRepositoryComponentFinderStrategy() - ); - componentFinder.findComponents(); - - assertEquals(1, container.getComponents().size()); - Component component = container.getComponentWithName("SomeCrudRepository"); - assertEquals("test.SpringRepositoryComponentFinderStrategy.crudRepository.SomeCrudRepository", component.getType()); - assertEquals("", component.getDescription()); - assertEquals("Spring Repository", component.getTechnology()); - } - -} diff --git a/structurizr-spring/test/unit/com/structurizr/analysis/SpringRestControllerComponentFinderStrategyTests.java b/structurizr-spring/test/unit/com/structurizr/analysis/SpringRestControllerComponentFinderStrategyTests.java deleted file mode 100644 index 1eb4f94ce..000000000 --- a/structurizr-spring/test/unit/com/structurizr/analysis/SpringRestControllerComponentFinderStrategyTests.java +++ /dev/null @@ -1,35 +0,0 @@ -package com.structurizr.analysis; - -import com.structurizr.Workspace; -import com.structurizr.model.Component; -import com.structurizr.model.Container; -import com.structurizr.model.Model; -import com.structurizr.model.SoftwareSystem; -import org.junit.Test; - -import static org.junit.Assert.assertEquals; - -public class SpringRestControllerComponentFinderStrategyTests { - - @Test - public void test_findComponents_FindsSpringRestControllers() throws Exception { - Workspace workspace = new Workspace("Name", "Description"); - Model model = workspace.getModel(); - SoftwareSystem softwareSystem = model.addSoftwareSystem("Name", "Description"); - Container container = softwareSystem.addContainer("Name", "Description", "Technology"); - - ComponentFinder componentFinder = new ComponentFinder( - container, - "test.SpringRestControllerComponentFinderStrategy", - new SpringRestControllerComponentFinderStrategy() - ); - componentFinder.findComponents(); - - assertEquals(1, container.getComponents().size()); - Component component = container.getComponentWithName("SomeController"); - assertEquals("test.SpringRestControllerComponentFinderStrategy.SomeController", component.getType()); - assertEquals("", component.getDescription()); - assertEquals("Spring REST Controller", component.getTechnology()); - } - -} diff --git a/structurizr-spring/test/unit/com/structurizr/analysis/SpringServiceComponentFinderStrategyTests.java b/structurizr-spring/test/unit/com/structurizr/analysis/SpringServiceComponentFinderStrategyTests.java deleted file mode 100644 index bf728d50d..000000000 --- a/structurizr-spring/test/unit/com/structurizr/analysis/SpringServiceComponentFinderStrategyTests.java +++ /dev/null @@ -1,35 +0,0 @@ -package com.structurizr.analysis; - -import com.structurizr.Workspace; -import com.structurizr.model.Component; -import com.structurizr.model.Container; -import com.structurizr.model.Model; -import com.structurizr.model.SoftwareSystem; -import org.junit.Test; - -import static org.junit.Assert.assertEquals; - -public class SpringServiceComponentFinderStrategyTests { - - @Test - public void test_findComponents_FindsSpringServices() throws Exception { - Workspace workspace = new Workspace("Name", "Description"); - Model model = workspace.getModel(); - SoftwareSystem softwareSystem = model.addSoftwareSystem("Name", "Description"); - Container container = softwareSystem.addContainer("Name", "Description", "Technology"); - - ComponentFinder componentFinder = new ComponentFinder( - container, - "test.SpringServiceComponentFinderStrategy", - new SpringServiceComponentFinderStrategy() - ); - componentFinder.findComponents(); - - assertEquals(1, container.getComponents().size()); - Component component = container.getComponentWithName("SomeService"); - assertEquals("test.SpringServiceComponentFinderStrategy.SomeService", component.getType()); - assertEquals("", component.getDescription()); - assertEquals("Spring Service", component.getTechnology()); - } - -} diff --git a/structurizr-spring/test/unit/com/structurizr/analysis/myapp/api/SomeApiController.java b/structurizr-spring/test/unit/com/structurizr/analysis/myapp/api/SomeApiController.java deleted file mode 100644 index c59a5b51d..000000000 --- a/structurizr-spring/test/unit/com/structurizr/analysis/myapp/api/SomeApiController.java +++ /dev/null @@ -1,20 +0,0 @@ -package com.structurizr.analysis.myapp.api; - -import com.structurizr.analysis.myapp.service.SomeService; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.web.bind.annotation.RestController; - -@RestController -public class SomeApiController { - - @Autowired - private SomeService someService; - - //@RequestMapping(value = "/do/something") - commenting this out removes a dependency on Spring MVC - public String findSomething() { - someService.doSomething(); - - return "{some json}"; - } - -} diff --git a/structurizr-spring/test/unit/com/structurizr/analysis/myapp/data/JdbcSomeRepository.java b/structurizr-spring/test/unit/com/structurizr/analysis/myapp/data/JdbcSomeRepository.java deleted file mode 100644 index 1ed484624..000000000 --- a/structurizr-spring/test/unit/com/structurizr/analysis/myapp/data/JdbcSomeRepository.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.structurizr.analysis.myapp.data; - -import com.structurizr.analysis.myapp.domain.Something; -import org.springframework.stereotype.Repository; - -@Repository -public class JdbcSomeRepository implements SomeRepository { - - @Override - public Something findSomething() { - return new Something(); - } - -} diff --git a/structurizr-spring/test/unit/com/structurizr/analysis/myapp/data/SomeNonPublicRepository.java b/structurizr-spring/test/unit/com/structurizr/analysis/myapp/data/SomeNonPublicRepository.java deleted file mode 100644 index 31cc0fcd2..000000000 --- a/structurizr-spring/test/unit/com/structurizr/analysis/myapp/data/SomeNonPublicRepository.java +++ /dev/null @@ -1,6 +0,0 @@ -package com.structurizr.analysis.myapp.data; - -import org.springframework.data.jpa.repository.JpaRepository; - -interface SomeNonPublicRepository extends JpaRepository { -} diff --git a/structurizr-spring/test/unit/com/structurizr/analysis/myapp/data/SomeOtherRepository.java b/structurizr-spring/test/unit/com/structurizr/analysis/myapp/data/SomeOtherRepository.java deleted file mode 100644 index 406519a0b..000000000 --- a/structurizr-spring/test/unit/com/structurizr/analysis/myapp/data/SomeOtherRepository.java +++ /dev/null @@ -1,6 +0,0 @@ -package com.structurizr.analysis.myapp.data; - -import org.springframework.data.jpa.repository.JpaRepository; - -public interface SomeOtherRepository extends JpaRepository { -} diff --git a/structurizr-spring/test/unit/com/structurizr/analysis/myapp/data/SomeRepository.java b/structurizr-spring/test/unit/com/structurizr/analysis/myapp/data/SomeRepository.java deleted file mode 100644 index 2b75e9824..000000000 --- a/structurizr-spring/test/unit/com/structurizr/analysis/myapp/data/SomeRepository.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.structurizr.analysis.myapp.data; - -import com.structurizr.analysis.myapp.domain.Something; - -public interface SomeRepository { - - Something findSomething(); - -} diff --git a/structurizr-spring/test/unit/com/structurizr/analysis/myapp/domain/Something.java b/structurizr-spring/test/unit/com/structurizr/analysis/myapp/domain/Something.java deleted file mode 100644 index 24e1990bc..000000000 --- a/structurizr-spring/test/unit/com/structurizr/analysis/myapp/domain/Something.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.structurizr.analysis.myapp.domain; - -/** - * Created by structurizr on 15/03/16. - */ -public class Something { -} diff --git a/structurizr-spring/test/unit/com/structurizr/analysis/myapp/service/SomeService.java b/structurizr-spring/test/unit/com/structurizr/analysis/myapp/service/SomeService.java deleted file mode 100644 index c3f7c39ae..000000000 --- a/structurizr-spring/test/unit/com/structurizr/analysis/myapp/service/SomeService.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.structurizr.analysis.myapp.service; - -public interface SomeService { - - void doSomething(); - -} diff --git a/structurizr-spring/test/unit/com/structurizr/analysis/myapp/service/SomeServiceImpl.java b/structurizr-spring/test/unit/com/structurizr/analysis/myapp/service/SomeServiceImpl.java deleted file mode 100644 index efc025ac4..000000000 --- a/structurizr-spring/test/unit/com/structurizr/analysis/myapp/service/SomeServiceImpl.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.structurizr.analysis.myapp.service; - -import com.structurizr.analysis.myapp.data.SomeOtherRepository; -import com.structurizr.analysis.myapp.data.SomeRepository; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Service; - -@Service -public class SomeServiceImpl implements SomeService { - - @Autowired - private SomeRepository someRepository; - - @Autowired - private SomeOtherRepository someOtherRepository; - - public void doSomething() { - someRepository.findSomething(); - } - -} diff --git a/structurizr-spring/test/unit/com/structurizr/analysis/myapp/web/SomeController.java b/structurizr-spring/test/unit/com/structurizr/analysis/myapp/web/SomeController.java deleted file mode 100644 index 13d27cad4..000000000 --- a/structurizr-spring/test/unit/com/structurizr/analysis/myapp/web/SomeController.java +++ /dev/null @@ -1,20 +0,0 @@ -package com.structurizr.analysis.myapp.web; - -import com.structurizr.analysis.myapp.service.SomeService; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Controller; - -@Controller -public class SomeController { - - @Autowired - private SomeService someService; - - //@RequestMapping(value = "/do/something") - commenting this out removes a dependency on Spring MVC - public String showHomePage() { - someService.doSomething(); - - return "/did/something"; - } - -} diff --git a/structurizr-spring/test/unit/test/AbstractSpringComponentFinderStrategy/SomeController.java b/structurizr-spring/test/unit/test/AbstractSpringComponentFinderStrategy/SomeController.java deleted file mode 100644 index fd9a86573..000000000 --- a/structurizr-spring/test/unit/test/AbstractSpringComponentFinderStrategy/SomeController.java +++ /dev/null @@ -1,7 +0,0 @@ -package test.AbstractSpringComponentFinderStrategy; - -import org.springframework.stereotype.Controller; - -@Controller -public class SomeController { -} diff --git a/structurizr-spring/test/unit/test/AbstractSpringComponentFinderStrategy/SomeNonPublicRepository.java b/structurizr-spring/test/unit/test/AbstractSpringComponentFinderStrategy/SomeNonPublicRepository.java deleted file mode 100644 index df5e39039..000000000 --- a/structurizr-spring/test/unit/test/AbstractSpringComponentFinderStrategy/SomeNonPublicRepository.java +++ /dev/null @@ -1,6 +0,0 @@ -package test.AbstractSpringComponentFinderStrategy; - -import org.springframework.data.jpa.repository.JpaRepository; - -interface SomeNonPublicRepository extends JpaRepository { -} diff --git a/structurizr-spring/test/unit/test/AbstractSpringComponentFinderStrategy/SomePublicRepository.java b/structurizr-spring/test/unit/test/AbstractSpringComponentFinderStrategy/SomePublicRepository.java deleted file mode 100644 index 698dc06a1..000000000 --- a/structurizr-spring/test/unit/test/AbstractSpringComponentFinderStrategy/SomePublicRepository.java +++ /dev/null @@ -1,6 +0,0 @@ -package test.AbstractSpringComponentFinderStrategy; - -import org.springframework.data.jpa.repository.JpaRepository; - -public interface SomePublicRepository extends JpaRepository { -} diff --git a/structurizr-spring/test/unit/test/SpringComponentComponentFinderStrategy/SomeComponent.java b/structurizr-spring/test/unit/test/SpringComponentComponentFinderStrategy/SomeComponent.java deleted file mode 100644 index 40bd7b768..000000000 --- a/structurizr-spring/test/unit/test/SpringComponentComponentFinderStrategy/SomeComponent.java +++ /dev/null @@ -1,4 +0,0 @@ -package test.SpringComponentComponentFinderStrategy; - -public interface SomeComponent { -} diff --git a/structurizr-spring/test/unit/test/SpringComponentComponentFinderStrategy/TheSomeComponentImpl.java b/structurizr-spring/test/unit/test/SpringComponentComponentFinderStrategy/TheSomeComponentImpl.java deleted file mode 100644 index 13160d237..000000000 --- a/structurizr-spring/test/unit/test/SpringComponentComponentFinderStrategy/TheSomeComponentImpl.java +++ /dev/null @@ -1,7 +0,0 @@ -package test.SpringComponentComponentFinderStrategy; - -import org.springframework.stereotype.Component; - -@Component -public class TheSomeComponentImpl implements SomeComponent { -} diff --git a/structurizr-spring/test/unit/test/SpringMvcControllerComponentFinderStrategy/SomeController.java b/structurizr-spring/test/unit/test/SpringMvcControllerComponentFinderStrategy/SomeController.java deleted file mode 100644 index bab504c48..000000000 --- a/structurizr-spring/test/unit/test/SpringMvcControllerComponentFinderStrategy/SomeController.java +++ /dev/null @@ -1,7 +0,0 @@ -package test.SpringMvcControllerComponentFinderStrategy; - -import org.springframework.stereotype.Controller; - -@Controller -public class SomeController { -} diff --git a/structurizr-spring/test/unit/test/SpringRepositoryComponentFinderStrategy/annotation/JdbcSomeRepository.java b/structurizr-spring/test/unit/test/SpringRepositoryComponentFinderStrategy/annotation/JdbcSomeRepository.java deleted file mode 100644 index 9293f71d8..000000000 --- a/structurizr-spring/test/unit/test/SpringRepositoryComponentFinderStrategy/annotation/JdbcSomeRepository.java +++ /dev/null @@ -1,4 +0,0 @@ -package test.SpringRepositoryComponentFinderStrategy.annotation; - -public class JdbcSomeRepository implements SomeRepository { -} diff --git a/structurizr-spring/test/unit/test/SpringRepositoryComponentFinderStrategy/annotation/SomeRepository.java b/structurizr-spring/test/unit/test/SpringRepositoryComponentFinderStrategy/annotation/SomeRepository.java deleted file mode 100644 index 5a32fdab7..000000000 --- a/structurizr-spring/test/unit/test/SpringRepositoryComponentFinderStrategy/annotation/SomeRepository.java +++ /dev/null @@ -1,7 +0,0 @@ -package test.SpringRepositoryComponentFinderStrategy.annotation; - -import org.springframework.stereotype.Repository; - -@Repository -public interface SomeRepository { -} diff --git a/structurizr-spring/test/unit/test/SpringRepositoryComponentFinderStrategy/crudRepository/SomeCrudRepository.java b/structurizr-spring/test/unit/test/SpringRepositoryComponentFinderStrategy/crudRepository/SomeCrudRepository.java deleted file mode 100644 index 9bb604e7a..000000000 --- a/structurizr-spring/test/unit/test/SpringRepositoryComponentFinderStrategy/crudRepository/SomeCrudRepository.java +++ /dev/null @@ -1,6 +0,0 @@ -package test.SpringRepositoryComponentFinderStrategy.crudRepository; - -import org.springframework.data.repository.CrudRepository; - -public interface SomeCrudRepository extends CrudRepository { -} diff --git a/structurizr-spring/test/unit/test/SpringRepositoryComponentFinderStrategy/jpaRepository/SomeJpaRepository.java b/structurizr-spring/test/unit/test/SpringRepositoryComponentFinderStrategy/jpaRepository/SomeJpaRepository.java deleted file mode 100644 index 714e8908c..000000000 --- a/structurizr-spring/test/unit/test/SpringRepositoryComponentFinderStrategy/jpaRepository/SomeJpaRepository.java +++ /dev/null @@ -1,6 +0,0 @@ -package test.SpringRepositoryComponentFinderStrategy.jpaRepository; - -import org.springframework.data.jpa.repository.JpaRepository; - -public interface SomeJpaRepository extends JpaRepository { -} diff --git a/structurizr-spring/test/unit/test/SpringRestControllerComponentFinderStrategy/SomeController.java b/structurizr-spring/test/unit/test/SpringRestControllerComponentFinderStrategy/SomeController.java deleted file mode 100644 index d40d58202..000000000 --- a/structurizr-spring/test/unit/test/SpringRestControllerComponentFinderStrategy/SomeController.java +++ /dev/null @@ -1,7 +0,0 @@ -package test.SpringRestControllerComponentFinderStrategy; - -import org.springframework.web.bind.annotation.RestController; - -@RestController -public class SomeController { -} diff --git a/structurizr-spring/test/unit/test/SpringServiceComponentFinderStrategy/SomeService.java b/structurizr-spring/test/unit/test/SpringServiceComponentFinderStrategy/SomeService.java deleted file mode 100644 index 1ff518047..000000000 --- a/structurizr-spring/test/unit/test/SpringServiceComponentFinderStrategy/SomeService.java +++ /dev/null @@ -1,4 +0,0 @@ -package test.SpringServiceComponentFinderStrategy; - -public interface SomeService { -} diff --git a/structurizr-spring/test/unit/test/SpringServiceComponentFinderStrategy/SomeServiceImpl.java b/structurizr-spring/test/unit/test/SpringServiceComponentFinderStrategy/SomeServiceImpl.java deleted file mode 100644 index ee551d589..000000000 --- a/structurizr-spring/test/unit/test/SpringServiceComponentFinderStrategy/SomeServiceImpl.java +++ /dev/null @@ -1,7 +0,0 @@ -package test.SpringServiceComponentFinderStrategy; - -import org.springframework.stereotype.Service; - -@Service -public class SomeServiceImpl implements SomeService { -}