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;