From d5e0d201e5fc9e1d6e6d4f3ea6d694e2706afbb0 Mon Sep 17 00:00:00 2001 From: Simon Brown <1009874+simonbrowndotje@users.noreply.github.com> Date: Thu, 26 Sep 2024 23:19:17 +0100 Subject: [PATCH] Adds a couple more supporting types strategies. --- changelog.md | 4 ++ .../java/com/structurizr/component/Type.java | 20 ++++++++ ...tionWithPrefixSupportingTypesStrategy.java | 38 +++++++++++++++ ...tionWithSuffixSupportingTypesStrategy.java | 38 +++++++++++++++ ...ithPrefixSupportingTypesStrategyTests.java | 44 ++++++++++++++++++ ...ithSuffixSupportingTypesStrategyTests.java | 46 +++++++++++++++++++ .../implementation/ExampleRepository.java | 4 ++ .../implementation/ExampleRepositoryImpl.java | 4 ++ .../implementation/JdbcExampleRepository.java | 4 ++ .../dsl/ComponentFinderStrategyParser.java | 22 ++++++++- .../ComponentFinderStrategyParserTests.java | 34 +++++++++++++- 11 files changed, 256 insertions(+), 2 deletions(-) create mode 100644 structurizr-component/src/main/java/com/structurizr/component/supporting/ImplementationWithPrefixSupportingTypesStrategy.java create mode 100644 structurizr-component/src/main/java/com/structurizr/component/supporting/ImplementationWithSuffixSupportingTypesStrategy.java create mode 100644 structurizr-component/src/test/java/com/structurizr/component/supporting/ImplementationWithPrefixSupportingTypesStrategyTests.java create mode 100644 structurizr-component/src/test/java/com/structurizr/component/supporting/ImplementationWithSuffixSupportingTypesStrategyTests.java create mode 100644 structurizr-component/src/test/java/com/structurizr/component/supporting/implementation/ExampleRepository.java create mode 100644 structurizr-component/src/test/java/com/structurizr/component/supporting/implementation/ExampleRepositoryImpl.java create mode 100644 structurizr-component/src/test/java/com/structurizr/component/supporting/implementation/JdbcExampleRepository.java diff --git a/changelog.md b/changelog.md index 568f80a3..7db1bca0 100644 --- a/changelog.md +++ b/changelog.md @@ -3,6 +3,10 @@ ## 3.1.0 (unreleased) - structurizr-client: Workspace archive file now includes the branch name in the filename. +- structurizr-component: Adds `ImplementationWithPrefixSupportingTypesStrategy`. +- structurizr-component: Adds `ImplementationWithSuffixSupportingTypesStrategy`. +- structurizr-dsl: Adds `supportingTypes implementation-prefix `. +- structurizr-dsl: Adds `supportingTypes implementation-suffix `. ## 3.0.0 (19th September 2024) diff --git a/structurizr-component/src/main/java/com/structurizr/component/Type.java b/structurizr-component/src/main/java/com/structurizr/component/Type.java index 3e3be19e..dea55129 100644 --- a/structurizr-component/src/main/java/com/structurizr/component/Type.java +++ b/structurizr-component/src/main/java/com/structurizr/component/Type.java @@ -2,6 +2,8 @@ import com.structurizr.util.StringUtils; import org.apache.bcel.classfile.*; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; import java.util.*; @@ -10,6 +12,8 @@ */ public class Type { + private static final Log log = LogFactory.getLog(Type.class); + private static final String STRUCTURIZR_TAG_ANNOTATION = "Lcom/structurizr/annotation/Tag;"; private static final String STRUCTURIZR_TAGS_ANNOTATION = "Lcom/structurizr/annotation/Tags;"; @@ -92,6 +96,10 @@ public boolean isAbstractClass() { return javaClass.isAbstract() && javaClass.isClass(); } + public boolean isInterface() { + return javaClass.isInterface(); + } + public List getTags() { List tags = new ArrayList<>(); @@ -159,4 +167,16 @@ public String toString() { return this.fullyQualifiedName; } + public boolean implementsInterface(Type type) { + if (javaClass != null) { + try { + return javaClass.implementationOf(type.javaClass); + } catch (ClassNotFoundException e) { + log.warn(e); + } + } + + return false; + } + } \ No newline at end of file diff --git a/structurizr-component/src/main/java/com/structurizr/component/supporting/ImplementationWithPrefixSupportingTypesStrategy.java b/structurizr-component/src/main/java/com/structurizr/component/supporting/ImplementationWithPrefixSupportingTypesStrategy.java new file mode 100644 index 00000000..1d0b64fe --- /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 00000000..f14006ce --- /dev/null +++ b/structurizr-component/src/main/java/com/structurizr/component/supporting/ImplementationWithSuffixSupportingTypesStrategy.java @@ -0,0 +1,38 @@ +package com.structurizr.component.supporting; + +import com.structurizr.component.Type; +import com.structurizr.component.TypeRepository; + +import java.util.Set; +import java.util.stream.Collectors; + +/** + * A strategy that, given an interface, finds the implementation class with the specified suffix. + */ +public class ImplementationWithSuffixSupportingTypesStrategy implements SupportingTypesStrategy { + + private final String suffix; + + public ImplementationWithSuffixSupportingTypesStrategy(String suffix) { + this.suffix = suffix; + } + + @Override + public Set findSupportingTypes(Type type, TypeRepository typeRepository) { + if (!type.isInterface()) { + throw new IllegalArgumentException("The type " + type.getFullyQualifiedName() + " is not an interface"); + } + + return typeRepository.getTypes().stream() + .filter(dependency -> dependency.implementsInterface(type) && dependency.getName().equals(type.getName() + suffix)) + .collect(Collectors.toSet()); + } + + @Override + public String toString() { + return "ImplementationWithSuffixSupportingTypesStrategy{" + + "suffix='" + suffix + '\'' + + '}'; + } + +} \ No newline at end of file diff --git a/structurizr-component/src/test/java/com/structurizr/component/supporting/ImplementationWithPrefixSupportingTypesStrategyTests.java b/structurizr-component/src/test/java/com/structurizr/component/supporting/ImplementationWithPrefixSupportingTypesStrategyTests.java new file mode 100644 index 00000000..6afccd31 --- /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 00000000..1929432a --- /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 00000000..6765edae --- /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 00000000..14447809 --- /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 00000000..3a565ab9 --- /dev/null +++ b/structurizr-component/src/test/java/com/structurizr/component/supporting/implementation/JdbcExampleRepository.java @@ -0,0 +1,4 @@ +package com.structurizr.component.supporting.implementation; + +public class JdbcExampleRepository implements ExampleRepository { +} \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/ComponentFinderStrategyParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/ComponentFinderStrategyParser.java index 340f437e..9c73a68b 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/ComponentFinderStrategyParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ComponentFinderStrategyParser.java @@ -39,8 +39,12 @@ final class ComponentFinderStrategyParser extends AbstractParser { private static final String SUPPORTING_TYPES_REFERENCED_IN_PACKAGE = "referenced-in-package"; private static final String SUPPORTING_TYPES_IN_PACKAGE = "in-package"; private static final String SUPPORTING_TYPES_UNDER_PACKAGE = "under-package"; + private static final String SUPPORTING_TYPES_IMPLEMENTATION_WITH_PREFIX = "implementation-prefix"; + private static final String SUPPORTING_TYPES_IMPLEMENTATION_WITH_SUFFIX = "implementation-suffix"; private static final String SUPPORTING_TYPES_NONE = "none"; - private static final String SUPPORTING_TYPES_GRAMMAR = "supportingTypes <" + String.join("|", List.of(SUPPORTING_TYPES_ALL_REFERENCED, SUPPORTING_TYPES_REFERENCED_IN_PACKAGE, SUPPORTING_TYPES_IN_PACKAGE, SUPPORTING_TYPES_UNDER_PACKAGE, SUPPORTING_TYPES_NONE)) + "> [parameters]"; + private static final String SUPPORTING_TYPES_GRAMMAR = "supportingTypes <" + String.join("|", List.of(SUPPORTING_TYPES_ALL_REFERENCED, SUPPORTING_TYPES_REFERENCED_IN_PACKAGE, SUPPORTING_TYPES_IN_PACKAGE, SUPPORTING_TYPES_UNDER_PACKAGE, SUPPORTING_TYPES_IMPLEMENTATION_WITH_PREFIX, SUPPORTING_TYPES_IMPLEMENTATION_WITH_SUFFIX, SUPPORTING_TYPES_NONE)) + "> [parameters]"; + private static final String SUPPORTING_TYPES_IMPLEMENTATION_WITH_PREFIX_GRAMMAR = "supportingTypes implementation-prefix "; + private static final String SUPPORTING_TYPES_IMPLEMENTATION_WITH_SUFFIX_GRAMMAR = "supportingTypes implementation-suffix "; private static final String NAME_TYPE_NAME = "type-name"; private static final String NAME_FQN = "fqn"; @@ -185,6 +189,22 @@ void parseSupportingTypes(ComponentFinderStrategyDslContext context, Tokens toke case SUPPORTING_TYPES_UNDER_PACKAGE: context.getComponentFinderStrategyBuilder().supportedBy(new AllTypesUnderPackageSupportingTypesStrategy()); break; + case SUPPORTING_TYPES_IMPLEMENTATION_WITH_PREFIX: + if (tokens.size() < 3) { + throw new RuntimeException("Too few tokens, expected: " + SUPPORTING_TYPES_IMPLEMENTATION_WITH_PREFIX_GRAMMAR); + } + + String prefix = tokens.get(2); + context.getComponentFinderStrategyBuilder().supportedBy(new ImplementationWithPrefixSupportingTypesStrategy(prefix)); + break; + case SUPPORTING_TYPES_IMPLEMENTATION_WITH_SUFFIX: + if (tokens.size() < 3) { + throw new RuntimeException("Too few tokens, expected: " + SUPPORTING_TYPES_IMPLEMENTATION_WITH_SUFFIX_GRAMMAR); + } + + String suffix = tokens.get(2); + context.getComponentFinderStrategyBuilder().supportedBy(new ImplementationWithSuffixSupportingTypesStrategy(suffix)); + break; case SUPPORTING_TYPES_NONE: context.getComponentFinderStrategyBuilder().supportedBy(new DefaultSupportingTypesStrategy()); break; diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/ComponentFinderStrategyParserTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/ComponentFinderStrategyParserTests.java index 6ea5eb25..f953aaae 100644 --- a/structurizr-dsl/src/test/java/com/structurizr/dsl/ComponentFinderStrategyParserTests.java +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/ComponentFinderStrategyParserTests.java @@ -201,10 +201,42 @@ void test_parseSupportingTypes_ThrowsAnException_WhenNoTypeIsSpecified() { parser.parseSupportingTypes(context, tokens("supportingTypes"), null); fail(); } catch (Exception e) { - assertEquals("Too few tokens, expected: supportingTypes [parameters]", e.getMessage()); + assertEquals("Too few tokens, expected: supportingTypes [parameters]", e.getMessage()); } } + @Test + void test_parseSupportingTypes_ThrowsAnException_WhenImplementationSuffixIsUsedWithoutASuffix() { + try { + parser.parseSupportingTypes(context, tokens("supportingTypes", "implementation-suffix"), null); + fail(); + } catch (Exception e) { + assertEquals("Too few tokens, expected: supportingTypes implementation-suffix ", e.getMessage()); + } + } + + @Test + void test_parseSupportingTypes_ImplementationSuffix() { + parser.parseSupportingTypes(context, tokens("supportingTypes", "implementation-suffix", "Impl"), null); + assertEquals("ComponentFinderStrategyBuilder{technology=null, typeMatcher=null, typeFilter=null, supportingTypesStrategy=ImplementationWithSuffixSupportingTypesStrategy{suffix='Impl'}, namingStrategy=null, descriptionStrategy=null, urlStrategy=null, componentVisitor=null}", context.getComponentFinderStrategyBuilder().toString()); + } + + @Test + void test_parseSupportingTypes_ThrowsAnException_WhenImplementationPrefixIsUsedWithoutAPrefix() { + try { + parser.parseSupportingTypes(context, tokens("supportingTypes", "implementation-prefix"), null); + fail(); + } catch (Exception e) { + assertEquals("Too few tokens, expected: supportingTypes implementation-prefix ", e.getMessage()); + } + } + + @Test + void test_parseSupportingTypes_ImplementationPrefix() { + parser.parseSupportingTypes(context, tokens("supportingTypes", "implementation-prefix", "Jdbc"), null); + assertEquals("ComponentFinderStrategyBuilder{technology=null, typeMatcher=null, typeFilter=null, supportingTypesStrategy=ImplementationWithPrefixSupportingTypesStrategy{prefix='Jdbc'}, namingStrategy=null, descriptionStrategy=null, urlStrategy=null, componentVisitor=null}", context.getComponentFinderStrategyBuilder().toString()); + } + @Test void test_parseName_ThrowsAnException_WhenNoTypeIsSpecified() { try {