1818import static com .tngtech .archunit .base .DescribedPredicate .*;
1919import static com .tngtech .archunit .core .domain .JavaClass .Predicates .*;
2020import static java .lang .System .*;
21+ import static java .util .Comparator .*;
2122import static org .springframework .modulith .core .SyntacticSugar .*;
2223import static org .springframework .modulith .core .Types .JavaXTypes .*;
2324import static org .springframework .modulith .core .Types .SpringDataTypes .*;
2425import static org .springframework .modulith .core .Types .SpringTypes .*;
2526
2627import java .util .Arrays ;
28+ import java .util .Collection ;
2729import java .util .Collections ;
2830import java .util .List ;
2931import java .util .Map ;
5759 *
5860 * @author Oliver Drotbohm
5961 */
60- public class ApplicationModule {
62+ public class ApplicationModule implements Comparable < ApplicationModule > {
6163
6264 /**
6365 * The base package of the {@link ApplicationModule}.
6466 */
6567 private final JavaPackage basePackage ;
68+ private final Classes classes ;
69+ private final JavaPackages exclusions ;
70+
6671 private final ApplicationModuleInformation information ;
6772
6873 /**
6974 * All {@link NamedInterfaces} of the {@link ApplicationModule} either declared explicitly via {@link NamedInterface}
7075 * or implicitly.
7176 */
7277 private final NamedInterfaces namedInterfaces ;
73- private final boolean useFullyQualifiedModuleNames ;
78+ private final ApplicationModuleSource source ;
7479
7580 private final Supplier <Classes > springBeans ;
7681 private final Supplier <Classes > aggregateRoots ;
7782 private final Supplier <List <JavaClass >> valueTypes ;
7883 private final Supplier <List <EventType >> publishedEvents ;
7984
85+ /**
86+ * Creates a new {@link ApplicationModule} from the given {@link ApplicationModuleSource}.
87+ *
88+ * @param source must not be {@literal null}.
89+ */
90+ ApplicationModule (ApplicationModuleSource source ) {
91+ this (source , JavaPackages .NONE );
92+ }
93+
8094 /**
8195 * Creates a new {@link ApplicationModule} for the given base package and whether to use fully-qualified module names.
8296 *
83- * @param basePackage must not be {@literal null}.
84- * @param useFullyQualifiedModuleNames
97+ * @param source must not be {@literal null}.
98+ * @param exclusions must not be {@literal null}.
8599 */
86- ApplicationModule (JavaPackage basePackage , boolean useFullyQualifiedModuleNames ) {
100+ ApplicationModule (ApplicationModuleSource source , JavaPackages exclusions ) {
87101
88- Assert .notNull (basePackage , "Base package must not be null!" );
102+ Assert .notNull (source , "Base package must not be null!" );
103+ Assert .notNull (exclusions , "Exclusions must not be null!" );
89104
105+ JavaPackage basePackage = source .moduleBasePackage ();
106+
107+ this .source = source ;
90108 this .basePackage = basePackage ;
109+ this .exclusions = exclusions ;
110+ this .classes = basePackage .getClasses (exclusions );
91111 this .information = ApplicationModuleInformation .of (basePackage );
92112 this .namedInterfaces = isOpen ()
93113 ? NamedInterfaces .forOpen (basePackage )
94114 : NamedInterfaces .discoverNamedInterfaces (basePackage );
95- this .useFullyQualifiedModuleNames = useFullyQualifiedModuleNames ;
96115
97- this .springBeans = SingletonSupplier .of (() -> filterSpringBeans (basePackage ));
98- this .aggregateRoots = SingletonSupplier .of (() -> findAggregateRoots (basePackage ));
116+ this .springBeans = SingletonSupplier .of (() -> filterSpringBeans (classes ));
117+ this .aggregateRoots = SingletonSupplier .of (() -> findAggregateRoots (classes ));
99118 this .valueTypes = SingletonSupplier
100119 .of (() -> findArchitecturallyEvidentType (ArchitecturallyEvidentType ::isValueObject ));
101120 this .publishedEvents = SingletonSupplier .of (() -> findPublishedEvents ());
@@ -125,7 +144,7 @@ public NamedInterfaces getNamedInterfaces() {
125144 * @return will never be {@literal null} or empty.
126145 */
127146 public String getName () {
128- return useFullyQualifiedModuleNames ? basePackage . getName () : basePackage . getLocalName ();
147+ return source . moduleName ();
129148 }
130149
131150 /**
@@ -274,10 +293,20 @@ public ArchitecturallyEvidentType getArchitecturallyEvidentType(Class<?> type) {
274293 FormatableType .of (type ).getAbbreviatedFullName (this ), getName ())));
275294 }
276295
296+ /**
297+ * Returns whether the current module contains the given type.
298+ *
299+ * @param type must not be {@literal null}.
300+ */
277301 public boolean contains (JavaClass type ) {
278- return basePackage .contains (type );
302+ return classes .contains (type );
279303 }
280304
305+ /**
306+ * Returns whether the current module contains the given type.
307+ *
308+ * @param type can be {@literal null}.
309+ */
281310 public boolean contains (@ Nullable Class <?> type ) {
282311 return type != null && getType (type .getName ()).isPresent ();
283312 }
@@ -292,7 +321,7 @@ public Optional<JavaClass> getType(String candidate) {
292321
293322 Assert .hasText (candidate , "Candidate must not be null or emtpy!" );
294323
295- return basePackage .stream ()
324+ return classes .stream ()
296325 .filter (hasSimpleOrFullyQualifiedName (candidate ))
297326 .findFirst ();
298327 }
@@ -363,9 +392,28 @@ public String toString(@Nullable ApplicationModules modules) {
363392
364393 builder .append ("\n " );
365394
395+ if (modules != null ) {
396+ modules .getParentOf (this ).ifPresent (it -> {
397+ builder .append ("> Parent module: " ).append (it .getName ()).append ("\n " );
398+ });
399+ }
400+
366401 builder .append ("> Logical name: " ).append (getName ()).append ('\n' );
367402 builder .append ("> Base package: " ).append (basePackage .getName ()).append ('\n' );
368403
404+ builder .append ("> Excluded packages: " );
405+
406+ if (!exclusions .iterator ().hasNext ()) {
407+ builder .append ("none" ).append ('\n' );
408+ } else {
409+
410+ builder .append ('\n' );
411+
412+ exclusions .stream ().forEach (it -> {
413+ builder .append (" - " ).append (it .getName ()).append ('\n' );
414+ });
415+ }
416+
369417 if (namedInterfaces .hasExplicitInterfaces ()) {
370418
371419 builder .append ("> Named interfaces:\n " );
@@ -475,6 +523,23 @@ boolean isOpen() {
475523 return information .isOpen ();
476524 }
477525
526+ /**
527+ * Returns whether the given type is contained in any of the parent modules of the current one.
528+ *
529+ * @param type must not be {@literal null}.
530+ * @param modules must not be {@literal null}.
531+ * @since 1.3
532+ */
533+ boolean containsTypeInAnyParent (JavaClass type , ApplicationModules modules ) {
534+
535+ Assert .notNull (type , "Type must not be null!" );
536+ Assert .notNull (modules , "ApplicationModules must not be null!" );
537+
538+ return modules .getParentOf (this )
539+ .filter (it -> it .contains (type ) || it .containsTypeInAnyParent (type , modules ))
540+ .isPresent ();
541+ }
542+
478543 /*
479544 * (non-Javadoc)
480545 * @see java.lang.Object#equals(java.lang.Object)
@@ -490,13 +555,13 @@ public boolean equals(Object obj) {
490555 return false ;
491556 }
492557
493- return Objects .equals (this .basePackage , that .basePackage ) //
558+ return Objects .equals (this .source , that .source )
559+ && Objects .equals (this .basePackage , that .basePackage ) //
494560 && Objects .equals (this .aggregateRoots , that .aggregateRoots ) //
495561 && Objects .equals (this .information , that .information ) //
496562 && Objects .equals (this .namedInterfaces , that .namedInterfaces ) //
497563 && Objects .equals (this .publishedEvents , that .publishedEvents ) //
498564 && Objects .equals (this .springBeans , that .springBeans ) //
499- && Objects .equals (this .useFullyQualifiedModuleNames , that .useFullyQualifiedModuleNames ) //
500565 && Objects .equals (this .valueTypes , that .valueTypes );
501566 }
502567
@@ -506,16 +571,25 @@ public boolean equals(Object obj) {
506571 */
507572 @ Override
508573 public int hashCode () {
509- return Objects .hash (basePackage , aggregateRoots , information , namedInterfaces , publishedEvents , springBeans ,
510- useFullyQualifiedModuleNames , valueTypes );
574+ return Objects .hash (source , basePackage , aggregateRoots , information , namedInterfaces , publishedEvents , springBeans ,
575+ valueTypes );
576+ }
577+
578+ /*
579+ * (non-Javadoc)
580+ * @see java.lang.Comparable#compareTo(java.lang.Object)
581+ */
582+ @ Override
583+ public int compareTo (ApplicationModule o ) {
584+ return getBasePackage ().compareTo (o .getBasePackage ());
511585 }
512586
513587 private List <EventType > findPublishedEvents () {
514588
515589 DescribedPredicate <JavaClass > isEvent = implement (JMoleculesTypes .DOMAIN_EVENT ) //
516590 .or (isAnnotatedWith (JMoleculesTypes .AT_DOMAIN_EVENT ));
517591
518- return basePackage .that (isEvent ).stream () //
592+ return classes .that (isEvent ).stream () //
519593 .map (EventType ::new ).toList ();
520594 }
521595
@@ -537,7 +611,7 @@ private Stream<JavaClass> resolveModuleSuperTypes(JavaClass type) {
537611
538612 private Stream <QualifiedDependency > getAllModuleDependencies (ApplicationModules modules ) {
539613
540- return basePackage .stream () //
614+ return classes .stream () //
541615 .flatMap (it -> getModuleDependenciesOf (it , modules ));
542616 }
543617
@@ -590,7 +664,7 @@ private boolean isDependencyToOtherModule(JavaClass dependency, ApplicationModul
590664 return modules .contains (dependency ) && !contains (dependency );
591665 }
592666
593- private Classes findAggregateRoots (JavaPackage source ) {
667+ private Classes findAggregateRoots (Classes source ) {
594668
595669 return source .stream () //
596670 .map (it -> ArchitecturallyEvidentType .of (it , getSpringBeansInternal ()))
@@ -599,11 +673,78 @@ private Classes findAggregateRoots(JavaPackage source) {
599673 .collect (Classes .toClasses ());
600674 }
601675
676+ /**
677+ * Returns the current module's immediate parent module, if present.
678+ *
679+ * @param modules must not be {@literal null}.
680+ * @return will never be {@literal null}.
681+ * @since 1.3
682+ */
683+ Optional <ApplicationModule > getParentModule (ApplicationModules modules ) {
684+
685+ Assert .notNull (modules , "ApplicationModules must not be null!" );
686+
687+ var byPackageDepth = comparing (ApplicationModule ::getBasePackage , JavaPackage .reverse ());
688+
689+ return modules .stream ()
690+ .filter (it -> basePackage .isSubPackageOf (it .getBasePackage ()))
691+ .sorted (byPackageDepth )
692+ .findFirst ();
693+ }
694+
695+ /**
696+ * Returns the {@link ApplicationModule}s directly nested inside the current one.
697+ *
698+ * @param modules must not be {@literal null}.
699+ * @return will never be {@literal null}.
700+ * @since 1.3
701+ */
702+ Collection <ApplicationModule > getDirectlyNestedModules (ApplicationModules modules ) {
703+
704+ Assert .notNull (modules , "ApplicationModules must not be null!" );
705+
706+ return doGetNestedModules (modules , false );
707+ }
708+
709+ /**
710+ * Returns all of the current {@link ApplicationModule}'s nested {@link ApplicationModule}s including ones contained
711+ * in nested modules in turn.
712+ *
713+ * @param modules must not be {@literal null}.
714+ * @return will never be {@literal null}.
715+ * @since 1.3
716+ */
717+ Collection <ApplicationModule > getNestedModules (ApplicationModules modules ) {
718+
719+ Assert .notNull (modules , "ApplicationModules must not be null!" );
720+
721+ return doGetNestedModules (modules , true );
722+ }
723+
724+ /**
725+ * @return the classes
726+ */
727+ Classes getClasses () {
728+ return classes ;
729+ }
730+
602731 private String getQualifiedName (NamedInterface namedInterface ) {
603732 return namedInterface .getQualifiedName (getName ());
604733 }
605734
606- private static Classes filterSpringBeans (JavaPackage source ) {
735+ private Collection <ApplicationModule > doGetNestedModules (ApplicationModules modules , boolean recursive ) {
736+
737+ var result = modules .stream ()
738+ .filter (it -> it .getParentModule (modules ).filter (this ::equals ).isPresent ());
739+
740+ if (recursive ) {
741+ result = result .flatMap (it -> Stream .concat (Stream .of (it ), it .getNestedModules (modules ).stream ()));
742+ }
743+
744+ return result .toList ();
745+ }
746+
747+ private static Classes filterSpringBeans (Classes source ) {
607748
608749 Map <Boolean , List <JavaClass >> collect = source .that (isConfiguration ()).stream () //
609750 .flatMap (it -> it .getMethods ().stream ()) //
@@ -632,7 +773,7 @@ private List<JavaClass> findArchitecturallyEvidentType(Predicate<Architecturally
632773
633774 var springBeansInternal = getSpringBeansInternal ();
634775
635- return basePackage .stream ()
776+ return classes .stream ()
636777 .map (it -> ArchitecturallyEvidentType .of (it , springBeansInternal ))
637778 .filter (selector )
638779 .map (ArchitecturallyEvidentType ::getType )
@@ -907,6 +1048,9 @@ static class QualifiedDependency {
9071048
9081049 private static final List <String > INJECTION_TYPES = Arrays .asList (AT_AUTOWIRED , AT_RESOURCE , AT_INJECT );
9091050
1051+ private static final String INVALID_SUB_MODULE_REFERENCE = "Invalid sub-module reference from module '%s' to module '%s' (via %s -> %s)!" ;
1052+ private static final String INTERNAL_REFERENCE = "Module '%s' depends on non-exposed type %s within module '%s'!" ;
1053+
9101054 private final JavaClass source , target ;
9111055 private final String description ;
9121056 private final DependencyType type ;
@@ -1043,15 +1187,32 @@ Violations isValidDependencyWithin(ApplicationModules modules) {
10431187 return violations ;
10441188 }
10451189
1190+ if (originModule .containsTypeInAnyParent (target , modules )) {
1191+ return violations ;
1192+ }
1193+
10461194 if (!targetModule .isExposed (target )) {
10471195
1048- var violationText = "Module '%s' depends on non-exposed type %s within module '%s'!"
1196+ var violationText = INTERNAL_REFERENCE
10491197 .formatted (originModule .getName (), target .getName (), targetModule .getName ());
10501198
10511199 return violations .and (new Violation (violationText + lineSeparator () + description ));
10521200 }
10531201
1054- return violations ;
1202+ // Parent child relationships
1203+
1204+ return targetModule .getParentModule (modules )
1205+ .filter (it -> !it .equals (originModule ))
1206+ .map (__ -> {
1207+
1208+ var violationText = INVALID_SUB_MODULE_REFERENCE
1209+ .formatted (originModule .getName (), targetModule .getName (),
1210+ FormatableType .of (source ).getAbbreviatedFullName (originModule ),
1211+ FormatableType .of (target ).getAbbreviatedFullName (targetModule ));
1212+
1213+ return violations .and (new Violation (violationText ));
1214+ })
1215+ .orElse (violations );
10551216 }
10561217
10571218 ApplicationModule getExistingModuleOf (JavaClass javaClass , ApplicationModules modules ) {
0 commit comments