diff --git a/archunit/src/main/java/com/tngtech/archunit/library/plantuml/rules/PlantUmlComponent.java b/archunit/src/main/java/com/tngtech/archunit/library/plantuml/rules/PlantUmlComponent.java index eed3bac4c5..ea71f46dde 100644 --- a/archunit/src/main/java/com/tngtech/archunit/library/plantuml/rules/PlantUmlComponent.java +++ b/archunit/src/main/java/com/tngtech/archunit/library/plantuml/rules/PlantUmlComponent.java @@ -15,6 +15,7 @@ */ package com.tngtech.archunit.library.plantuml.rules; +import java.util.Arrays; import java.util.Collection; import java.util.List; import java.util.Objects; @@ -33,12 +34,15 @@ class PlantUmlComponent { private final ComponentName componentName; private final Set stereotypes; private final Optional alias; + + private final ComponentType componentType; private List dependencies = emptyList(); private PlantUmlComponent(Builder builder) { this.componentName = checkNotNull(builder.componentName); this.stereotypes = checkNotNull(builder.stereotypes); this.alias = checkNotNull(builder.alias); + this.componentType = checkNotNull(builder.componentType); } List getDependencies() { @@ -57,6 +61,10 @@ Optional getAlias() { return alias; } + public ComponentType getComponentType() { + return componentType; + } + void finish(List dependencies) { this.dependencies = ImmutableList.copyOf(dependencies); } @@ -68,7 +76,7 @@ ComponentIdentifier getIdentifier() { @Override public int hashCode() { - return Objects.hash(componentName, stereotypes, alias); + return Objects.hash(componentName, stereotypes, alias, componentType); } @Override @@ -82,7 +90,8 @@ public boolean equals(Object obj) { PlantUmlComponent other = (PlantUmlComponent) obj; return Objects.equals(this.componentName, other.componentName) && Objects.equals(this.stereotypes, other.stereotypes) - && Objects.equals(this.alias, other.alias); + && Objects.equals(this.alias, other.alias) + && Objects.equals(this.componentType, other.componentType); } @Override @@ -91,6 +100,7 @@ public String toString() { "componentName=" + componentName + ", stereotypes=" + stereotypes + ", alias=" + alias + + ", componentType=" + componentType + ", dependencies=" + dependencies + '}'; } @@ -103,10 +113,29 @@ static class Functions { }; } + enum ComponentType { + COMPONENT("component"), + DATABASE("database"); + + private final String stringValue; + + ComponentType(String stringValue) { + this.stringValue = stringValue; + } + + static Optional parseString(String value) { + String notNullValue = Optional.ofNullable(value).orElse(""); + return Arrays.stream(ComponentType.values()) + .filter(it -> it.stringValue.equals(notNullValue.trim().toLowerCase())) + .findFirst(); + } + } + static class Builder { private ComponentName componentName; private Set stereotypes; private Optional alias; + private ComponentType componentType; Builder withComponentName(ComponentName componentName) { this.componentName = componentName; @@ -123,6 +152,11 @@ Builder withAlias(Optional alias) { return this; } + Builder withComponentType(ComponentType componentType) { + this.componentType = componentType; + return this; + } + PlantUmlComponent build() { return new PlantUmlComponent(this); } diff --git a/archunit/src/main/java/com/tngtech/archunit/library/plantuml/rules/PlantUmlDiagram.java b/archunit/src/main/java/com/tngtech/archunit/library/plantuml/rules/PlantUmlDiagram.java index 6a56a5bb2d..cc9d91792c 100644 --- a/archunit/src/main/java/com/tngtech/archunit/library/plantuml/rules/PlantUmlDiagram.java +++ b/archunit/src/main/java/com/tngtech/archunit/library/plantuml/rules/PlantUmlDiagram.java @@ -17,6 +17,7 @@ import java.util.List; import java.util.Set; +import java.util.stream.Collectors; import com.google.common.collect.FluentIterable; import com.google.common.collect.ImmutableList; @@ -33,11 +34,15 @@ private PlantUmlDiagram(PlantUmlDiagram.Builder builder) { } Set getAllComponents() { - return ImmutableSet.copyOf(plantUmlComponents.getAllComponents()); + return plantUmlComponents.getAllComponents().stream() + .filter(it -> it.getComponentType() == PlantUmlComponent.ComponentType.COMPONENT) + .collect(Collectors.toSet()); } Set getComponentsWithAlias() { - return ImmutableSet.copyOf(plantUmlComponents.getComponentsWithAlias()); + return plantUmlComponents.getComponentsWithAlias().stream() + .filter(it -> it.getComponentType() == PlantUmlComponent.ComponentType.COMPONENT) + .collect(Collectors.toSet()); } static class Builder { diff --git a/archunit/src/main/java/com/tngtech/archunit/library/plantuml/rules/PlantUmlParser.java b/archunit/src/main/java/com/tngtech/archunit/library/plantuml/rules/PlantUmlParser.java index 1c63a15590..3ce12b0b0c 100644 --- a/archunit/src/main/java/com/tngtech/archunit/library/plantuml/rules/PlantUmlParser.java +++ b/archunit/src/main/java/com/tngtech/archunit/library/plantuml/rules/PlantUmlParser.java @@ -30,6 +30,8 @@ import com.tngtech.archunit.library.plantuml.rules.PlantUmlPatterns.PlantUmlDependencyMatcher; import static com.google.common.base.Preconditions.checkNotNull; +import static com.tngtech.archunit.library.plantuml.rules.PlantUmlComponent.ComponentType.COMPONENT; +import static com.tngtech.archunit.library.plantuml.rules.PlantUmlComponent.ComponentType.DATABASE; import static java.nio.charset.StandardCharsets.UTF_8; import static java.util.stream.Collectors.toList; import static java.util.stream.Collectors.toSet; @@ -75,11 +77,16 @@ private Set parseComponents(List plantUmlDiagramLines .collect(toSet()); } - private ImmutableList parseDependencies(PlantUmlComponents plantUmlComponents, List plantUmlDiagramLines) { + private ImmutableList parseDependencies( + PlantUmlComponents plantUmlComponents, + List plantUmlDiagramLines) { ImmutableList.Builder result = ImmutableList.builder(); for (PlantUmlDependencyMatcher matcher : plantUmlPatterns.matchDependencies(plantUmlDiagramLines)) { PlantUmlComponent origin = findComponentMatching(plantUmlComponents, matcher.matchOrigin()); PlantUmlComponent target = findComponentMatching(plantUmlComponents, matcher.matchTarget()); + if (origin.getComponentType() == DATABASE || target.getComponentType() == DATABASE) { + continue; + } result.add(new ParsedDependency(origin.getIdentifier(), target.getIdentifier())); } return result.build(); @@ -89,24 +96,29 @@ private PlantUmlComponent createNewComponent(String input) { PlantUmlComponentMatcher matcher = plantUmlPatterns.matchComponent(input); ComponentName componentName = new ComponentName(matcher.matchComponentName()); - ImmutableSet immutableStereotypes = identifyStereotypes(matcher, componentName); + PlantUmlComponent.ComponentType componentType = matcher.matchComponentType().orElse(COMPONENT); + ImmutableSet immutableStereotypes = identifyStereotypes(matcher, componentName, componentType); Optional alias = matcher.matchAlias().map(Alias::new); return new PlantUmlComponent.Builder() .withComponentName(componentName) .withStereotypes(immutableStereotypes) .withAlias(alias) + .withComponentType(componentType) .build(); } - private ImmutableSet identifyStereotypes(PlantUmlComponentMatcher matcher, ComponentName componentName) { + private ImmutableSet identifyStereotypes( + PlantUmlComponentMatcher matcher, + ComponentName componentName, + PlantUmlComponent.ComponentType componentType) { ImmutableSet.Builder stereotypes = ImmutableSet.builder(); for (String stereotype : matcher.matchStereoTypes()) { stereotypes.add(new Stereotype(stereotype)); } ImmutableSet result = stereotypes.build(); - if (result.isEmpty()) { + if (result.isEmpty() && componentType == COMPONENT) { throw new IllegalDiagramException(String.format("Components must include at least one stereotype" + " specifying the package identifier(<<..>>), but component '%s' does not", componentName.asString())); } diff --git a/archunit/src/main/java/com/tngtech/archunit/library/plantuml/rules/PlantUmlPatterns.java b/archunit/src/main/java/com/tngtech/archunit/library/plantuml/rules/PlantUmlPatterns.java index 6e5f48214a..0ab673b4de 100644 --- a/archunit/src/main/java/com/tngtech/archunit/library/plantuml/rules/PlantUmlPatterns.java +++ b/archunit/src/main/java/com/tngtech/archunit/library/plantuml/rules/PlantUmlPatterns.java @@ -16,6 +16,7 @@ package com.tngtech.archunit.library.plantuml.rules; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.HashSet; import java.util.List; @@ -36,17 +37,20 @@ class PlantUmlPatterns { private static final String COMPONENT_NAME_GROUP_NAME = "componentName"; private static final String COMPONENT_NAME_FORMAT = "\\[" + capture(anythingBut("\\[\\]"), COMPONENT_NAME_GROUP_NAME) + "]"; - + private static final String DATABASE_NAME_FORMAT = "\\\"" + capture(anythingBut("\\\""), COMPONENT_NAME_GROUP_NAME) + "\""; private static final String STEREOTYPE_FORMAT = "(?:<<" + capture(anythingBut("<>")) + ">>\\s*)"; private static final Pattern STEREOTYPE_PATTERN = Pattern.compile(STEREOTYPE_FORMAT); - private static final String ALIAS_GROUP_NAME = "alias"; private static final String ALIAS_FORMAT = "\\s*(?:as \"?" + capture("[^\" ]+", ALIAS_GROUP_NAME) + "\"?)?"; private static final String COLOR_FORMAT = "\\s*(?:#" + anyOf("\\w|/\\\\-") + "+)?"; - + private static final String COMPONENT_TYPE_GROUP_NAME = "componentType"; + private static final String COMPONENT_TYPE_FORMAT = capture("component", COMPONENT_TYPE_GROUP_NAME) + "?"; + private static final String DATABASE_TYPE_FORMAT = capture("database", COMPONENT_TYPE_GROUP_NAME); private static final Pattern PLANTUML_COMPONENT_PATTERN = Pattern.compile( - "^\\s*" + COMPONENT_NAME_FORMAT + "\\s*" + STEREOTYPE_FORMAT + "*" + ALIAS_FORMAT + COLOR_FORMAT + "\\s*"); + "^\\s*" + COMPONENT_TYPE_FORMAT + "\\s*" + COMPONENT_NAME_FORMAT + "\\s*" + STEREOTYPE_FORMAT + "*" + ALIAS_FORMAT + COLOR_FORMAT + "\\s*");; + private static final Pattern PLANTUML_DATABASE_PATTERN = Pattern.compile( + "^\\s*" + DATABASE_TYPE_FORMAT + "\\s*" + DATABASE_NAME_FORMAT + "\\s*" + STEREOTYPE_FORMAT + "*" + ALIAS_FORMAT + COLOR_FORMAT + "\\s*"); private static String capture(String pattern) { return "(" + pattern + ")"; @@ -66,15 +70,15 @@ private static String anythingBut(String charsJoined) { } Stream filterComponents(List lines) { - return lines.stream().filter(matches(PLANTUML_COMPONENT_PATTERN)); + return lines.stream().filter(matchesToAny(PLANTUML_COMPONENT_PATTERN, PLANTUML_DATABASE_PATTERN)); } PlantUmlComponentMatcher matchComponent(String input) { return new PlantUmlComponentMatcher(input); } - private Predicate matches(Pattern pattern) { - return input -> pattern.matcher(input).matches(); + private Predicate matchesToAny(Pattern... patterns) { + return input -> Arrays.stream(patterns).anyMatch(pattern -> pattern.matcher(input).matches()); } Iterable matchDependencies(List diagramLines) { @@ -91,8 +95,13 @@ static class PlantUmlComponentMatcher { private final Matcher stereotypeMatcher; PlantUmlComponentMatcher(String input) { - componentMatcher = PLANTUML_COMPONENT_PATTERN.matcher(input); - checkState(componentMatcher.matches(), "input %s does not match pattern %s", input, PLANTUML_COMPONENT_PATTERN); + Matcher componentMatcher = PLANTUML_COMPONENT_PATTERN.matcher(input); + if (!componentMatcher.matches()) { + componentMatcher = PLANTUML_DATABASE_PATTERN.matcher(input); + } + checkState(componentMatcher.matches(), + "input %s does not match either pattern %s or %s", input, PLANTUML_COMPONENT_PATTERN, PLANTUML_DATABASE_PATTERN); + this.componentMatcher = componentMatcher; stereotypeMatcher = STEREOTYPE_PATTERN.matcher(input); } @@ -112,6 +121,10 @@ Set matchStereoTypes() { Optional matchAlias() { return Optional.ofNullable(componentMatcher.group(ALIAS_GROUP_NAME)); } + + Optional matchComponentType() { + return PlantUmlComponent.ComponentType.parseString(componentMatcher.group(COMPONENT_TYPE_GROUP_NAME)); + } } static class PlantUmlDependencyMatcher { diff --git a/archunit/src/test/java/com/tngtech/archunit/library/plantuml/rules/PlantUmlParserTest.java b/archunit/src/test/java/com/tngtech/archunit/library/plantuml/rules/PlantUmlParserTest.java index 12a02d67b8..1d549ca496 100644 --- a/archunit/src/test/java/com/tngtech/archunit/library/plantuml/rules/PlantUmlParserTest.java +++ b/archunit/src/test/java/com/tngtech/archunit/library/plantuml/rules/PlantUmlParserTest.java @@ -4,8 +4,12 @@ import java.net.MalformedURLException; import java.net.URL; import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; import java.util.List; +import java.util.Set; import java.util.function.Function; +import java.util.stream.Collectors; import com.google.common.collect.ImmutableList; import com.tngtech.java.junit.dataprovider.DataProvider; @@ -418,6 +422,31 @@ public void parses_a_component_diagram_that_uses_alias_with_and_without_brackets assertThat(bar.getDependencies()).isEmpty(); } + @Test + public void parse_a_database_component() { + File file = TestDiagram.in(temporaryFolder) + .component("A").withAlias("foo").withStereoTypes("..origin..") + .component("B").withAlias("bar").withStereoTypes("..target..") + .database("DB").build() + .dependencyFrom("foo").to("bar") + .dependencyFrom("bar").to("DB") + .write(); + + PlantUmlDiagram diagram = createDiagram(file); + + Set components = diagram.getAllComponents().stream() + .map(PlantUmlComponent::getComponentName) + .map(it -> it.asString()) + .collect(Collectors.toSet()); + + assertThat(components).as("DB components is filter out").isEqualTo(new HashSet<>(Arrays.asList("A", "B"))); + + PlantUmlComponent foo = getComponentWithAlias(new Alias("foo"), diagram); + PlantUmlComponent bar = getComponentWithAlias(new Alias("bar"), diagram); + assertThat(foo.getDependencies()).containsOnly(bar); + assertThat(bar.getDependencies()).as("bar has no dependency to DB").isEmpty(); + } + private PlantUmlComponent getComponentWithName(String componentName, PlantUmlDiagram diagram) { PlantUmlComponent component = diagram.getAllComponents().stream() .filter(c -> c.getComponentName().asString().equals(componentName)) diff --git a/archunit/src/test/java/com/tngtech/archunit/library/plantuml/rules/TestDiagram.java b/archunit/src/test/java/com/tngtech/archunit/library/plantuml/rules/TestDiagram.java index 3c3fe4a789..430cf451fb 100644 --- a/archunit/src/test/java/com/tngtech/archunit/library/plantuml/rules/TestDiagram.java +++ b/archunit/src/test/java/com/tngtech/archunit/library/plantuml/rules/TestDiagram.java @@ -41,6 +41,25 @@ private TestDiagram addComponent(ComponentCreator creator) { return this; } + DatabaseCreator database(String databaseName) { + return new DatabaseCreator(databaseName); + } + + private TestDiagram addDatabase(DatabaseCreator creator) { + String stereotypes = creator.stereotypes + .stream().map(input -> "<<" + input + ">>") + .collect(joining(" ")); + String line = String.format("database \"%s\" %s", creator.databaseName, stereotypes); + if (creator.alias != null) { + line += " as " + creator.alias; + } + if (creator.color != null) { + line += " #" + creator.color; + } + lines.add(line); + return this; + } + DependencyFromCreator dependencyFrom(String origin) { return new DependencyFromCreator(origin); } @@ -96,7 +115,7 @@ ComponentCreator withAlias(String alias) { return this; } - public ComponentCreator withColor(String color) { + ComponentCreator withColor(String color) { this.color = color; return this; } @@ -107,6 +126,36 @@ TestDiagram withStereoTypes(String... stereoTypes) { } } + class DatabaseCreator { + private final String databaseName; + private final List stereotypes = new ArrayList<>(); + private String alias; + private String color; + + DatabaseCreator(String databaseName) { + this.databaseName = databaseName; + } + + DatabaseCreator withAlias(String alias) { + this.alias = alias; + return this; + } + + DatabaseCreator withColor(String color) { + this.color = color; + return this; + } + + TestDiagram withStereoTypes(String... stereoTypes) { + this.stereotypes.addAll(ImmutableList.copyOf(stereoTypes)); + return addDatabase(this); + } + + public TestDiagram build() { + return addDatabase(this); + } + } + class DependencyFromCreator { private final String origin;