diff --git a/logging.properties b/logging.properties new file mode 100644 index 00000000..f6c3fb46 --- /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 8035bdd0..092c80c0 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,5 +1,6 @@ rootProject.name = 'structurizr-java' +include 'structurizr-annotation' include 'structurizr-autolayout' include 'structurizr-client' include 'structurizr-component' diff --git a/structurizr-annotation/README.md b/structurizr-annotation/README.md new file mode 100644 index 00000000..a8626a37 --- /dev/null +++ b/structurizr-annotation/README.md @@ -0,0 +1,4 @@ +# structurizr-annotation + +[![Maven Central](https://img.shields.io/maven-central/v/com.structurizr/structurizr-annotation.svg?label=Maven%20Central)](https://search.maven.org/artifact/com.structurizr/structurizr-annotation) + diff --git a/structurizr-annotation/build.gradle b/structurizr-annotation/build.gradle new file mode 100644 index 00000000..0ce6a166 --- /dev/null +++ b/structurizr-annotation/build.gradle @@ -0,0 +1,3 @@ +dependencies { + +} \ No newline at end of file diff --git a/structurizr-annotation/src/main/java/com/structurizr/annotation/Properties.java b/structurizr-annotation/src/main/java/com/structurizr/annotation/Properties.java new file mode 100644 index 00000000..b477c2dd --- /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 00000000..405f6241 --- /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 00000000..5da19234 --- /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 00000000..0af4f4b2 --- /dev/null +++ b/structurizr-annotation/src/main/java/com/structurizr/annotation/Tags.java @@ -0,0 +1,15 @@ +package com.structurizr.annotation; + +import java.lang.annotation.*; + +/** + * A wrapper for @Tag annotations. + */ +@Documented +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +public @interface Tags { + + Tag[] value(); + +} \ No newline at end of file diff --git a/structurizr-component/build.gradle b/structurizr-component/build.gradle index 3a2fd6c6..6c49d879 100644 --- a/structurizr-component/build.gradle +++ b/structurizr-component/build.gradle @@ -4,6 +4,7 @@ dependencies { implementation 'org.apache.bcel:bcel:6.8.1' implementation 'com.github.javaparser:javaparser-symbol-solver-core:3.26.1' + testImplementation project(':structurizr-annotation') testImplementation 'org.junit.jupiter:junit-jupiter:5.9.2' } diff --git a/structurizr-component/src/main/java/com/structurizr/component/ComponentFinder.java b/structurizr-component/src/main/java/com/structurizr/component/ComponentFinder.java index 1055c453..c73e0228 100644 --- a/structurizr-component/src/main/java/com/structurizr/component/ComponentFinder.java +++ b/structurizr-component/src/main/java/com/structurizr/component/ComponentFinder.java @@ -1,13 +1,11 @@ package com.structurizr.component; +import com.structurizr.component.filter.TypeFilter; import com.structurizr.component.provider.TypeProvider; import com.structurizr.model.Component; import com.structurizr.model.Container; import com.structurizr.util.StringUtils; import org.apache.bcel.Repository; -import org.apache.bcel.classfile.ConstantPool; -import org.apache.bcel.classfile.Method; -import org.apache.bcel.generic.*; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -28,83 +26,40 @@ public final class ComponentFinder { private final Container container; private final List componentFinderStrategies = new ArrayList<>(); - ComponentFinder(Container container, Collection typeProviders, List componentFinderStrategies) { + ComponentFinder(Container container, TypeFilter typeFilter, Collection typeProviders, List componentFinderStrategies) { this.container = container; this.componentFinderStrategies.addAll(componentFinderStrategies); - findTypes(typeProviders); - } - - private void findTypes(Collection typeProviders) { + log.debug("Initialising component finder:"); + log.debug(" - for: " + container.getCanonicalName()); for (TypeProvider typeProvider : typeProviders) { - Set types = typeProvider.getTypes(); - for (com.structurizr.component.Type type : types) { - if (type.getJavaClass() != null) { - // this is the BCEL identified type - typeRepository.add(type); - } else { - // this is the source code identified type - com.structurizr.component.Type bcelType = typeRepository.getType(type.getFullyQualifiedName()); - if (bcelType != null) { - bcelType.setDescription(type.getDescription()); - bcelType.setSource(type.getSource()); - } - } - } + log.debug(" - from: " + typeProvider); + } + log.debug(" - filtered by: " + typeFilter); + for (ComponentFinderStrategy strategy : componentFinderStrategies) { + log.debug(" - with strategy: " + strategy); } + new TypeFinder().run(typeProviders, typeFilter, typeRepository); Repository.clearCache(); - for (com.structurizr.component.Type type : typeRepository.getTypes()) { + for (Type type : typeRepository.getTypes()) { if (type.getJavaClass() != null) { Repository.addClass(type.getJavaClass()); - findDependencies(type); - } - } - } - - private void findDependencies(com.structurizr.component.Type type) { - ConstantPool cp = type.getJavaClass().getConstantPool(); - ConstantPoolGen cpg = new ConstantPoolGen(cp); - for (Method m : type.getJavaClass().getMethods()) { - MethodGen mg = new MethodGen(m, type.getJavaClass().getClassName(), cpg); - InstructionList il = mg.getInstructionList(); - if (il == null) { - continue; - } - - InstructionHandle[] instructionHandles = il.getInstructionHandles(); - for (InstructionHandle instructionHandle : instructionHandles) { - Instruction instruction = instructionHandle.getInstruction(); - if (!(instruction instanceof InvokeInstruction)) { - continue; - } - - InvokeInstruction invokeInstruction = (InvokeInstruction)instruction; - ReferenceType referenceType = invokeInstruction.getReferenceType(cpg); - if (!(referenceType instanceof ObjectType)) { - continue; - } - - ObjectType objectType = (ObjectType)referenceType; - String referencedClassName = objectType.getClassName(); - com.structurizr.component.Type referencedType = typeRepository.getType(referencedClassName); - if (referencedType != null) { - type.addDependency(referencedType); - } + new TypeDependencyFinder().run(type, typeRepository); } } } /** - * Find components, using all configured rules, in the order they were added. + * Find components, using all configured strategies, in the order they were added. */ - public Set findComponents() { + public Set run() { Set discoveredComponents = new LinkedHashSet<>(); Map componentMap = new HashMap<>(); Set componentSet = new LinkedHashSet<>(); for (ComponentFinderStrategy componentFinderStrategy : componentFinderStrategies) { - Set set = componentFinderStrategy.findComponents(typeRepository); + Set set = componentFinderStrategy.run(typeRepository); if (set.isEmpty()) { throw new RuntimeException("No components were found by " + componentFinderStrategy); } @@ -120,28 +75,41 @@ public Set findComponents() { component.setDescription(discoveredComponent.getDescription()); component.setTechnology(discoveredComponent.getTechnology()); component.setUrl(discoveredComponent.getUrl()); + + component.addTags(discoveredComponent.getTags().toArray(new String[0])); + for (String name : discoveredComponent.getProperties().keySet()) { + component.addProperty(name, discoveredComponent.getProperties().get(name)); + } + componentMap.put(discoveredComponent, component); componentSet.add(component); } // find dependencies between all components for (DiscoveredComponent discoveredComponent : discoveredComponents) { + Component component = componentMap.get(discoveredComponent); + log.debug("Component dependencies for \"" + component.getName() + "\":"); Set typeDependencies = discoveredComponent.getAllDependencies(); for (Type typeDependency : typeDependencies) { for (DiscoveredComponent c : discoveredComponents) { if (c != discoveredComponent) { if (c.getAllTypes().contains(typeDependency)) { Component componentDependency = componentMap.get(c); - componentMap.get(discoveredComponent).uses(componentDependency, ""); + log.debug(" -> " + componentDependency.getName()); + component.uses(componentDependency, ""); } } } } + if (component.getRelationships().isEmpty()) { + log.debug(" - none"); + } } // now visit all components for (DiscoveredComponent discoveredComponent : componentMap.keySet()) { Component component = componentMap.get(discoveredComponent); + log.debug("Visiting \"" + component.getName() + "\""); discoveredComponent.getComponentFinderStrategy().visit(component); } diff --git a/structurizr-component/src/main/java/com/structurizr/component/ComponentFinderBuilder.java b/structurizr-component/src/main/java/com/structurizr/component/ComponentFinderBuilder.java index 86cd0a84..21f1a37e 100644 --- a/structurizr-component/src/main/java/com/structurizr/component/ComponentFinderBuilder.java +++ b/structurizr-component/src/main/java/com/structurizr/component/ComponentFinderBuilder.java @@ -1,5 +1,7 @@ package com.structurizr.component; +import com.structurizr.component.filter.DefaultTypeFilter; +import com.structurizr.component.filter.TypeFilter; import com.structurizr.component.provider.ClassDirectoryTypeProvider; import com.structurizr.component.provider.ClassJarFileTypeProvider; import com.structurizr.component.provider.SourceDirectoryTypeProvider; @@ -19,6 +21,7 @@ public class ComponentFinderBuilder { private Container container; private final List typeProviders = new ArrayList<>(); + private TypeFilter typeFilter; private final List componentFinderStrategies = new ArrayList<>(); public ComponentFinderBuilder forContainer(Container container) { @@ -57,6 +60,12 @@ public ComponentFinderBuilder fromSource(File path) { return this; } + public ComponentFinderBuilder filteredBy(TypeFilter typeFilter) { + this.typeFilter = typeFilter; + + return this; + } + public ComponentFinderBuilder withStrategy(ComponentFinderStrategy componentFinderStrategy) { this.componentFinderStrategies.add(componentFinderStrategy); @@ -72,11 +81,25 @@ public ComponentFinder build() { throw new RuntimeException("One or more type providers must be configured"); } + if (typeFilter == null) { + typeFilter = new DefaultTypeFilter(); + } + if (componentFinderStrategies.isEmpty()) { throw new RuntimeException("One or more component finder strategies must be configured"); } - return new ComponentFinder(container, typeProviders, componentFinderStrategies); + return new ComponentFinder(container, typeFilter, typeProviders, componentFinderStrategies); + } + + @Override + public String toString() { + return "ComponentFinderBuilder{" + + "container=" + container + + ", typeProviders=" + typeProviders + + ", typeFilter=" + typeFilter + + ", componentFinderStrategies=" + componentFinderStrategies + + '}'; } } diff --git a/structurizr-component/src/main/java/com/structurizr/component/ComponentFinderStrategy.java b/structurizr-component/src/main/java/com/structurizr/component/ComponentFinderStrategy.java index d1dc8f87..6862e7ab 100644 --- a/structurizr-component/src/main/java/com/structurizr/component/ComponentFinderStrategy.java +++ b/structurizr-component/src/main/java/com/structurizr/component/ComponentFinderStrategy.java @@ -8,8 +8,11 @@ import com.structurizr.component.url.UrlStrategy; import com.structurizr.component.visitor.ComponentVisitor; import com.structurizr.model.Component; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; import java.util.LinkedHashSet; +import java.util.Map; import java.util.Set; /** @@ -23,6 +26,8 @@ */ public final class ComponentFinderStrategy { + private static final Log log = LogFactory.getLog(ComponentFinderStrategy.class); + private final String technology; private final TypeMatcher typeMatcher; private final TypeFilter typeFilter; @@ -43,22 +48,49 @@ public final class ComponentFinderStrategy { this.componentVisitor = componentVisitor; } - Set findComponents(TypeRepository typeRepository) { + Set run(TypeRepository typeRepository) { Set components = new LinkedHashSet<>(); + log.debug("Running " + this.toString()); Set types = typeRepository.getTypes(); for (Type type : types) { - if (typeMatcher.matches(type) && typeFilter.accept(type)) { + + boolean matched = typeMatcher.matches(type); + boolean accepted = typeFilter.accept(type); + + if (matched) { + if (accepted) { + log.debug(" + " + type.getFullyQualifiedName() + " (matched=true, accepted=true)"); + } else { + log.debug(" - " + type.getFullyQualifiedName() + " (matched=true, accepted=false)"); + } + } else { + log.debug(" - " + type.getFullyQualifiedName() + " (matched=false)"); + } + + if (matched && accepted) { DiscoveredComponent component = new DiscoveredComponent(namingStrategy.nameOf(type), type); component.setDescription(descriptionStrategy.descriptionOf(type)); component.setTechnology(this.technology); component.setUrl(urlStrategy.urlOf(type)); + component.addTags(type.getTags()); + Map properties = type.getProperties(); + for (String name : properties.keySet()) { + component.addProperty(name, properties.get(name)); + } component.setComponentFinderStrategy(this); components.add(component); // now find supporting types Set supportingTypes = supportingTypesStrategy.findSupportingTypes(type, typeRepository); - component.addSupportingTypes(supportingTypes); + if (supportingTypes.isEmpty()) { + log.debug(" - none"); + } else { + for (Type supportingType : supportingTypes) { + log.debug(" + supporting type: " + supportingType.getFullyQualifiedName()); + } + component.addSupportingTypes(supportingTypes); + } } } diff --git a/structurizr-component/src/main/java/com/structurizr/component/DiscoveredComponent.java b/structurizr-component/src/main/java/com/structurizr/component/DiscoveredComponent.java index 70443932..cd77da3c 100644 --- a/structurizr-component/src/main/java/com/structurizr/component/DiscoveredComponent.java +++ b/structurizr-component/src/main/java/com/structurizr/component/DiscoveredComponent.java @@ -1,7 +1,6 @@ package com.structurizr.component; -import java.util.HashSet; -import java.util.Set; +import java.util.*; final class DiscoveredComponent { @@ -10,6 +9,8 @@ final class DiscoveredComponent { private String description; private String technology; private String url; + private final List tags = new ArrayList<>(); + private final Map properties = new HashMap<>(); private final Set supportingTypes = new HashSet<>(); private ComponentFinderStrategy componentFinderStrategy; @@ -55,6 +56,22 @@ void setUrl(String url) { this.url = url; } + void addTags(List tags) { + this.tags.addAll(tags); + } + + List getTags() { + return List.copyOf(tags); + } + + void addProperty(String key, String value) { + properties.put(key, value); + } + + Map getProperties() { + return Map.copyOf(properties); + } + Set getSupportingTypes() { return new HashSet<>(supportingTypes); } diff --git a/structurizr-component/src/main/java/com/structurizr/component/Type.java b/structurizr-component/src/main/java/com/structurizr/component/Type.java index 3c9fafe0..f267a0e8 100644 --- a/structurizr-component/src/main/java/com/structurizr/component/Type.java +++ b/structurizr-component/src/main/java/com/structurizr/component/Type.java @@ -1,17 +1,21 @@ package com.structurizr.component; import com.structurizr.util.StringUtils; -import org.apache.bcel.classfile.JavaClass; +import org.apache.bcel.classfile.*; -import java.util.LinkedHashSet; -import java.util.Objects; -import java.util.Set; +import java.util.*; /** * Represents a Java type (e.g. class or interface) - it's a wrapper around a BCEL JavaClass. */ public class Type { + private static final String STRUCTURIZR_TAG_ANNOTATION = "Lcom/structurizr/annotation/Tag;"; + private static final String STRUCTURIZR_TAGS_ANNOTATION = "Lcom/structurizr/annotation/Tags;"; + + private static final String STRUCTURIZR_PROPERTY_ANNOTATION = "Lcom/structurizr/annotation/Property;"; + private static final String STRUCTURIZR_PROPERTIES_ANNOTATION = "Lcom/structurizr/annotation/Properties;"; + private final JavaClass javaClass; private final String fullyQualifiedName; private String description; @@ -76,10 +80,63 @@ public Set getDependencies() { return new LinkedHashSet<>(dependencies); } + public boolean hasDependency(Type type) { + return dependencies.contains(type); + } + public boolean isAbstractClass() { return javaClass.isAbstract() && javaClass.isClass(); } + public List getTags() { + List tags = new ArrayList<>(); + + AnnotationEntry[] annotationEntries = javaClass.getAnnotationEntries(); + for (AnnotationEntry annotationEntry : annotationEntries) { + if (STRUCTURIZR_TAG_ANNOTATION.equals(annotationEntry.getAnnotationType())) { + ElementValuePair elementValuePair = annotationEntry.getElementValuePairs()[0]; + String tag = elementValuePair.getValue().stringifyValue(); + tags.add(tag); + } else if (STRUCTURIZR_TAGS_ANNOTATION.equals(annotationEntry.getAnnotationType())) { + ElementValuePair elementValuePair = annotationEntry.getElementValuePairs()[0]; + ArrayElementValue elementValue = (ArrayElementValue)elementValuePair.getValue(); + for (ElementValue value : elementValue.getElementValuesArray()) { + AnnotationElementValue annotationElementValue = (AnnotationElementValue)value; + AnnotationEntry tagAannotationEntry = annotationElementValue.getAnnotationEntry(); + String tag = tagAannotationEntry.getElementValuePairs()[0].getValue().stringifyValue(); + tags.add(tag); + } + } + } + + return tags; + } + + public Map getProperties() { + Map properties = new HashMap<>(); + + AnnotationEntry[] annotationEntries = javaClass.getAnnotationEntries(); + for (AnnotationEntry annotationEntry : annotationEntries) { + if (STRUCTURIZR_PROPERTY_ANNOTATION.equals(annotationEntry.getAnnotationType())) { + String name = annotationEntry.getElementValuePairs()[0].getValue().stringifyValue(); + String value = annotationEntry.getElementValuePairs()[1].getValue().stringifyValue(); + properties.put(name, value); + } else if (STRUCTURIZR_PROPERTIES_ANNOTATION.equals(annotationEntry.getAnnotationType())) { + ArrayElementValue arrayElementValue = (ArrayElementValue)annotationEntry.getElementValuePairs()[0].getValue(); + for (ElementValue elementValue : arrayElementValue.getElementValuesArray()) { + AnnotationElementValue annotationElementValue = (AnnotationElementValue)elementValue; + AnnotationEntry tagAannotationEntry = annotationElementValue.getAnnotationEntry(); + + String name = tagAannotationEntry.getElementValuePairs()[0].getValue().stringifyValue(); + String value = tagAannotationEntry.getElementValuePairs()[1].getValue().stringifyValue(); + properties.put(name, value); + } + } + } + + return properties; + } + @Override public boolean equals(Object o) { if (this == o) return true; diff --git a/structurizr-component/src/main/java/com/structurizr/component/TypeDependencyFinder.java b/structurizr-component/src/main/java/com/structurizr/component/TypeDependencyFinder.java new file mode 100644 index 00000000..2b8352b4 --- /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 00000000..d1b84346 --- /dev/null +++ b/structurizr-component/src/main/java/com/structurizr/component/TypeFinder.java @@ -0,0 +1,46 @@ +package com.structurizr.component; + +import com.structurizr.component.filter.TypeFilter; +import com.structurizr.component.provider.TypeProvider; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import java.util.Collection; +import java.util.Set; + +class TypeFinder { + + private static final Log log = LogFactory.getLog(TypeFinder.class); + + void run(Collection typeProviders, TypeFilter typeFilter, TypeRepository typeRepository) { + for (TypeProvider typeProvider : typeProviders) { + log.debug("Running " + typeProvider.toString()); + + Set types = typeProvider.getTypes(); + for (com.structurizr.component.Type type : types) { + + boolean accepted = typeFilter.accept(type); + if (accepted) { + log.debug(" + " + type.getFullyQualifiedName() + " (accepted=true)"); + } else { + log.debug(" - " + type.getFullyQualifiedName() + " (accepted=false)"); + } + + if (accepted) { + if (type.getJavaClass() != null) { + // this is the BCEL identified type + typeRepository.add(type); + } else { + // this is the source code identified type + Type bcelType = typeRepository.getType(type.getFullyQualifiedName()); + if (bcelType != null) { + bcelType.setDescription(type.getDescription()); + bcelType.setSource(type.getSource()); + } + } + } + } + } + } + +} \ No newline at end of file diff --git a/structurizr-component/src/main/java/com/structurizr/component/description/FirstSentenceDescriptionStrategy.java b/structurizr-component/src/main/java/com/structurizr/component/description/FirstSentenceDescriptionStrategy.java index accb274f..bdeccb14 100644 --- a/structurizr-component/src/main/java/com/structurizr/component/description/FirstSentenceDescriptionStrategy.java +++ b/structurizr-component/src/main/java/com/structurizr/component/description/FirstSentenceDescriptionStrategy.java @@ -12,11 +12,15 @@ public class FirstSentenceDescriptionStrategy implements DescriptionStrategy { public String descriptionOf(Type type) { String description = type.getDescription(); + if (StringUtils.isNullOrEmpty(description)) { + return ""; + } + int index = description.indexOf('.'); if (index == -1) { - return description; + return description.trim(); } else { - return description.substring(0, index+1); + return description.trim().substring(0, index+1); } } diff --git a/structurizr-component/src/main/java/com/structurizr/component/filter/ExcludeTypesByRegexFilter.java b/structurizr-component/src/main/java/com/structurizr/component/filter/ExcludeFullyQualifiedNameRegexFilter.java similarity index 77% rename from structurizr-component/src/main/java/com/structurizr/component/filter/ExcludeTypesByRegexFilter.java rename to structurizr-component/src/main/java/com/structurizr/component/filter/ExcludeFullyQualifiedNameRegexFilter.java index 52e7d284..c1ddaa9d 100644 --- a/structurizr-component/src/main/java/com/structurizr/component/filter/ExcludeTypesByRegexFilter.java +++ b/structurizr-component/src/main/java/com/structurizr/component/filter/ExcludeFullyQualifiedNameRegexFilter.java @@ -6,11 +6,11 @@ /** * A type filter that excludes by matching a regex against the fully qualified type name. */ -public class ExcludeTypesByRegexFilter implements TypeFilter { +public class ExcludeFullyQualifiedNameRegexFilter implements TypeFilter { private final String regex; - public ExcludeTypesByRegexFilter(String regex) { + public ExcludeFullyQualifiedNameRegexFilter(String regex) { if (StringUtils.isNullOrEmpty(regex)) { throw new IllegalArgumentException("A regex must be supplied"); } @@ -25,7 +25,7 @@ public boolean accept(Type type) { @Override public String toString() { - return "ExcludeTypesByRegexFilter{" + + return "ExcludeFullyQualifiedNameRegexFilter{" + "regex='" + regex + '\'' + '}'; } diff --git a/structurizr-component/src/main/java/com/structurizr/component/filter/IncludeTypesByRegexFilter.java b/structurizr-component/src/main/java/com/structurizr/component/filter/IncludeFullyQualifiedNameRegexFilter.java similarity index 77% rename from structurizr-component/src/main/java/com/structurizr/component/filter/IncludeTypesByRegexFilter.java rename to structurizr-component/src/main/java/com/structurizr/component/filter/IncludeFullyQualifiedNameRegexFilter.java index 3de92d28..dcf3a959 100644 --- a/structurizr-component/src/main/java/com/structurizr/component/filter/IncludeTypesByRegexFilter.java +++ b/structurizr-component/src/main/java/com/structurizr/component/filter/IncludeFullyQualifiedNameRegexFilter.java @@ -6,11 +6,11 @@ /** * A type filter that includes by matching a regex against the fully qualified type name. */ -public class IncludeTypesByRegexFilter implements TypeFilter { +public class IncludeFullyQualifiedNameRegexFilter implements TypeFilter { private final String regex; - public IncludeTypesByRegexFilter(String regex) { + public IncludeFullyQualifiedNameRegexFilter(String regex) { if (StringUtils.isNullOrEmpty(regex)) { throw new IllegalArgumentException("A regex must be supplied"); } @@ -25,7 +25,7 @@ public boolean accept(Type type) { @Override public String toString() { - return "IncludeTypesByRegexFilter{" + + return "IncludeFullyQualifiedNameRegexFilter{" + "regex='" + regex + '\'' + '}'; } diff --git a/structurizr-component/src/main/java/com/structurizr/component/provider/ClassDirectoryTypeProvider.java b/structurizr-component/src/main/java/com/structurizr/component/provider/ClassDirectoryTypeProvider.java index 1c18fb9d..a2560676 100644 --- a/structurizr-component/src/main/java/com/structurizr/component/provider/ClassDirectoryTypeProvider.java +++ b/structurizr-component/src/main/java/com/structurizr/component/provider/ClassDirectoryTypeProvider.java @@ -72,5 +72,11 @@ private Set findClassFiles(File path) { return classFiles; } + @Override + public String toString() { + return "ClassDirectoryTypeProvider{" + + "directory=" + directory + + '}'; + } } \ No newline at end of file diff --git a/structurizr-component/src/main/java/com/structurizr/component/provider/ClassJarFileTypeProvider.java b/structurizr-component/src/main/java/com/structurizr/component/provider/ClassJarFileTypeProvider.java index 10f00de7..908f2c34 100644 --- a/structurizr-component/src/main/java/com/structurizr/component/provider/ClassJarFileTypeProvider.java +++ b/structurizr-component/src/main/java/com/structurizr/component/provider/ClassJarFileTypeProvider.java @@ -58,4 +58,11 @@ public Set getTypes() { return types; } + @Override + public String toString() { + return "ClassJarFileTypeProvider{" + + "jarFile=" + jarFile + + '}'; + } + } \ No newline at end of file diff --git a/structurizr-component/src/main/java/com/structurizr/component/provider/SourceDirectoryTypeProvider.java b/structurizr-component/src/main/java/com/structurizr/component/provider/SourceDirectoryTypeProvider.java index 96f934f5..94a0a986 100644 --- a/structurizr-component/src/main/java/com/structurizr/component/provider/SourceDirectoryTypeProvider.java +++ b/structurizr-component/src/main/java/com/structurizr/component/provider/SourceDirectoryTypeProvider.java @@ -128,4 +128,11 @@ private String relativePath(File path) { return relativePath; } + @Override + public String toString() { + return "SourceDirectoryTypeProvider{" + + "directory=" + directory + + '}'; + } + } \ No newline at end of file diff --git a/structurizr-component/src/test/java/com/structurizr/component/ComponentFinderStrategyBuilderTests.java b/structurizr-component/src/test/java/com/structurizr/component/ComponentFinderStrategyBuilderTests.java index e71590df..b7937f09 100644 --- a/structurizr-component/src/test/java/com/structurizr/component/ComponentFinderStrategyBuilderTests.java +++ b/structurizr-component/src/test/java/com/structurizr/component/ComponentFinderStrategyBuilderTests.java @@ -2,8 +2,8 @@ import com.structurizr.component.description.FirstSentenceDescriptionStrategy; import com.structurizr.component.description.TruncatedDescriptionStrategy; -import com.structurizr.component.filter.ExcludeTypesByRegexFilter; -import com.structurizr.component.filter.IncludeTypesByRegexFilter; +import com.structurizr.component.filter.ExcludeFullyQualifiedNameRegexFilter; +import com.structurizr.component.filter.IncludeFullyQualifiedNameRegexFilter; import com.structurizr.component.matcher.NameSuffixTypeMatcher; import com.structurizr.component.naming.FullyQualifiedNamingStrategy; import com.structurizr.component.naming.TypeNamingStrategy; @@ -48,7 +48,7 @@ void filteredBy_ThrowsAnException_WhenPassedNull() { @Test void filteredBy_ThrowsAnException_WhenCalledTwice() { try { - new ComponentFinderStrategyBuilder().filteredBy(new IncludeTypesByRegexFilter(".*")).filteredBy(new ExcludeTypesByRegexFilter(".*")); + new ComponentFinderStrategyBuilder().filteredBy(new IncludeFullyQualifiedNameRegexFilter(".*")).filteredBy(new ExcludeFullyQualifiedNameRegexFilter(".*")); fail(); } catch (Exception e) { assertEquals("A type filter has already been configured", e.getMessage()); @@ -160,12 +160,12 @@ void build() { ComponentFinderStrategy strategy = new ComponentFinderStrategyBuilder() .withTechnology("Spring MVC Controller") .matchedBy(new NameSuffixTypeMatcher("Controller")) - .filteredBy(new IncludeTypesByRegexFilter("com.example.web.\\.*")) + .filteredBy(new IncludeFullyQualifiedNameRegexFilter("com.example.web.\\.*")) .withName(new TypeNamingStrategy()) .withDescription(new FirstSentenceDescriptionStrategy()) .build(); - assertEquals("ComponentFinderStrategy{technology='Spring MVC Controller', typeMatcher=NameSuffixTypeMatcher{suffix='Controller'}, typeFilter=IncludeTypesByRegexFilter{regex='com.example.web.\\.*'}, supportingTypesStrategy=DefaultSupportingTypesStrategy{}, namingStrategy=TypeNamingStrategy{}, descriptionStrategy=FirstSentenceDescriptionStrategy{}, urlStrategy=DefaultUrlStrategy{}, componentVisitor=DefaultComponentVisitor{}}", strategy.toString()); + assertEquals("ComponentFinderStrategy{technology='Spring MVC Controller', typeMatcher=NameSuffixTypeMatcher{suffix='Controller'}, typeFilter=IncludeFullyQualifiedNameRegexFilter{regex='com.example.web.\\.*'}, supportingTypesStrategy=DefaultSupportingTypesStrategy{}, namingStrategy=TypeNamingStrategy{}, descriptionStrategy=FirstSentenceDescriptionStrategy{}, urlStrategy=DefaultUrlStrategy{}, componentVisitor=DefaultComponentVisitor{}}", strategy.toString()); } } \ No newline at end of file diff --git a/structurizr-component/src/test/java/com/structurizr/component/ComponentFinderTests.java b/structurizr-component/src/test/java/com/structurizr/component/ComponentFinderTests.java new file mode 100644 index 00000000..5e7ad05c --- /dev/null +++ b/structurizr-component/src/test/java/com/structurizr/component/ComponentFinderTests.java @@ -0,0 +1,64 @@ +package com.structurizr.component; + +import com.structurizr.Workspace; +import com.structurizr.component.description.FirstSentenceDescriptionStrategy; +import com.structurizr.component.filter.IncludeFullyQualifiedNameRegexFilter; +import com.structurizr.component.matcher.ImplementsTypeMatcher; +import com.structurizr.component.naming.TypeNamingStrategy; +import com.structurizr.model.Component; +import com.structurizr.model.Container; +import com.structurizr.model.SoftwareSystem; +import org.junit.jupiter.api.Test; + +import java.io.File; + +import static com.github.javaparser.utils.Utils.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class ComponentFinderTests { + + @Test + void run() { + Workspace workspace = new Workspace("Name", "Description"); + SoftwareSystem softwareSystem = workspace.getModel().addSoftwareSystem("Name"); + Container container = softwareSystem.addContainer("Name"); + + ComponentFinder componentFinder = new ComponentFinderBuilder() + .forContainer(container) + .fromClasses(new File("build/classes/java/test")) + .fromSource(new File("src/test/java")) + .filteredBy(new IncludeFullyQualifiedNameRegexFilter("com\\.structurizr\\.component\\.example\\..*")) + .withStrategy(new ComponentFinderStrategyBuilder() + .withTechnology("Web Controller") + .matchedBy(new ImplementsTypeMatcher("com.structurizr.component.example.Controller")) + .filteredBy(new IncludeFullyQualifiedNameRegexFilter("com\\.structurizr\\.component\\.example\\..*")) + .withName(new TypeNamingStrategy()) + .withDescription(new FirstSentenceDescriptionStrategy()) + .build() + ) + .withStrategy(new ComponentFinderStrategyBuilder() + .withTechnology("Data Repository") + .matchedBy(new ImplementsTypeMatcher("com.structurizr.component.example.Repository")) + .filteredBy(new IncludeFullyQualifiedNameRegexFilter("com\\.structurizr\\.component\\.example\\..*")) + .withName(new TypeNamingStrategy()) + .withDescription(new FirstSentenceDescriptionStrategy()) + .build() + ) + .build(); + + componentFinder.run(); + + assertEquals(2, container.getComponents().size()); + Component exampleController = container.getComponentWithName("ExampleController"); + assertNotNull(exampleController); + assertTrue(exampleController.hasTag("Controller")); + assertEquals("https://example.com", exampleController.getProperties().get("Documentation")); + + Component exampleRepository = container.getComponentWithName("ExampleRepository"); + assertNotNull(exampleRepository); + + assertTrue(exampleController.hasEfferentRelationshipWith(exampleRepository)); + } + +} diff --git a/structurizr-component/src/test/java/com/structurizr/component/SpringPetClinicTests.java b/structurizr-component/src/test/java/com/structurizr/component/SpringPetClinicTests.java index e8d70c0f..a4158f36 100644 --- a/structurizr-component/src/test/java/com/structurizr/component/SpringPetClinicTests.java +++ b/structurizr-component/src/test/java/com/structurizr/component/SpringPetClinicTests.java @@ -2,7 +2,8 @@ import com.structurizr.Workspace; import com.structurizr.component.description.FirstSentenceDescriptionStrategy; -import com.structurizr.component.filter.ExcludeTypesByRegexFilter; +import com.structurizr.component.filter.ExcludeFullyQualifiedNameRegexFilter; +import com.structurizr.component.filter.IncludeFullyQualifiedNameRegexFilter; import com.structurizr.component.matcher.AnnotationTypeMatcher; import com.structurizr.component.matcher.ImplementsTypeMatcher; import com.structurizr.component.url.PrefixSourceUrlStrategy; @@ -36,10 +37,11 @@ void springPetClinic() { .forContainer(webApplication) .fromClasses(new File(springPetClinicHome, "target/spring-petclinic-3.3.0-SNAPSHOT.jar")) .fromSource(new File(springPetClinicHome, "src/main/java")) + .filteredBy(new IncludeFullyQualifiedNameRegexFilter("org\\.springframework\\.samples\\.petclinic\\..*")) .withStrategy( new ComponentFinderStrategyBuilder() .matchedBy(new AnnotationTypeMatcher("org.springframework.stereotype.Controller")) - .filteredBy(new ExcludeTypesByRegexFilter(".*.CrashController")) + .filteredBy(new ExcludeFullyQualifiedNameRegexFilter(".*.CrashController")) .withTechnology("Spring MVC Controller") .withUrl(new PrefixSourceUrlStrategy("https://github.com/spring-projects/spring-petclinic/blob/main/src/main/java")) .forEach((component -> { @@ -62,7 +64,7 @@ void springPetClinic() { ) .build(); - componentFinder.findComponents(); + componentFinder.run(); assertEquals(7, webApplication.getComponents().size()); Component welcomeController = webApplication.getComponentWithName("Welcome Controller"); diff --git a/structurizr-component/src/test/java/com/structurizr/component/TypeTests.java b/structurizr-component/src/test/java/com/structurizr/component/TypeTests.java index d73f761b..1df5874d 100644 --- a/structurizr-component/src/test/java/com/structurizr/component/TypeTests.java +++ b/structurizr-component/src/test/java/com/structurizr/component/TypeTests.java @@ -1,8 +1,15 @@ package com.structurizr.component; +import com.structurizr.component.matcher.ImplementsTypeMatcher; +import org.apache.bcel.classfile.ClassParser; import org.junit.jupiter.api.Test; -import static org.junit.jupiter.api.Assertions.assertEquals; +import java.io.File; +import java.util.List; +import java.util.Map; +import java.util.Properties; + +import static org.junit.jupiter.api.Assertions.*; public class TypeTests { @@ -20,4 +27,52 @@ void packageName() { assertEquals("com.example", type.getPackageName()); } + @Test + void getTags_WhenTypeHasOneTag() throws Exception { + File classes = new File("build/classes/java/test"); + ClassParser parser = new ClassParser(new File(classes, "com/structurizr/component/types/TypeWithTag.class").getAbsolutePath()); + Type type = new Type(parser.parse()); + + List tags = type.getTags(); + assertEquals(1, tags.size()); + assertTrue(tags.contains("Tag 1")); + } + + @Test + void getTags_WhenTypeHasManyTags() throws Exception { + File classes = new File("build/classes/java/test"); + ClassParser parser = new ClassParser(new File(classes, "com/structurizr/component/types/TypeWithTags.class").getAbsolutePath()); + Type type = new Type(parser.parse()); + + List tags = type.getTags(); + assertEquals(3, tags.size()); + assertEquals("Tag 1", tags.get(0)); + assertEquals("Tag 2", tags.get(1)); + assertEquals("Tag 3", tags.get(2)); + } + + @Test + void getTags_WhenTypeHasOneProperty() throws Exception { + File classes = new File("build/classes/java/test"); + ClassParser parser = new ClassParser(new File(classes, "com/structurizr/component/types/TypeWithProperty.class").getAbsolutePath()); + Type type = new Type(parser.parse()); + + Map properties = type.getProperties(); + assertEquals(1, properties.size()); + assertEquals("Value", properties.get("Name")); + } + + @Test + void getTags_WhenTypeHasManyProperties() throws Exception { + File classes = new File("build/classes/java/test"); + ClassParser parser = new ClassParser(new File(classes, "com/structurizr/component/types/TypeWithProperties.class").getAbsolutePath()); + Type type = new Type(parser.parse()); + + Map properties = type.getProperties(); + assertEquals(3, properties.size()); + assertEquals("Value1", properties.get("Name1")); + assertEquals("Value2", properties.get("Name2")); + assertEquals("Value3", properties.get("Name3")); + } + } \ No newline at end of file diff --git a/structurizr-component/src/test/java/com/structurizr/component/description/FirstSentenceDescriptionStrategyTests.java b/structurizr-component/src/test/java/com/structurizr/component/description/FirstSentenceDescriptionStrategyTests.java index f9c2515e..4039cf8f 100644 --- a/structurizr-component/src/test/java/com/structurizr/component/description/FirstSentenceDescriptionStrategyTests.java +++ b/structurizr-component/src/test/java/com/structurizr/component/description/FirstSentenceDescriptionStrategyTests.java @@ -4,9 +4,24 @@ import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; public class FirstSentenceDescriptionStrategyTests { + @Test + void descriptionOf_WhenTheDescriptionIsNull() { + Type type = new Type("com.example.ClassName"); + type.setDescription(null); + assertEquals("", new FirstSentenceDescriptionStrategy().descriptionOf(type)); + } + + @Test + void descriptionOf_WhenTheDescriptionIsEmpty() { + Type type = new Type("com.example.ClassName"); + type.setDescription(" "); + assertEquals("", new FirstSentenceDescriptionStrategy().descriptionOf(type)); + } + @Test void descriptionOf_WhenThereIsASentence() { Type type = new Type("com.example.ClassName"); diff --git a/structurizr-component/src/test/java/com/structurizr/component/example/Controller.java b/structurizr-component/src/test/java/com/structurizr/component/example/Controller.java new file mode 100644 index 00000000..ebaf960c --- /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 00000000..f6ff0569 --- /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 00000000..ce3afddf --- /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 00000000..276ed87a --- /dev/null +++ b/structurizr-component/src/test/java/com/structurizr/component/example/Repository.java @@ -0,0 +1,4 @@ +package com.structurizr.component.example; + +interface Repository { +} diff --git a/structurizr-component/src/test/java/com/structurizr/component/filter/ExcludeTypesByRegexFilterTests.java b/structurizr-component/src/test/java/com/structurizr/component/filter/ExcludeFullyQualifiedNameRegexFilterTests.java similarity index 60% rename from structurizr-component/src/test/java/com/structurizr/component/filter/ExcludeTypesByRegexFilterTests.java rename to structurizr-component/src/test/java/com/structurizr/component/filter/ExcludeFullyQualifiedNameRegexFilterTests.java index 4a0b459f..c7d156e8 100644 --- a/structurizr-component/src/test/java/com/structurizr/component/filter/ExcludeTypesByRegexFilterTests.java +++ b/structurizr-component/src/test/java/com/structurizr/component/filter/ExcludeFullyQualifiedNameRegexFilterTests.java @@ -5,28 +5,28 @@ import static org.junit.jupiter.api.Assertions.*; -public class ExcludeTypesByRegexFilterTests { +public class ExcludeFullyQualifiedNameRegexFilterTests { @Test void construction_ThrowsAnException_WhenPassedANullSuffix() { - assertThrowsExactly(IllegalArgumentException.class, () -> new ExcludeTypesByRegexFilter(null)); + assertThrowsExactly(IllegalArgumentException.class, () -> new ExcludeFullyQualifiedNameRegexFilter(null)); } @Test void construction_ThrowsAnException_WhenPassedAnEmptySuffix() { - assertThrowsExactly(IllegalArgumentException.class, () -> new ExcludeTypesByRegexFilter("")); - assertThrowsExactly(IllegalArgumentException.class, () -> new ExcludeTypesByRegexFilter(" ")); + assertThrowsExactly(IllegalArgumentException.class, () -> new ExcludeFullyQualifiedNameRegexFilter("")); + assertThrowsExactly(IllegalArgumentException.class, () -> new ExcludeFullyQualifiedNameRegexFilter(" ")); } @Test void filter_ReturnsTrue_WhenTheTypeDoesNotMatchRegex() { - assertTrue(new ExcludeTypesByRegexFilter(".*Utils").accept(new Type("com.example.CustomerComponent"))); + assertTrue(new ExcludeFullyQualifiedNameRegexFilter(".*Utils").accept(new Type("com.example.CustomerComponent"))); } @Test void filter_ReturnsFalse_WhenTheTypeMatchesRegex() { - assertFalse(new ExcludeTypesByRegexFilter(".*Utils").accept(new Type("com.example.DateUtils"))); + assertFalse(new ExcludeFullyQualifiedNameRegexFilter(".*Utils").accept(new Type("com.example.DateUtils"))); } } \ No newline at end of file diff --git a/structurizr-component/src/test/java/com/structurizr/component/filter/IncludeTypesByRegexFilterTests.java b/structurizr-component/src/test/java/com/structurizr/component/filter/IncludeFullyQualifiedNameRegexFilterTests.java similarity index 60% rename from structurizr-component/src/test/java/com/structurizr/component/filter/IncludeTypesByRegexFilterTests.java rename to structurizr-component/src/test/java/com/structurizr/component/filter/IncludeFullyQualifiedNameRegexFilterTests.java index 25b223c5..dfa1ed90 100644 --- a/structurizr-component/src/test/java/com/structurizr/component/filter/IncludeTypesByRegexFilterTests.java +++ b/structurizr-component/src/test/java/com/structurizr/component/filter/IncludeFullyQualifiedNameRegexFilterTests.java @@ -5,28 +5,28 @@ import static org.junit.jupiter.api.Assertions.*; -public class IncludeTypesByRegexFilterTests { +public class IncludeFullyQualifiedNameRegexFilterTests { @Test void construction_ThrowsAnException_WhenPassedANullSuffix() { - assertThrowsExactly(IllegalArgumentException.class, () -> new IncludeTypesByRegexFilter(null)); + assertThrowsExactly(IllegalArgumentException.class, () -> new IncludeFullyQualifiedNameRegexFilter(null)); } @Test void construction_ThrowsAnException_WhenPassedAnEmptySuffix() { - assertThrowsExactly(IllegalArgumentException.class, () -> new IncludeTypesByRegexFilter("")); - assertThrowsExactly(IllegalArgumentException.class, () -> new IncludeTypesByRegexFilter(" ")); + assertThrowsExactly(IllegalArgumentException.class, () -> new IncludeFullyQualifiedNameRegexFilter("")); + assertThrowsExactly(IllegalArgumentException.class, () -> new IncludeFullyQualifiedNameRegexFilter(" ")); } @Test void filter_ReturnsFalse_WhenTheTypeDoesNotMatchRegex() { - assertFalse(new IncludeTypesByRegexFilter(".*Component").accept(new Type("com.example.DateUtils"))); + assertFalse(new IncludeFullyQualifiedNameRegexFilter(".*Component").accept(new Type("com.example.DateUtils"))); } @Test void filter_ReturnsTrue_WhenTheTypeMatchesRegex() { - assertTrue(new IncludeTypesByRegexFilter(".*Component").accept(new Type("com.example.CustomerComponent"))); + assertTrue(new IncludeFullyQualifiedNameRegexFilter(".*Component").accept(new Type("com.example.CustomerComponent"))); } } \ No newline at end of file diff --git a/structurizr-component/src/test/java/com/structurizr/component/provider/ClassDirectoryTypeProviderTests.java b/structurizr-component/src/test/java/com/structurizr/component/provider/ClassDirectoryTypeProviderTests.java index 60d90a8c..924ecabe 100644 --- a/structurizr-component/src/test/java/com/structurizr/component/provider/ClassDirectoryTypeProviderTests.java +++ b/structurizr-component/src/test/java/com/structurizr/component/provider/ClassDirectoryTypeProviderTests.java @@ -32,7 +32,7 @@ void getTypes() { TypeProvider typeProvider = new ClassDirectoryTypeProvider(classes); Set types = typeProvider.getTypes(); - assertTrue(types.size() > 0); + assertFalse(types.isEmpty()); assertNotNull(types.stream().filter(t -> t.getFullyQualifiedName().equals("com.structurizr.component.provider.ClassDirectoryTypeProviderTests"))); } diff --git a/structurizr-component/src/test/java/com/structurizr/component/types/TypeWithProperties.java b/structurizr-component/src/test/java/com/structurizr/component/types/TypeWithProperties.java new file mode 100644 index 00000000..f9f0157d --- /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 00000000..d6de2577 --- /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 00000000..74cea4cd --- /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 00000000..b577d6ca --- /dev/null +++ b/structurizr-component/src/test/java/com/structurizr/component/types/TypeWithTags.java @@ -0,0 +1,9 @@ +package com.structurizr.component.types; + +import com.structurizr.annotation.Tag; + +@Tag(name = "Tag 1") +@Tag(name = "Tag 2") +@Tag(name = "Tag 3") +public class TypeWithTags { +} diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/ComponentFinderDslContext.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/ComponentFinderDslContext.java index 0172fd65..89f030ff 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/ComponentFinderDslContext.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ComponentFinderDslContext.java @@ -32,7 +32,7 @@ ComponentFinderBuilder getComponentFinderBuilder() { @Override void end() { - Set components = componentFinderBuilder.build().findComponents(); + Set components = componentFinderBuilder.build().run(); for (Component component : components) { dslParser.registerIdentifier(IdentifiersRegister.toIdentifier(component.getName()), component); } diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/ComponentFinderParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/ComponentFinderParser.java index 61d0d49d..f0931bc5 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/ComponentFinderParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ComponentFinderParser.java @@ -1,10 +1,18 @@ package com.structurizr.dsl; +import com.structurizr.component.filter.ExcludeFullyQualifiedNameRegexFilter; +import com.structurizr.component.filter.IncludeFullyQualifiedNameRegexFilter; + final class ComponentFinderParser extends AbstractParser { private static final String CLASSES_GRAMMAR = "classes "; private static final String SOURCE_GRAMMAR = "source "; + private static final String FILTER_INCLUDE = "include"; + private static final String FILTER_EXCLUDE = "exclude"; + private static final String FILTER_FQN_REGEX = "fqn-regex"; + private static final String FILTER_GRAMMAR = "filter <" + FILTER_INCLUDE + "|" + FILTER_EXCLUDE + "> <" + FILTER_FQN_REGEX + "> [parameters]"; + void parseClasses(ComponentFinderDslContext context, Tokens tokens) { // classes @@ -25,4 +33,34 @@ void parseSource(ComponentFinderDslContext context, Tokens tokens) { context.getComponentFinderBuilder().fromSource(tokens.get(1)); } + void parseFilter(ComponentFinderDslContext context, Tokens tokens) { + if (tokens.size() < 3) { + throw new RuntimeException("Too few tokens, expected: " + FILTER_GRAMMAR); + } + + String includeOrExclude = tokens.get(1).toLowerCase(); + if (!"include".equalsIgnoreCase(includeOrExclude) && !"exclude".equalsIgnoreCase(includeOrExclude)) { + throw new RuntimeException("Filter mode should be \"" + FILTER_INCLUDE + "\" or \"" + FILTER_EXCLUDE + "\": " + FILTER_GRAMMAR); + } + + String type = tokens.get(2).toLowerCase(); + switch (type) { + case FILTER_FQN_REGEX: + if (tokens.size() == 4) { + String regex = tokens.get(3); + + if (FILTER_INCLUDE.equalsIgnoreCase(includeOrExclude)) { + context.getComponentFinderBuilder().filteredBy(new IncludeFullyQualifiedNameRegexFilter(regex)); + } else { + context.getComponentFinderBuilder().filteredBy(new ExcludeFullyQualifiedNameRegexFilter(regex)); + } + } else { + throw new RuntimeException("Expected: " + FILTER_GRAMMAR); + } + break; + default: + throw new IllegalArgumentException("Unknown filter: " + type); + } + } + } \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/ComponentFinderStrategyParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/ComponentFinderStrategyParser.java index e8480088..340f437e 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/ComponentFinderStrategyParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ComponentFinderStrategyParser.java @@ -2,8 +2,8 @@ import com.structurizr.component.description.FirstSentenceDescriptionStrategy; import com.structurizr.component.description.TruncatedDescriptionStrategy; -import com.structurizr.component.filter.ExcludeTypesByRegexFilter; -import com.structurizr.component.filter.IncludeTypesByRegexFilter; +import com.structurizr.component.filter.ExcludeFullyQualifiedNameRegexFilter; +import com.structurizr.component.filter.IncludeFullyQualifiedNameRegexFilter; import com.structurizr.component.matcher.*; import com.structurizr.component.naming.DefaultPackageNamingStrategy; import com.structurizr.component.naming.TypeNamingStrategy; @@ -153,9 +153,9 @@ void parseFilter(ComponentFinderStrategyDslContext context, Tokens tokens, File String regex = tokens.get(3); if (FILTER_INCLUDE.equalsIgnoreCase(includeOrExclude)) { - context.getComponentFinderStrategyBuilder().filteredBy(new IncludeTypesByRegexFilter(regex)); + context.getComponentFinderStrategyBuilder().filteredBy(new IncludeFullyQualifiedNameRegexFilter(regex)); } else { - context.getComponentFinderStrategyBuilder().filteredBy(new ExcludeTypesByRegexFilter(regex)); + context.getComponentFinderStrategyBuilder().filteredBy(new ExcludeFullyQualifiedNameRegexFilter(regex)); } } else { throw new RuntimeException("Expected: " + FILTER_GRAMMAR); diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java index abbaa8a6..6b7e307d 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java @@ -433,6 +433,9 @@ void parse(List lines, File dslFile, boolean fragment, boolean includeIn } else if (COMPONENT_FINDER_SOURCE_TOKEN.equalsIgnoreCase(firstToken) && inContext(ComponentFinderDslContext.class)) { new ComponentFinderParser().parseSource(getContext(ComponentFinderDslContext.class), tokens); + } else if (COMPONENT_FINDER_FILTER_TOKEN.equalsIgnoreCase(firstToken) && inContext(ComponentFinderDslContext.class)) { + new ComponentFinderParser().parseFilter(getContext(ComponentFinderDslContext.class), tokens); + } else if (COMPONENT_FINDER_STRATEGY_TOKEN.equalsIgnoreCase(firstToken) && inContext(ComponentFinderDslContext.class)) { if (shouldStartContext(tokens)) { startContext(new ComponentFinderStrategyDslContext(getContext(ComponentFinderDslContext.class).getComponentFinderBuilder())); diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslTokens.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslTokens.java index fa382993..69616260 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslTokens.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslTokens.java @@ -116,6 +116,7 @@ class StructurizrDslTokens { static final String COMPONENT_FINDER_TOKEN = "!components"; static final String COMPONENT_FINDER_CLASSES_TOKEN = "classes"; static final String COMPONENT_FINDER_SOURCE_TOKEN = "source"; + static final String COMPONENT_FINDER_FILTER_TOKEN = "filter"; static final String COMPONENT_FINDER_STRATEGY_TOKEN = "strategy"; static final String COMPONENT_FINDER_STRATEGY_TECHNOLOGY_TOKEN = "technology"; static final String COMPONENT_FINDER_STRATEGY_MATCHER_TOKEN = "matcher"; diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/ComponentFinderParserTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/ComponentFinderParserTests.java new file mode 100644 index 00000000..414de072 --- /dev/null +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/ComponentFinderParserTests.java @@ -0,0 +1,59 @@ +package com.structurizr.dsl; + +import com.structurizr.component.ComponentFinderBuilder; +import com.structurizr.component.matcher.NameSuffixTypeMatcher; +import org.junit.jupiter.api.Test; + +import java.io.File; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; + +class ComponentFinderParserTests extends AbstractTests { + + private final ComponentFinderParser parser = new ComponentFinderParser(); + private final ComponentFinderDslContext context = new ComponentFinderDslContext(null, null); + + @Test + void test_parseFilter_ThrowsAnException_WhenNoModeAndTypeAreSpecified() { + try { + parser.parseFilter(context, tokens("filter")); + fail(); + } catch (Exception e) { + assertEquals("Too few tokens, expected: filter [parameters]", e.getMessage()); + } + } + + @Test + void test_parseFilter_ThrowsAnException_WhenNoTypeIsSpecified() { + try { + parser.parseFilter(context, tokens("filter", "include")); + fail(); + } catch (Exception e) { + assertEquals("Too few tokens, expected: filter [parameters]", e.getMessage()); + } + } + + @Test + void test_parseFilter_ThrowsAnException_WhenInvalidModeIsSpecified() { + try { + parser.parseFilter(context, tokens("filter", "mode", "fqn-regex")); + fail(); + } catch (Exception e) { + assertEquals("Filter mode should be \"include\" or \"exclude\": filter [parameters]", e.getMessage()); + } + } + + @Test + void test_parseFilter_WhenIncludeFullyQualifiedNameRegexTypeFilterIsUsed() { + parser.parseFilter(context, tokens("filter", "include", "fqn-regex", ".*")); + assertEquals("ComponentFinderBuilder{container=null, typeProviders=[], typeFilter=IncludeFullyQualifiedNameRegexFilter{regex='.*'}, componentFinderStrategies=[]}", context.getComponentFinderBuilder().toString()); + } + + @Test + void test_parseFilter_WhenExcludeFullyQualifiedNameRegexTypeFilterIsUsed() { + parser.parseFilter(context, tokens("filter", "exclude", "fqn-regex", ".*")); + assertEquals("ComponentFinderBuilder{container=null, typeProviders=[], typeFilter=ExcludeFullyQualifiedNameRegexFilter{regex='.*'}, componentFinderStrategies=[]}", context.getComponentFinderBuilder().toString()); + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/ComponentFinderStrategyParserTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/ComponentFinderStrategyParserTests.java index 17d9cf0a..6ea5eb25 100644 --- a/structurizr-dsl/src/test/java/com/structurizr/dsl/ComponentFinderStrategyParserTests.java +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/ComponentFinderStrategyParserTests.java @@ -186,13 +186,13 @@ void test_parseFilter_ThrowsAnException_WhenInvalidModeIsSpecified() { @Test void test_parseFilter_WhenIncludeFullyQualifiedNameRegexTypeFilterIsUsed() { parser.parseFilter(context, tokens("filter", "include", "fqn-regex", ".*"), new File(".")); - assertEquals("ComponentFinderStrategyBuilder{technology=null, typeMatcher=null, typeFilter=IncludeTypesByRegexFilter{regex='.*'}, supportingTypesStrategy=null, namingStrategy=null, descriptionStrategy=null, urlStrategy=null, componentVisitor=null}", context.getComponentFinderStrategyBuilder().toString()); + assertEquals("ComponentFinderStrategyBuilder{technology=null, typeMatcher=null, typeFilter=IncludeFullyQualifiedNameRegexFilter{regex='.*'}, supportingTypesStrategy=null, namingStrategy=null, descriptionStrategy=null, urlStrategy=null, componentVisitor=null}", context.getComponentFinderStrategyBuilder().toString()); } @Test void test_parseFilter_WhenExcludeFullyQualifiedNameRegexTypeFilterIsUsed() { parser.parseFilter(context, tokens("filter", "exclude", "fqn-regex", ".*"), new File(".")); - assertEquals("ComponentFinderStrategyBuilder{technology=null, typeMatcher=null, typeFilter=ExcludeTypesByRegexFilter{regex='.*'}, supportingTypesStrategy=null, namingStrategy=null, descriptionStrategy=null, urlStrategy=null, componentVisitor=null}", context.getComponentFinderStrategyBuilder().toString()); + assertEquals("ComponentFinderStrategyBuilder{technology=null, typeMatcher=null, typeFilter=ExcludeFullyQualifiedNameRegexFilter{regex='.*'}, supportingTypesStrategy=null, namingStrategy=null, descriptionStrategy=null, urlStrategy=null, componentVisitor=null}", context.getComponentFinderStrategyBuilder().toString()); } @Test diff --git a/structurizr-dsl/src/test/resources/dsl/spring-petclinic/workspace.dsl b/structurizr-dsl/src/test/resources/dsl/spring-petclinic/workspace.dsl index 3638bcc0..9144e5fc 100644 --- a/structurizr-dsl/src/test/resources/dsl/spring-petclinic/workspace.dsl +++ b/structurizr-dsl/src/test/resources/dsl/spring-petclinic/workspace.dsl @@ -22,6 +22,7 @@ workspace "Spring PetClinic" "A C4 model of the Spring PetClinic sample app (htt !components { classes "${SPRING_PETCLINIC_HOME}/target/spring-petclinic-3.3.0-SNAPSHOT.jar" source "${SPRING_PETCLINIC_HOME}/src/main/java" + filter include fqn-regex "org.springframework.samples.petclinic..*" strategy { technology "Spring MVC Controller" matcher annotation "org.springframework.stereotype.Controller"