Skip to content

Commit

Permalink
Add database support to plantUML
Browse files Browse the repository at this point in the history
Issue: TNG#1132
  • Loading branch information
tfij committed Jul 5, 2023
1 parent 5fbb9f7 commit 3cb7470
Show file tree
Hide file tree
Showing 5 changed files with 155 additions and 16 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -33,12 +34,15 @@ class PlantUmlComponent {
private final ComponentName componentName;
private final Set<Stereotype> stereotypes;
private final Optional<Alias> alias;

private final ComponentType componentType;
private List<PlantUmlComponentDependency> 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<PlantUmlComponent> getDependencies() {
Expand All @@ -57,6 +61,10 @@ Optional<Alias> getAlias() {
return alias;
}

public ComponentType getComponentType() {
return componentType;
}

void finish(List<PlantUmlComponentDependency> dependencies) {
this.dependencies = ImmutableList.copyOf(dependencies);
}
Expand All @@ -68,7 +76,7 @@ ComponentIdentifier getIdentifier() {

@Override
public int hashCode() {
return Objects.hash(componentName, stereotypes, alias);
return Objects.hash(componentName, stereotypes, alias, componentType);
}

@Override
Expand All @@ -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
Expand All @@ -91,6 +100,7 @@ public String toString() {
"componentName=" + componentName +
", stereotypes=" + stereotypes +
", alias=" + alias +
", componentType=" + componentType +
", dependencies=" + dependencies +
'}';
}
Expand All @@ -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<ComponentType> 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<Stereotype> stereotypes;
private Optional<Alias> alias;
private ComponentType componentType;

Builder withComponentName(ComponentName componentName) {
this.componentName = componentName;
Expand All @@ -123,6 +152,11 @@ Builder withAlias(Optional<Alias> alias) {
return this;
}

Builder withComponentType(ComponentType componentType) {
this.componentType = componentType;
return this;
}

PlantUmlComponent build() {
return new PlantUmlComponent(this);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -75,11 +77,16 @@ private Set<PlantUmlComponent> parseComponents(List<String> plantUmlDiagramLines
.collect(toSet());
}

private ImmutableList<ParsedDependency> parseDependencies(PlantUmlComponents plantUmlComponents, List<String> plantUmlDiagramLines) {
private ImmutableList<ParsedDependency> parseDependencies(
PlantUmlComponents plantUmlComponents,
List<String> plantUmlDiagramLines) {
ImmutableList.Builder<ParsedDependency> 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();
Expand All @@ -89,24 +96,29 @@ private PlantUmlComponent createNewComponent(String input) {
PlantUmlComponentMatcher matcher = plantUmlPatterns.matchComponent(input);

ComponentName componentName = new ComponentName(matcher.matchComponentName());
ImmutableSet<Stereotype> immutableStereotypes = identifyStereotypes(matcher, componentName);
PlantUmlComponent.ComponentType componentType = matcher.matchComponentType().orElse(COMPONENT);
ImmutableSet<Stereotype> immutableStereotypes = identifyStereotypes(matcher, componentName, componentType);
Optional<Alias> alias = matcher.matchAlias().map(Alias::new);

return new PlantUmlComponent.Builder()
.withComponentName(componentName)
.withStereotypes(immutableStereotypes)
.withAlias(alias)
.withComponentType(componentType)
.build();
}

private ImmutableSet<Stereotype> identifyStereotypes(PlantUmlComponentMatcher matcher, ComponentName componentName) {
private ImmutableSet<Stereotype> identifyStereotypes(
PlantUmlComponentMatcher matcher,
ComponentName componentName,
PlantUmlComponent.ComponentType componentType) {
ImmutableSet.Builder<Stereotype> stereotypes = ImmutableSet.builder();
for (String stereotype : matcher.matchStereoTypes()) {
stereotypes.add(new Stereotype(stereotype));
}

ImmutableSet<Stereotype> 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()));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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 + ")";
Expand All @@ -66,15 +70,15 @@ private static String anythingBut(String charsJoined) {
}

Stream<String> filterComponents(List<String> 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<String> matches(Pattern pattern) {
return input -> pattern.matcher(input).matches();
private Predicate<String> matchesToAny(Pattern... patterns) {
return input -> Arrays.stream(patterns).anyMatch(pattern -> pattern.matcher(input).matches());
}

Iterable<PlantUmlDependencyMatcher> matchDependencies(List<String> diagramLines) {
Expand All @@ -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);
}
Expand All @@ -112,6 +121,10 @@ Set<String> matchStereoTypes() {
Optional<String> matchAlias() {
return Optional.ofNullable(componentMatcher.group(ALIAS_GROUP_NAME));
}

Optional<PlantUmlComponent.ComponentType> matchComponentType() {
return PlantUmlComponent.ComponentType.parseString(componentMatcher.group(COMPONENT_TYPE_GROUP_NAME));
}
}

static class PlantUmlDependencyMatcher {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -418,6 +422,33 @@ 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<String> components = diagram.getAllComponents().stream()
.map(PlantUmlComponent::getComponentName)
.map(it -> it.asString())
.collect(Collectors.toSet());

assertThat(components).as("All components in diagram").isEqualTo(new HashSet<>(Arrays.asList("A", "B", "DB")));

PlantUmlComponent foo = getComponentWithAlias(new Alias("foo"), diagram);
PlantUmlComponent bar = getComponentWithAlias(new Alias("bar"), diagram);
PlantUmlComponent db = getComponentWithName("DB", diagram);
assertThat(foo.getDependencies()).containsOnly(bar);
assertThat(bar.getDependencies()).as("bar has no dependency to DB").isEmpty();
assertThat(db.getDependencies()).as("DB has no dependencies").isEmpty();
}

private PlantUmlComponent getComponentWithName(String componentName, PlantUmlDiagram diagram) {
PlantUmlComponent component = diagram.getAllComponents().stream()
.filter(c -> c.getComponentName().asString().equals(componentName))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down Expand Up @@ -96,7 +115,7 @@ ComponentCreator withAlias(String alias) {
return this;
}

public ComponentCreator withColor(String color) {
ComponentCreator withColor(String color) {
this.color = color;
return this;
}
Expand All @@ -107,6 +126,36 @@ TestDiagram withStereoTypes(String... stereoTypes) {
}
}

class DatabaseCreator {
private final String databaseName;
private final List<String> 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;

Expand Down

0 comments on commit 3cb7470

Please sign in to comment.