diff --git a/archunit-example/example-junit4/src/test/java/com/tngtech/archunit/exampletest/junit4/ControllerRulesTest.java b/archunit-example/example-junit4/src/test/java/com/tngtech/archunit/exampletest/junit4/ControllerRulesTest.java index e9b9e6f9a8..9ba5123ff0 100644 --- a/archunit-example/example-junit4/src/test/java/com/tngtech/archunit/exampletest/junit4/ControllerRulesTest.java +++ b/archunit-example/example-junit4/src/test/java/com/tngtech/archunit/exampletest/junit4/ControllerRulesTest.java @@ -1,9 +1,9 @@ package com.tngtech.archunit.exampletest.junit4; import com.tngtech.archunit.base.DescribedPredicate; -import com.tngtech.archunit.base.PackageMatchers; import com.tngtech.archunit.core.domain.JavaClass; import com.tngtech.archunit.core.domain.JavaMember; +import com.tngtech.archunit.core.domain.PackageMatchers; import com.tngtech.archunit.example.layers.security.Secured; import com.tngtech.archunit.junit.AnalyzeClasses; import com.tngtech.archunit.junit.ArchTest; diff --git a/archunit-example/example-junit4/src/test/java/com/tngtech/archunit/exampletest/junit4/OnionArchitectureTest.java b/archunit-example/example-junit4/src/test/java/com/tngtech/archunit/exampletest/junit4/OnionArchitectureTest.java index c070cbee99..ef94dfed65 100644 --- a/archunit-example/example-junit4/src/test/java/com/tngtech/archunit/exampletest/junit4/OnionArchitectureTest.java +++ b/archunit-example/example-junit4/src/test/java/com/tngtech/archunit/exampletest/junit4/OnionArchitectureTest.java @@ -1,7 +1,17 @@ package com.tngtech.archunit.exampletest.junit4; +import java.lang.annotation.Annotation; + +import com.tngtech.archunit.base.DescribedPredicate; +import com.tngtech.archunit.core.domain.JavaAnnotation; +import com.tngtech.archunit.core.domain.JavaClass; +import com.tngtech.archunit.core.domain.properties.CanBeAnnotated; import com.tngtech.archunit.example.onionarchitecture.domain.model.OrderItem; import com.tngtech.archunit.example.onionarchitecture.domain.service.OrderQuantity; +import com.tngtech.archunit.example.onionarchitecture_by_annotations.annotations.Adapter; +import com.tngtech.archunit.example.onionarchitecture_by_annotations.annotations.Application; +import com.tngtech.archunit.example.onionarchitecture_by_annotations.annotations.DomainModel; +import com.tngtech.archunit.example.onionarchitecture_by_annotations.annotations.DomainService; import com.tngtech.archunit.junit.AnalyzeClasses; import com.tngtech.archunit.junit.ArchTest; import com.tngtech.archunit.junit.ArchUnitRunner; @@ -9,11 +19,17 @@ import org.junit.experimental.categories.Category; import org.junit.runner.RunWith; +import static com.tngtech.archunit.base.DescribedPredicate.describe; +import static com.tngtech.archunit.core.domain.JavaClass.Predicates.belongTo; +import static com.tngtech.archunit.core.domain.properties.CanBeAnnotated.Predicates.annotatedWith; import static com.tngtech.archunit.library.Architectures.onionArchitecture; @Category(Example.class) @RunWith(ArchUnitRunner.class) -@AnalyzeClasses(packages = "com.tngtech.archunit.example.onionarchitecture") +@AnalyzeClasses(packages = { + "com.tngtech.archunit.example.onionarchitecture", + "com.tngtech.archunit.example.onionarchitecture_by_annotations" +}) public class OnionArchitectureTest { @ArchTest @@ -35,4 +51,30 @@ public class OnionArchitectureTest { .adapter("rest", "..adapter.rest..") .ignoreDependency(OrderItem.class, OrderQuantity.class); + + @ArchTest + static final ArchRule onion_architecture_defined_by_annotations = onionArchitecture() + .domainModels(byAnnotation(DomainModel.class)) + .domainServices(byAnnotation(DomainService.class)) + .applicationServices(byAnnotation(Application.class)) + .adapter("cli", byAnnotation(adapter("cli"))) + .adapter("persistence", byAnnotation(adapter("persistence"))) + .adapter("rest", byAnnotation(adapter("rest"))); + + private static DescribedPredicate byAnnotation(Class annotationType) { + DescribedPredicate annotatedWith = annotatedWith(annotationType); + return belongTo(annotatedWith).as(annotatedWith.getDescription()); + } + + private static DescribedPredicate byAnnotation(DescribedPredicate> annotationType) { + DescribedPredicate annotatedWith = annotatedWith(annotationType); + return belongTo(annotatedWith).as(annotatedWith.getDescription()); + } + + private static DescribedPredicate> adapter(String adapterName) { + return describe( + String.format("@%s(\"%s\")", Adapter.class.getSimpleName(), adapterName), + a -> a.getRawType().isEquivalentTo(Adapter.class) && a.as(Adapter.class).value().equals(adapterName) + ); + } } diff --git a/archunit-example/example-junit4/src/test/java/com/tngtech/archunit/exampletest/junit4/PlantUmlArchitectureTest.java b/archunit-example/example-junit4/src/test/java/com/tngtech/archunit/exampletest/junit4/PlantUmlArchitectureTest.java index 03662eef38..a0240f786e 100644 --- a/archunit-example/example-junit4/src/test/java/com/tngtech/archunit/exampletest/junit4/PlantUmlArchitectureTest.java +++ b/archunit-example/example-junit4/src/test/java/com/tngtech/archunit/exampletest/junit4/PlantUmlArchitectureTest.java @@ -2,7 +2,7 @@ import java.net.URL; -import com.tngtech.archunit.base.PackageMatchers; +import com.tngtech.archunit.core.domain.PackageMatchers; import com.tngtech.archunit.example.plantuml.catalog.ProductCatalog; import com.tngtech.archunit.example.plantuml.order.Order; import com.tngtech.archunit.example.plantuml.product.Product; diff --git a/archunit-example/example-junit5/src/test/java/com/tngtech/archunit/exampletest/junit5/ControllerRulesTest.java b/archunit-example/example-junit5/src/test/java/com/tngtech/archunit/exampletest/junit5/ControllerRulesTest.java index 0164e9de67..ed69fd4ee0 100644 --- a/archunit-example/example-junit5/src/test/java/com/tngtech/archunit/exampletest/junit5/ControllerRulesTest.java +++ b/archunit-example/example-junit5/src/test/java/com/tngtech/archunit/exampletest/junit5/ControllerRulesTest.java @@ -1,9 +1,9 @@ package com.tngtech.archunit.exampletest.junit5; import com.tngtech.archunit.base.DescribedPredicate; -import com.tngtech.archunit.base.PackageMatchers; import com.tngtech.archunit.core.domain.JavaClass; import com.tngtech.archunit.core.domain.JavaMember; +import com.tngtech.archunit.core.domain.PackageMatchers; import com.tngtech.archunit.example.layers.security.Secured; import com.tngtech.archunit.junit.AnalyzeClasses; import com.tngtech.archunit.junit.ArchTag; diff --git a/archunit-example/example-junit5/src/test/java/com/tngtech/archunit/exampletest/junit5/OnionArchitectureTest.java b/archunit-example/example-junit5/src/test/java/com/tngtech/archunit/exampletest/junit5/OnionArchitectureTest.java index 8012b95cdb..60ee3cbbbd 100644 --- a/archunit-example/example-junit5/src/test/java/com/tngtech/archunit/exampletest/junit5/OnionArchitectureTest.java +++ b/archunit-example/example-junit5/src/test/java/com/tngtech/archunit/exampletest/junit5/OnionArchitectureTest.java @@ -1,16 +1,32 @@ package com.tngtech.archunit.exampletest.junit5; +import java.lang.annotation.Annotation; + +import com.tngtech.archunit.base.DescribedPredicate; +import com.tngtech.archunit.core.domain.JavaAnnotation; +import com.tngtech.archunit.core.domain.JavaClass; +import com.tngtech.archunit.core.domain.properties.CanBeAnnotated; import com.tngtech.archunit.example.onionarchitecture.domain.model.OrderItem; import com.tngtech.archunit.example.onionarchitecture.domain.service.OrderQuantity; +import com.tngtech.archunit.example.onionarchitecture_by_annotations.annotations.Adapter; +import com.tngtech.archunit.example.onionarchitecture_by_annotations.annotations.Application; +import com.tngtech.archunit.example.onionarchitecture_by_annotations.annotations.DomainModel; +import com.tngtech.archunit.example.onionarchitecture_by_annotations.annotations.DomainService; import com.tngtech.archunit.junit.AnalyzeClasses; import com.tngtech.archunit.junit.ArchTag; import com.tngtech.archunit.junit.ArchTest; import com.tngtech.archunit.lang.ArchRule; +import static com.tngtech.archunit.base.DescribedPredicate.describe; +import static com.tngtech.archunit.core.domain.JavaClass.Predicates.belongTo; +import static com.tngtech.archunit.core.domain.properties.CanBeAnnotated.Predicates.annotatedWith; import static com.tngtech.archunit.library.Architectures.onionArchitecture; @ArchTag("example") -@AnalyzeClasses(packages = "com.tngtech.archunit.example.onionarchitecture") +@AnalyzeClasses(packages = { + "com.tngtech.archunit.example.onionarchitecture", + "com.tngtech.archunit.example.onionarchitecture_by_annotations" +}) public class OnionArchitectureTest { @ArchTest @@ -32,4 +48,30 @@ public class OnionArchitectureTest { .adapter("rest", "..adapter.rest..") .ignoreDependency(OrderItem.class, OrderQuantity.class); + + @ArchTest + static final ArchRule onion_architecture_defined_by_annotations = onionArchitecture() + .domainModels(byAnnotation(DomainModel.class)) + .domainServices(byAnnotation(DomainService.class)) + .applicationServices(byAnnotation(Application.class)) + .adapter("cli", byAnnotation(adapter("cli"))) + .adapter("persistence", byAnnotation(adapter("persistence"))) + .adapter("rest", byAnnotation(adapter("rest"))); + + private static DescribedPredicate byAnnotation(Class annotationType) { + DescribedPredicate annotatedWith = annotatedWith(annotationType); + return belongTo(annotatedWith).as(annotatedWith.getDescription()); + } + + private static DescribedPredicate byAnnotation(DescribedPredicate> annotationType) { + DescribedPredicate annotatedWith = annotatedWith(annotationType); + return belongTo(annotatedWith).as(annotatedWith.getDescription()); + } + + private static DescribedPredicate> adapter(String adapterName) { + return describe( + String.format("@%s(\"%s\")", Adapter.class.getSimpleName(), adapterName), + a -> a.getRawType().isEquivalentTo(Adapter.class) && a.as(Adapter.class).value().equals(adapterName) + ); + } } diff --git a/archunit-example/example-junit5/src/test/java/com/tngtech/archunit/exampletest/junit5/PlantUmlArchitectureTest.java b/archunit-example/example-junit5/src/test/java/com/tngtech/archunit/exampletest/junit5/PlantUmlArchitectureTest.java index 85d49fdbf4..9b1abe0ef4 100644 --- a/archunit-example/example-junit5/src/test/java/com/tngtech/archunit/exampletest/junit5/PlantUmlArchitectureTest.java +++ b/archunit-example/example-junit5/src/test/java/com/tngtech/archunit/exampletest/junit5/PlantUmlArchitectureTest.java @@ -2,7 +2,7 @@ import java.net.URL; -import com.tngtech.archunit.base.PackageMatchers; +import com.tngtech.archunit.core.domain.PackageMatchers; import com.tngtech.archunit.example.plantuml.catalog.ProductCatalog; import com.tngtech.archunit.example.plantuml.order.Order; import com.tngtech.archunit.example.plantuml.product.Product; diff --git a/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/onionarchitecture_by_annotations/annotations/Adapter.java b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/onionarchitecture_by_annotations/annotations/Adapter.java new file mode 100644 index 0000000000..513588690f --- /dev/null +++ b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/onionarchitecture_by_annotations/annotations/Adapter.java @@ -0,0 +1,5 @@ +package com.tngtech.archunit.example.onionarchitecture_by_annotations.annotations; + +public @interface Adapter { + String value(); +} diff --git a/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/onionarchitecture_by_annotations/annotations/Application.java b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/onionarchitecture_by_annotations/annotations/Application.java new file mode 100644 index 0000000000..79d7897740 --- /dev/null +++ b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/onionarchitecture_by_annotations/annotations/Application.java @@ -0,0 +1,4 @@ +package com.tngtech.archunit.example.onionarchitecture_by_annotations.annotations; + +public @interface Application { +} diff --git a/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/onionarchitecture_by_annotations/annotations/DomainModel.java b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/onionarchitecture_by_annotations/annotations/DomainModel.java new file mode 100644 index 0000000000..0caca4939f --- /dev/null +++ b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/onionarchitecture_by_annotations/annotations/DomainModel.java @@ -0,0 +1,4 @@ +package com.tngtech.archunit.example.onionarchitecture_by_annotations.annotations; + +public @interface DomainModel { +} diff --git a/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/onionarchitecture_by_annotations/annotations/DomainService.java b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/onionarchitecture_by_annotations/annotations/DomainService.java new file mode 100644 index 0000000000..cdffb95c64 --- /dev/null +++ b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/onionarchitecture_by_annotations/annotations/DomainService.java @@ -0,0 +1,4 @@ +package com.tngtech.archunit.example.onionarchitecture_by_annotations.annotations; + +public @interface DomainService { +} diff --git a/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/onionarchitecture_by_annotations/onion/AdministrationPort.java b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/onionarchitecture_by_annotations/onion/AdministrationPort.java new file mode 100644 index 0000000000..d73731730e --- /dev/null +++ b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/onionarchitecture_by_annotations/onion/AdministrationPort.java @@ -0,0 +1,8 @@ +package com.tngtech.archunit.example.onionarchitecture_by_annotations.onion; + +import com.tngtech.archunit.example.onionarchitecture_by_annotations.annotations.Application; + +@Application +public interface AdministrationPort { + T getInstanceOf(Class type); +} diff --git a/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/onionarchitecture_by_annotations/onion/ShoppingApplication.java b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/onionarchitecture_by_annotations/onion/ShoppingApplication.java new file mode 100644 index 0000000000..6562cf0068 --- /dev/null +++ b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/onionarchitecture_by_annotations/onion/ShoppingApplication.java @@ -0,0 +1,19 @@ +package com.tngtech.archunit.example.onionarchitecture_by_annotations.onion; + +import com.tngtech.archunit.example.onionarchitecture_by_annotations.annotations.Application; + +@Application +public class ShoppingApplication { + public static void main(String[] args) { + // start the whole application / provide IOC features + } + + public static AdministrationPort openAdministrationPort() { + return new AdministrationPort() { + @Override + public T getInstanceOf(Class type) { + throw new UnsupportedOperationException("Not yet implemented"); + } + }; + } +} diff --git a/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/onionarchitecture_by_annotations/onion/administration/AdministrationCLI.java b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/onionarchitecture_by_annotations/onion/administration/AdministrationCLI.java new file mode 100644 index 0000000000..66ffecd482 --- /dev/null +++ b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/onionarchitecture_by_annotations/onion/administration/AdministrationCLI.java @@ -0,0 +1,22 @@ +package com.tngtech.archunit.example.onionarchitecture_by_annotations.onion.administration; + +import com.tngtech.archunit.example.onionarchitecture_by_annotations.annotations.Adapter; +import com.tngtech.archunit.example.onionarchitecture_by_annotations.onion.AdministrationPort; +import com.tngtech.archunit.example.onionarchitecture_by_annotations.onion.ShoppingApplication; +import com.tngtech.archunit.example.onionarchitecture_by_annotations.onion.product.ProductRepository; + +@Adapter("cli") +@SuppressWarnings("unused") +public class AdministrationCLI { + public static void main(String[] args) { + AdministrationPort port = ShoppingApplication.openAdministrationPort(); + handle(args, port); + } + + private static void handle(String[] args, AdministrationPort port) { + // violates the pairwise independence of adapters + ProductRepository repository = port.getInstanceOf(ProductRepository.class); + long count = repository.getTotalCount(); + // parse arguments and re-configure application according to count through port + } +} diff --git a/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/onionarchitecture_by_annotations/onion/order/OrderItem.java b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/onionarchitecture_by_annotations/onion/order/OrderItem.java new file mode 100644 index 0000000000..015b11d2d9 --- /dev/null +++ b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/onionarchitecture_by_annotations/onion/order/OrderItem.java @@ -0,0 +1,26 @@ +package com.tngtech.archunit.example.onionarchitecture_by_annotations.onion.order; + +import com.tngtech.archunit.example.onionarchitecture_by_annotations.annotations.DomainModel; +import com.tngtech.archunit.example.onionarchitecture_by_annotations.onion.product.Product; + +@DomainModel +public class OrderItem { + private final Product product; + private final OrderQuantity quantity; + + public OrderItem(Product product, OrderQuantity quantity) { + if (product == null) { + throw new IllegalArgumentException("Product must not be null"); + } + if (quantity == null) { + throw new IllegalArgumentException("Quantity not be null"); + } + this.product = product; + this.quantity = quantity; + } + + @Override + public String toString() { + return getClass().getSimpleName() + "{product=" + product + ", quantity=" + quantity + '}'; + } +} diff --git a/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/onionarchitecture_by_annotations/onion/order/OrderQuantity.java b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/onionarchitecture_by_annotations/onion/order/OrderQuantity.java new file mode 100644 index 0000000000..4e9e4226a8 --- /dev/null +++ b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/onionarchitecture_by_annotations/onion/order/OrderQuantity.java @@ -0,0 +1,21 @@ +package com.tngtech.archunit.example.onionarchitecture_by_annotations.onion.order; + +import com.tngtech.archunit.example.onionarchitecture_by_annotations.annotations.DomainService; + +@DomainService +@SuppressWarnings("unused") +public class OrderQuantity { + private final int quantity; + + public OrderQuantity(int quantity) { + if (quantity <= 0) { + throw new IllegalArgumentException("Quantity must be positive"); + } + this.quantity = quantity; + } + + @Override + public String toString() { + return getClass().getSimpleName() + "{quantity=" + quantity + '}'; + } +} diff --git a/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/onionarchitecture_by_annotations/onion/order/PaymentMethod.java b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/onionarchitecture_by_annotations/onion/order/PaymentMethod.java new file mode 100644 index 0000000000..3e27ac4a2c --- /dev/null +++ b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/onionarchitecture_by_annotations/onion/order/PaymentMethod.java @@ -0,0 +1,7 @@ +package com.tngtech.archunit.example.onionarchitecture_by_annotations.onion.order; + +import com.tngtech.archunit.example.onionarchitecture_by_annotations.annotations.DomainModel; + +@DomainModel +public class PaymentMethod { +} diff --git a/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/onionarchitecture_by_annotations/onion/product/Product.java b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/onionarchitecture_by_annotations/onion/product/Product.java new file mode 100644 index 0000000000..394373a288 --- /dev/null +++ b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/onionarchitecture_by_annotations/onion/product/Product.java @@ -0,0 +1,27 @@ +package com.tngtech.archunit.example.onionarchitecture_by_annotations.onion.product; + +import com.tngtech.archunit.example.onionarchitecture_by_annotations.annotations.DomainModel; + +@DomainModel +public class Product { + // Dependency on ProductId violates the architecture, since ProductId resides with persistence adapter + private final ProductId id; + // Dependency on ProductName violates the architecture, since ProductName is located in the DomainService layer + private final ProductName name; + + public Product(ProductId id, ProductName name) { + if (id == null) { + throw new IllegalArgumentException("Product id must not be null"); + } + if (name == null) { + throw new IllegalArgumentException("Product name must not be null"); + } + this.id = id; + this.name = name; + } + + @Override + public String toString() { + return getClass().getSimpleName() + "{id=" + id + ", name=" + name + '}'; + } +} diff --git a/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/onionarchitecture_by_annotations/onion/product/ProductId.java b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/onionarchitecture_by_annotations/onion/product/ProductId.java new file mode 100644 index 0000000000..5e38d7e0c7 --- /dev/null +++ b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/onionarchitecture_by_annotations/onion/product/ProductId.java @@ -0,0 +1,23 @@ +package com.tngtech.archunit.example.onionarchitecture_by_annotations.onion.product; + +import java.util.UUID; + +import com.tngtech.archunit.example.onionarchitecture_by_annotations.annotations.Adapter; + +@Adapter("persistence") +@SuppressWarnings("unused") +public class ProductId { + private final UUID id; + + public ProductId(UUID id) { + if (id == null) { + throw new IllegalArgumentException("ID must not be null"); + } + this.id = id; + } + + @Override + public String toString() { + return getClass().getSimpleName() + "{id=" + id + '}'; + } +} diff --git a/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/onionarchitecture_by_annotations/onion/product/ProductJpaRepository.java b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/onionarchitecture_by_annotations/onion/product/ProductJpaRepository.java new file mode 100644 index 0000000000..d05b8aea2f --- /dev/null +++ b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/onionarchitecture_by_annotations/onion/product/ProductJpaRepository.java @@ -0,0 +1,17 @@ +package com.tngtech.archunit.example.onionarchitecture_by_annotations.onion.product; + +import com.tngtech.archunit.example.onionarchitecture_by_annotations.annotations.Adapter; + +@Adapter("persistence") +@SuppressWarnings("unused") +public class ProductJpaRepository implements ProductRepository { + @Override + public Product read(ProductId id) { + return new Product(id, new ProductName("would normally be read")); + } + + @Override + public long getTotalCount() { + return 0; + } +} diff --git a/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/onionarchitecture_by_annotations/onion/product/ProductName.java b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/onionarchitecture_by_annotations/onion/product/ProductName.java new file mode 100644 index 0000000000..5cddb618cc --- /dev/null +++ b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/onionarchitecture_by_annotations/onion/product/ProductName.java @@ -0,0 +1,21 @@ +package com.tngtech.archunit.example.onionarchitecture_by_annotations.onion.product; + +import com.tngtech.archunit.example.onionarchitecture_by_annotations.annotations.DomainService; + +@DomainService +@SuppressWarnings("unused") +public class ProductName { + private final String name; + + public ProductName(String name) { + if (name == null || name.isEmpty()) { + throw new IllegalArgumentException("Name must not be empty"); + } + this.name = name; + } + + @Override + public String toString() { + return getClass().getSimpleName() + "{name='" + name + '\'' + '}'; + } +} diff --git a/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/onionarchitecture_by_annotations/onion/product/ProductRepository.java b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/onionarchitecture_by_annotations/onion/product/ProductRepository.java new file mode 100644 index 0000000000..097cba1e0c --- /dev/null +++ b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/onionarchitecture_by_annotations/onion/product/ProductRepository.java @@ -0,0 +1,11 @@ +package com.tngtech.archunit.example.onionarchitecture_by_annotations.onion.product; + +import com.tngtech.archunit.example.onionarchitecture_by_annotations.annotations.Adapter; + +// Violates the architecture because Domain must be the owner of the interfaces, not the persistence adapter +@Adapter("persistence") +public interface ProductRepository { + Product read(ProductId id); + + long getTotalCount(); +} diff --git a/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/onionarchitecture_by_annotations/onion/shopping/ShoppingCart.java b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/onionarchitecture_by_annotations/onion/shopping/ShoppingCart.java new file mode 100644 index 0000000000..e88bd32c9e --- /dev/null +++ b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/onionarchitecture_by_annotations/onion/shopping/ShoppingCart.java @@ -0,0 +1,32 @@ +package com.tngtech.archunit.example.onionarchitecture_by_annotations.onion.shopping; + +import java.util.HashSet; +import java.util.Set; + +import com.tngtech.archunit.example.onionarchitecture_by_annotations.annotations.DomainModel; +import com.tngtech.archunit.example.onionarchitecture_by_annotations.onion.order.OrderItem; +import com.tngtech.archunit.example.onionarchitecture_by_annotations.onion.order.PaymentMethod; + +@DomainModel +@SuppressWarnings("unused") +public class ShoppingCart { + private final ShoppingCartId id; + private final Set orderItems = new HashSet<>(); + + public ShoppingCart(ShoppingCartId id) { + this.id = id; + } + + public void add(OrderItem orderItem) { + orderItems.add(orderItem); + } + + public void executeOrder(PaymentMethod method) { + // complete financial transaction and initiate shipping process + } + + @Override + public String toString() { + return getClass().getSimpleName() + "{id=" + id + ", orderItems=" + orderItems + '}'; + } +} diff --git a/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/onionarchitecture_by_annotations/onion/shopping/ShoppingCartId.java b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/onionarchitecture_by_annotations/onion/shopping/ShoppingCartId.java new file mode 100644 index 0000000000..b4cff159a4 --- /dev/null +++ b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/onionarchitecture_by_annotations/onion/shopping/ShoppingCartId.java @@ -0,0 +1,22 @@ +package com.tngtech.archunit.example.onionarchitecture_by_annotations.onion.shopping; + +import java.util.UUID; + +import com.tngtech.archunit.example.onionarchitecture_by_annotations.annotations.Adapter; + +@Adapter("persistence") +public class ShoppingCartId { + private final UUID id; + + public ShoppingCartId(UUID id) { + if (id == null) { + throw new IllegalArgumentException("ID must not be null"); + } + this.id = id; + } + + @Override + public String toString() { + return getClass().getSimpleName() + "{id=" + id + '}'; + } +} diff --git a/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/onionarchitecture_by_annotations/onion/shopping/ShoppingCartJpaRepository.java b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/onionarchitecture_by_annotations/onion/shopping/ShoppingCartJpaRepository.java new file mode 100644 index 0000000000..89cff6865d --- /dev/null +++ b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/onionarchitecture_by_annotations/onion/shopping/ShoppingCartJpaRepository.java @@ -0,0 +1,18 @@ +package com.tngtech.archunit.example.onionarchitecture_by_annotations.onion.shopping; + +import com.tngtech.archunit.example.onionarchitecture_by_annotations.annotations.Adapter; + +@Adapter("persistence") +@SuppressWarnings("unused") +public class ShoppingCartJpaRepository implements ShoppingCartRepository { + @Override + public ShoppingCart read(ShoppingCartId id) { + // would normally load fully initialized shopping cart + return new ShoppingCart(id); + } + + @Override + public void save(ShoppingCart shoppingCart) { + // store shopping cart via JPA + } +} diff --git a/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/onionarchitecture_by_annotations/onion/shopping/ShoppingCartRepository.java b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/onionarchitecture_by_annotations/onion/shopping/ShoppingCartRepository.java new file mode 100644 index 0000000000..9ea700e10b --- /dev/null +++ b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/onionarchitecture_by_annotations/onion/shopping/ShoppingCartRepository.java @@ -0,0 +1,11 @@ +package com.tngtech.archunit.example.onionarchitecture_by_annotations.onion.shopping; + +import com.tngtech.archunit.example.onionarchitecture_by_annotations.annotations.Adapter; + +// Violates the architecture because Domain must be the owner of the interfaces, not the persistence adapter +@Adapter("persistence") +public interface ShoppingCartRepository { + ShoppingCart read(ShoppingCartId id); + + void save(ShoppingCart shoppingCart); +} diff --git a/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/onionarchitecture_by_annotations/onion/shopping/ShoppingController.java b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/onionarchitecture_by_annotations/onion/shopping/ShoppingController.java new file mode 100644 index 0000000000..8dfeff5f80 --- /dev/null +++ b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/onionarchitecture_by_annotations/onion/shopping/ShoppingController.java @@ -0,0 +1,22 @@ +package com.tngtech.archunit.example.onionarchitecture_by_annotations.onion.shopping; + +import java.util.UUID; + +import com.tngtech.archunit.example.onionarchitecture_by_annotations.annotations.Adapter; +import com.tngtech.archunit.example.onionarchitecture_by_annotations.onion.order.OrderQuantity; +import com.tngtech.archunit.example.onionarchitecture_by_annotations.onion.product.ProductId; + +@Adapter("rest") +@SuppressWarnings("unused") +public class ShoppingController { + private final ShoppingService shoppingService; + + public ShoppingController(ShoppingService shoppingService) { + this.shoppingService = shoppingService; + } + + // @POST or similar + public void addToShoppingCart(UUID shoppingCartId, UUID productId, int quantity) { + shoppingService.addToShoppingCart(new ShoppingCartId(shoppingCartId), new ProductId(productId), new OrderQuantity(quantity)); + } +} diff --git a/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/onionarchitecture_by_annotations/onion/shopping/ShoppingService.java b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/onionarchitecture_by_annotations/onion/shopping/ShoppingService.java new file mode 100644 index 0000000000..11a74616cc --- /dev/null +++ b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/onionarchitecture_by_annotations/onion/shopping/ShoppingService.java @@ -0,0 +1,27 @@ +package com.tngtech.archunit.example.onionarchitecture_by_annotations.onion.shopping; + +import com.tngtech.archunit.example.onionarchitecture_by_annotations.annotations.DomainService; +import com.tngtech.archunit.example.onionarchitecture_by_annotations.onion.order.OrderItem; +import com.tngtech.archunit.example.onionarchitecture_by_annotations.onion.order.OrderQuantity; +import com.tngtech.archunit.example.onionarchitecture_by_annotations.onion.product.Product; +import com.tngtech.archunit.example.onionarchitecture_by_annotations.onion.product.ProductId; +import com.tngtech.archunit.example.onionarchitecture_by_annotations.onion.product.ProductRepository; + +@DomainService +public class ShoppingService { + private final ShoppingCartRepository shoppingCartRepository; + private final ProductRepository productRepository; + + public ShoppingService(ShoppingCartRepository shoppingCartRepository, ProductRepository productRepository) { + this.shoppingCartRepository = shoppingCartRepository; + this.productRepository = productRepository; + } + + public void addToShoppingCart(ShoppingCartId shoppingCartId, ProductId productId, OrderQuantity quantity) { + ShoppingCart shoppingCart = shoppingCartRepository.read(shoppingCartId); + Product product = productRepository.read(productId); + OrderItem newItem = new OrderItem(product, quantity); + shoppingCart.add(newItem); + shoppingCartRepository.save(shoppingCart); + } +} diff --git a/archunit-example/example-plain/src/test/java/com/tngtech/archunit/exampletest/ControllerRulesTest.java b/archunit-example/example-plain/src/test/java/com/tngtech/archunit/exampletest/ControllerRulesTest.java index 6a0ec56301..f8ef44aee6 100644 --- a/archunit-example/example-plain/src/test/java/com/tngtech/archunit/exampletest/ControllerRulesTest.java +++ b/archunit-example/example-plain/src/test/java/com/tngtech/archunit/exampletest/ControllerRulesTest.java @@ -1,10 +1,10 @@ package com.tngtech.archunit.exampletest; import com.tngtech.archunit.base.DescribedPredicate; -import com.tngtech.archunit.base.PackageMatchers; import com.tngtech.archunit.core.domain.JavaClass; import com.tngtech.archunit.core.domain.JavaClasses; import com.tngtech.archunit.core.domain.JavaMember; +import com.tngtech.archunit.core.domain.PackageMatchers; import com.tngtech.archunit.core.importer.ClassFileImporter; import com.tngtech.archunit.example.layers.security.Secured; import org.junit.Test; diff --git a/archunit-example/example-plain/src/test/java/com/tngtech/archunit/exampletest/OnionArchitectureTest.java b/archunit-example/example-plain/src/test/java/com/tngtech/archunit/exampletest/OnionArchitectureTest.java index 3e17bd72dd..026551d002 100644 --- a/archunit-example/example-plain/src/test/java/com/tngtech/archunit/exampletest/OnionArchitectureTest.java +++ b/archunit-example/example-plain/src/test/java/com/tngtech/archunit/exampletest/OnionArchitectureTest.java @@ -1,17 +1,32 @@ package com.tngtech.archunit.exampletest; +import java.lang.annotation.Annotation; + +import com.tngtech.archunit.base.DescribedPredicate; +import com.tngtech.archunit.core.domain.JavaAnnotation; +import com.tngtech.archunit.core.domain.JavaClass; import com.tngtech.archunit.core.domain.JavaClasses; +import com.tngtech.archunit.core.domain.properties.CanBeAnnotated; import com.tngtech.archunit.core.importer.ClassFileImporter; import com.tngtech.archunit.example.onionarchitecture.domain.model.OrderItem; import com.tngtech.archunit.example.onionarchitecture.domain.service.OrderQuantity; +import com.tngtech.archunit.example.onionarchitecture_by_annotations.annotations.Adapter; +import com.tngtech.archunit.example.onionarchitecture_by_annotations.annotations.Application; +import com.tngtech.archunit.example.onionarchitecture_by_annotations.annotations.DomainModel; +import com.tngtech.archunit.example.onionarchitecture_by_annotations.annotations.DomainService; import org.junit.Test; import org.junit.experimental.categories.Category; +import static com.tngtech.archunit.base.DescribedPredicate.describe; +import static com.tngtech.archunit.core.domain.JavaClass.Predicates.belongTo; +import static com.tngtech.archunit.core.domain.properties.CanBeAnnotated.Predicates.annotatedWith; import static com.tngtech.archunit.library.Architectures.onionArchitecture; @Category(Example.class) public class OnionArchitectureTest { - private final JavaClasses classes = new ClassFileImporter().importPackages("com.tngtech.archunit.example.onionarchitecture"); + private final JavaClasses classes = new ClassFileImporter().importPackages( + "com.tngtech.archunit.example.onionarchitecture", + "com.tngtech.archunit.example.onionarchitecture_by_annotations"); @Test public void onion_architecture_is_respected() { @@ -39,4 +54,33 @@ public void onion_architecture_is_respected_with_exception() { .check(classes); } + + @Test + public void onion_architecture_defined_by_annotations() { + onionArchitecture() + .domainModels(byAnnotation(DomainModel.class)) + .domainServices(byAnnotation(DomainService.class)) + .applicationServices(byAnnotation(Application.class)) + .adapter("cli", byAnnotation(adapter("cli"))) + .adapter("persistence", byAnnotation(adapter("persistence"))) + .adapter("rest", byAnnotation(adapter("rest"))) + .check(classes); + } + + private static DescribedPredicate byAnnotation(Class annotationType) { + DescribedPredicate annotatedWith = annotatedWith(annotationType); + return belongTo(annotatedWith).as(annotatedWith.getDescription()); + } + + private static DescribedPredicate byAnnotation(DescribedPredicate> annotationType) { + DescribedPredicate annotatedWith = annotatedWith(annotationType); + return belongTo(annotatedWith).as(annotatedWith.getDescription()); + } + + private static DescribedPredicate> adapter(String adapterName) { + return describe( + String.format("@%s(\"%s\")", Adapter.class.getSimpleName(), adapterName), + a -> a.getRawType().isEquivalentTo(Adapter.class) && a.as(Adapter.class).value().equals(adapterName) + ); + } } diff --git a/archunit-example/example-plain/src/test/java/com/tngtech/archunit/exampletest/PlantUmlArchitectureTest.java b/archunit-example/example-plain/src/test/java/com/tngtech/archunit/exampletest/PlantUmlArchitectureTest.java index 38836e99b3..516e0084b7 100644 --- a/archunit-example/example-plain/src/test/java/com/tngtech/archunit/exampletest/PlantUmlArchitectureTest.java +++ b/archunit-example/example-plain/src/test/java/com/tngtech/archunit/exampletest/PlantUmlArchitectureTest.java @@ -2,8 +2,8 @@ import java.net.URL; -import com.tngtech.archunit.base.PackageMatchers; import com.tngtech.archunit.core.domain.JavaClasses; +import com.tngtech.archunit.core.domain.PackageMatchers; import com.tngtech.archunit.core.importer.ClassFileImporter; import com.tngtech.archunit.example.plantuml.catalog.ProductCatalog; import com.tngtech.archunit.example.plantuml.order.Order; diff --git a/archunit-integration-test/src/test/java/com/tngtech/archunit/integration/ExamplesIntegrationTest.java b/archunit-integration-test/src/test/java/com/tngtech/archunit/integration/ExamplesIntegrationTest.java index d405a7ea6c..b1364a1d85 100644 --- a/archunit-integration-test/src/test/java/com/tngtech/archunit/integration/ExamplesIntegrationTest.java +++ b/archunit-integration-test/src/test/java/com/tngtech/archunit/integration/ExamplesIntegrationTest.java @@ -1099,6 +1099,8 @@ Stream OnionArchitectureTest() { addExpectedCommonFailure.accept("onion_architecture_is_respected_with_exception", expectedTestFailures); + ExpectedOnionArchitectureByAnnotationFailures.addTo(expectedTestFailures); + return expectedTestFailures.toDynamicTests(); } diff --git a/archunit-integration-test/src/test/java/com/tngtech/archunit/integration/ExpectedOnionArchitectureByAnnotationFailures.java b/archunit-integration-test/src/test/java/com/tngtech/archunit/integration/ExpectedOnionArchitectureByAnnotationFailures.java new file mode 100644 index 0000000000..8099ce0eb9 --- /dev/null +++ b/archunit-integration-test/src/test/java/com/tngtech/archunit/integration/ExpectedOnionArchitectureByAnnotationFailures.java @@ -0,0 +1,77 @@ +package com.tngtech.archunit.integration; + +import java.util.UUID; + +import com.tngtech.archunit.example.onionarchitecture_by_annotations.onion.AdministrationPort; +import com.tngtech.archunit.example.onionarchitecture_by_annotations.onion.administration.AdministrationCLI; +import com.tngtech.archunit.example.onionarchitecture_by_annotations.onion.order.OrderItem; +import com.tngtech.archunit.example.onionarchitecture_by_annotations.onion.order.OrderQuantity; +import com.tngtech.archunit.example.onionarchitecture_by_annotations.onion.product.Product; +import com.tngtech.archunit.example.onionarchitecture_by_annotations.onion.product.ProductId; +import com.tngtech.archunit.example.onionarchitecture_by_annotations.onion.product.ProductName; +import com.tngtech.archunit.example.onionarchitecture_by_annotations.onion.product.ProductRepository; +import com.tngtech.archunit.example.onionarchitecture_by_annotations.onion.shopping.ShoppingCart; +import com.tngtech.archunit.example.onionarchitecture_by_annotations.onion.shopping.ShoppingCartId; +import com.tngtech.archunit.example.onionarchitecture_by_annotations.onion.shopping.ShoppingCartRepository; +import com.tngtech.archunit.example.onionarchitecture_by_annotations.onion.shopping.ShoppingController; +import com.tngtech.archunit.example.onionarchitecture_by_annotations.onion.shopping.ShoppingService; +import com.tngtech.archunit.testutils.ExpectedTestFailures; + +import static com.tngtech.archunit.testutils.ExpectedAccess.callFromMethod; +import static com.tngtech.archunit.testutils.ExpectedDependency.constructor; +import static com.tngtech.archunit.testutils.ExpectedDependency.field; +import static com.tngtech.archunit.testutils.ExpectedDependency.method; +import static java.lang.System.lineSeparator; + +class ExpectedOnionArchitectureByAnnotationFailures { + // This is only extracted to avoid the import clashes. + // Otherwise, it would be really bloated two write down with fully qualified class names everywhere + static void addTo(ExpectedTestFailures expectedTestFailures) { + expectedTestFailures + .ofRule("onion_architecture_defined_by_annotations", + "Onion architecture consisting of" + lineSeparator() + + "domain models (annotated with @DomainModel)" + lineSeparator() + + "domain services (annotated with @DomainService)" + lineSeparator() + + "application services (annotated with @Application)" + lineSeparator() + + "adapter 'cli' (annotated with @Adapter(\"cli\"))" + lineSeparator() + + "adapter 'persistence' (annotated with @Adapter(\"persistence\"))" + lineSeparator() + + "adapter 'rest' (annotated with @Adapter(\"rest\"))") + .by(constructor(Product.class).withParameter(ProductId.class)) + .by(constructor(Product.class).withParameter(ProductName.class)) + .by(constructor(ShoppingCart.class).withParameter(ShoppingCartId.class)) + .by(constructor(ShoppingService.class).withParameter(ProductRepository.class)) + .by(constructor(ShoppingService.class).withParameter(ShoppingCartRepository.class)) + + .by(field(Product.class, "id").ofType(ProductId.class)) + .by(field(Product.class, "name").ofType(ProductName.class)) + .by(field(ShoppingCart.class, "id").ofType(ShoppingCartId.class)) + .by(field(ShoppingService.class, "productRepository").ofType(ProductRepository.class)) + .by(field(ShoppingService.class, "shoppingCartRepository").ofType(ShoppingCartRepository.class)) + + .by(method(AdministrationCLI.class, "handle") + .referencingClassObject(ProductRepository.class) + .inLine(18)) + .by(callFromMethod(AdministrationCLI.class, "handle", String[].class, AdministrationPort.class) + .toMethod(ProductRepository.class, "getTotalCount") + .inLine(19).asDependency()) + .by(callFromMethod(ShoppingController.class, "addToShoppingCart", UUID.class, UUID.class, int.class) + .toConstructor(ProductId.class, UUID.class) + .inLine(20).asDependency()) + .by(callFromMethod(ShoppingController.class, "addToShoppingCart", UUID.class, UUID.class, int.class) + .toConstructor(ShoppingCartId.class, UUID.class) + .inLine(20).asDependency()) + .by(method(ShoppingService.class, "addToShoppingCart").withParameter(ProductId.class)) + .by(method(ShoppingService.class, "addToShoppingCart").withParameter(ShoppingCartId.class)) + .by(callFromMethod(ShoppingService.class, "addToShoppingCart", ShoppingCartId.class, ProductId.class, OrderQuantity.class) + .toMethod(ShoppingCartRepository.class, "read", ShoppingCartId.class) + .inLine(21).asDependency()) + .by(callFromMethod(ShoppingService.class, "addToShoppingCart", ShoppingCartId.class, ProductId.class, OrderQuantity.class) + .toMethod(ProductRepository.class, "read", ProductId.class) + .inLine(22).asDependency()) + .by(callFromMethod(ShoppingService.class, "addToShoppingCart", ShoppingCartId.class, ProductId.class, OrderQuantity.class) + .toMethod(ShoppingCartRepository.class, "save", ShoppingCart.class) + .inLine(25).asDependency()) + .by(constructor(OrderItem.class).withParameter(OrderQuantity.class)) + .by(field(OrderItem.class, "quantity").ofType(OrderQuantity.class)); + } +} diff --git a/archunit/src/main/java/com/tngtech/archunit/core/domain/Formatters.java b/archunit/src/main/java/com/tngtech/archunit/core/domain/Formatters.java index 9949b3aeb6..2971d619c0 100644 --- a/archunit/src/main/java/com/tngtech/archunit/core/domain/Formatters.java +++ b/archunit/src/main/java/com/tngtech/archunit/core/domain/Formatters.java @@ -16,6 +16,8 @@ package com.tngtech.archunit.core.domain; import java.util.List; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; import com.google.common.base.Joiner; import com.google.common.collect.ImmutableList; @@ -24,6 +26,8 @@ import static com.google.common.base.Strings.repeat; import static com.google.common.collect.ImmutableList.copyOf; import static com.tngtech.archunit.PublicAPI.Usage.ACCESS; +import static java.util.Arrays.stream; +import static java.util.stream.Collectors.joining; import static java.util.stream.Collectors.toList; public final class Formatters { @@ -97,18 +101,19 @@ public static List formatNamesOf(Iterable> paramTypes return result.build(); } - // Excluding the '$' character might be incorrect, but since '$' is a valid character of a class name - // and also the delimiter within the fully qualified name between an inner class and its enclosing class, - // there is no clean way to derive the simple name from just a fully qualified class name without - // further information - // Luckily for imported classes we can read this information from the bytecode /** * @param name A possibly fully qualified class name * @return A best guess of the simple name, i.e. prefixes like 'a.b.c.' cut off, 'Some$' of 'Some$Inner' as well. - * Returns an empty String, if the name belongs to an anonymous class (e.g. some.Type$1). + * Returns an empty String, if the name belongs to an anonymous class (e.g. some.Type$1). */ @PublicAPI(usage = ACCESS) public static String ensureSimpleName(String name) { + // Excluding the '$' character might be incorrect, but since '$' is a valid character of a class name + // and also the delimiter within the fully qualified name between an inner class and its enclosing class, + // there is no clean way to derive the simple name from just a fully qualified class name without + // further information + // Luckily for imported classes we can read this information from the bytecode + int lastIndexOfDot = name.lastIndexOf('.'); String partAfterDot = lastIndexOfDot >= 0 ? name.substring(lastIndexOfDot + 1) : name; @@ -151,4 +156,26 @@ public static String ensureCanonicalArrayTypeName(String typeName) { private static boolean isNoArrayClassName(String typeName) { return !typeName.startsWith("["); } + + /** + * @see #joinSingleQuoted(Iterable) + */ + @PublicAPI(usage = ACCESS) + public static String joinSingleQuoted(String... strings) { + return joinSingleQuoted(stream(strings)); + } + + /** + * @param strings Any number of strings + * @return The strings concatenated on ',' and each wrapped in single quotes. E.g. {@code ["a", "b", "c"] -> "'a', 'b', 'c'"} + */ + @PublicAPI(usage = ACCESS) + public static String joinSingleQuoted(Iterable strings) { + return joinSingleQuoted(StreamSupport.stream(strings.spliterator(), false)); + } + + private static String joinSingleQuoted(Stream strings) { + String joinedElements = strings.collect(joining("', '")); + return joinedElements.isEmpty() ? joinedElements : "'" + joinedElements + "'"; + } } diff --git a/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaClass.java b/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaClass.java index c20b174d4e..cf1dffc137 100644 --- a/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaClass.java +++ b/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaClass.java @@ -25,7 +25,6 @@ import java.util.function.Function; import java.util.function.Supplier; -import com.google.common.base.Joiner; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; import com.tngtech.archunit.PublicAPI; @@ -34,7 +33,6 @@ import com.tngtech.archunit.base.DescribedPredicate; import com.tngtech.archunit.base.MayResolveTypesViaReflection; import com.tngtech.archunit.base.Optionals; -import com.tngtech.archunit.base.PackageMatcher; import com.tngtech.archunit.base.ResolvesTypesViaReflection; import com.tngtech.archunit.base.Suppliers; import com.tngtech.archunit.core.domain.properties.CanBeAnnotated; @@ -53,6 +51,7 @@ import static com.tngtech.archunit.base.DescribedPredicate.equalTo; import static com.tngtech.archunit.base.DescribedPredicate.not; import static com.tngtech.archunit.core.domain.Formatters.formatNamesOf; +import static com.tngtech.archunit.core.domain.Formatters.joinSingleQuoted; import static com.tngtech.archunit.core.domain.JavaClass.Functions.GET_CODE_UNITS; import static com.tngtech.archunit.core.domain.JavaClass.Functions.GET_CONSTRUCTORS; import static com.tngtech.archunit.core.domain.JavaClass.Functions.GET_FIELDS; @@ -2326,7 +2325,7 @@ public static DescribedPredicate resideInAPackage(final String packag @PublicAPI(usage = ACCESS) public static DescribedPredicate resideInAnyPackage(final String... packageIdentifiers) { return resideInAnyPackage(packageIdentifiers, - String.format("reside in any package ['%s']", Joiner.on("', '").join(packageIdentifiers))); + String.format("reside in any package [%s]", joinSingleQuoted(packageIdentifiers))); } private static DescribedPredicate resideInAnyPackage(final String[] packageIdentifiers, final String description) { @@ -2343,7 +2342,7 @@ public static DescribedPredicate resideOutsideOfPackage(String packag @PublicAPI(usage = ACCESS) public static DescribedPredicate resideOutsideOfPackages(String... packageIdentifiers) { return not(JavaClass.Predicates.resideInAnyPackage(packageIdentifiers)) - .as("reside outside of packages ['%s']", Joiner.on("', '").join(packageIdentifiers)); + .as("reside outside of packages [%s]", joinSingleQuoted(packageIdentifiers)); } /** @@ -2356,15 +2355,74 @@ public static DescribedPredicate equivalentTo(final Class clazz) { /** * A predicate to determine if a {@link JavaClass} "belongs" to one of the passed {@link Class classes}, - * where we define "belong" as being equivalent to the class itself or any inner/anonymous class of this class. + * where "belong" means that this {@link JavaClass} is equivalent to + *
    + *
  • any of the passed {@link Class classes}
  • + *
  • any nested/inner/anonymous class of one of the passed {@link Class classes}
  • + *
* - * @param classes The {@link Class classes} to check the {@link JavaClass} against + * For example {@code belongToAnyOf(Outer.class)} would apply to the following cases + *

+         * class Outer {
+         *     // Inner would match belongToAnyOf(Outer.class) since it is an inner class of Outer
+         *     class Inner {}
+         *
+         *     void call() {
+         *         // this anonymous class would also match belongToAnyOf(Outer.class) since it is declared within Outer
+         *         new Serializable() {}
+         *     }
+         * }
+         *
+         * // this class would not match, since it is neither Outer itself nor nested within the class body of Outer
+         * class Other {}
+         * 
+ * + * @param classes The {@link Class classes} to check the {@link JavaClass} and its enclosing classes against * @return A {@link DescribedPredicate} returning true, if and only if the tested {@link JavaClass} is equivalent to - * one of the supplied {@link Class classes} or to one of its inner/anonymous classes. + * one of the supplied {@link Class classes} or is a nested/inner/anonymous class of one of those classes. + * + * @see #belongTo(DescribedPredicate) */ @PublicAPI(usage = ACCESS) public static DescribedPredicate belongToAnyOf(Class... classes) { - return new BelongToAnyOfPredicate(classes); + return belongTo(DescribedPredicate.describe( + "any of " + formatNamesOf(classes), + javaClass -> stream(classes).anyMatch(javaClass::isEquivalentTo) + )); + } + + /** + * A predicate to determine if a {@link JavaClass} "belongs" to a class matching the given predicate, + * where "belong" means that this {@link JavaClass} is + *
    + *
  • directly matching the given predicate
  • + *
  • a nested/inner/anonymous class of another {@link JavaClass} matching the predicate
  • + *
+ * + * For example {@code belongTo(annotatedWith(Something.class))} would apply to the following cases + *

+         *{@literal @}Something
+         * class Outer {
+         *     // Inner would match belongTo(annotatedWith(Something.class))
+         *     class Inner {}
+         *
+         *     void call() {
+         *         // this anonymous class would also match belongTo(annotatedWith(Something.class))
+         *         new Serializable() {}
+         *     }
+         * }
+         *
+         * // this class would not match, since it does not belong to a class annotated with @Something
+         * class Other {}
+         * 
+ * + * @param predicate The {@link DescribedPredicate predicate} to check the {@link JavaClass} and enclosing classes against + * @return A {@link DescribedPredicate} returning true, if and only if the tested {@link JavaClass} or one of + * its enclosing classes matches the given predicate. + */ + @PublicAPI(usage = ACCESS) + public static DescribedPredicate belongTo(DescribedPredicate predicate) { + return new BelongToPredicate(predicate); } /** @@ -2441,25 +2499,23 @@ public static DescribedPredicate containAnyStaticInitializersThat(Des private static final Function, Set> AS_SET = Optionals::asSet; - private static class BelongToAnyOfPredicate extends DescribedPredicate { - private final Class[] classes; + private static class BelongToPredicate extends DescribedPredicate { + private final DescribedPredicate predicate; - BelongToAnyOfPredicate(Class... classes) { - super("belong to any of " + formatNamesOf(classes)); - this.classes = classes; + BelongToPredicate(DescribedPredicate predicate) { + super("belong to " + predicate.getDescription()); + this.predicate = predicate; } @Override public boolean test(JavaClass input) { - return stream(classes).anyMatch(clazz -> belongsTo(input, clazz)); - } - - private boolean belongsTo(JavaClass input, Class clazz) { JavaClass toTest = input; - while (!toTest.isEquivalentTo(clazz) && toTest.getEnclosingClass().isPresent()) { + boolean matches = predicate.test(toTest); + while (!matches && toTest.getEnclosingClass().isPresent()) { toTest = toTest.getEnclosingClass().get(); + matches = predicate.test(toTest); } - return toTest.isEquivalentTo(clazz); + return matches; } } diff --git a/archunit/src/main/java/com/tngtech/archunit/base/PackageMatcher.java b/archunit/src/main/java/com/tngtech/archunit/core/domain/PackageMatcher.java similarity index 99% rename from archunit/src/main/java/com/tngtech/archunit/base/PackageMatcher.java rename to archunit/src/main/java/com/tngtech/archunit/core/domain/PackageMatcher.java index 7d539264c2..059e7b1d0c 100644 --- a/archunit/src/main/java/com/tngtech/archunit/base/PackageMatcher.java +++ b/archunit/src/main/java/com/tngtech/archunit/core/domain/PackageMatcher.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.tngtech.archunit.base; +package com.tngtech.archunit.core.domain; import java.util.List; import java.util.Optional; diff --git a/archunit/src/main/java/com/tngtech/archunit/base/PackageMatchers.java b/archunit/src/main/java/com/tngtech/archunit/core/domain/PackageMatchers.java similarity index 88% rename from archunit/src/main/java/com/tngtech/archunit/base/PackageMatchers.java rename to archunit/src/main/java/com/tngtech/archunit/core/domain/PackageMatchers.java index eccab551a3..d1a3d8c8c3 100644 --- a/archunit/src/main/java/com/tngtech/archunit/base/PackageMatchers.java +++ b/archunit/src/main/java/com/tngtech/archunit/core/domain/PackageMatchers.java @@ -13,23 +13,24 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.tngtech.archunit.base; +package com.tngtech.archunit.core.domain; import java.util.Collection; import java.util.Set; -import com.google.common.base.Joiner; import com.google.common.collect.ImmutableSet; import com.tngtech.archunit.PublicAPI; +import com.tngtech.archunit.base.DescribedPredicate; import static com.tngtech.archunit.PublicAPI.Usage.ACCESS; +import static com.tngtech.archunit.core.domain.Formatters.joinSingleQuoted; @PublicAPI(usage = ACCESS) public final class PackageMatchers extends DescribedPredicate { private final Set packageMatchers; private PackageMatchers(Set packageIdentifiers) { - super("matches any of ['%s']", Joiner.on("', '").join(packageIdentifiers)); + super("matches any of [%s]", joinSingleQuoted(packageIdentifiers)); ImmutableSet.Builder matchers = ImmutableSet.builder(); for (String identifier : packageIdentifiers) { matchers.add(PackageMatcher.of(identifier)); diff --git a/archunit/src/main/java/com/tngtech/archunit/lang/conditions/ArchConditions.java b/archunit/src/main/java/com/tngtech/archunit/lang/conditions/ArchConditions.java index 2ac14dd2e3..1e9233b8c8 100644 --- a/archunit/src/main/java/com/tngtech/archunit/lang/conditions/ArchConditions.java +++ b/archunit/src/main/java/com/tngtech/archunit/lang/conditions/ArchConditions.java @@ -28,8 +28,6 @@ import com.tngtech.archunit.base.ChainableFunction; import com.tngtech.archunit.base.DescribedPredicate; import com.tngtech.archunit.base.HasDescription; -import com.tngtech.archunit.base.PackageMatcher; -import com.tngtech.archunit.base.PackageMatchers; import com.tngtech.archunit.core.domain.AccessTarget; import com.tngtech.archunit.core.domain.AccessTarget.CodeUnitCallTarget; import com.tngtech.archunit.core.domain.AccessTarget.ConstructorCallTarget; @@ -50,6 +48,8 @@ import com.tngtech.archunit.core.domain.JavaMethod; import com.tngtech.archunit.core.domain.JavaMethodCall; import com.tngtech.archunit.core.domain.JavaModifier; +import com.tngtech.archunit.core.domain.PackageMatcher; +import com.tngtech.archunit.core.domain.PackageMatchers; import com.tngtech.archunit.core.domain.properties.CanBeAnnotated; import com.tngtech.archunit.core.domain.properties.HasAnnotations; import com.tngtech.archunit.core.domain.properties.HasModifiers; @@ -73,6 +73,7 @@ import static com.tngtech.archunit.core.domain.Dependency.Predicates.dependencyTarget; import static com.tngtech.archunit.core.domain.Formatters.ensureSimpleName; import static com.tngtech.archunit.core.domain.Formatters.formatNamesOf; +import static com.tngtech.archunit.core.domain.Formatters.joinSingleQuoted; import static com.tngtech.archunit.core.domain.JavaClass.Functions.GET_ACCESSES_FROM_SELF; import static com.tngtech.archunit.core.domain.JavaClass.Functions.GET_ACCESSES_TO_SELF; import static com.tngtech.archunit.core.domain.JavaClass.Functions.GET_CODE_UNIT_CALLS_FROM_SELF; @@ -362,8 +363,7 @@ public static ArchCondition onlyBeAccessedByAnyPackage(String... pack */ @PublicAPI(usage = ACCESS) public static ArchCondition onlyHaveDependentsInAnyPackage(String... packageIdentifiers) { - String description = String.format("only have dependents in any package ['%s']", - Joiner.on("', '").join(packageIdentifiers)); + String description = String.format("only have dependents in any package [%s]", joinSingleQuoted(packageIdentifiers)); return onlyHaveDependentsWhere(dependencyOrigin(GET_PACKAGE_NAME.is(PackageMatchers.of(packageIdentifiers)))) .as(description); } @@ -398,8 +398,7 @@ public static ArchCondition onlyHaveDependentsWhere(DescribedPredicat */ @PublicAPI(usage = ACCESS) public static AllDependenciesCondition onlyHaveDependenciesInAnyPackage(String... packageIdentifiers) { - String description = String.format("only have dependencies in any package ['%s']", - Joiner.on("', '").join(packageIdentifiers)); + String description = String.format("only have dependencies in any package [%s]", joinSingleQuoted(packageIdentifiers)); return onlyHaveDependenciesWhere(dependencyTarget(GET_PACKAGE_NAME.is(PackageMatchers.of(packageIdentifiers)))) .as(description); } diff --git a/archunit/src/main/java/com/tngtech/archunit/lang/conditions/JavaAccessPackagePredicate.java b/archunit/src/main/java/com/tngtech/archunit/lang/conditions/JavaAccessPackagePredicate.java index 33bf462d5a..bd239b2c46 100644 --- a/archunit/src/main/java/com/tngtech/archunit/lang/conditions/JavaAccessPackagePredicate.java +++ b/archunit/src/main/java/com/tngtech/archunit/lang/conditions/JavaAccessPackagePredicate.java @@ -17,17 +17,18 @@ import java.util.function.Function; -import com.google.common.base.Joiner; import com.tngtech.archunit.base.DescribedPredicate; -import com.tngtech.archunit.base.PackageMatchers; import com.tngtech.archunit.core.domain.JavaAccess; +import com.tngtech.archunit.core.domain.PackageMatchers; + +import static com.tngtech.archunit.core.domain.Formatters.joinSingleQuoted; class JavaAccessPackagePredicate extends DescribedPredicate> { private final Function, String> getPackageName; private final PackageMatchers packageMatchers; private JavaAccessPackagePredicate(String[] packageIdentifiers, Function, String> getPackageName) { - super(String.format("any package ['%s']", Joiner.on("', '").join(packageIdentifiers))); + super(String.format("any package [%s]", joinSingleQuoted(packageIdentifiers))); this.getPackageName = getPackageName; packageMatchers = PackageMatchers.of(packageIdentifiers); } diff --git a/archunit/src/main/java/com/tngtech/archunit/lang/syntax/elements/ClassesShould.java b/archunit/src/main/java/com/tngtech/archunit/lang/syntax/elements/ClassesShould.java index 853699cd6c..8c4cbc5171 100644 --- a/archunit/src/main/java/com/tngtech/archunit/lang/syntax/elements/ClassesShould.java +++ b/archunit/src/main/java/com/tngtech/archunit/lang/syntax/elements/ClassesShould.java @@ -19,7 +19,6 @@ import com.tngtech.archunit.PublicAPI; import com.tngtech.archunit.base.DescribedPredicate; -import com.tngtech.archunit.base.PackageMatcher; import com.tngtech.archunit.core.domain.AccessTarget; import com.tngtech.archunit.core.domain.JavaAccess; import com.tngtech.archunit.core.domain.JavaAnnotation; @@ -34,6 +33,7 @@ import com.tngtech.archunit.core.domain.JavaMethod; import com.tngtech.archunit.core.domain.JavaMethodCall; import com.tngtech.archunit.core.domain.JavaModifier; +import com.tngtech.archunit.core.domain.PackageMatcher; import com.tngtech.archunit.core.domain.properties.HasName.Predicates; import com.tngtech.archunit.lang.conditions.ArchConditions; import com.tngtech.archunit.lang.syntax.ArchRuleDefinition; diff --git a/archunit/src/main/java/com/tngtech/archunit/lang/syntax/elements/ClassesThat.java b/archunit/src/main/java/com/tngtech/archunit/lang/syntax/elements/ClassesThat.java index 0f5e005874..c705a9472c 100644 --- a/archunit/src/main/java/com/tngtech/archunit/lang/syntax/elements/ClassesThat.java +++ b/archunit/src/main/java/com/tngtech/archunit/lang/syntax/elements/ClassesThat.java @@ -19,7 +19,6 @@ import com.tngtech.archunit.PublicAPI; import com.tngtech.archunit.base.DescribedPredicate; -import com.tngtech.archunit.base.PackageMatcher; import com.tngtech.archunit.core.domain.JavaAnnotation; import com.tngtech.archunit.core.domain.JavaClass; import com.tngtech.archunit.core.domain.JavaCodeUnit; @@ -29,6 +28,7 @@ import com.tngtech.archunit.core.domain.JavaMethod; import com.tngtech.archunit.core.domain.JavaModifier; import com.tngtech.archunit.core.domain.JavaStaticInitializer; +import com.tngtech.archunit.core.domain.PackageMatcher; import com.tngtech.archunit.core.domain.properties.HasName.Predicates; import static com.tngtech.archunit.PublicAPI.Usage.ACCESS; diff --git a/archunit/src/main/java/com/tngtech/archunit/lang/syntax/elements/OnlyBeAccessedSpecification.java b/archunit/src/main/java/com/tngtech/archunit/lang/syntax/elements/OnlyBeAccessedSpecification.java index b14d4d7d99..ba7fa97bfd 100644 --- a/archunit/src/main/java/com/tngtech/archunit/lang/syntax/elements/OnlyBeAccessedSpecification.java +++ b/archunit/src/main/java/com/tngtech/archunit/lang/syntax/elements/OnlyBeAccessedSpecification.java @@ -17,8 +17,8 @@ import com.tngtech.archunit.PublicAPI; import com.tngtech.archunit.base.DescribedPredicate; -import com.tngtech.archunit.base.PackageMatcher; import com.tngtech.archunit.core.domain.JavaClass; +import com.tngtech.archunit.core.domain.PackageMatcher; import static com.tngtech.archunit.PublicAPI.Usage.ACCESS; diff --git a/archunit/src/main/java/com/tngtech/archunit/library/Architectures.java b/archunit/src/main/java/com/tngtech/archunit/library/Architectures.java index a582a13d3a..cd63627dd6 100644 --- a/archunit/src/main/java/com/tngtech/archunit/library/Architectures.java +++ b/archunit/src/main/java/com/tngtech/archunit/library/Architectures.java @@ -16,7 +16,6 @@ package com.tngtech.archunit.library; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collection; import java.util.Iterator; import java.util.LinkedHashMap; @@ -31,10 +30,10 @@ import com.google.common.base.Joiner; import com.tngtech.archunit.PublicAPI; import com.tngtech.archunit.base.DescribedPredicate; -import com.tngtech.archunit.base.PackageMatcher; import com.tngtech.archunit.core.domain.Dependency; import com.tngtech.archunit.core.domain.JavaClass; import com.tngtech.archunit.core.domain.JavaClasses; +import com.tngtech.archunit.core.domain.PackageMatcher; import com.tngtech.archunit.lang.ArchCondition; import com.tngtech.archunit.lang.ArchRule; import com.tngtech.archunit.lang.ConditionEvents; @@ -51,11 +50,13 @@ import static com.tngtech.archunit.PublicAPI.Usage.ACCESS; import static com.tngtech.archunit.base.DescribedPredicate.alwaysFalse; import static com.tngtech.archunit.base.DescribedPredicate.not; +import static com.tngtech.archunit.base.DescribedPredicate.or; import static com.tngtech.archunit.core.domain.Dependency.Functions.GET_ORIGIN_CLASS; import static com.tngtech.archunit.core.domain.Dependency.Functions.GET_TARGET_CLASS; import static com.tngtech.archunit.core.domain.Dependency.Predicates.dependency; import static com.tngtech.archunit.core.domain.Dependency.Predicates.dependencyOrigin; import static com.tngtech.archunit.core.domain.Dependency.Predicates.dependencyTarget; +import static com.tngtech.archunit.core.domain.Formatters.joinSingleQuoted; import static com.tngtech.archunit.core.domain.JavaClass.Predicates.equivalentTo; import static com.tngtech.archunit.core.domain.JavaClass.Predicates.resideInAnyPackage; import static com.tngtech.archunit.core.domain.JavaClass.Predicates.resideOutsideOfPackages; @@ -81,7 +82,7 @@ private Architectures() { } /** - * Can be used to assert a typical layered architecture, e.g. with an UI layer, a business logic layer and + * Can be used to assert a typical layered architecture, e.g. with a UI layer, a business logic layer and * a persistence layer, where specific access rules should be adhered to, like UI may not access persistence * and each layer may only access lower layers, i.e. UI → business logic → persistence. *

@@ -457,8 +458,7 @@ public LayeredArchitecture definedBy(DescribedPredicate predi */ @PublicAPI(usage = ACCESS) public LayeredArchitecture definedBy(String... packageIdentifiers) { - String description = String.format("'%s'", Joiner.on("', '").join(packageIdentifiers)); - return definedBy(resideInAnyPackage(packageIdentifiers).as(description)); + return definedBy(resideInAnyPackage(packageIdentifiers).as(joinSingleQuoted(packageIdentifiers))); } boolean isOptional() { @@ -506,7 +506,7 @@ public LayeredArchitecture mayNotBeAccessedByAnyLayer() { */ @PublicAPI(usage = ACCESS) public LayeredArchitecture mayOnlyBeAccessedByLayers(String... layerNames) { - return restrictLayers(LayerDependencyConstraint.ORIGIN, layerNames, "may only be accessed by layers ['%s']"); + return restrictLayers(LayerDependencyConstraint.ORIGIN, layerNames, "may only be accessed by layers [%s]"); } /** @@ -525,7 +525,7 @@ public LayeredArchitecture mayNotAccessAnyLayer() { */ @PublicAPI(usage = ACCESS) public LayeredArchitecture mayOnlyAccessLayers(String... layerNames) { - return restrictLayers(LayerDependencyConstraint.TARGET, layerNames, "may only access layers ['%s']"); + return restrictLayers(LayerDependencyConstraint.TARGET, layerNames, "may only access layers [%s]"); } private LayeredArchitecture denyLayerAccess(LayerDependencyConstraint constraint, String description) { @@ -540,7 +540,7 @@ private LayeredArchitecture restrictLayers(LayerDependencyConstraint constraint, checkLayerNamesExist(layerNames); allowedLayers.addAll(asList(layerNames)); this.constraint = constraint; - descriptionSuffix = String.format(descriptionTemplate, Joiner.on("', '").join(layerNames)); + descriptionSuffix = String.format(descriptionTemplate, joinSingleQuoted(layerNames)); return LayeredArchitecture.this.addDependencySpecification(this); } @@ -635,7 +635,7 @@ private DependencySettings setToConsideringAllDependencies() { private DependencySettings setToConsideringOnlyDependenciesInAnyPackage(String[] packageIdentifiers) { DescribedPredicate outsideOfRelevantPackage = resideOutsideOfPackages(packageIdentifiers); return new DependencySettings( - String.format("considering only dependencies in any package ['%s']", Joiner.on("', '").join(packageIdentifiers)), + String.format("considering only dependencies in any package [%s]", joinSingleQuoted(packageIdentifiers)), (__, predicate) -> predicate.or(originOrTargetIs(outsideOfRelevantPackage)) ); } @@ -668,10 +668,10 @@ public static final class OnionArchitecture implements ArchRule { private static final String ADAPTER_LAYER = "adapter"; private final Optional overriddenDescription; - private String[] domainModelPackageIdentifiers = new String[0]; - private String[] domainServicePackageIdentifiers = new String[0]; - private String[] applicationPackageIdentifiers = new String[0]; - private Map adapterPackageIdentifiers = new LinkedHashMap<>(); + private Optional> domainModelPredicate = Optional.empty(); + private Optional> domainServicePredicate = Optional.empty(); + private Optional> applicationPredicate = Optional.empty(); + private Map> adapterPredicates = new LinkedHashMap<>(); private boolean optionalLayers = false; private List ignoredDependencies = new ArrayList<>(); @@ -679,41 +679,62 @@ private OnionArchitecture() { overriddenDescription = Optional.empty(); } - private OnionArchitecture(String[] domainModelPackageIdentifiers, - String[] domainServicePackageIdentifiers, - String[] applicationPackageIdentifiers, - Map adapterPackageIdentifiers, + private OnionArchitecture( + Optional> domainModelPredicate, + Optional> domainServicePredicate, + Optional> applicationPredicate, + Map> adapterPredicates, List ignoredDependencies, Optional overriddenDescription) { - this.domainModelPackageIdentifiers = domainModelPackageIdentifiers; - this.domainServicePackageIdentifiers = domainServicePackageIdentifiers; - this.applicationPackageIdentifiers = applicationPackageIdentifiers; - this.adapterPackageIdentifiers = adapterPackageIdentifiers; + this.domainModelPredicate = domainModelPredicate; + this.domainServicePredicate = domainServicePredicate; + this.applicationPredicate = applicationPredicate; + this.adapterPredicates = adapterPredicates; this.ignoredDependencies = ignoredDependencies; this.overriddenDescription = overriddenDescription; } @PublicAPI(usage = ACCESS) public OnionArchitecture domainModels(String... packageIdentifiers) { - domainModelPackageIdentifiers = packageIdentifiers; + return domainModels(byPackagePredicate(packageIdentifiers)); + } + + @PublicAPI(usage = ACCESS) + public OnionArchitecture domainModels(DescribedPredicate predicate) { + domainModelPredicate = Optional.of(predicate); return this; } @PublicAPI(usage = ACCESS) public OnionArchitecture domainServices(String... packageIdentifiers) { - domainServicePackageIdentifiers = packageIdentifiers; + return domainServices(byPackagePredicate(packageIdentifiers)); + } + + @PublicAPI(usage = ACCESS) + public OnionArchitecture domainServices(DescribedPredicate predicate) { + domainServicePredicate = Optional.of(predicate); return this; } @PublicAPI(usage = ACCESS) public OnionArchitecture applicationServices(String... packageIdentifiers) { - applicationPackageIdentifiers = packageIdentifiers; + return applicationServices(byPackagePredicate(packageIdentifiers)); + } + + @PublicAPI(usage = ACCESS) + public OnionArchitecture applicationServices(DescribedPredicate predicate) { + applicationPredicate = Optional.of(predicate); return this; } @PublicAPI(usage = ACCESS) public OnionArchitecture adapter(String name, String... packageIdentifiers) { - adapterPackageIdentifiers.put(name, packageIdentifiers); + return adapter(name, byPackagePredicate(packageIdentifiers)); + } + + @PublicAPI(usage = ACCESS) + public OnionArchitecture adapter(String name, DescribedPredicate predicate) { + adapterPredicates.put(name, predicate); return this; } @@ -743,18 +764,22 @@ public OnionArchitecture ignoreDependency(DescribedPredicate return this; } + private DescribedPredicate byPackagePredicate(String[] packageIdentifiers) { + return resideInAnyPackage(packageIdentifiers).as(joinSingleQuoted(packageIdentifiers)); + } + private LayeredArchitecture layeredArchitectureDelegate() { LayeredArchitecture layeredArchitectureDelegate = layeredArchitecture().consideringAllDependencies() - .layer(DOMAIN_MODEL_LAYER).definedBy(domainModelPackageIdentifiers) - .layer(DOMAIN_SERVICE_LAYER).definedBy(domainServicePackageIdentifiers) - .layer(APPLICATION_SERVICE_LAYER).definedBy(applicationPackageIdentifiers) - .layer(ADAPTER_LAYER).definedBy(concatenateAll(adapterPackageIdentifiers.values())) + .layer(DOMAIN_MODEL_LAYER).definedBy(domainModelPredicate.orElse(alwaysFalse())) + .layer(DOMAIN_SERVICE_LAYER).definedBy(domainServicePredicate.orElse(alwaysFalse())) + .layer(APPLICATION_SERVICE_LAYER).definedBy(applicationPredicate.orElse(alwaysFalse())) + .layer(ADAPTER_LAYER).definedBy(or(adapterPredicates.values())) .whereLayer(DOMAIN_MODEL_LAYER).mayOnlyBeAccessedByLayers(DOMAIN_SERVICE_LAYER, APPLICATION_SERVICE_LAYER, ADAPTER_LAYER) .whereLayer(DOMAIN_SERVICE_LAYER).mayOnlyBeAccessedByLayers(APPLICATION_SERVICE_LAYER, ADAPTER_LAYER) .whereLayer(APPLICATION_SERVICE_LAYER).mayOnlyBeAccessedByLayers(ADAPTER_LAYER) .withOptionalLayers(optionalLayers); - for (Map.Entry adapter : adapterPackageIdentifiers.entrySet()) { + for (Map.Entry> adapter : adapterPredicates.entrySet()) { String adapterLayer = getAdapterLayer(adapter.getKey()); layeredArchitectureDelegate = layeredArchitectureDelegate .layer(adapterLayer).definedBy(adapter.getValue()) @@ -766,10 +791,6 @@ private LayeredArchitecture layeredArchitectureDelegate() { return layeredArchitectureDelegate.as(getDescription()); } - private String[] concatenateAll(Collection arrays) { - return arrays.stream().flatMap(Arrays::stream).toArray(String[]::new); - } - private String getAdapterLayer(String name) { return String.format("%s %s", name, ADAPTER_LAYER); } @@ -795,8 +816,8 @@ public ArchRule allowEmptyShould(boolean allowEmptyShould) { @Override public OnionArchitecture as(String newDescription) { - return new OnionArchitecture(domainModelPackageIdentifiers, domainServicePackageIdentifiers, - applicationPackageIdentifiers, adapterPackageIdentifiers, ignoredDependencies, + return new OnionArchitecture(domainModelPredicate, domainServicePredicate, + applicationPredicate, adapterPredicates, ignoredDependencies, Optional.of(newDescription)); } @@ -812,21 +833,23 @@ public String getDescription() { } List lines = newArrayList("Onion architecture consisting of" + (optionalLayers ? " (optional)" : "")); - if (domainModelPackageIdentifiers.length > 0) { - lines.add(String.format("domain models ('%s')", Joiner.on("', '").join(domainModelPackageIdentifiers))); - } - if (domainServicePackageIdentifiers.length > 0) { - lines.add(String.format("domain services ('%s')", Joiner.on("', '").join(domainServicePackageIdentifiers))); - } - if (applicationPackageIdentifiers.length > 0) { - lines.add(String.format("application services ('%s')", Joiner.on("', '").join(applicationPackageIdentifiers))); - } - for (Map.Entry adapter : adapterPackageIdentifiers.entrySet()) { - lines.add(String.format("adapter '%s' ('%s')", adapter.getKey(), Joiner.on("', '").join(adapter.getValue()))); + domainModelPredicate.ifPresent(describedPredicate -> + lines.add(String.format("domain models (%s)", describedPredicate.getDescription()))); + domainServicePredicate.ifPresent(describedPredicate -> + lines.add(String.format("domain services (%s)", describedPredicate.getDescription()))); + applicationPredicate.ifPresent(describedPredicate -> + lines.add(String.format("application services (%s)", describedPredicate.getDescription()))); + for (Map.Entry> adapter : adapterPredicates.entrySet()) { + lines.add(String.format("adapter '%s' (%s)", adapter.getKey(), adapter.getValue().getDescription())); } return Joiner.on(lineSeparator()).join(lines); } + @Override + public String toString() { + return getDescription(); + } + private static class IgnoredDependency { private final DescribedPredicate origin; private final DescribedPredicate target; diff --git a/archunit/src/main/java/com/tngtech/archunit/library/dependencies/Slices.java b/archunit/src/main/java/com/tngtech/archunit/library/dependencies/Slices.java index c0ffaba466..c191547999 100644 --- a/archunit/src/main/java/com/tngtech/archunit/library/dependencies/Slices.java +++ b/archunit/src/main/java/com/tngtech/archunit/library/dependencies/Slices.java @@ -30,10 +30,10 @@ import com.tngtech.archunit.PublicAPI; import com.tngtech.archunit.base.DescribedIterable; import com.tngtech.archunit.base.DescribedPredicate; -import com.tngtech.archunit.base.PackageMatcher; import com.tngtech.archunit.core.domain.Dependency; import com.tngtech.archunit.core.domain.JavaClass; import com.tngtech.archunit.core.domain.JavaClasses; +import com.tngtech.archunit.core.domain.PackageMatcher; import com.tngtech.archunit.core.domain.properties.CanOverrideDescription; import com.tngtech.archunit.lang.ArchRule; import com.tngtech.archunit.lang.ClassesTransformer; @@ -41,8 +41,8 @@ import static com.google.common.base.Preconditions.checkNotNull; import static com.tngtech.archunit.PublicAPI.Usage.ACCESS; -import static com.tngtech.archunit.base.PackageMatcher.TO_GROUPS; import static com.tngtech.archunit.core.domain.Dependency.toTargetClasses; +import static com.tngtech.archunit.core.domain.PackageMatcher.TO_GROUPS; import static java.util.stream.Collectors.toList; import static java.util.stream.Collectors.toSet; diff --git a/archunit/src/main/java/com/tngtech/archunit/library/plantuml/JavaClassDiagramAssociation.java b/archunit/src/main/java/com/tngtech/archunit/library/plantuml/JavaClassDiagramAssociation.java index 2bbc27bbe5..f69813878d 100644 --- a/archunit/src/main/java/com/tngtech/archunit/library/plantuml/JavaClassDiagramAssociation.java +++ b/archunit/src/main/java/com/tngtech/archunit/library/plantuml/JavaClassDiagramAssociation.java @@ -21,8 +21,8 @@ import com.google.common.base.Joiner; import com.google.common.collect.ImmutableSet; -import com.tngtech.archunit.base.PackageMatcher; import com.tngtech.archunit.core.domain.JavaClass; +import com.tngtech.archunit.core.domain.PackageMatcher; import static com.google.common.collect.Iterables.getOnlyElement; import static java.util.stream.Collectors.toCollection; diff --git a/archunit/src/main/java/com/tngtech/archunit/library/plantuml/PlantUmlArchCondition.java b/archunit/src/main/java/com/tngtech/archunit/library/plantuml/PlantUmlArchCondition.java index d145c59fc9..de83eeadd4 100644 --- a/archunit/src/main/java/com/tngtech/archunit/library/plantuml/PlantUmlArchCondition.java +++ b/archunit/src/main/java/com/tngtech/archunit/library/plantuml/PlantUmlArchCondition.java @@ -23,15 +23,14 @@ import java.util.List; import java.util.function.Function; -import com.google.common.base.Joiner; import com.google.common.collect.FluentIterable; import com.google.common.collect.Sets; import com.tngtech.archunit.PublicAPI; import com.tngtech.archunit.base.DescribedPredicate; -import com.tngtech.archunit.base.PackageMatcher; -import com.tngtech.archunit.base.PackageMatchers; import com.tngtech.archunit.core.domain.Dependency; import com.tngtech.archunit.core.domain.JavaClass; +import com.tngtech.archunit.core.domain.PackageMatcher; +import com.tngtech.archunit.core.domain.PackageMatchers; import com.tngtech.archunit.lang.ArchCondition; import com.tngtech.archunit.lang.ConditionEvents; @@ -39,6 +38,7 @@ import static com.tngtech.archunit.PublicAPI.Usage.ACCESS; import static com.tngtech.archunit.core.domain.Dependency.Functions.GET_ORIGIN_CLASS; import static com.tngtech.archunit.core.domain.Dependency.Functions.GET_TARGET_CLASS; +import static com.tngtech.archunit.core.domain.Formatters.joinSingleQuoted; import static com.tngtech.archunit.core.domain.properties.HasName.Predicates.name; import static com.tngtech.archunit.lang.conditions.ArchConditions.onlyHaveDependenciesInAnyPackage; import static java.util.Collections.singleton; @@ -281,7 +281,7 @@ private static class NotContainedInPackagesPredicate extends DescribedPredicate< private final List packageIdentifiers; NotContainedInPackagesPredicate(List packageIdentifiers) { - super(" while ignoring dependencies outside of packages ['%s']", Joiner.on("', '").join(packageIdentifiers)); + super(" while ignoring dependencies outside of packages [%s]", joinSingleQuoted(packageIdentifiers)); this.packageIdentifiers = packageIdentifiers; } diff --git a/archunit/src/test/java/com/tngtech/archunit/core/domain/FormattersTest.java b/archunit/src/test/java/com/tngtech/archunit/core/domain/FormattersTest.java index f83c8b512a..52d8ba5f4f 100644 --- a/archunit/src/test/java/com/tngtech/archunit/core/domain/FormattersTest.java +++ b/archunit/src/test/java/com/tngtech/archunit/core/domain/FormattersTest.java @@ -21,10 +21,13 @@ import static com.google.common.collect.ImmutableList.of; import static com.google.common.collect.Sets.union; import static com.google.common.primitives.Primitives.allPrimitiveTypes; +import static com.tngtech.archunit.core.domain.Formatters.joinSingleQuoted; import static com.tngtech.archunit.testutil.Assertions.assertThat; import static com.tngtech.java.junit.dataprovider.DataProviders.$; import static com.tngtech.java.junit.dataprovider.DataProviders.$$; +import static java.util.Arrays.stream; import static java.util.Collections.singleton; +import static java.util.stream.Collectors.toList; @RunWith(DataProviderRunner.class) public class FormattersTest { @@ -117,4 +120,22 @@ private static List> generateCanonicalNameTestCases(Iterable allFields = excludeJavaLangObject(javaClass.getAllFields()); + assertThat(allFields).hasSize(3); assertThat(javaClass.getMethods()).hasSize(2); + Set allMethods = excludeJavaLangObject(javaClass.getAllMethods()); + assertThat(allMethods).hasSize(4); for (JavaField field : javaClass.getFields()) { assertThatType(field.getOwner()).isSameAs(javaClass); } + assertThatTypes(allFields.stream().map(JavaMember::getOwner).collect(toSet())) + .matchInAnyOrder(ClassWithTwoFieldsAndTwoMethods.class, SuperclassWithFieldAndMethod.class); for (JavaCodeUnit method : javaClass.getCodeUnits()) { assertThatType(method.getOwner()).isSameAs(javaClass); } + assertThatTypes(allMethods.stream().map(JavaMember::getOwner).collect(toSet())) + .matchInAnyOrder(ClassWithTwoFieldsAndTwoMethods.class, SuperclassWithFieldAndMethod.class, InterfaceWithMethod.class); } @Test @@ -228,6 +238,11 @@ public void finds_constructors() { assertThat(javaClass.getConstructors()).is(containing(codeUnitWithSignature(CONSTRUCTOR_NAME))); assertThat(javaClass.getConstructors()).is(containing(codeUnitWithSignature(CONSTRUCTOR_NAME, String.class))); assertThat(javaClass.getConstructors()).is(containing(codeUnitWithSignature(CONSTRUCTOR_NAME, int.class, Object[].class))); + + Set allConstructors = excludeJavaLangObject(javaClass.getAllConstructors()); + assertThat(allConstructors).as("all constructors").hasSize(5); + assertThatTypes(allConstructors.stream().map(JavaConstructor::getOwner).collect(toSet())) + .matchInAnyOrder(ClassWithSeveralConstructorsFieldsAndMethods.class, ParentOfClassWithSeveralConstructorsFieldsAndMethods.class); } @Test @@ -1817,15 +1832,26 @@ public void predicate_equivalentTo() { .hasDescription("equivalent to " + Parent.class.getName()); } + @DataProvider + public static Object[][] data_predicate_belong_to() { + return testForEach( + belongToAnyOf(Object.class, ClassWithNamedAndAnonymousInnerClasses.class), + belongTo(describe( + String.format("any of [%s, %s]", Object.class.getName(), ClassWithNamedAndAnonymousInnerClasses.class.getName()), + javaClass -> javaClass.isEquivalentTo(Object.class) || javaClass.isEquivalentTo(ClassWithNamedAndAnonymousInnerClasses.class))) + ); + } + @Test - public void predicate_belong_to() { + @UseDataProvider + public void test_predicate_belong_to(DescribedPredicate belongToPredicate) { JavaClasses classes = new ClassFileImporter().importPackagesOf(getClass()); JavaClass outerAnonymous = getOnlyClassSettingField(classes, ClassWithNamedAndAnonymousInnerClasses.name_of_fieldIndicatingOuterAnonymousInnerClass); JavaClass nestedAnonymous = getOnlyClassSettingField(classes, ClassWithNamedAndAnonymousInnerClasses.name_of_fieldIndicatingNestedAnonymousInnerClass); - assertThat(belongToAnyOf(Object.class, ClassWithNamedAndAnonymousInnerClasses.class)) + assertThat(belongToPredicate) .hasDescription(String.format("belong to any of [%s, %s]", Object.class.getName(), ClassWithNamedAndAnonymousInnerClasses.class.getName())) .accepts(classes.get(ClassWithNamedAndAnonymousInnerClasses.class)) @@ -1955,6 +1981,10 @@ class Mismatch { .rejects(classes.get(Mismatch.class)); } + private Set excludeJavaLangObject(Set members) { + return members.stream().filter(it -> !it.getOwner().isEquivalentTo(Object.class)).collect(toSet()); + } + private JavaClass getOnlyClassSettingField(JavaClasses classes, final String fieldName) { return getOnlyElement(classes.that(new DescribedPredicate("") { @Override @@ -2259,7 +2289,16 @@ abstract static class Parent { } @SuppressWarnings("unused") - static class ClassWithSeveralConstructorsFieldsAndMethods { + static class ParentOfClassWithSeveralConstructorsFieldsAndMethods { + ParentOfClassWithSeveralConstructorsFieldsAndMethods() { + } + + ParentOfClassWithSeveralConstructorsFieldsAndMethods(Object anyParam) { + } + } + + @SuppressWarnings("unused") + static class ClassWithSeveralConstructorsFieldsAndMethods extends ParentOfClassWithSeveralConstructorsFieldsAndMethods { String stringField; private int intField; diff --git a/archunit/src/test/java/com/tngtech/archunit/base/PackageMatcherTest.java b/archunit/src/test/java/com/tngtech/archunit/core/domain/PackageMatcherTest.java similarity index 97% rename from archunit/src/test/java/com/tngtech/archunit/base/PackageMatcherTest.java rename to archunit/src/test/java/com/tngtech/archunit/core/domain/PackageMatcherTest.java index 76958715f1..d64d2cc86a 100644 --- a/archunit/src/test/java/com/tngtech/archunit/base/PackageMatcherTest.java +++ b/archunit/src/test/java/com/tngtech/archunit/core/domain/PackageMatcherTest.java @@ -1,8 +1,8 @@ -package com.tngtech.archunit.base; +package com.tngtech.archunit.core.domain; import java.util.Optional; -import com.tngtech.archunit.base.PackageMatcher.Result; +import com.tngtech.archunit.core.domain.PackageMatcher.Result; import com.tngtech.java.junit.dataprovider.DataProvider; import com.tngtech.java.junit.dataprovider.DataProviderRunner; import org.junit.Rule; @@ -10,7 +10,7 @@ import org.junit.rules.ExpectedException; import org.junit.runner.RunWith; -import static com.tngtech.archunit.base.PackageMatcher.TO_GROUPS; +import static com.tngtech.archunit.core.domain.PackageMatcher.TO_GROUPS; import static com.tngtech.archunit.testutil.Assertions.assertThat; @RunWith(DataProviderRunner.class) diff --git a/archunit/src/test/java/com/tngtech/archunit/base/PackageMatchersTest.java b/archunit/src/test/java/com/tngtech/archunit/core/domain/PackageMatchersTest.java similarity index 93% rename from archunit/src/test/java/com/tngtech/archunit/base/PackageMatchersTest.java rename to archunit/src/test/java/com/tngtech/archunit/core/domain/PackageMatchersTest.java index 1c0fce82f5..c6662e1874 100644 --- a/archunit/src/test/java/com/tngtech/archunit/base/PackageMatchersTest.java +++ b/archunit/src/test/java/com/tngtech/archunit/core/domain/PackageMatchersTest.java @@ -1,4 +1,4 @@ -package com.tngtech.archunit.base; +package com.tngtech.archunit.core.domain; import org.junit.Test; @@ -20,4 +20,4 @@ public void description() { assertThat(PackageMatchers.of("..foo..", "..bar..")) .hasDescription("matches any of ['..foo..', '..bar..']"); } -} \ No newline at end of file +} diff --git a/archunit/src/test/java/com/tngtech/archunit/lang/ArchRuleTest.java b/archunit/src/test/java/com/tngtech/archunit/lang/ArchRuleTest.java index 15eb59be03..f510d91526 100644 --- a/archunit/src/test/java/com/tngtech/archunit/lang/ArchRuleTest.java +++ b/archunit/src/test/java/com/tngtech/archunit/lang/ArchRuleTest.java @@ -26,6 +26,7 @@ import org.junit.rules.ExpectedException; import static com.google.common.collect.Lists.newArrayList; +import static com.tngtech.archunit.core.domain.Formatters.joinSingleQuoted; import static com.tngtech.archunit.core.domain.TestUtils.importClasses; import static com.tngtech.archunit.core.domain.TestUtils.importClassesWithContext; import static com.tngtech.archunit.lang.ArchRule.Assertions.ARCHUNIT_IGNORE_PATTERNS_FILE_NAME; @@ -238,7 +239,7 @@ private TypeSafeMatcher containingOnlyLinesWith(final String[] messages) return new TypeSafeMatcher() { @Override public void describeTo(Description description) { - description.appendText(String.format("Only the error messages '%s'", Joiner.on("', '").join(messages))); + description.appendText(String.format("Only the error messages %s", joinSingleQuoted(messages))); } @Override diff --git a/archunit/src/test/java/com/tngtech/archunit/lang/syntax/elements/ClassesShouldTest.java b/archunit/src/test/java/com/tngtech/archunit/lang/syntax/elements/ClassesShouldTest.java index 300bf24fc7..f775099a70 100644 --- a/archunit/src/test/java/com/tngtech/archunit/lang/syntax/elements/ClassesShouldTest.java +++ b/archunit/src/test/java/com/tngtech/archunit/lang/syntax/elements/ClassesShouldTest.java @@ -45,6 +45,7 @@ import static com.tngtech.archunit.base.DescribedPredicate.not; import static com.tngtech.archunit.core.domain.Formatters.formatNamesOf; +import static com.tngtech.archunit.core.domain.Formatters.joinSingleQuoted; import static com.tngtech.archunit.core.domain.JavaClass.Predicates.type; import static com.tngtech.archunit.core.domain.JavaClassTest.expectInvalidSyntaxUsageForClassInsteadOfInterface; import static com.tngtech.archunit.core.domain.JavaConstructor.CONSTRUCTOR_NAME; @@ -425,8 +426,7 @@ public void resideInAnyPackage(ArchRule rule, String... packageIdentifiers) { ArchRule.class, ArchConfiguration.class, GivenObjects.class)); assertThat(singleLineFailureReportOf(result)) - .contains(String.format("classes should reside in any package ['%s']", - Joiner.on("', '").join(packageIdentifiers))) + .contains(String.format("classes should reside in any package [%s]", joinSingleQuoted(packageIdentifiers))) .containsPattern(doesntResideInAnyPackagePatternFor(GivenObjects.class, packageIdentifiers)) .doesNotContain(String.format("%s", ArchRule.class.getSimpleName())) .doesNotContain(String.format("%s", ArchConfiguration.class.getSimpleName())); @@ -483,8 +483,7 @@ public void resideOutsideOfPackages(ArchRule rule, String... packageIdentifiers) ArchRule.class, ArchCondition.class, ArchConfiguration.class, GivenObjects.class)); assertThat(singleLineFailureReportOf(result)) - .contains(String.format("classes should reside outside of packages ['%s']", - Joiner.on("', '").join(packageIdentifiers))) + .contains(String.format("classes should reside outside of packages [%s]", joinSingleQuoted(packageIdentifiers))) .containsPattern(doesntResideOutsideOfPackagesPatternFor(ArchRule.class, packageIdentifiers)) .containsPattern(doesntResideOutsideOfPackagesPatternFor(ArchCondition.class, packageIdentifiers)) .doesNotContain(String.format("%s", GivenObjects.class.getSimpleName())); @@ -1835,13 +1834,13 @@ private String doesntResideOutsideOfPackagePatternFor(Class clazz, String pac @SuppressWarnings("SameParameterValue") private String doesntResideInAnyPackagePatternFor(Class clazz, String[] packageIdentifiers) { - return String.format("Class <%s> does not reside in any package \\['%s'\\] in %s", - quote(clazz.getName()), quote(Joiner.on("', '").join(packageIdentifiers)), locationPattern(clazz)); + return String.format("Class <%s> does not reside in any package \\[%s\\] in %s", + quote(clazz.getName()), quote(joinSingleQuoted(packageIdentifiers)), locationPattern(clazz)); } private String doesntResideOutsideOfPackagesPatternFor(Class clazz, String[] packageIdentifiers) { - return String.format("Class <%s> does not reside outside of packages \\['%s'\\] in %s", - quote(clazz.getName()), quote(Joiner.on("', '").join(packageIdentifiers)), locationPattern(clazz)); + return String.format("Class <%s> does not reside outside of packages \\[%s\\] in %s", + quote(clazz.getName()), quote(joinSingleQuoted(packageIdentifiers)), locationPattern(clazz)); } private static DescribedPredicate> callTargetIs(Class type) { diff --git a/archunit/src/test/java/com/tngtech/archunit/lang/syntax/elements/GivenClassShouldTest.java b/archunit/src/test/java/com/tngtech/archunit/lang/syntax/elements/GivenClassShouldTest.java index e87c630425..91fc2308c6 100644 --- a/archunit/src/test/java/com/tngtech/archunit/lang/syntax/elements/GivenClassShouldTest.java +++ b/archunit/src/test/java/com/tngtech/archunit/lang/syntax/elements/GivenClassShouldTest.java @@ -5,7 +5,6 @@ import java.util.List; import java.util.regex.Pattern; -import com.google.common.base.Joiner; import com.tngtech.archunit.core.domain.JavaClasses; import com.tngtech.archunit.core.domain.JavaModifier; import com.tngtech.archunit.core.domain.properties.CanBeAnnotatedTest.RuntimeRetentionAnnotation; @@ -22,6 +21,7 @@ import org.junit.rules.ExpectedException; import org.junit.runner.RunWith; +import static com.tngtech.archunit.core.domain.Formatters.joinSingleQuoted; import static com.tngtech.archunit.core.domain.JavaModifier.PUBLIC; import static com.tngtech.archunit.core.domain.TestUtils.importClasses; import static com.tngtech.archunit.lang.conditions.ArchConditions.haveOnlyPrivateConstructors; @@ -520,15 +520,15 @@ public void theClass_should_resideInAnyPackage(ArchRule satisfiedRule, ArchRule String[] packageIdentifiers = {firstPackage, secondPackage}; assertThatRules(satisfiedRule, unsatisfiedRule, SomeClass.class, Object.class) - .haveSuccessfulRuleText("the class %s should reside in any package ['%s']", + .haveSuccessfulRuleText("the class %s should reside in any package [%s]", SomeClass.class.getName(), - Joiner.on("', '").join(packageIdentifiers)) - .haveFailingRuleText("the class %s should reside outside of packages ['%s']", + joinSingleQuoted(packageIdentifiers)) + .haveFailingRuleText("the class %s should reside outside of packages [%s]", SomeClass.class.getName(), - Joiner.on("', '").join(packageIdentifiers)) - .containFailureDetail(String.format("Class <%s> does not reside outside of packages \\['%s'\\] in %s", + joinSingleQuoted(packageIdentifiers)) + .containFailureDetail(String.format("Class <%s> does not reside outside of packages \\[%s\\] in %s", quote(SomeClass.class.getName()), - quote(Joiner.on("', '").join(packageIdentifiers)), + quote(joinSingleQuoted(packageIdentifiers)), locationPattern(SomeClass.class))) .doNotContainFailureDetail(quote(Object.class.getName())); } @@ -558,15 +558,15 @@ public void noClass_should_resideInAnyPackage(ArchRule satisfiedRule, ArchRule u String[] packageIdentifiers = {firstPackage, secondPackage}; assertThatRules(satisfiedRule, unsatisfiedRule, SomeClass.class, Object.class) - .haveSuccessfulRuleText("no class %s should reside outside of packages ['%s']", + .haveSuccessfulRuleText("no class %s should reside outside of packages [%s]", SomeClass.class.getName(), - Joiner.on("', '").join(packageIdentifiers)) - .haveFailingRuleText("no class %s should reside in any package ['%s']", + joinSingleQuoted(packageIdentifiers)) + .haveFailingRuleText("no class %s should reside in any package [%s]", SomeClass.class.getName(), - Joiner.on("', '").join(packageIdentifiers)) - .containFailureDetail(String.format("Class <%s> does reside in any package \\['%s'\\] in %s", + joinSingleQuoted(packageIdentifiers)) + .containFailureDetail(String.format("Class <%s> does reside in any package \\[%s\\] in %s", quote(SomeClass.class.getName()), - quote(Joiner.on("', '").join(packageIdentifiers)), + quote(joinSingleQuoted(packageIdentifiers)), locationPattern(SomeClass.class))) .doNotContainFailureDetail(quote(Object.class.getName())); } diff --git a/archunit/src/test/java/com/tngtech/archunit/library/OnionArchitectureTest.java b/archunit/src/test/java/com/tngtech/archunit/library/OnionArchitectureTest.java index ef5bcea795..0b3b4ed93e 100644 --- a/archunit/src/test/java/com/tngtech/archunit/library/OnionArchitectureTest.java +++ b/archunit/src/test/java/com/tngtech/archunit/library/OnionArchitectureTest.java @@ -15,12 +15,15 @@ import com.tngtech.archunit.library.testclasses.onionarchitecture.application.ApplicationLayerClass; import com.tngtech.archunit.library.testclasses.onionarchitecture.domain.model.DomainModelLayerClass; import com.tngtech.archunit.library.testclasses.onionarchitecture.domain.service.DomainServiceLayerClass; +import com.tngtech.java.junit.dataprovider.DataProvider; import com.tngtech.java.junit.dataprovider.DataProviderRunner; +import com.tngtech.java.junit.dataprovider.UseDataProvider; import org.junit.Test; import org.junit.runner.RunWith; import static com.tngtech.archunit.base.DescribedPredicate.alwaysTrue; import static com.tngtech.archunit.core.domain.JavaClass.Predicates.equivalentTo; +import static com.tngtech.archunit.core.domain.JavaClass.Predicates.resideInAnyPackage; import static com.tngtech.archunit.core.domain.JavaClass.Predicates.simpleNameContaining; import static com.tngtech.archunit.core.domain.JavaClass.Predicates.simpleNameStartingWith; import static com.tngtech.archunit.library.Architectures.onionArchitecture; @@ -29,6 +32,7 @@ import static com.tngtech.archunit.library.LayeredArchitectureTest.expectedAccessViolationPattern; import static com.tngtech.archunit.library.LayeredArchitectureTest.expectedEmptyLayerPattern; import static com.tngtech.archunit.library.LayeredArchitectureTest.expectedFieldTypePattern; +import static com.tngtech.java.junit.dataprovider.DataProviders.testForEach; import static java.beans.Introspector.decapitalize; import static java.lang.System.lineSeparator; import static java.util.stream.Collectors.toSet; @@ -37,16 +41,29 @@ @RunWith(DataProviderRunner.class) public class OnionArchitectureTest { - @Test - public void onion_architecture_description() { - OnionArchitecture architecture = onionArchitecture() - .domainModels("onionarchitecture.domain.model..") - .domainServices("onionarchitecture.domain.service..") - .applicationServices("onionarchitecture.application..") - .adapter("cli", "onionarchitecture.adapter.cli..") - .adapter("persistence", "onionarchitecture.adapter.persistence..") - .adapter("rest", "onionarchitecture.adapter.rest.command..", "onionarchitecture.adapter.rest.query.."); + @DataProvider + public static Object[][] data_onion_architecture_description() { + return testForEach( + onionArchitecture() + .domainModels("onionarchitecture.domain.model..") + .domainServices("onionarchitecture.domain.service..") + .applicationServices("onionarchitecture.application..") + .adapter("cli", "onionarchitecture.adapter.cli..") + .adapter("persistence", "onionarchitecture.adapter.persistence..") + .adapter("rest", "onionarchitecture.adapter.rest.command..", "onionarchitecture.adapter.rest.query.."), + onionArchitecture() + .domainModels(alwaysTrue().as("'onionarchitecture.domain.model..'")) + .domainServices(alwaysTrue().as("'onionarchitecture.domain.service..'")) + .applicationServices(alwaysTrue().as("'onionarchitecture.application..'")) + .adapter("cli", alwaysTrue().as("'onionarchitecture.adapter.cli..'")) + .adapter("persistence", alwaysTrue().as("'onionarchitecture.adapter.persistence..'")) + .adapter("rest", alwaysTrue().as("'onionarchitecture.adapter.rest.command..', 'onionarchitecture.adapter.rest.query..'")) + ); + } + @Test + @UseDataProvider + public void test_onion_architecture_description(OnionArchitecture architecture) { assertThat(architecture.getDescription()).isEqualTo( "Onion architecture consisting of" + lineSeparator() + "domain models ('onionarchitecture.domain.model..')" + lineSeparator() + @@ -94,9 +111,23 @@ public void onion_architecture_because_clause() { assertThat(architecture.getDescription()).isEqualTo("overridden, because some reason"); } + @DataProvider + public static Object[][] data_onion_architecture_gathers_all_violations() { + return testForEach( + getTestOnionArchitecture(), + onionArchitecture() + .domainModels(resideInAnyPackage(absolute("onionarchitecture.domain.model"))) + .domainServices(resideInAnyPackage(absolute("onionarchitecture.domain.service"))) + .applicationServices(resideInAnyPackage(absolute("onionarchitecture.application"))) + .adapter("cli", resideInAnyPackage(absolute("onionarchitecture.adapter.cli"))) + .adapter("persistence", resideInAnyPackage(absolute("onionarchitecture.adapter.persistence"))) + .adapter("rest", resideInAnyPackage(absolute("onionarchitecture.adapter.rest"))) + ); + } + @Test - public void onion_architecture_gathers_all_violations() { - OnionArchitecture architecture = getTestOnionArchitecture(); + @UseDataProvider + public void test_onion_architecture_gathers_all_violations(OnionArchitecture architecture) { JavaClasses classes = new ClassFileImporter().importPackages(absolute("onionarchitecture")); EvaluationResult result = architecture.evaluate(classes); @@ -164,7 +195,7 @@ public void onion_architecture_rejects_empty_layers_if_layers_are_explicitly_not assertFailureOnionArchitectureWithEmptyLayers(result); } - private OnionArchitecture getTestOnionArchitecture() { + private static OnionArchitecture getTestOnionArchitecture() { return onionArchitecture() .domainModels(absolute("onionarchitecture.domain.model")) .domainServices(absolute("onionarchitecture.domain.service"))