From d96263fe3095f14163f2bd15c713d64b0d0a6b99 Mon Sep 17 00:00:00 2001 From: Oliver Drotbohm Date: Fri, 11 Nov 2022 13:52:42 +0100 Subject: [PATCH] GH-11, GH-66, GH-67, GH-68 - Improve module canvas. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Overhauled the module canvas in various ways. By default, we now skip "empty" rows. In other words, if we do not find any published events for example, we skip the entire row. CanvasOptions.revealEmptyRows() can be used to keep those empty rows around. We now also expose all value types and Spring bean references. Refactored the dependency lookup APIs on ApplicationModule. Both DependencyType and DependencyDepth are now top-level types. Also, ApplicationModuleDependency and ApplicationModuleDependencies were introduced as explicit types. In the course of that, ApplicationModule.getDependencies(…) has got its return type changed from List to ApplicationModuleDependencies. The internal ModuleDependency type has been renamed to QualifiedDependency. --- .../modulith/model/ApplicationModule.java | 562 +++++++++--------- .../model/ApplicationModuleDependencies.java | 93 +++ .../model/ApplicationModuleDependency.java | 54 ++ .../model/ArchitecturallyEvidentType.java | 90 ++- .../modulith/model/DependencyDepth.java | 25 + .../modulith/model/DependencyType.java | 172 ++++++ .../ArchitecturallyEvidentTypeUnitTest.java | 4 +- .../model/ModuleDependencyUnitTest.java | 8 +- .../modulith/docs/Asciidoctor.java | 17 +- .../modulith/docs/Documenter.java | 110 ++-- .../java/com/acme/myproject/ModulithTest.java | 14 +- .../modulith/docs/DocumenterTest.java | 2 +- .../model/ModulesIntegrationTest.java | 4 +- .../modulith/test/ApplicationModuleTest.java | 2 +- 14 files changed, 784 insertions(+), 373 deletions(-) create mode 100644 spring-modulith-core/src/main/java/org/springframework/modulith/model/ApplicationModuleDependencies.java create mode 100644 spring-modulith-core/src/main/java/org/springframework/modulith/model/ApplicationModuleDependency.java create mode 100644 spring-modulith-core/src/main/java/org/springframework/modulith/model/DependencyDepth.java create mode 100644 spring-modulith-core/src/main/java/org/springframework/modulith/model/DependencyType.java diff --git a/spring-modulith-core/src/main/java/org/springframework/modulith/model/ApplicationModule.java b/spring-modulith-core/src/main/java/org/springframework/modulith/model/ApplicationModule.java index 776c94e0..0310c51c 100644 --- a/spring-modulith-core/src/main/java/org/springframework/modulith/model/ApplicationModule.java +++ b/spring-modulith-core/src/main/java/org/springframework/modulith/model/ApplicationModule.java @@ -28,15 +28,14 @@ import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.NonNull; import lombok.RequiredArgsConstructor; +import lombok.ToString; import lombok.Value; import java.util.Arrays; -import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Optional; -import java.util.Set; import java.util.function.Predicate; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -67,15 +66,30 @@ import com.tngtech.archunit.thirdparty.com.google.common.base.Suppliers; @EqualsAndHashCode(doNotUseGetters = true) public class ApplicationModule { + /** + * The base package of the {@link ApplicationModule}. + */ private final @Getter JavaPackage basePackage; private final ApplicationModuleInformation information; + + /** + * All {@link NamedInterfaces} of the {@link ApplicationModule} either declared explicitly via {@link NamedInterface} + * or implicitly. + */ private final @Getter NamedInterfaces namedInterfaces; private final boolean useFullyQualifiedModuleNames; private final Supplier springBeans; private final Supplier entities; + private final Supplier> valueTypes; private final Supplier> publishedEvents; + /** + * Creates a new {@link ApplicationModule} for the given base package and whether to use fully-qualified module names. + * + * @param basePackage must not be {@literal null}. + * @param useFullyQualifiedModuleNames + */ ApplicationModule(JavaPackage basePackage, boolean useFullyQualifiedModuleNames) { this.basePackage = basePackage; @@ -85,6 +99,8 @@ public class ApplicationModule { this.springBeans = Suppliers.memoize(() -> filterSpringBeans(basePackage)); this.entities = Suppliers.memoize(() -> findEntities(basePackage)); + this.valueTypes = Suppliers + .memoize(() -> findArchitecturallyEvidentType(ArchitecturallyEvidentType::isValueObject)); this.publishedEvents = Suppliers.memoize(() -> findPublishedEvents()); } @@ -108,14 +124,25 @@ public class ApplicationModule { .orElseGet(() -> getName()); } - public List getDependencies(ApplicationModules modules, DependencyType... type) { + /** + * Returns {@link DeclaredDependencies} of the current {@link ApplicationModule}. + * + * @param modules must not be {@literal null}. + * @param type must not be {@literal null}. + * @return will never be {@literal null}. + */ + public ApplicationModuleDependencies getDependencies(ApplicationModules modules, DependencyType... type) { - return getAllModuleDependencies(modules) // + Assert.notNull(modules, "ApplicationModules must not be null!"); + Assert.notNull(type, "DependencyTypes must not be null!"); + + var foo = getAllModuleDependencies(modules) // .filter(it -> type.length == 0 ? true : Arrays.stream(type).anyMatch(it::hasType)) // - .map(it -> modules.getModuleByType(it.target)) // .distinct() // - .flatMap(it -> it.map(Stream::of).orElseGet(Stream::empty)) // - .collect(Collectors.toList()); + . flatMap(it -> DefaultApplicationModuleDependency.of(it, modules)) // + .toList(); + + return ApplicationModuleDependencies.of(foo, modules); } /** @@ -130,8 +157,8 @@ public class ApplicationModule { return getAllModuleDependencies(modules) // .filter(it -> it.type == DependencyType.EVENT_LISTENER) // - .map(ModuleDependency::getTarget) // - .collect(Collectors.toList()); + .map(QualifiedDependency::getTarget) // + .toList(); } /** @@ -143,6 +170,15 @@ public class ApplicationModule { return publishedEvents.get(); } + /** + * Returns all value types contained in the module. + * + * @return will never be {@literal null}. + */ + public List getValueTypes() { + return valueTypes.get(); + } + /** * Returns all types that are considered aggregate roots. * @@ -156,7 +192,7 @@ public class ApplicationModule { .map(ArchitecturallyEvidentType::getType) // .flatMap(this::resolveModuleSuperTypes) // .distinct() // - .collect(Collectors.toList()); + .toList(); } /** @@ -192,20 +228,22 @@ public class ApplicationModule { Assert.notNull(modules, "Modules must not be null!"); Assert.notNull(depth, "Dependency depth must not be null!"); - Stream dependencies = streamBootstrapDependencies(modules, depth); + var dependencies = streamBootstrapDependencies(modules, depth); return Stream.concat(Stream.of(this), dependencies) // .map(ApplicationModule::getBasePackage); } + /** + * Returns all {@link SpringBean}s contained in the module. + * + * @return will never be {@literal null}. + */ public List getSpringBeans() { + return getSpringBeansInternal().stream() // .map(it -> SpringBean.of(it, this)) // - .collect(Collectors.toList()); - } - - Classes getSpringBeansInternal() { - return springBeans.get(); + .toList(); } public boolean contains(JavaClass type) { @@ -267,7 +305,8 @@ public class ApplicationModule { public String toString(@Nullable ApplicationModules modules) { - StringBuilder builder = new StringBuilder("## ").append(getDisplayName()).append(" ##\n"); + var builder = new StringBuilder("## ").append(getDisplayName()).append(" ##\n"); + builder.append("> Logical name: ").append(getName()).append('\n'); builder.append("> Base package: ").append(basePackage.getName()).append('\n'); @@ -282,7 +321,7 @@ public class ApplicationModule { if (modules != null) { - List dependencies = getBootstrapDependencies(modules).collect(Collectors.toList()); + List dependencies = getBootstrapDependencies(modules).toList(); builder.append("> Direct module dependencies: "); builder.append(dependencies.isEmpty() ? "none" @@ -307,6 +346,10 @@ public class ApplicationModule { return builder.toString(); } + Classes getSpringBeansInternal() { + return springBeans.get(); + } + /** * Returns all allowed module dependencies, either explicitly declared or defined as shared on the given * {@link ApplicationModules} instance. @@ -314,25 +357,25 @@ public class ApplicationModule { * @param modules must not be {@literal null}. * @return */ - ApplicationModuleDependencies getAllowedDependencies(ApplicationModules modules) { + DeclaredDependencies getAllowedDependencies(ApplicationModules modules) { Assert.notNull(modules, "Modules must not be null!"); var allowedDependencyNames = information.getAllowedDependencies(); if (allowedDependencyNames.isEmpty()) { - return new ApplicationModuleDependencies(Collections.emptyList()); + return new DeclaredDependencies(Collections.emptyList()); } var explicitlyDeclaredModules = allowedDependencyNames.stream() // - .map(it -> ApplicationModuleDependency.of(it, this, modules)); + .map(it -> DeclaredDependency.of(it, this, modules)); var sharedDependencies = modules.getSharedModules().stream() - .map(ApplicationModuleDependency::to); + .map(DeclaredDependency::to); return Stream.concat(explicitlyDeclaredModules, sharedDependencies) // .distinct() // - .collect(Collectors.collectingAndThen(Collectors.toList(), ApplicationModuleDependencies::new)); + .collect(Collectors.collectingAndThen(Collectors.toList(), DeclaredDependencies::new)); } /** @@ -354,8 +397,7 @@ public class ApplicationModule { .or(isAnnotatedWith(JMoleculesTypes.AT_DOMAIN_EVENT)); return basePackage.that(isEvent).stream() // - .map(EventType::new) - .collect(Collectors.toList()); + .map(EventType::new).toList(); } /** @@ -374,7 +416,7 @@ public class ApplicationModule { Stream.of(type)); } - private Stream getAllModuleDependencies(ApplicationModules modules) { + private Stream getAllModuleDependencies(ApplicationModules modules) { return basePackage.stream() // .flatMap(it -> getModuleDependenciesOf(it, modules)); @@ -382,18 +424,19 @@ public class ApplicationModule { private Stream streamBootstrapDependencies(ApplicationModules modules, DependencyDepth depth) { - switch (depth) { + return switch (depth) { + case NONE -> Stream.empty(); + case IMMEDIATE -> getDirectModuleBootstrapDependencies(modules); + case ALL -> getAllBootstrapDependencies(modules); + default -> getAllBootstrapDependencies(modules); + }; + } - case NONE: - return Stream.empty(); - case IMMEDIATE: - return getDirectModuleBootstrapDependencies(modules); - case ALL: - default: - return getDirectModuleBootstrapDependencies(modules) // - .flatMap(it -> Stream.concat(Stream.of(it), it.streamBootstrapDependencies(modules, DependencyDepth.ALL))) // - .distinct(); - } + private Stream getAllBootstrapDependencies(ApplicationModules modules) { + + return getDirectModuleBootstrapDependencies(modules) // + .flatMap(it -> Stream.concat(Stream.of(it), it.streamBootstrapDependencies(modules, DependencyDepth.ALL))) // + .distinct(); } private Stream getDirectModuleBootstrapDependencies(ApplicationModules modules) { @@ -402,7 +445,7 @@ public class ApplicationModule { return beans.stream() // .map(it -> ArchitecturallyEvidentType.of(it, beans)) // - .flatMap(it -> ModuleDependency.fromType(it)) // + .flatMap(it -> QualifiedDependency.fromType(it)) // .filter(it -> isDependencyToOtherModule(it.target, modules)) // .filter(it -> it.hasType(DependencyType.USES_COMPONENT)) // .map(it -> modules.getModuleByType(it.target)) // @@ -410,16 +453,16 @@ public class ApplicationModule { .flatMap(it -> it.map(Stream::of).orElseGet(Stream::empty)); } - private Stream getModuleDependenciesOf(JavaClass type, ApplicationModules modules) { + private Stream getModuleDependenciesOf(JavaClass type, ApplicationModules modules) { var evidentType = ArchitecturallyEvidentType.of(type, getSpringBeansInternal()); - var injections = ModuleDependency.fromType(evidentType) // + var injections = QualifiedDependency.fromType(evidentType) // .filter(it -> isDependencyToOtherModule(it.getTarget(), modules)); // var directDependencies = type.getDirectDependenciesFromSelf().stream() // .filter(it -> isDependencyToOtherModule(it.getTargetClass(), modules)) // - .map(ModuleDependency::new); + .map(QualifiedDependency::new); return Stream.concat(injections, directDependencies).distinct(); } @@ -459,18 +502,20 @@ public class ApplicationModule { return it -> it.getSimpleName().equals(candidate) || it.getFullName().equals(candidate); } - public enum DependencyDepth { + private List findArchitecturallyEvidentType(Predicate selector) { - NONE, + var springBeansInternal = getSpringBeansInternal(); - IMMEDIATE, - - ALL; + return basePackage.stream() + .map(it -> ArchitecturallyEvidentType.of(it, springBeansInternal)) + .filter(selector) + .map(ArchitecturallyEvidentType::getType) + .toList(); } @Value @RequiredArgsConstructor(access = AccessLevel.PRIVATE) - static class ApplicationModuleDependency { + static class DeclaredDependency { private static final String INVALID_EXPLICIT_MODULE_DEPENDENCY = "Invalid explicit module dependency in %s! No module found with name '%s'."; private static final String INVALID_NAMED_INTERFACE_DECLARATION = "No named interface named '%s' found! Original dependency declaration: %s -> %s."; @@ -479,7 +524,7 @@ public class ApplicationModule { @NonNull NamedInterface namedInterface; /** - * Creates an {@link ApplicationModuleDependency} to the module and optionally named interface defined by the given + * Creates an {@link DeclaredDependency} to the module and optionally named interface defined by the given * identifier. * * @param identifier must not be {@literal null} or empty. Follows the @@ -490,7 +535,7 @@ public class ApplicationModule { * @throws IllegalArgumentException in case the given identifier is invalid, i.e. does not refer to an existing * module or named interface. */ - public static ApplicationModuleDependency of(String identifier, ApplicationModule source, + public static DeclaredDependency of(String identifier, ApplicationModule source, ApplicationModules modules) { Assert.hasText(identifier, "Module dependency identifier must not be null or empty!"); @@ -509,42 +554,42 @@ public class ApplicationModule { .orElseThrow(() -> new IllegalArgumentException( INVALID_NAMED_INTERFACE_DECLARATION.formatted(namedInterfacename, source.getName(), identifier))); - return new ApplicationModuleDependency(target, namedInterface); + return new DeclaredDependency(target, namedInterface); } /** - * Creates a new {@link ApplicationModuleDependency} to the unnamed interface of the given - * {@link ApplicationModule}. + * Creates a new {@link DeclaredDependency} to the unnamed interface of the given {@link ApplicationModule}. * * @param module must not be {@literal null}. * @return */ - public static ApplicationModuleDependency to(ApplicationModule module) { - return new ApplicationModuleDependency(module, module.getNamedInterfaces().getUnnamedInterface()); + public static DeclaredDependency to(ApplicationModule module) { + return new DeclaredDependency(module, module.getNamedInterfaces().getUnnamedInterface()); } public boolean contains(JavaClass type) { return namedInterface.contains(type); } + /* + * (non-Javadoc) + * @see java.lang.Object#toString() + */ @Override public String toString() { - - return namedInterface.isUnnamed() // - ? target.getName() // - : target.getName() + "::" + namedInterface.getName(); + return namedInterface.isUnnamed() ? target.getName() : target.getName() + "::" + namedInterface.getName(); } } /** - * A collection wrapper for {@link ApplicationModuleDependency} instances. + * A collection wrapper for {@link DeclaredDependency} instances. * * @author Oliver Drotbohm */ @Value - static class ApplicationModuleDependencies { + static class DeclaredDependencies { - List dependencies; + List dependencies; /** * Returns whether any of the dependencies contains the given {@link JavaClass}. @@ -568,23 +613,23 @@ public class ApplicationModule { public String toString() { return dependencies.stream() // - .map(ApplicationModuleDependency::toString) + .map(DeclaredDependency::toString) .collect(Collectors.joining(", ")); } } @EqualsAndHashCode @RequiredArgsConstructor - static class ModuleDependency { + static class QualifiedDependency { private static final List INJECTION_TYPES = Arrays.asList(// AT_AUTOWIRED, AT_RESOURCE, AT_INJECT); - private final @NonNull @Getter JavaClass origin, target; + private final @NonNull @Getter JavaClass source, target; private final @NonNull String description; private final @NonNull DependencyType type; - ModuleDependency(Dependency dependency) { + QualifiedDependency(Dependency dependency) { this(dependency.getOriginClass(), // dependency.getTargetClass(), // dependency.getDescription(), // @@ -597,10 +642,10 @@ public class ApplicationModule { Violations isValidDependencyWithin(ApplicationModules modules) { - var originModule = getExistingModuleOf(origin, modules); + var originModule = getExistingModuleOf(source, modules); var targetModule = getExistingModuleOf(target, modules); - ApplicationModuleDependencies allowedTargets = originModule.getAllowedDependencies(modules); + DeclaredDependencies allowedTargets = originModule.getAllowedDependencies(modules); Violations violations = Violations.NONE; // Check explicitly defined allowed targets @@ -608,7 +653,7 @@ public class ApplicationModule { if (!allowedTargets.isEmpty() && !allowedTargets.contains(target)) { var message = "Module '%s' depends on module '%s' via %s -> %s. Allowed targets: %s." // - .formatted(originModule.getName(), targetModule.getName(), origin.getName(), target.getName(), + .formatted(originModule.getName(), targetModule.getName(), source.getName(), target.getName(), allowedTargets.toString()); return violations.and(new IllegalStateException(message)); @@ -629,120 +674,115 @@ public class ApplicationModule { ApplicationModule getExistingModuleOf(JavaClass javaClass, ApplicationModules modules) { - Optional module = modules.getModuleByType(javaClass); - - return module.orElseThrow(() -> new IllegalStateException( - String.format("Origin/Target of a %s should always be within a module, but %s is not", + return modules.getModuleByType(javaClass).orElseThrow(() -> new IllegalStateException( + String.format("Source/Target of a %s should always be within a module, but %s is not", getClass().getSimpleName(), javaClass.getName()))); } - static ModuleDependency fromCodeUnitParameter(JavaCodeUnit codeUnit, JavaClass parameter) { - - String description = createDescription(codeUnit, parameter, "parameter"); - - DependencyType type = DependencyType.forCodeUnit(codeUnit) // - .or(() -> DependencyType.forParameter(parameter)); - - return new ModuleDependency(codeUnit.getOwner(), parameter, description, type); - } - - static ModuleDependency fromCodeUnitReturnType(JavaCodeUnit codeUnit) { - - String description = createDescription(codeUnit, codeUnit.getRawReturnType(), "return type"); - - return new ModuleDependency(codeUnit.getOwner(), codeUnit.getRawReturnType(), description, - DependencyType.DEFAULT); - } - - static Stream fromType(ArchitecturallyEvidentType type) { - - JavaClass source = type.getType(); - - return Stream.concat(Stream.concat(fromConstructorOf(type), fromMethodsOf(source)), fromFieldsOf(source)); - } - - private static Stream fromConstructorOf(ArchitecturallyEvidentType source) { - - JavaClass type = source.getType(); - Set constructors = type.getConstructors(); - - return constructors.stream() // - .filter(it -> constructors.size() == 1 || isInjectionPoint(it)) // - .flatMap(it -> it.getRawParameterTypes().stream() // - .map(parameter -> { - return source.isConfigurationProperties() - ? new ModuleDependency(type, parameter, createDescription(it, parameter, "parameter"), - DependencyType.DEFAULT) - : new InjectionModuleDependency(type, parameter, it); - })); - } - - private static Stream fromFieldsOf(JavaClass source) { - - Stream fieldInjections = source.getAllFields().stream() // - .filter(ModuleDependency::isInjectionPoint) // - .map(field -> new InjectionModuleDependency(source, field.getRawType(), field)); - - return fieldInjections; - } - - private static Stream fromMethodsOf(JavaClass source) { - - Set methods = source.getAllMethods().stream() // - .filter(it -> !it.getOwner().isEquivalentTo(Object.class)) // - .collect(Collectors.toSet()); - - if (methods.isEmpty()) { - return Stream.empty(); - } - - Stream returnTypes = methods.stream() // - .filter(it -> !it.getRawReturnType().isPrimitive()) // - .filter(it -> !it.getRawReturnType().getPackageName().startsWith("java")) // - .map(it -> fromCodeUnitReturnType(it)); - - Set injectionMethods = methods.stream() // - .filter(ModuleDependency::isInjectionPoint) // - .collect(Collectors.toSet()); - - Stream methodInjections = injectionMethods.stream() // - .flatMap(it -> it.getRawParameterTypes().stream() // - .map(parameter -> new InjectionModuleDependency(source, parameter, it))); - - Stream otherMethods = methods.stream() // - .filter(it -> !injectionMethods.contains(it)) // - .flatMap(it -> it.getRawParameterTypes().stream() // - .map(parameter -> fromCodeUnitParameter(it, parameter))); - - return Stream.concat(Stream.concat(methodInjections, otherMethods), returnTypes); - } - - static Stream allFrom(JavaCodeUnit codeUnit) { - - Stream parameterDependencies = codeUnit.getRawParameterTypes()// - .stream() // - .map(it -> fromCodeUnitParameter(codeUnit, it)); - - Stream returnType = Stream.of(fromCodeUnitReturnType(codeUnit)); - - return Stream.concat(parameterDependencies, returnType); - } - /* * (non-Javadoc) * @see java.lang.Object#toString() */ @Override public String toString() { - return type.format(FormatableJavaClass.of(origin), FormatableJavaClass.of(target)); + return type.format(FormatableJavaClass.of(source), FormatableJavaClass.of(target)); + } + + static QualifiedDependency fromCodeUnitParameter(JavaCodeUnit codeUnit, JavaClass parameter) { + + var description = createDescription(codeUnit, parameter, "parameter"); + var type = DependencyType.forCodeUnit(codeUnit) // + .defaultOr(() -> DependencyType.forParameter(parameter)); + + return new QualifiedDependency(codeUnit.getOwner(), parameter, description, type); + } + + static QualifiedDependency fromCodeUnitReturnType(JavaCodeUnit codeUnit) { + + var description = createDescription(codeUnit, codeUnit.getRawReturnType(), "return type"); + + return new QualifiedDependency(codeUnit.getOwner(), codeUnit.getRawReturnType(), description, + DependencyType.DEFAULT); + } + + static Stream fromType(ArchitecturallyEvidentType type) { + + var source = type.getType(); + + return Stream.concat(Stream.concat(fromConstructorOf(type), fromMethodsOf(source)), fromFieldsOf(source)); + } + + static Stream allFrom(JavaCodeUnit codeUnit) { + + var parameterDependencies = codeUnit.getRawParameterTypes()// + .stream() // + .map(it -> fromCodeUnitParameter(codeUnit, it)); + + var returnType = Stream.of(fromCodeUnitReturnType(codeUnit)); + + return Stream.concat(parameterDependencies, returnType); + } + + private static Stream fromConstructorOf(ArchitecturallyEvidentType source) { + + var type = source.getType(); + var constructors = type.getConstructors(); + + return constructors.stream() // + .filter(it -> constructors.size() == 1 || isInjectionPoint(it)) // + .flatMap(it -> it.getRawParameterTypes().stream() // + .map(parameter -> { + return source.isInjectable() && !source.isConfigurationProperties() + ? new InjectionDependency(it, parameter) + : new QualifiedDependency(type, parameter, createDescription(it, parameter, "parameter"), + DependencyType.DEFAULT); + })); + } + + private static Stream fromFieldsOf(JavaClass source) { + + return source.getAllFields().stream() // + .filter(QualifiedDependency::isInjectionPoint) // + .map(field -> new InjectionDependency(field, field.getRawType())); + } + + private static Stream fromMethodsOf(JavaClass source) { + + var methods = source.getAllMethods().stream() // + .filter(it -> !it.getOwner().isEquivalentTo(Object.class)) // + .collect(Collectors.toSet()); + + if (methods.isEmpty()) { + return Stream.empty(); + } + + var returnTypes = methods.stream() // + .filter(it -> !it.getRawReturnType().isPrimitive()) // + .filter(it -> !it.getRawReturnType().getPackageName().startsWith("java")) // + .map(it -> fromCodeUnitReturnType(it)); + + var injectionMethods = methods.stream() // + .filter(QualifiedDependency::isInjectionPoint) // + .collect(Collectors.toSet()); + + var methodInjections = injectionMethods.stream() // + .flatMap(it -> it.getRawParameterTypes().stream() // + .map(parameter -> new InjectionDependency(it, parameter))); + + var otherMethods = methods.stream() // + .filter(it -> !injectionMethods.contains(it)) // + .flatMap(it -> it.getRawParameterTypes().stream() // + .map(parameter -> fromCodeUnitParameter(it, parameter))); + + return Stream.concat(Stream.concat(methodInjections, otherMethods), returnTypes); } private static String createDescription(JavaMember codeUnit, JavaClass declaringElement, String declarationDescription) { - String type = declaringElement.getSimpleName(); + var type = declaringElement.getSimpleName(); - String codeUnitDescription = JavaConstructor.class.isInstance(codeUnit) // + var codeUnitDescription = JavaConstructor.class.isInstance(codeUnit) // ? String.format("%s", declaringElement.getSimpleName()) // : String.format("%s.%s", declaringElement.getSimpleName(), codeUnit.getName()); @@ -753,15 +793,15 @@ public class ApplicationModule { .collect(Collectors.joining(", "))); } - String annotations = codeUnit.getAnnotations().stream() // + var annotations = codeUnit.getAnnotations().stream() // .filter(it -> INJECTION_TYPES.contains(it.getRawType().getName())) // .map(it -> "@" + it.getRawType().getSimpleName()) // .collect(Collectors.joining(" ", "", " ")); annotations = StringUtils.hasText(annotations) ? annotations : ""; - String declaration = declarationDescription + " " + annotations + codeUnitDescription; - String location = SourceCodeLocation.of(codeUnit.getOwner(), 0).toString(); + var declaration = declarationDescription + " " + annotations + codeUnitDescription; + var location = SourceCodeLocation.of(codeUnit.getOwner(), 0).toString(); return String.format("%s declares %s in %s", type, declaration, location); } @@ -771,23 +811,27 @@ public class ApplicationModule { } } - private static class InjectionModuleDependency extends ModuleDependency { + private static class InjectionDependency extends QualifiedDependency { - private final JavaMember member; + private final JavaMember source; private final boolean isConfigurationClass; /** - * @param origin - * @param target - * @param member + * Creates a new {@link InjectionDependency} for the given source, target and originating member. + * + * @param source must not be {@literal null}. + * @param target must not be {@literal null}. */ - public InjectionModuleDependency(JavaClass origin, JavaClass target, JavaMember member) { + public InjectionDependency(JavaMember source, JavaClass target) { - super(origin, target, ModuleDependency.createDescription(member, origin, getDescriptionFor(member)), + super(source.getOwner(), target, + QualifiedDependency.createDescription(source, source.getOwner(), getDescriptionFor(source)), DependencyType.USES_COMPONENT); - this.member = member; - this.isConfigurationClass = isConfiguration().test(origin); + Assert.notNull(source, "Originating member must not be null!"); + + this.source = source; + this.isConfigurationClass = isConfiguration().test(source.getOwner()); } /* @@ -799,13 +843,13 @@ public class ApplicationModule { Violations violations = super.isValidDependencyWithin(modules); - if (JavaField.class.isInstance(member) && !isConfigurationClass) { + if (JavaField.class.isInstance(source) && !isConfigurationClass) { - ApplicationModule module = getExistingModuleOf(member.getOwner(), modules); + ApplicationModule module = getExistingModuleOf(source.getOwner(), modules); violations = violations.and(new IllegalStateException( String.format("Module %s uses field injection in %s. Prefer constructor injection instead!", - module.getDisplayName(), member.getFullName()))); + module.getDisplayName(), source.getFullName()))); } return violations; @@ -825,130 +869,54 @@ public class ApplicationModule { } } - public enum DependencyType { + @ToString + @EqualsAndHashCode + @RequiredArgsConstructor(access = AccessLevel.PRIVATE) + private static class DefaultApplicationModuleDependency implements ApplicationModuleDependency { - /** - * Indicates that the module depends on the other one by a component dependency, i.e. that other module needs to be - * bootstrapped to run the source module. + private final QualifiedDependency dependency; + private final ApplicationModule target; + + static Stream of(QualifiedDependency dependency, ApplicationModules modules) { + + return modules.getModuleByType(dependency.getTarget()).stream() + .map(it -> new DefaultApplicationModuleDependency(dependency, it)); + } + + /* + * (non-Javadoc) + * @see org.springframework.modulith.model.MaterializedDependency#getSourceType() */ - USES_COMPONENT { + @Override + public JavaClass getSourceType() { + return dependency.source; + } - /* - * (non-Javadoc) - * @see org.springframework.modulith.model.Module.DependencyType#format(org.springframework.modulith.model.FormatableJavaClass, org.springframework.modulith.model.FormatableJavaClass) - */ - @Override - public String format(FormatableJavaClass source, FormatableJavaClass target) { - return String.format("Component %s using %s", source.getAbbreviatedFullName(), target.getAbbreviatedFullName()); - } - }, - - /** - * Indicates that the module refers to an entity of the other. + /* + * (non-Javadoc) + * @see org.springframework.modulith.model.MaterializedDependency#getTargetType() */ - ENTITY { + @Override + public JavaClass getTargetType() { + return dependency.target; + } - /* - * (non-Javadoc) - * @see org.springframework.modulith.model.Module.DependencyType#format(org.springframework.modulith.model.FormatableJavaClass, org.springframework.modulith.model.FormatableJavaClass) - */ - @Override - public String format(FormatableJavaClass source, FormatableJavaClass target) { - return String.format("Entity %s depending on %s", source.getAbbreviatedFullName(), - target.getAbbreviatedFullName()); - } - }, - - /** - * Indicates that the module depends on the other by declaring an event listener for an event exposed by the other - * module. Thus, the target module does not have to be bootstrapped to run the source one. + /* + * (non-Javadoc) + * @see org.springframework.modulith.model.MaterializedDependency#getDependencyType() */ - EVENT_LISTENER { - - /* - * (non-Javadoc) - * @see org.springframework.modulith.model.Module.DependencyType#format(org.springframework.modulith.model.FormatableJavaClass, org.springframework.modulith.model.FormatableJavaClass) - */ - @Override - public String format(FormatableJavaClass source, FormatableJavaClass target) { - return String.format("%s listening to events of type %s", source.getAbbreviatedFullName(), - target.getAbbreviatedFullName()); - } - }, - - DEFAULT { - - /* - * (non-Javadoc) - * @see org.springframework.modulith.model.Module.DependencyType#or(com.tngtech.archunit.thirdparty.com.google.common.base.Supplier) - */ - @Override - public DependencyType or(Supplier supplier) { - return supplier.get(); - } - - /* - * (non-Javadoc) - * @see org.springframework.modulith.model.Module.DependencyType#format(org.springframework.modulith.model.FormatableJavaClass, org.springframework.modulith.model.FormatableJavaClass) - */ - @Override - public String format(FormatableJavaClass source, FormatableJavaClass target) { - return String.format("%s depending on %s", source.getAbbreviatedFullName(), target.getAbbreviatedFullName()); - } - }; - - public static DependencyType forParameter(JavaClass type) { - return type.isAnnotatedWith("javax.persistence.Entity") ? ENTITY : DEFAULT; + @Override + public DependencyType getDependencyType() { + return dependency.type; } - public static DependencyType forCodeUnit(JavaCodeUnit codeUnit) { - return Types.isAnnotatedWith(SpringTypes.AT_EVENT_LISTENER).test(codeUnit) // - || Types.isAnnotatedWith(JMoleculesTypes.AT_DOMAIN_EVENT_HANDLER).test(codeUnit) // - ? EVENT_LISTENER - : DEFAULT; - } - - public static DependencyType forDependency(Dependency dependency) { - return forParameter(dependency.getTargetClass()); - } - - public abstract String format(FormatableJavaClass source, FormatableJavaClass target); - - public DependencyType or(Supplier supplier) { - return this; - } - - /** - * Returns all {@link DependencyType}s except the given ones. - * - * @param types must not be {@literal null}. - * @return + /* + * (non-Javadoc) + * @see org.springframework.modulith.model.MaterializedDependency#getTargetModule() */ - public static Stream allBut(Collection types) { - - Assert.notNull(types, "Types must not be null!"); - - Predicate isIncluded = types::contains; - - return Arrays.stream(values()) // - .filter(isIncluded.negate()); - } - - public static Stream allBut(Stream types) { - return allBut(types.collect(Collectors.toList())); - } - - /** - * Returns all {@link DependencyType}s except the given ones. - * - * @param types must not be {@literal null}. - * @return - */ - public static Stream allBut(DependencyType... types) { - - Assert.notNull(types, "Types must not be null!"); - - return allBut(Arrays.asList(types)); + @Override + public ApplicationModule getTargetModule() { + return target; } } } diff --git a/spring-modulith-core/src/main/java/org/springframework/modulith/model/ApplicationModuleDependencies.java b/spring-modulith-core/src/main/java/org/springframework/modulith/model/ApplicationModuleDependencies.java new file mode 100644 index 00000000..61c19be5 --- /dev/null +++ b/spring-modulith-core/src/main/java/org/springframework/modulith/model/ApplicationModuleDependencies.java @@ -0,0 +1,93 @@ +/* + * Copyright 2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.modulith.model; + +import lombok.RequiredArgsConstructor; + +import java.util.List; +import java.util.stream.Stream; + +import org.springframework.util.Assert; + +/** + * The materialized, in other words actually present dependencies of the current module towards other modules. + * + * @author Oliver Drotbohm + */ +@RequiredArgsConstructor(staticName = "of") +public class ApplicationModuleDependencies { + + private final List dependencies; + private final ApplicationModules modules; + + /** + * Returns whether the dependencies contain the given {@link ApplicationModule}. + * + * @param module must not be {@literal null}. + * @return will never be {@literal null}. + */ + public boolean contains(ApplicationModule module) { + + Assert.notNull(module, "ApplicationModule must not be null!"); + + return dependencies.stream() + .map(ApplicationModuleDependency::getTargetModule) + .anyMatch(module::equals); + } + + /** + * Returns whether the dependencies contain the {@link ApplicationModule} with the given name. + * + * @param name must not be {@literal null} or empty. + * @return will never be {@literal null}. + */ + public boolean containsModuleNamed(String name) { + + Assert.hasText(name, "Module name must not be null or empty!"); + + return dependencies.stream() + .map(ApplicationModuleDependency::getTargetModule) + .map(ApplicationModule::getName) + .anyMatch(name::equals); + } + + /** + * Returns all {@link DefaultMaterializedDependency} instances as {@link Stream}. + * + * @return will never be {@literal null}. + */ + public Stream stream() { + return dependencies.stream(); + } + + public ApplicationModuleDependencies withType(DependencyType type) { + + var filtered = dependencies.stream() + .filter(it -> it.getDependencyType().equals(type)) + .toList(); + + return ApplicationModuleDependencies.of(filtered, modules); + } + + /** + * Returns whether there are any dependencies at all. + * + * @return will never be {@literal null}. + */ + public boolean isEmpty() { + return dependencies.isEmpty(); + } +} diff --git a/spring-modulith-core/src/main/java/org/springframework/modulith/model/ApplicationModuleDependency.java b/spring-modulith-core/src/main/java/org/springframework/modulith/model/ApplicationModuleDependency.java new file mode 100644 index 00000000..169a436e --- /dev/null +++ b/spring-modulith-core/src/main/java/org/springframework/modulith/model/ApplicationModuleDependency.java @@ -0,0 +1,54 @@ +/* + * Copyright 2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.modulith.model; + +import com.tngtech.archunit.core.domain.JavaClass; + +/** + * A dependency between two {@link ApplicationModule}s. + * + * @author Oliver Drotbohm + */ +public interface ApplicationModuleDependency { + + /** + * The source java type establishing the dependency. + * + * @return will never be {@literal null}. + */ + JavaClass getSourceType(); + + /** + * The dependency target type. + * + * @return will never be {@literal null}. + */ + JavaClass getTargetType(); + + /** + * The type of the dependency. + * + * @return will never be {@literal null}. + */ + DependencyType getDependencyType(); + + /** + * The target {@link ApplicationModule}. + * + * @return will never be {@literal null}. + */ + ApplicationModule getTargetModule(); +} diff --git a/spring-modulith-core/src/main/java/org/springframework/modulith/model/ArchitecturallyEvidentType.java b/spring-modulith-core/src/main/java/org/springframework/modulith/model/ArchitecturallyEvidentType.java index 96305fff..c1b568a3 100644 --- a/spring-modulith-core/src/main/java/org/springframework/modulith/model/ArchitecturallyEvidentType.java +++ b/spring-modulith-core/src/main/java/org/springframework/modulith/model/ArchitecturallyEvidentType.java @@ -32,7 +32,6 @@ import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.function.Predicate; -import java.util.stream.Collectors; import java.util.stream.Stream; import org.springframework.data.repository.core.RepositoryMetadata; @@ -81,7 +80,7 @@ public abstract class ArchitecturallyEvidentType { delegates.add(new SpringDataAwareArchitecturallyEvidentType(type, beanTypes)); } - delegates.add(new SpringAwareArchitecturallyEvidentType(type)); + delegates.add(new SpringAwareArchitecturallyEvidentType(type, beanTypes)); return DelegatingType.of(type, delegates); }); @@ -135,6 +134,14 @@ public abstract class ArchitecturallyEvidentType { return false; } + public boolean isInjectable() { + return isService() || isController() || isEventListener() || isConfigurationProperties(); + } + + public boolean isValueObject() { + return false; + } + /** * Returns other types that are interesting in the context of the current {@link ArchitecturallyEvidentType}. For * example, for an event listener this might be the event types the particular listener is interested in. @@ -195,8 +202,13 @@ public abstract class ArchitecturallyEvidentType { private static final Predicate IS_EVENT_LISTENER = IS_ANNOTATED_EVENT_LISTENER .or(IS_IMPLEMENTING_EVENT_LISTENER); - public SpringAwareArchitecturallyEvidentType(JavaClass type) { + private final boolean injectable; + + public SpringAwareArchitecturallyEvidentType(JavaClass type, Classes beanTypes) { + super(type); + + this.injectable = beanTypes.contains(type); } /* @@ -253,6 +265,15 @@ public abstract class ArchitecturallyEvidentType { return Types.isAnnotatedWith(SpringTypes.AT_CONFIGURATION_PROPERTIES).test(getType()); } + /* + * (non-Javadoc) + * @see org.springframework.modulith.model.ArchitecturallyEvidentType#isInjectable() + */ + @Override + public boolean isInjectable() { + return injectable || SpringTypes.isComponent().test(getType()) || super.isInjectable(); + } + /* * (non-Javadoc) * @see org.springframework.modulith.model.ArchitecturallyEvidentType#getOtherTypeReferences() @@ -411,13 +432,26 @@ public abstract class ArchitecturallyEvidentType { public boolean isEventListener() { return getType().getMethods().stream().anyMatch(IS_ANNOTATED_EVENT_LISTENER); } + + /* + * (non-Javadoc) + * @see org.springframework.modulith.model.ArchitecturallyEvidentType#isValueObject() + */ + @Override + public boolean isValueObject() { + + JavaClass type = getType(); + + return Types.isAnnotatedWith(org.jmolecules.ddd.annotation.ValueObject.class).test(type) + || type.isAssignableTo(org.jmolecules.ddd.types.ValueObject.class); + } } static class DelegatingType extends ArchitecturallyEvidentType { private final Supplier isAggregateRoot, isRepository, isEntity, isService, isController, - isEventListener, - isConfigurationProperties; + isEventListener, isConfigurationProperties, isInjectable, isValueObject; + private final Supplier> referenceTypes; private final Supplier> referenceMethods; @@ -425,6 +459,8 @@ public abstract class ArchitecturallyEvidentType { Supplier isRepository, Supplier isEntity, Supplier isService, Supplier isController, Supplier isEventListener, Supplier isConfigurationProperties, + Supplier isInjectable, + Supplier isValueObject, Supplier> referenceTypes, Supplier> referenceMethods) { @@ -437,43 +473,49 @@ public abstract class ArchitecturallyEvidentType { this.isController = isController; this.isEventListener = isEventListener; this.isConfigurationProperties = isConfigurationProperties; + this.isInjectable = isInjectable; + this.isValueObject = isValueObject; this.referenceTypes = referenceTypes; this.referenceMethods = referenceMethods; } public static DelegatingType of(JavaClass type, List types) { - Supplier isAggregateRoot = Suppliers + var isAggregateRoot = Suppliers .memoize(() -> types.stream().anyMatch(ArchitecturallyEvidentType::isAggregateRoot)); - Supplier isRepository = Suppliers + var isRepository = Suppliers .memoize(() -> types.stream().anyMatch(ArchitecturallyEvidentType::isRepository)); - Supplier isEntity = Suppliers + var isEntity = Suppliers .memoize(() -> types.stream().anyMatch(ArchitecturallyEvidentType::isEntity)); - Supplier isService = Suppliers + var isService = Suppliers .memoize(() -> types.stream().anyMatch(ArchitecturallyEvidentType::isService)); - Supplier isController = Suppliers + var isController = Suppliers .memoize(() -> types.stream().anyMatch(ArchitecturallyEvidentType::isController)); - Supplier isEventListener = Suppliers + var isEventListener = Suppliers .memoize(() -> types.stream().anyMatch(ArchitecturallyEvidentType::isEventListener)); - Supplier isConfigurationProperties = Suppliers + var isConfigurationProperties = Suppliers .memoize(() -> types.stream().anyMatch(ArchitecturallyEvidentType::isConfigurationProperties)); + var isInjectable = Suppliers.memoize(() -> types.stream().anyMatch(ArchitecturallyEvidentType::isInjectable)); + + var isValueObject = Suppliers.memoize(() -> types.stream().anyMatch(ArchitecturallyEvidentType::isValueObject)); + Supplier> referenceTypes = Suppliers.memoize(() -> types.stream() // .flatMap(ArchitecturallyEvidentType::getReferenceTypes) // - .collect(Collectors.toList())); + .toList()); Supplier> referenceMethods = Suppliers.memoize(() -> types.stream() // .flatMap(ArchitecturallyEvidentType::getReferenceMethods) // - .collect(Collectors.toList())); + .toList()); return new DelegatingType(type, isAggregateRoot, isRepository, isEntity, isService, isController, - isEventListener, isConfigurationProperties, referenceTypes, referenceMethods); + isEventListener, isConfigurationProperties, isInjectable, isValueObject, referenceTypes, referenceMethods); } /* @@ -541,6 +583,24 @@ public abstract class ArchitecturallyEvidentType { return isConfigurationProperties.get(); } + /* + * (non-Javadoc) + * @see org.springframework.modulith.model.ArchitecturallyEvidentType#isInjectable() + */ + @Override + public boolean isInjectable() { + return isInjectable.get(); + } + + /* + * (non-Javadoc) + * @see org.springframework.modulith.model.ArchitecturallyEvidentType#isValueObject() + */ + @Override + public boolean isValueObject() { + return isValueObject.get(); + } + /* * (non-Javadoc) * @see org.springframework.modulith.model.ArchitecturallyEvidentType#getOtherTypeReferences() diff --git a/spring-modulith-core/src/main/java/org/springframework/modulith/model/DependencyDepth.java b/spring-modulith-core/src/main/java/org/springframework/modulith/model/DependencyDepth.java new file mode 100644 index 00000000..2b7800b9 --- /dev/null +++ b/spring-modulith-core/src/main/java/org/springframework/modulith/model/DependencyDepth.java @@ -0,0 +1,25 @@ +/* + * Copyright 2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.modulith.model; + +public enum DependencyDepth { + + NONE, + + IMMEDIATE, + + ALL; +} diff --git a/spring-modulith-core/src/main/java/org/springframework/modulith/model/DependencyType.java b/spring-modulith-core/src/main/java/org/springframework/modulith/model/DependencyType.java new file mode 100644 index 00000000..15418f56 --- /dev/null +++ b/spring-modulith-core/src/main/java/org/springframework/modulith/model/DependencyType.java @@ -0,0 +1,172 @@ +/* + * Copyright 2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.modulith.model; + +import java.util.Arrays; +import java.util.Collection; +import java.util.function.Predicate; +import java.util.stream.Stream; + +import org.springframework.modulith.model.Types.JMoleculesTypes; +import org.springframework.modulith.model.Types.SpringTypes; +import org.springframework.util.Assert; + +import com.tngtech.archunit.core.domain.Dependency; +import com.tngtech.archunit.core.domain.JavaClass; +import com.tngtech.archunit.core.domain.JavaCodeUnit; +import com.tngtech.archunit.thirdparty.com.google.common.base.Supplier; + +/** + * The type of dependency between {@link ApplicationModule}s. + * + * @author Oliver Drotbohm + */ +public enum DependencyType { + + /** + * Indicates that the module depends on the other one by a component dependency, i.e. that other module needs to be + * bootstrapped to run the source module. + */ + USES_COMPONENT { + + /* + * (non-Javadoc) + * @see org.springframework.modulith.model.Module.DependencyType#format(org.springframework.modulith.model.FormatableJavaClass, org.springframework.modulith.model.FormatableJavaClass) + */ + @Override + public String format(FormatableJavaClass source, FormatableJavaClass target) { + return String.format("Component %s using %s", source.getAbbreviatedFullName(), target.getAbbreviatedFullName()); + } + }, + + /** + * Indicates that the module refers to an entity of the other. + */ + ENTITY { + + /* + * (non-Javadoc) + * @see org.springframework.modulith.model.Module.DependencyType#format(org.springframework.modulith.model.FormatableJavaClass, org.springframework.modulith.model.FormatableJavaClass) + */ + @Override + public String format(FormatableJavaClass source, FormatableJavaClass target) { + return String.format("Entity %s depending on %s", source.getAbbreviatedFullName(), + target.getAbbreviatedFullName()); + } + }, + + /** + * Indicates that the module depends on the other by declaring an event listener for an event exposed by the other + * module. Thus, the target module does not have to be bootstrapped to run the source one. + */ + EVENT_LISTENER { + + /* + * (non-Javadoc) + * @see org.springframework.modulith.model.Module.DependencyType#format(org.springframework.modulith.model.FormatableJavaClass, org.springframework.modulith.model.FormatableJavaClass) + */ + @Override + public String format(FormatableJavaClass source, FormatableJavaClass target) { + return String.format("%s listening to events of type %s", source.getAbbreviatedFullName(), + target.getAbbreviatedFullName()); + } + }, + + DEFAULT { + + /* + * (non-Javadoc) + * @see org.springframework.modulith.model.Module.DependencyType#or(com.tngtech.archunit.thirdparty.com.google.common.base.Supplier) + */ + @Override + public DependencyType defaultOr(Supplier supplier) { + return supplier.get(); + } + + /* + * (non-Javadoc) + * @see org.springframework.modulith.model.Module.DependencyType#format(org.springframework.modulith.model.FormatableJavaClass, org.springframework.modulith.model.FormatableJavaClass) + */ + @Override + public String format(FormatableJavaClass source, FormatableJavaClass target) { + return String.format("%s depending on %s", source.getAbbreviatedFullName(), target.getAbbreviatedFullName()); + } + }; + + /** + * Returns the current {@link DependencyType} or obtains the one provided by the given supplier if the current one is + * {@link DependencyType#DEFAULT}. + * + * @param supplier must not be {@literal null}. + * @return + */ + DependencyType defaultOr(Supplier supplier) { + + Assert.notNull(supplier, "The fallback supplier must not be null!"); + + return this; + } + + static DependencyType forParameter(JavaClass type) { + return type.isAnnotatedWith("javax.persistence.Entity") ? ENTITY : DEFAULT; + } + + static DependencyType forCodeUnit(JavaCodeUnit codeUnit) { + return Types.isAnnotatedWith(SpringTypes.AT_EVENT_LISTENER).test(codeUnit) // + || Types.isAnnotatedWith(JMoleculesTypes.AT_DOMAIN_EVENT_HANDLER).test(codeUnit) // + ? EVENT_LISTENER + : DEFAULT; + } + + static DependencyType forDependency(Dependency dependency) { + return forParameter(dependency.getTargetClass()); + } + + public abstract String format(FormatableJavaClass source, FormatableJavaClass target); + + /** + * Returns all {@link DependencyType}s except the given ones. + * + * @param types must not be {@literal null}. + * @return + */ + public static Stream allBut(Collection types) { + + Assert.notNull(types, "Types must not be null!"); + + Predicate isIncluded = types::contains; + + return Arrays.stream(values()) // + .filter(isIncluded.negate()); + } + + public static Stream allBut(Stream types) { + return allBut(types.toList()); + } + + /** + * Returns all {@link DependencyType}s except the given ones. + * + * @param types must not be {@literal null}. + * @return + */ + public static Stream allBut(DependencyType... types) { + + Assert.notNull(types, "Types must not be null!"); + + return allBut(Arrays.asList(types)); + } +} diff --git a/spring-modulith-core/src/test/java/org/springframework/modulith/model/ArchitecturallyEvidentTypeUnitTest.java b/spring-modulith-core/src/test/java/org/springframework/modulith/model/ArchitecturallyEvidentTypeUnitTest.java index 381649bc..976079f0 100644 --- a/spring-modulith-core/src/test/java/org/springframework/modulith/model/ArchitecturallyEvidentTypeUnitTest.java +++ b/spring-modulith-core/src/test/java/org/springframework/modulith/model/ArchitecturallyEvidentTypeUnitTest.java @@ -71,7 +71,7 @@ class ArchitecturallyEvidentTypeUnitTest { void detectsSpringAnnotatedRepositories() { ArchitecturallyEvidentType type = new SpringAwareArchitecturallyEvidentType( - classes.getRequiredClass(SpringRepository.class)); + classes.getRequiredClass(SpringRepository.class), Classes.NONE); assertThat(type.isRepository()).isTrue(); } @@ -80,7 +80,7 @@ class ArchitecturallyEvidentTypeUnitTest { void doesNotConsiderEntityAggregateRoot() { ArchitecturallyEvidentType type = new SpringAwareArchitecturallyEvidentType( - classes.getRequiredClass(SampleEntity.class)); + classes.getRequiredClass(SampleEntity.class), Classes.NONE); assertThat(type.isEntity()).isTrue(); assertThat(type.isAggregateRoot()).isFalse(); diff --git a/spring-modulith-core/src/test/java/org/springframework/modulith/model/ModuleDependencyUnitTest.java b/spring-modulith-core/src/test/java/org/springframework/modulith/model/ModuleDependencyUnitTest.java index ef524524..a67fb1d1 100644 --- a/spring-modulith-core/src/test/java/org/springframework/modulith/model/ModuleDependencyUnitTest.java +++ b/spring-modulith-core/src/test/java/org/springframework/modulith/model/ModuleDependencyUnitTest.java @@ -21,13 +21,13 @@ import java.util.stream.Stream; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.modulith.model.ApplicationModule.ModuleDependency; +import org.springframework.modulith.model.ApplicationModule.QualifiedDependency; import com.tngtech.archunit.core.domain.JavaClass; import com.tngtech.archunit.core.importer.ClassFileImporter; /** - * Unit tests for {@link ModuleDependency}. + * Unit tests for {@link QualifiedDependency}. * * @author Oliver Drotbohm */ @@ -61,8 +61,8 @@ class ModuleDependencyUnitTest { var imported = importer.importClass(type); var evidentType = ArchitecturallyEvidentType.of(imported, Classes.NONE); - return ModuleDependency.fromType(evidentType) // - .map(ModuleDependency::getTarget) // + return QualifiedDependency.fromType(evidentType) // + .map(QualifiedDependency::getTarget) // .map(JavaClass::reflect); } diff --git a/spring-modulith-docs/src/main/java/org/springframework/modulith/docs/Asciidoctor.java b/spring-modulith-docs/src/main/java/org/springframework/modulith/docs/Asciidoctor.java index 09c7887f..95ee0fda 100644 --- a/spring-modulith-docs/src/main/java/org/springframework/modulith/docs/Asciidoctor.java +++ b/spring-modulith-docs/src/main/java/org/springframework/modulith/docs/Asciidoctor.java @@ -31,6 +31,7 @@ import org.springframework.modulith.docs.Documenter.CanvasOptions.Groupings; import org.springframework.modulith.model.ApplicationModule; import org.springframework.modulith.model.ApplicationModules; import org.springframework.modulith.model.ArchitecturallyEvidentType; +import org.springframework.modulith.model.DependencyType; import org.springframework.modulith.model.EventType; import org.springframework.modulith.model.FormatableJavaClass; import org.springframework.modulith.model.Source; @@ -129,7 +130,7 @@ class Asciidoctor { return String.format("%s (via %s)", interfacesAsString, base); } - public String renderSpringBeans(CanvasOptions options, ApplicationModule module) { + public String renderSpringBeans(ApplicationModule module, CanvasOptions options) { StringBuilder builder = new StringBuilder(); Groupings groupings = options.groupBeans(module); @@ -355,4 +356,18 @@ class Asciidoctor { return source; } + + /** + * @param module + * @return + */ + public String renderBeanReferences(ApplicationModule module) { + + var bullets = module.getDependencies(modules, DependencyType.USES_COMPONENT).stream() + .map(it -> "%s (in %s)".formatted(toInlineCode(it.getTargetType()), it.getTargetModule().getDisplayName())) + .map(this::toBulletPoint) + .collect(Collectors.joining("\n")); + + return bullets.isBlank() ? "None" : bullets; + } } diff --git a/spring-modulith-docs/src/main/java/org/springframework/modulith/docs/Documenter.java b/spring-modulith-docs/src/main/java/org/springframework/modulith/docs/Documenter.java index b30ac058..3698cc82 100644 --- a/spring-modulith-docs/src/main/java/org/springframework/modulith/docs/Documenter.java +++ b/spring-modulith-docs/src/main/java/org/springframework/modulith/docs/Documenter.java @@ -44,9 +44,10 @@ import java.util.stream.Stream; import org.springframework.lang.Nullable; import org.springframework.modulith.model.ApplicationModule; -import org.springframework.modulith.model.ApplicationModule.DependencyDepth; -import org.springframework.modulith.model.ApplicationModule.DependencyType; +import org.springframework.modulith.model.ApplicationModuleDependency; import org.springframework.modulith.model.ApplicationModules; +import org.springframework.modulith.model.DependencyDepth; +import org.springframework.modulith.model.DependencyType; import org.springframework.modulith.model.SpringBean; import org.springframework.util.Assert; import org.springframework.util.LinkedMultiValueMap; @@ -315,28 +316,47 @@ public class Documenter { String toModuleCanvas(ApplicationModule module, CanvasOptions options) { - Asciidoctor asciidoctor = Asciidoctor.withJavadocBase(modules, options.getApiBase()); + var asciidoctor = Asciidoctor.withJavadocBase(modules, options.getApiBase()); + var filter = options.hideInternalFilter(module); + var aggregates = module.getAggregateRoots().stream().filter(filter).toList(); + var valueTypes = module.getValueTypes().stream().filter(filter).toList(); + Function, String> mapper = asciidoctor::typesToBulletPoints; - List aggregates = options.hideInternals - ? module.getAggregateRoots().stream().filter(module::isExposed).collect(Collectors.toList()) - : module.getAggregateRoots(); - StringBuilder builder = new StringBuilder(); - builder.append(startTable("%autowidth.stretch, cols=\"h,a\"")); - builder.append(writeTableRow("Base package", asciidoctor.toInlineCode(module.getBasePackage().getName()))); - builder.append(writeTableRow("Spring components", asciidoctor.renderSpringBeans(options, module))); - builder.append(addTableRow(aggregates, "Aggregate roots", mapper)); - builder.append(writeTableRow("Published events", asciidoctor.renderEvents(module))); - builder.append(addTableRow(module.getEventsListenedTo(modules), "Events listened to", mapper)); - builder.append(writeTableRow("Properties", - asciidoctor.renderConfigurationProperties(module, properties.getModuleProperties(module)))); - builder.append(startOrEndTable()); + return new StringBuilder() // - return builder.toString(); + .append(startTable("%autowidth.stretch, cols=\"h,a\"")) + .append(addTableRow("Base package", asciidoctor.toInlineCode(module.getBasePackage().getName()), options)) + + // Spring components + .append(addTableRow("Spring components", asciidoctor.renderSpringBeans(module, options), options)) // + .append(addTableRow("Bean references", asciidoctor.renderBeanReferences(module), options)) // + + // Aggregates + .append(addTableRow(aggregates, "Aggregate roots", mapper, options)) // + .append(addTableRow(valueTypes, "Value types", mapper, options)) // + + // Events + .append(addTableRow("Published events", asciidoctor.renderEvents(module), options)) // + .append(addTableRow(module.getEventsListenedTo(modules), "Events listened to", mapper, options)) // + + // Properties + .append(addTableRow("Properties", + asciidoctor.renderConfigurationProperties(module, properties.getModuleProperties(module)), options)) // + .append(startOrEndTable()) + .toString(); } - private String addTableRow(List types, String header, Function, String> mapper) { - return types.isEmpty() ? "" : writeTableRow(header, mapper.apply(types)); + private static String addTableRow(String title, String content, CanvasOptions options) { + + return options.hideEmptyLines && (content.isBlank() || content.equalsIgnoreCase("none")) + ? "" + : writeTableRow(title, content); + } + + private static String addTableRow(List types, String header, Function, String> mapper, + CanvasOptions options) { + return options.hideEmptyLines && types.isEmpty() ? "" : writeTableRow(header, mapper.apply(types)); } String toPlantUml() { @@ -348,20 +368,17 @@ public class Documenter { DEPENDENCY_DESCRIPTIONS.entrySet().stream().forEach(entry -> { module.getDependencies(modules, entry.getKey()).stream() // + .map(ApplicationModuleDependency::getTargetModule) // .map(it -> getComponents(options).get(it)) // - // .filter(it -> !component.hasEfferentRelationshipWith(it)) // - .forEach(it -> { - - Relationship relationship = component.uses(it, entry.getValue()); - relationship.addTags(entry.getKey().toString()); - }); + .map(it -> component.uses(it, entry.getValue())) // + .filter(it -> it != null) // + .forEach(it -> it.addTags(entry.getKey().toString())); }); - module.getBootstrapDependencies(modules) // - .forEach(it -> { - Relationship relationship = component.uses(getComponents(options).get(it), "uses"); - relationship.addTags(DependencyType.USES_COMPONENT.toString()); - }); + module.getBootstrapDependencies(modules) + .map(it -> component.uses(getComponents(options).get(it), "uses")) + .filter(it -> it != null) + .forEach(it -> it.addTags(DependencyType.USES_COMPONENT.toString())); } private Map getComponents(DiagramOptions options) { @@ -383,7 +400,8 @@ public class Documenter { Supplier> bootstrapDependencies = () -> module.getBootstrapDependencies(modules, options.getDependencyDepth()); Supplier> otherDependencies = () -> options.getDependencyTypes() - .flatMap(it -> module.getDependencies(modules, it).stream()); + .flatMap(it -> module.getDependencies(modules, it).stream() + .map(ApplicationModuleDependency::getTargetModule)); Supplier> dependencies = () -> Stream.concat(bootstrapDependencies.get(), otherDependencies.get()); @@ -592,8 +610,7 @@ public class Documenter { private final @With Predicate exclusions; /** - * A {@link Predicate} to define which Structurizr {@link Component}s to be included in the diagram to be - * created. + * A {@link Predicate} to define which Structurizr {@link Component}s to be included in the diagram to be created. */ private final @With Predicate componentFilter; @@ -616,8 +633,8 @@ public class Documenter { private final @With Function> colorSelector; /** - * A callback to return a default display names for a given {@link ApplicationModule}. Default implementation - * just forwards to {@link ApplicationModule#getDisplayName()}. + * A callback to return a default display names for a given {@link ApplicationModule}. Default implementation just + * forwards to {@link ApplicationModule#getDisplayName()}. */ private final @With Function defaultDisplayName; @@ -629,8 +646,8 @@ public class Documenter { /** * Configuration setting to define whether modules that do not have a relationship to any other module shall be * retained in the diagrams created. The default is {@value ElementsWithoutRelationships#HIDDEN}. See - * {@link DiagramOptions#withExclusions(Predicate)} for a more fine-grained way of defining which modules to - * exclude in case you flip this to {@link ElementsWithoutRelationships#VISIBLE}. + * {@link DiagramOptions#withExclusions(Predicate)} for a more fine-grained way of defining which modules to exclude + * in case you flip this to {@link ElementsWithoutRelationships#VISIBLE}. * * @see #withExclusions(Predicate) */ @@ -701,8 +718,8 @@ public class Documenter { /** * Configuration setting to define whether modules that do not have a relationship to any other module shall be * retained in the diagrams created. The default is {@value ElementsWithoutRelationships#HIDDEN}. See - * {@link DiagramOptions#withExclusions(Predicate)} for a more fine-grained way of defining which modules to - * exclude in case you flip this to {@link ElementsWithoutRelationships#VISIBLE}. + * {@link DiagramOptions#withExclusions(Predicate)} for a more fine-grained way of defining which modules to exclude + * in case you flip this to {@link ElementsWithoutRelationships#VISIBLE}. * * @author Oliver Drotbohm * @see DiagramOptions#withExclusions(Predicate) @@ -722,6 +739,7 @@ public class Documenter { private final @With @Getter @Nullable String apiBase; private final @With @Nullable String targetFileName; private final boolean hideInternals; + private final boolean hideEmptyLines; public static CanvasOptions defaults() { @@ -735,7 +753,7 @@ public class Documenter { } public static CanvasOptions withoutDefaultGroupings() { - return new CanvasOptions(new ArrayList<>(), null, null, true); + return new CanvasOptions(new ArrayList<>(), null, null, true, true); } public CanvasOptions groupingBy(Grouping... groupings) { @@ -743,7 +761,7 @@ public class Documenter { List result = new ArrayList<>(groupers); result.addAll(Arrays.asList(groupings)); - return new CanvasOptions(result, apiBase, targetFileName, hideInternals); + return new CanvasOptions(result, apiBase, targetFileName, hideInternals, hideEmptyLines); } public CanvasOptions groupingBy(String name, Predicate filter) { @@ -756,7 +774,11 @@ public class Documenter { * @return will never be {@literal null}. */ public CanvasOptions revealInternals() { - return new CanvasOptions(groupers, apiBase, targetFileName, false); + return new CanvasOptions(groupers, apiBase, targetFileName, false, hideEmptyLines); + } + + public CanvasOptions revealEmptyLines() { + return new CanvasOptions(groupers, apiBase, targetFileName, hideInternals, false); } Groupings groupBeans(ApplicationModule module) { @@ -785,6 +807,10 @@ public class Documenter { return Groupings.of(result); } + Predicate hideInternalFilter(ApplicationModule module) { + return hideInternals ? module::isExposed : __ -> true; + } + private Optional getTargetFileName() { return Optional.ofNullable(targetFileName); } diff --git a/spring-modulith-integration-test/src/test/java/com/acme/myproject/ModulithTest.java b/spring-modulith-integration-test/src/test/java/com/acme/myproject/ModulithTest.java index c729819a..5b5e1c64 100644 --- a/spring-modulith-integration-test/src/test/java/com/acme/myproject/ModulithTest.java +++ b/spring-modulith-integration-test/src/test/java/com/acme/myproject/ModulithTest.java @@ -19,9 +19,10 @@ import static org.assertj.core.api.Assertions.*; import org.junit.jupiter.api.Test; import org.springframework.modulith.model.ApplicationModule; -import org.springframework.modulith.model.ApplicationModule.DependencyType; +import org.springframework.modulith.model.ApplicationModuleDependencies; import org.springframework.modulith.model.ApplicationModules; import org.springframework.modulith.model.ApplicationModules.Filters; +import org.springframework.modulith.model.DependencyType; import org.springframework.modulith.model.Violations; import com.acme.myproject.invalid.InvalidComponent; @@ -43,14 +44,14 @@ class ModulithTest { @Test void verifyModules() { - String componentName = InternalComponentB.class.getSimpleName(); + String componentName = InternalComponentB.class.getName(); assertThatExceptionOfType(Violations.class) // .isThrownBy(() -> ApplicationModules.of(Application.class, DEFAULT_EXCLUSIONS).verify()) // .withMessageContaining(String.format("Module '%s' depends on non-exposed type %s within module 'moduleB'", "invalid", InternalComponentB.class.getName())) - .withMessageContaining(String.format("%s declares constructor %s(%s)", InvalidComponent.class.getSimpleName(), - InvalidComponent.class.getSimpleName(), componentName)); + .withMessageContaining( + String.format("Constructor <%s.(%s)>", InvalidComponent.class.getName(), componentName)); } @Test @@ -94,11 +95,10 @@ class ModulithTest { assertThat(modules.getModuleByName("moduleD")).hasValueSatisfying(it -> { assertThat(it.getDependencies(modules, DependencyType.DEFAULT)) - .map(ApplicationModule::getName) - .contains("moduleC"); // ConfigurationProperties -> Value + .matches(inner -> inner.containsModuleNamed("moduleC")); assertThat(it.getDependencies(modules, DependencyType.USES_COMPONENT)) - .isEmpty(); + .matches(ApplicationModuleDependencies::isEmpty); }); } } diff --git a/spring-modulith-integration-test/src/test/java/org/springframework/modulith/docs/DocumenterTest.java b/spring-modulith-integration-test/src/test/java/org/springframework/modulith/docs/DocumenterTest.java index b983147f..5a50b7f6 100644 --- a/spring-modulith-integration-test/src/test/java/org/springframework/modulith/docs/DocumenterTest.java +++ b/spring-modulith-integration-test/src/test/java/org/springframework/modulith/docs/DocumenterTest.java @@ -28,7 +28,7 @@ import java.util.Optional; import org.junit.jupiter.api.Test; import org.springframework.modulith.docs.Documenter.DiagramOptions; import org.springframework.modulith.model.ApplicationModule; -import org.springframework.modulith.model.ApplicationModule.DependencyType; +import org.springframework.modulith.model.DependencyType; import com.acme.myproject.Application; diff --git a/spring-modulith-integration-test/src/test/java/org/springframework/modulith/model/ModulesIntegrationTest.java b/spring-modulith-integration-test/src/test/java/org/springframework/modulith/model/ModulesIntegrationTest.java index 1e365807..c2990261 100644 --- a/spring-modulith-integration-test/src/test/java/org/springframework/modulith/model/ModulesIntegrationTest.java +++ b/spring-modulith-integration-test/src/test/java/org/springframework/modulith/model/ModulesIntegrationTest.java @@ -23,7 +23,6 @@ import java.util.stream.Collectors; import java.util.stream.Stream; import org.junit.jupiter.api.Test; -import org.springframework.modulith.model.ApplicationModule.DependencyType; import com.acme.myproject.Application; import com.acme.myproject.complex.internal.FirstTypeBasedPort; @@ -117,8 +116,7 @@ class ModulesIntegrationTest { ApplicationModule moduleA = modules.getModuleByName("moduleA").orElseThrow(IllegalStateException::new); assertThat(module).hasValueSatisfying(it -> { - assertThat(it.getDependencies(modules, DependencyType.EVENT_LISTENER)) // - .contains(moduleA); + assertThat(it.getDependencies(modules, DependencyType.EVENT_LISTENER).contains(moduleA)).isTrue(); }); } diff --git a/spring-modulith-test/src/main/java/org/springframework/modulith/test/ApplicationModuleTest.java b/spring-modulith-test/src/main/java/org/springframework/modulith/test/ApplicationModuleTest.java index f9688bc9..29cd5e36 100644 --- a/spring-modulith-test/src/main/java/org/springframework/modulith/test/ApplicationModuleTest.java +++ b/spring-modulith-test/src/main/java/org/springframework/modulith/test/ApplicationModuleTest.java @@ -28,7 +28,7 @@ import org.springframework.boot.autoconfigure.ImportAutoConfiguration; import org.springframework.boot.test.autoconfigure.filter.TypeExcludeFilters; import org.springframework.boot.test.context.SpringBootTestContextBootstrapper; import org.springframework.core.annotation.AliasFor; -import org.springframework.modulith.model.ApplicationModule.DependencyDepth; +import org.springframework.modulith.model.DependencyDepth; import org.springframework.test.context.BootstrapWith; import org.springframework.test.context.TestConstructor; import org.springframework.test.context.TestConstructor.AutowireMode;