From f09f3aa8271af523abbded8d9ca937748cbda7eb Mon Sep 17 00:00:00 2001 From: Oliver Drotbohm Date: Thu, 5 Sep 2024 20:55:11 +0200 Subject: [PATCH] GH-802 - Allow transitive application module dependency resolution. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ApplicationModule now exposes both getDirectDependencies(…) and getAllDependencies(…), the former as alias for the now deprecated getDependencies(…) for symmetry reasons. The latter recursively resolves transitive dependencies. We now optimize the dependency analysis by skipping types residing java and javax packages as they're not relevant to our dependency arrangement model. A few additional optimizations in ApplicationModuleDependencies to avoid iterating over each establishing dependency if all we need to look at is the general module dependency arrangement. Improve performance of ApplicationModule.contains(…) checks by checking whether the given type can even live inside the package space of the module. --- .../modulith/core/ApplicationModule.java | 253 +++++++++++------- .../core/ApplicationModuleDependencies.java | 77 ++++-- .../modulith/core/ApplicationModules.java | 3 +- .../modulith/core/PackageName.java | 35 ++- .../springframework/modulith/core/Types.java | 9 + .../core/util/ApplicationModulesExporter.java | 2 +- .../modulith/core/PackageNameUnitTests.java | 11 + .../modulith/docs/Asciidoctor.java | 2 +- .../modulith/docs/Documenter.java | 7 +- .../java/com/acme/myproject/ModulithTest.java | 4 +- .../ApplicationModulesIntegrationTest.java | 13 +- 11 files changed, 295 insertions(+), 121 deletions(-) diff --git a/spring-modulith-core/src/main/java/org/springframework/modulith/core/ApplicationModule.java b/spring-modulith-core/src/main/java/org/springframework/modulith/core/ApplicationModule.java index cd1fad9e..6ffe741d 100644 --- a/spring-modulith-core/src/main/java/org/springframework/modulith/core/ApplicationModule.java +++ b/spring-modulith-core/src/main/java/org/springframework/modulith/core/ApplicationModule.java @@ -38,6 +38,7 @@ import org.springframework.lang.Nullable; import org.springframework.modulith.core.Types.JMoleculesTypes; +import org.springframework.modulith.core.Types.JavaTypes; import org.springframework.modulith.core.Types.SpringTypes; import org.springframework.modulith.core.Violations.Violation; import org.springframework.util.Assert; @@ -163,19 +164,34 @@ public String getDisplayName() { * @param modules must not be {@literal null}. * @param type must not be {@literal null}. * @return will never be {@literal null}. + * @deprecated since 1.3. Use {@link #getDirectDependencies(ApplicationModules, DependencyType...)} instead. */ + @Deprecated public ApplicationModuleDependencies getDependencies(ApplicationModules modules, DependencyType... type) { + return getDirectDependencies(modules, type); + } - Assert.notNull(modules, "ApplicationModules must not be null!"); - Assert.notNull(type, "DependencyTypes must not be null!"); - - var dependencies = getAllModuleDependencies(modules) // - .filter(it -> type.length == 0 ? true : Arrays.stream(type).anyMatch(it::hasType)) // - .distinct() // - . flatMap(it -> DefaultApplicationModuleDependency.of(it, modules)) // - .toList(); + /** + * Returns the direct {@link ApplicationModuleDependencies} 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 getDirectDependencies(ApplicationModules modules, DependencyType... type) { + return getDependencies(modules, DependencyDepth.IMMEDIATE, type); + } - return ApplicationModuleDependencies.of(dependencies, modules); + /** + * Returns the all {@link ApplicationModuleDependencies} (including transitive ones) 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 getAllDependencies(ApplicationModules modules, DependencyType... type) { + return getDependencies(modules, DependencyDepth.ALL, type); } /** @@ -299,20 +315,20 @@ public ArchitecturallyEvidentType getArchitecturallyEvidentType(Class type) { * @param type must not be {@literal null}. */ public boolean contains(JavaClass type) { - return classes.contains(type); + return contains(type.getName()); } /** * Returns whether the current module contains the given type. * - * @param type can be {@literal null}. + * @param type must not be {@literal null}. */ - public boolean contains(@Nullable Class type) { - return type != null && getType(type.getName()).isPresent(); + public boolean contains(Class type) { + return contains(type.getName()); } /** - * Returns the {@link JavaClass} for the given candidate simple of fully-qualified type name. + * Returns the {@link JavaClass} for the given candidate simple or fully-qualified type name. * * @param candidate must not be {@literal null} or empty. * @return will never be {@literal null}. @@ -363,23 +379,17 @@ public boolean isRootModule() { } /** - * Returns whether the module has a base package with the given name. + * Returns whether the given module contains a type with the given simple or fully qualified name. * - * @param candidate must not be {@literal null} or empty. - * @return whether the module has a base package with the given name. - * @since 1.1 + * @param candidate must not be {@literal null}. + * @since 1.3 */ - boolean hasBasePackage(String candidate) { - return basePackage.getName().equals(candidate); - } + public boolean contains(String candidate) { - /* - * (non-Javadoc) - * @see java.lang.Object#toString() - */ - @Override - public String toString() { - return toString(null); + var candidatePackageName = PackageName.ofType(candidate); + + return (candidatePackageName.isEmpty() || basePackage.getPackageName().contains(candidatePackageName)) + && getType(candidate).isPresent(); } public String toString(@Nullable ApplicationModules modules) { @@ -450,6 +460,17 @@ public String toString(@Nullable ApplicationModules modules) { return builder.toString(); } + /** + * Returns whether the module has a base package with the given name. + * + * @param candidate must not be {@literal null} or empty. + * @return whether the module has a base package with the given name. + * @since 1.1 + */ + boolean hasBasePackage(String candidate) { + return basePackage.getName().equals(candidate); + } + Classes getSpringBeansInternal() { return springBeans.get(); } @@ -482,19 +503,6 @@ DeclaredDependencies getDeclaredDependencies(ApplicationModules modules) { .collect(Collectors.collectingAndThen(Collectors.toList(), DeclaredDependencies::closed)); } - /** - * Returns whether the given module contains a type with the given simple or fully qualified name. - * - * @param candidate must not be {@literal null} or empty. - * @return - */ - boolean contains(String candidate) { - - Assert.hasText(candidate, "Candidate must not be null or empty!"); - - return getType(candidate).isPresent(); - } - /** * Returns whether the {@link ApplicationModule} contains the package with the given name, which means the given * package is either the module's base package or a sub package of it. @@ -540,6 +548,26 @@ boolean containsTypeInAnyParent(JavaClass type, ApplicationModules modules) { .isPresent(); } + /* + * (non-Javadoc) + * @see org.springframework.modulith.core.ApplicationModuleDependenciesAware#getDependencies(org.springframework.modulith.core.ApplicationModules, org.springframework.modulith.core.DependencyDepth, org.springframework.modulith.core.DependencyType[]) + */ + public ApplicationModuleDependencies getDependencies(ApplicationModules modules, DependencyDepth depth, + DependencyType... types) { + + Assert.notNull(modules, "ApplicationModules must not be null!"); + Assert.notNull(depth, "DependencyDepth must not be null!"); + Assert.notNull(types, "DependencyTypes must not be null!"); + + return getAllModuleDependencies(modules) // + .filter(it -> types.length == 0 ? true : Arrays.stream(types).anyMatch(it::hasType)) // + .distinct() // + .flatMap(it -> DefaultApplicationModuleDependency.of(it, modules)) // + .distinct() // + .flatMap(it -> resolveRecurseively(modules, it, depth, types)) // + .collect(Collectors.collectingAndThen(Collectors.toList(), ApplicationModuleDependencies::of)); + } + /* * (non-Javadoc) * @see java.lang.Object#equals(java.lang.Object) @@ -575,6 +603,15 @@ public int hashCode() { valueTypes); } + /* + * (non-Javadoc) + * @see java.lang.Object#toString() + */ + @Override + public String toString() { + return toString(null); + } + /* * (non-Javadoc) * @see java.lang.Comparable#compareTo(java.lang.Object) @@ -646,33 +683,6 @@ private Stream getDirectModuleBootstrapDependencies(Applicati .flatMap(it -> it.map(Stream::of).orElseGet(Stream::empty)); } - private Stream getModuleDependenciesOf(JavaClass type, ApplicationModules modules) { - - var evidentType = ArchitecturallyEvidentType.of(type, getSpringBeansInternal()); - - var injections = QualifiedDependency.fromType(evidentType) // - .filter(it -> isDependencyToOtherModule(it.getTarget(), modules)); // - - var directDependencies = type.getDirectDependenciesFromSelf().stream() // - .filter(it -> isDependencyToOtherModule(it.getTargetClass(), modules)) // - .map(QualifiedDependency::new); - - return Stream.concat(injections, directDependencies).distinct(); - } - - private boolean isDependencyToOtherModule(JavaClass dependency, ApplicationModules modules) { - return modules.contains(dependency) && !contains(dependency); - } - - private Classes findAggregateRoots(Classes source) { - - return source.stream() // - .map(it -> ArchitecturallyEvidentType.of(it, getSpringBeansInternal())) - .filter(ArchitecturallyEvidentType::isAggregateRoot) // - .map(ArchitecturallyEvidentType::getType) // - .collect(Classes.toClasses()); - } - /** * Returns the current module's immediate parent module, if present. * @@ -744,6 +754,58 @@ private Collection doGetNestedModules(ApplicationModules modu return result.toList(); } + private List findArchitecturallyEvidentType(Predicate selector) { + + var springBeansInternal = getSpringBeansInternal(); + + return classes.stream() + .map(it -> ArchitecturallyEvidentType.of(it, springBeansInternal)) + .filter(selector) + .map(ArchitecturallyEvidentType::getType) + .toList(); + } + + private Stream getModuleDependenciesOf(JavaClass type, ApplicationModules modules) { + + var evidentType = ArchitecturallyEvidentType.of(type, getSpringBeansInternal()); + + var injections = QualifiedDependency.fromType(evidentType) // + .filter(it -> isDependencyToOtherModule(it.getTarget(), modules)); // + + var directDependencies = type.getDirectDependenciesFromSelf().stream() // + .filter(it -> JavaTypes.IS_NOT_CORE_JAVA_TYPE.test(it.getTargetClass())) // + .filter(it -> isDependencyToOtherModule(it.getTargetClass(), modules)) // + .map(QualifiedDependency::new); + + return Stream.concat(injections, directDependencies).distinct(); + } + + private boolean isDependencyToOtherModule(JavaClass dependency, ApplicationModules modules) { + return modules.contains(dependency) && !contains(dependency); + } + + private Classes findAggregateRoots(Classes source) { + + return source.stream() // + .map(it -> ArchitecturallyEvidentType.of(it, getSpringBeansInternal())) + .filter(ArchitecturallyEvidentType::isAggregateRoot) // + .map(ArchitecturallyEvidentType::getType) // + .collect(Classes.toClasses()); + } + + private Stream resolveRecurseively(ApplicationModules modules, + DefaultApplicationModuleDependency dependency, DependencyDepth depth, DependencyType... type) { + + var tail = depth == DependencyDepth.ALL + ? dependency.getTargetModule() + .getDependencies(modules, depth, type) + .stream() + .distinct() + : Stream. empty(); + + return Stream.concat(Stream.of(dependency), tail); + } + private static Classes filterSpringBeans(Classes source) { Map> collect = source.that(isConfiguration()).stream() // @@ -769,17 +831,6 @@ private static Predicate hasSimpleOrFullyQualifiedName(String candida return it -> it.getSimpleName().equals(candidate) || it.getFullName().equals(candidate); } - private List findArchitecturallyEvidentType(Predicate selector) { - - var springBeansInternal = getSpringBeansInternal(); - - return classes.stream() - .map(it -> ArchitecturallyEvidentType.of(it, springBeansInternal)) - .filter(selector) - .map(ArchitecturallyEvidentType::getType) - .toList(); - } - static class DeclaredDependency { private static final String INVALID_EXPLICIT_MODULE_DEPENDENCY = "Invalid explicit module dependency in %s! No module found with name '%s'."; @@ -1084,21 +1135,31 @@ public QualifiedDependency(JavaClass source, JavaClass target, String descriptio DependencyType.forDependency(dependency)); } - static QualifiedDependency fromCodeUnitParameter(JavaCodeUnit codeUnit, JavaClass parameter) { + static Stream fromCodeUnitParameter(JavaCodeUnit codeUnit, JavaClass parameter) { + + if (JavaTypes.IS_CORE_JAVA_TYPE.test(parameter)) { + return Stream.empty(); + } var description = createDescription(codeUnit, parameter, "parameter"); var type = DependencyType.forCodeUnit(codeUnit) // .defaultOr(() -> DependencyType.forParameter(parameter)); - return new QualifiedDependency(codeUnit.getOwner(), parameter, description, type); + return Stream.of(new QualifiedDependency(codeUnit.getOwner(), parameter, description, type)); } - static QualifiedDependency fromCodeUnitReturnType(JavaCodeUnit codeUnit) { + static Stream fromCodeUnitReturnType(JavaCodeUnit codeUnit) { + + var returnType = codeUnit.getRawReturnType(); + + if (JavaTypes.IS_CORE_JAVA_TYPE.test(returnType)) { + return Stream.empty(); + } var description = createDescription(codeUnit, codeUnit.getRawReturnType(), "return type"); - return new QualifiedDependency(codeUnit.getOwner(), codeUnit.getRawReturnType(), description, - DependencyType.DEFAULT); + return Stream.of(new QualifiedDependency(codeUnit.getOwner(), codeUnit.getRawReturnType(), description, + DependencyType.DEFAULT)); } static Stream fromType(ArchitecturallyEvidentType type) { @@ -1112,9 +1173,10 @@ static Stream allFrom(JavaCodeUnit codeUnit) { var parameterDependencies = codeUnit.getRawParameterTypes()// .stream() // - .map(it -> fromCodeUnitParameter(codeUnit, it)); + .filter(JavaTypes.IS_NOT_CORE_JAVA_TYPE) // + .flatMap(it -> fromCodeUnitParameter(codeUnit, it)); - var returnType = Stream.of(fromCodeUnitReturnType(codeUnit)); + var returnType = fromCodeUnitReturnType(codeUnit); return Stream.concat(parameterDependencies, returnType); } @@ -1268,6 +1330,7 @@ private static Stream fromConstructorOf(ArchitecturallyEvid return constructors.stream() // .filter(it -> constructors.size() == 1 || isInjectionPoint(it)) // .flatMap(it -> it.getRawParameterTypes().stream() // + .filter(Predicate.not(JavaTypes.IS_CORE_JAVA_TYPE)) .map(parameter -> { return source.isInjectable() && !source.isConfigurationProperties() ? new InjectionDependency(it, parameter) @@ -1279,12 +1342,17 @@ private static Stream fromConstructorOf(ArchitecturallyEvid private static Stream fromFieldsOf(JavaClass source) { return source.getAllFields().stream() // + .filter(it -> JavaTypes.IS_NOT_CORE_JAVA_TYPE.test(it.getRawType())) // .filter(QualifiedDependency::isInjectionPoint) // .map(field -> new InjectionDependency(field, field.getRawType())); } private static Stream fromMethodsOf(JavaClass source) { + if (JavaTypes.IS_CORE_JAVA_TYPE.test(source)) { + return Stream.empty(); + } + var methods = source.getAllMethods().stream() // .filter(it -> !it.getOwner().isEquivalentTo(Object.class)) // .collect(Collectors.toSet()); @@ -1295,8 +1363,8 @@ private static Stream fromMethodsOf(JavaClass source) { var returnTypes = methods.stream() // .filter(it -> !it.getRawReturnType().isPrimitive()) // - .filter(it -> !it.getRawReturnType().getPackageName().startsWith("java")) // - .map(it -> fromCodeUnitReturnType(it)); + .filter(it -> JavaTypes.IS_NOT_CORE_JAVA_TYPE.test(it.getRawReturnType())) // + .flatMap(it -> fromCodeUnitReturnType(it)); var injectionMethods = methods.stream() // .filter(QualifiedDependency::isInjectionPoint) // @@ -1304,12 +1372,14 @@ private static Stream fromMethodsOf(JavaClass source) { var methodInjections = injectionMethods.stream() // .flatMap(it -> it.getRawParameterTypes().stream() // + .filter(JavaTypes.IS_NOT_CORE_JAVA_TYPE) // .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))); + .filter(JavaTypes.IS_NOT_CORE_JAVA_TYPE) // + .flatMap(parameter -> fromCodeUnitParameter(it, parameter))); return Stream.concat(Stream.concat(methodInjections, otherMethods), returnTypes); } @@ -1427,7 +1497,8 @@ private static String getDescriptionFor(JavaMember member) { } } - private static class DefaultApplicationModuleDependency implements ApplicationModuleDependency { + private static class DefaultApplicationModuleDependency + implements ApplicationModuleDependency { private final QualifiedDependency dependency; private final ApplicationModule target; @@ -1455,7 +1526,7 @@ private DefaultApplicationModuleDependency(QualifiedDependency dependency, Appli * @param dependency must not be {@literal null}. * @param modules must not be {@literal null}. */ - static Stream of(QualifiedDependency dependency, ApplicationModules modules) { + static Stream of(QualifiedDependency dependency, ApplicationModules modules) { return modules.getModuleByType(dependency.getTarget()).stream() .map(it -> new DefaultApplicationModuleDependency(dependency, it)); diff --git a/spring-modulith-core/src/main/java/org/springframework/modulith/core/ApplicationModuleDependencies.java b/spring-modulith-core/src/main/java/org/springframework/modulith/core/ApplicationModuleDependencies.java index 365f7dbe..c876d99b 100644 --- a/spring-modulith-core/src/main/java/org/springframework/modulith/core/ApplicationModuleDependencies.java +++ b/spring-modulith-core/src/main/java/org/springframework/modulith/core/ApplicationModuleDependencies.java @@ -15,6 +15,7 @@ */ package org.springframework.modulith.core; +import java.util.Collection; import java.util.HashSet; import java.util.List; import java.util.function.Function; @@ -30,22 +31,23 @@ public class ApplicationModuleDependencies { private final List dependencies; - private final ApplicationModules modules; + private final Collection modules; /** * Creates a new {@link ApplicationModuleDependencies} for the given {@link List} of * {@link ApplicationModuleDependency} and {@link ApplicationModules}. * * @param dependencies must not be {@literal null}. - * @param modules must not be {@literal null}. */ - private ApplicationModuleDependencies(List dependencies, ApplicationModules modules) { + private ApplicationModuleDependencies(List dependencies) { Assert.notNull(dependencies, "ApplicationModuleDependency list must not be null!"); - Assert.notNull(modules, "ApplicationModules must not be null!"); this.dependencies = dependencies; - this.modules = modules; + this.modules = dependencies.stream() + .map(ApplicationModuleDependency::getTargetModule) + .distinct() + .toList(); } /** @@ -56,10 +58,8 @@ private ApplicationModuleDependencies(List dependen * @param modules must not be {@literal null}. * @return will never be {@literal null}. */ - static ApplicationModuleDependencies of(List dependencies, - ApplicationModules modules) { - - return new ApplicationModuleDependencies(dependencies, modules); + static ApplicationModuleDependencies of(List dependencies) { + return new ApplicationModuleDependencies(dependencies); } /** @@ -72,9 +72,7 @@ public boolean contains(ApplicationModule module) { Assert.notNull(module, "ApplicationModule must not be null!"); - return dependencies.stream() - .map(ApplicationModuleDependency::getTargetModule) - .anyMatch(module::equals); + return modules.contains(module); } /** @@ -87,8 +85,7 @@ public boolean containsModuleNamed(String name) { Assert.hasText(name, "Module name must not be null or empty!"); - return dependencies.stream() - .map(ApplicationModuleDependency::getTargetModule) + return modules.stream() .map(ApplicationModule::getName) .anyMatch(name::equals); } @@ -119,13 +116,22 @@ public Stream uniqueStream(Function seenTargets.add(extractor.apply(it))); } + /** + * Returns a new {@link ApplicationModuleDependencies} instance containing only the dependencies of the given + * {@link DependencyType}. + * + * @param type must not be {@literal null}. + * @return + */ public ApplicationModuleDependencies withType(DependencyType type) { + Assert.notNull(type, "DependencyType must not be null!"); + var filtered = dependencies.stream() .filter(it -> it.getDependencyType().equals(type)) .toList(); - return ApplicationModuleDependencies.of(filtered, modules); + return ApplicationModuleDependencies.of(filtered); } /** @@ -134,6 +140,45 @@ public ApplicationModuleDependencies withType(DependencyType type) { * @return will never be {@literal null}. */ public boolean isEmpty() { - return dependencies.isEmpty(); + return modules.isEmpty(); + } + + /** + * Returns whether the dependencies contain the type with the given fully-qualified name. + * + * @param type must not be {@literal null} or empty. + * @return + * @since 1.3 + */ + public boolean contains(String type) { + + Assert.hasText(type, "Type must not be null or empty!"); + + return uniqueModules().anyMatch(it -> it.contains(type)); + } + + /** + * Returns all unique {@link ApplicationModule}s involved in the dependencies. + * + * @return will never be {@literal null}. + */ + public Stream uniqueModules() { + return modules.stream(); + } + + /** + * Returns the {@link ApplicationModule} containing the given type. + * + * @param name must not be {@literal null} or empty. + * @return will never be {@literal null}. + */ + public ApplicationModule getModuleByType(String name) { + + Assert.hasText(name, "Name must not be null or empty!"); + + return uniqueModules() + .filter(it -> it.contains(name)) + .findFirst() + .orElse(null); } } diff --git a/spring-modulith-core/src/main/java/org/springframework/modulith/core/ApplicationModules.java b/spring-modulith-core/src/main/java/org/springframework/modulith/core/ApplicationModules.java index a50ac793..359cd010 100644 --- a/spring-modulith-core/src/main/java/org/springframework/modulith/core/ApplicationModules.java +++ b/spring-modulith-core/src/main/java/org/springframework/modulith/core/ApplicationModules.java @@ -778,8 +778,7 @@ private static List topologicallySortModules(ApplicationModules modules) graph.addVertex(project); - project.getDependencies(modules).stream() // - .map(ApplicationModuleDependency::getTargetModule) // + project.getDirectDependencies(modules).uniqueModules() // .forEach(dependency -> { graph.addVertex(dependency); graph.addEdge(project, dependency); diff --git a/spring-modulith-core/src/main/java/org/springframework/modulith/core/PackageName.java b/spring-modulith-core/src/main/java/org/springframework/modulith/core/PackageName.java index 67662eca..f0ed5b02 100644 --- a/spring-modulith-core/src/main/java/org/springframework/modulith/core/PackageName.java +++ b/spring-modulith-core/src/main/java/org/springframework/modulith/core/PackageName.java @@ -16,6 +16,7 @@ package org.springframework.modulith.core; import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; /** * The name of a Java package. Packages are sortable comparing their individual segments and deeper packages sorted @@ -32,16 +33,29 @@ class PackageName implements Comparable { /** * Creates a new {@link PackageName} with the given name. * - * @param name must not be {@literal null} or empty. + * @param name must not be {@literal null}. */ public PackageName(String name) { - Assert.hasText(name, "Name must not be null or empty!"); + Assert.notNull(name, "Name must not be null!"); this.name = name; this.segments = name.split("\\."); } + /** + * Creates a new {@link PackageName} for the given fully-qualified type name. + * + * @param fullyQualifiedName must not be {@literal null} or empty. + * @return will never be {@literal null}. + */ + public static PackageName ofType(String fullyQualifiedName) { + + Assert.notNull(fullyQualifiedName, "Type name must not be null!"); + + return new PackageName(ClassUtils.getPackageName(fullyQualifiedName)); + } + /** * Returns the length of the package name. * @@ -118,6 +132,19 @@ boolean isParentPackageOf(PackageName reference) { return reference.name.startsWith(name + "."); } + /** + * Returns whether the package name contains the given one, i.e. if the given one either is the current one or a + * sub-package of it. + * + * @param reference must not be {@literal null}. + */ + boolean contains(PackageName reference) { + + Assert.notNull(reference, "Reference package name must not be null!"); + + return this.equals(reference) || reference.isSubPackageOf(this); + } + /** * Returns whether the current {@link PackageName} is the name of a sub-package with the given name. * @@ -131,6 +158,10 @@ boolean isSubPackageOf(PackageName reference) { return name.startsWith(reference.name + "."); } + boolean isEmpty() { + return length() == 0; + } + /* * (non-Javadoc) * @see java.lang.Comparable#compareTo(java.lang.Object) diff --git a/spring-modulith-core/src/main/java/org/springframework/modulith/core/Types.java b/spring-modulith-core/src/main/java/org/springframework/modulith/core/Types.java index 0a7d5b80..0d1f6ff5 100644 --- a/spring-modulith-core/src/main/java/org/springframework/modulith/core/Types.java +++ b/spring-modulith-core/src/main/java/org/springframework/modulith/core/Types.java @@ -19,6 +19,7 @@ import static org.springframework.modulith.core.SyntacticSugar.*; import java.lang.annotation.Annotation; +import java.util.function.Predicate; import org.springframework.lang.Nullable; import org.springframework.modulith.PackageInfo; @@ -79,6 +80,14 @@ public static boolean areRulesPresent() { } } + static class JavaTypes { + + static Predicate IS_CORE_JAVA_TYPE = it -> it.getName().startsWith("java.") + || it.getName().startsWith("javax."); + + static Predicate IS_NOT_CORE_JAVA_TYPE = Predicate.not(IS_CORE_JAVA_TYPE); + } + static class JavaXTypes { private static final String BASE_PACKAGE = "jakarta"; diff --git a/spring-modulith-core/src/main/java/org/springframework/modulith/core/util/ApplicationModulesExporter.java b/spring-modulith-core/src/main/java/org/springframework/modulith/core/util/ApplicationModulesExporter.java index 12e7c198..3a389bac 100644 --- a/spring-modulith-core/src/main/java/org/springframework/modulith/core/util/ApplicationModulesExporter.java +++ b/spring-modulith-core/src/main/java/org/springframework/modulith/core/util/ApplicationModulesExporter.java @@ -138,7 +138,7 @@ private static Map toInfo(ApplicationModule module, ApplicationM json.put("namedInterfaces", toNamedInterfaces(module.getNamedInterfaces())); } - json.put("dependencies", module.getDependencies(modules).stream() // + json.put("dependencies", module.getDirectDependencies(modules).stream() // .collect(Collectors.groupingBy(ApplicationModuleDependency::getTargetModule, MAPPER)) .entrySet() // .stream() // diff --git a/spring-modulith-core/src/test/java/org/springframework/modulith/core/PackageNameUnitTests.java b/spring-modulith-core/src/test/java/org/springframework/modulith/core/PackageNameUnitTests.java index e3085a31..3f5f469e 100644 --- a/spring-modulith-core/src/test/java/org/springframework/modulith/core/PackageNameUnitTests.java +++ b/spring-modulith-core/src/test/java/org/springframework/modulith/core/PackageNameUnitTests.java @@ -44,4 +44,15 @@ void sortsPackagesByNameAndDepth() { .map(it -> it.getLocalName("com"))) .containsExactly("acme.b", "acme.a.second", "acme.a.first.one", "acme.a.first", "acme.a", "acme"); } + + @Test // GH-802 + void caculatesNestingCorrectly() { + + var comAcme = new PackageName("com.acme"); + var comAcmeA = new PackageName("com.acme.a"); + + assertThat(comAcme.contains(comAcme)).isTrue(); + assertThat(comAcme.contains(comAcmeA)).isTrue(); + assertThat(comAcmeA.contains(comAcme)).isFalse(); + } } 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 23da80b8..7a1e5c1e 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 @@ -359,7 +359,7 @@ public String toAsciidoctor(String source) { */ public String renderBeanReferences(ApplicationModule module) { - var bullets = module.getDependencies(modules, DependencyType.USES_COMPONENT) + var bullets = module.getDirectDependencies(modules, DependencyType.USES_COMPONENT) .uniqueStream(ApplicationModuleDependency::getTargetType) .map(it -> "%s (in %s)".formatted(toInlineCode(it.getTargetType()), it.getTargetModule().getDisplayName())) .map(this::toBulletPoint) 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 dc6c79de..662dbcd6 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 @@ -36,7 +36,6 @@ import org.springframework.lang.Nullable; import org.springframework.modulith.core.ApplicationModule; -import org.springframework.modulith.core.ApplicationModuleDependency; import org.springframework.modulith.core.ApplicationModules; import org.springframework.modulith.core.DependencyDepth; import org.springframework.modulith.core.DependencyType; @@ -442,8 +441,7 @@ private void addDependencies(ApplicationModule module, Component component, Diag DEPENDENCY_DESCRIPTIONS.entrySet().stream().forEach(entry -> { - module.getDependencies(modules, entry.getKey()).stream() // - .map(ApplicationModuleDependency::getTargetModule) // + module.getDirectDependencies(modules, entry.getKey()).uniqueModules() // .map(it -> getComponents(options).get(it)) // .map(it -> component.uses(it, entry.getValue())) // .filter(it -> it != null) // @@ -475,8 +473,7 @@ private void addComponentsToView(ApplicationModule module, ComponentView view, D Supplier> bootstrapDependencies = () -> module.getBootstrapDependencies(modules, options.dependencyDepth); Supplier> otherDependencies = () -> options.getDependencyTypes() - .flatMap(it -> module.getDependencies(modules, it).stream() - .map(ApplicationModuleDependency::getTargetModule)); + .flatMap(it -> module.getDirectDependencies(modules, it).uniqueModules()); Supplier> dependencies = () -> Stream.concat(bootstrapDependencies.get(), otherDependencies.get()); 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 0cfbc8ee..7b874c8d 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 @@ -110,10 +110,10 @@ void configrationPropertiesTypesEstablishSimpleDependency() { assertThat(modules.getModuleByName("moduleD")).hasValueSatisfying(it -> { - assertThat(it.getDependencies(modules, DependencyType.DEFAULT)) + assertThat(it.getDirectDependencies(modules, DependencyType.DEFAULT)) .matches(inner -> inner.containsModuleNamed("moduleC")); - assertThat(it.getDependencies(modules, DependencyType.USES_COMPONENT)) + assertThat(it.getDirectDependencies(modules, DependencyType.USES_COMPONENT)) .matches(ApplicationModuleDependencies::isEmpty); }); } diff --git a/spring-modulith-integration-test/src/test/java/org/springframework/modulith/core/ApplicationModulesIntegrationTest.java b/spring-modulith-integration-test/src/test/java/org/springframework/modulith/core/ApplicationModulesIntegrationTest.java index 62e20c02..6eb216e6 100644 --- a/spring-modulith-integration-test/src/test/java/org/springframework/modulith/core/ApplicationModulesIntegrationTest.java +++ b/spring-modulith-integration-test/src/test/java/org/springframework/modulith/core/ApplicationModulesIntegrationTest.java @@ -127,7 +127,7 @@ void moduleBListensToModuleA() { ApplicationModule moduleA = modules.getModuleByName("moduleA").orElseThrow(IllegalStateException::new); assertThat(module).hasValueSatisfying(it -> { - assertThat(it.getDependencies(modules, DependencyType.EVENT_LISTENER).contains(moduleA)).isTrue(); + assertThat(it.getDirectDependencies(modules, DependencyType.EVENT_LISTENER).contains(moduleA)).isTrue(); }); } @@ -261,6 +261,17 @@ void detectsContributedApplicationModules() { } } + @Test // GH-802 + void detectsFullDependencyChain() { + + assertThat(modules.getModuleByName("moduleC")).hasValueSatisfying(it -> { + + assertThat(it.getAllDependencies(modules).uniqueModules()) + .extracting(ApplicationModule::getName) + .containsExactlyInAnyOrder("moduleB", "moduleA"); + }); + } + private static void verifyNamedInterfaces(NamedInterfaces interfaces, String name, Class... types) { Stream.of(types).forEach(type -> {