18
18
import static com .tngtech .archunit .base .DescribedPredicate .*;
19
19
import static com .tngtech .archunit .core .domain .JavaClass .Predicates .*;
20
20
import static java .lang .System .*;
21
+ import static java .util .Comparator .*;
21
22
import static org .springframework .modulith .core .SyntacticSugar .*;
22
23
import static org .springframework .modulith .core .Types .JavaXTypes .*;
23
24
import static org .springframework .modulith .core .Types .SpringDataTypes .*;
24
25
import static org .springframework .modulith .core .Types .SpringTypes .*;
25
26
26
27
import java .util .Arrays ;
28
+ import java .util .Collection ;
27
29
import java .util .Collections ;
28
30
import java .util .List ;
29
31
import java .util .Map ;
57
59
*
58
60
* @author Oliver Drotbohm
59
61
*/
60
- public class ApplicationModule {
62
+ public class ApplicationModule implements Comparable < ApplicationModule > {
61
63
62
64
/**
63
65
* The base package of the {@link ApplicationModule}.
64
66
*/
65
67
private final JavaPackage basePackage ;
68
+ private final Classes classes ;
69
+ private final JavaPackages exclusions ;
70
+
66
71
private final ApplicationModuleInformation information ;
67
72
68
73
/**
69
74
* All {@link NamedInterfaces} of the {@link ApplicationModule} either declared explicitly via {@link NamedInterface}
70
75
* or implicitly.
71
76
*/
72
77
private final NamedInterfaces namedInterfaces ;
73
- private final boolean useFullyQualifiedModuleNames ;
78
+ private final ApplicationModuleSource source ;
74
79
75
80
private final Supplier <Classes > springBeans ;
76
81
private final Supplier <Classes > aggregateRoots ;
77
82
private final Supplier <List <JavaClass >> valueTypes ;
78
83
private final Supplier <List <EventType >> publishedEvents ;
79
84
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
+
80
94
/**
81
95
* Creates a new {@link ApplicationModule} for the given base package and whether to use fully-qualified module names.
82
96
*
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}.
85
99
*/
86
- ApplicationModule (JavaPackage basePackage , boolean useFullyQualifiedModuleNames ) {
100
+ ApplicationModule (ApplicationModuleSource source , JavaPackages exclusions ) {
87
101
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!" );
89
104
105
+ JavaPackage basePackage = source .moduleBasePackage ();
106
+
107
+ this .source = source ;
90
108
this .basePackage = basePackage ;
109
+ this .exclusions = exclusions ;
110
+ this .classes = basePackage .getClasses (exclusions );
91
111
this .information = ApplicationModuleInformation .of (basePackage );
92
112
this .namedInterfaces = isOpen ()
93
113
? NamedInterfaces .forOpen (basePackage )
94
114
: NamedInterfaces .discoverNamedInterfaces (basePackage );
95
- this .useFullyQualifiedModuleNames = useFullyQualifiedModuleNames ;
96
115
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 ));
99
118
this .valueTypes = SingletonSupplier
100
119
.of (() -> findArchitecturallyEvidentType (ArchitecturallyEvidentType ::isValueObject ));
101
120
this .publishedEvents = SingletonSupplier .of (() -> findPublishedEvents ());
@@ -125,7 +144,7 @@ public NamedInterfaces getNamedInterfaces() {
125
144
* @return will never be {@literal null} or empty.
126
145
*/
127
146
public String getName () {
128
- return useFullyQualifiedModuleNames ? basePackage . getName () : basePackage . getLocalName ();
147
+ return source . moduleName ();
129
148
}
130
149
131
150
/**
@@ -274,10 +293,20 @@ public ArchitecturallyEvidentType getArchitecturallyEvidentType(Class<?> type) {
274
293
FormatableType .of (type ).getAbbreviatedFullName (this ), getName ())));
275
294
}
276
295
296
+ /**
297
+ * Returns whether the current module contains the given type.
298
+ *
299
+ * @param type must not be {@literal null}.
300
+ */
277
301
public boolean contains (JavaClass type ) {
278
- return basePackage .contains (type );
302
+ return classes .contains (type );
279
303
}
280
304
305
+ /**
306
+ * Returns whether the current module contains the given type.
307
+ *
308
+ * @param type can be {@literal null}.
309
+ */
281
310
public boolean contains (@ Nullable Class <?> type ) {
282
311
return type != null && getType (type .getName ()).isPresent ();
283
312
}
@@ -292,7 +321,7 @@ public Optional<JavaClass> getType(String candidate) {
292
321
293
322
Assert .hasText (candidate , "Candidate must not be null or emtpy!" );
294
323
295
- return basePackage .stream ()
324
+ return classes .stream ()
296
325
.filter (hasSimpleOrFullyQualifiedName (candidate ))
297
326
.findFirst ();
298
327
}
@@ -363,9 +392,28 @@ public String toString(@Nullable ApplicationModules modules) {
363
392
364
393
builder .append ("\n " );
365
394
395
+ if (modules != null ) {
396
+ modules .getParentOf (this ).ifPresent (it -> {
397
+ builder .append ("> Parent module: " ).append (it .getName ()).append ("\n " );
398
+ });
399
+ }
400
+
366
401
builder .append ("> Logical name: " ).append (getName ()).append ('\n' );
367
402
builder .append ("> Base package: " ).append (basePackage .getName ()).append ('\n' );
368
403
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
+
369
417
if (namedInterfaces .hasExplicitInterfaces ()) {
370
418
371
419
builder .append ("> Named interfaces:\n " );
@@ -475,6 +523,23 @@ boolean isOpen() {
475
523
return information .isOpen ();
476
524
}
477
525
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
+
478
543
/*
479
544
* (non-Javadoc)
480
545
* @see java.lang.Object#equals(java.lang.Object)
@@ -490,13 +555,13 @@ public boolean equals(Object obj) {
490
555
return false ;
491
556
}
492
557
493
- return Objects .equals (this .basePackage , that .basePackage ) //
558
+ return Objects .equals (this .source , that .source )
559
+ && Objects .equals (this .basePackage , that .basePackage ) //
494
560
&& Objects .equals (this .aggregateRoots , that .aggregateRoots ) //
495
561
&& Objects .equals (this .information , that .information ) //
496
562
&& Objects .equals (this .namedInterfaces , that .namedInterfaces ) //
497
563
&& Objects .equals (this .publishedEvents , that .publishedEvents ) //
498
564
&& Objects .equals (this .springBeans , that .springBeans ) //
499
- && Objects .equals (this .useFullyQualifiedModuleNames , that .useFullyQualifiedModuleNames ) //
500
565
&& Objects .equals (this .valueTypes , that .valueTypes );
501
566
}
502
567
@@ -506,16 +571,25 @@ public boolean equals(Object obj) {
506
571
*/
507
572
@ Override
508
573
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 ());
511
585
}
512
586
513
587
private List <EventType > findPublishedEvents () {
514
588
515
589
DescribedPredicate <JavaClass > isEvent = implement (JMoleculesTypes .DOMAIN_EVENT ) //
516
590
.or (isAnnotatedWith (JMoleculesTypes .AT_DOMAIN_EVENT ));
517
591
518
- return basePackage .that (isEvent ).stream () //
592
+ return classes .that (isEvent ).stream () //
519
593
.map (EventType ::new ).toList ();
520
594
}
521
595
@@ -537,7 +611,7 @@ private Stream<JavaClass> resolveModuleSuperTypes(JavaClass type) {
537
611
538
612
private Stream <QualifiedDependency > getAllModuleDependencies (ApplicationModules modules ) {
539
613
540
- return basePackage .stream () //
614
+ return classes .stream () //
541
615
.flatMap (it -> getModuleDependenciesOf (it , modules ));
542
616
}
543
617
@@ -590,7 +664,7 @@ private boolean isDependencyToOtherModule(JavaClass dependency, ApplicationModul
590
664
return modules .contains (dependency ) && !contains (dependency );
591
665
}
592
666
593
- private Classes findAggregateRoots (JavaPackage source ) {
667
+ private Classes findAggregateRoots (Classes source ) {
594
668
595
669
return source .stream () //
596
670
.map (it -> ArchitecturallyEvidentType .of (it , getSpringBeansInternal ()))
@@ -599,11 +673,78 @@ private Classes findAggregateRoots(JavaPackage source) {
599
673
.collect (Classes .toClasses ());
600
674
}
601
675
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
+
602
731
private String getQualifiedName (NamedInterface namedInterface ) {
603
732
return namedInterface .getQualifiedName (getName ());
604
733
}
605
734
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 ) {
607
748
608
749
Map <Boolean , List <JavaClass >> collect = source .that (isConfiguration ()).stream () //
609
750
.flatMap (it -> it .getMethods ().stream ()) //
@@ -632,7 +773,7 @@ private List<JavaClass> findArchitecturallyEvidentType(Predicate<Architecturally
632
773
633
774
var springBeansInternal = getSpringBeansInternal ();
634
775
635
- return basePackage .stream ()
776
+ return classes .stream ()
636
777
.map (it -> ArchitecturallyEvidentType .of (it , springBeansInternal ))
637
778
.filter (selector )
638
779
.map (ArchitecturallyEvidentType ::getType )
@@ -907,6 +1048,9 @@ static class QualifiedDependency {
907
1048
908
1049
private static final List <String > INJECTION_TYPES = Arrays .asList (AT_AUTOWIRED , AT_RESOURCE , AT_INJECT );
909
1050
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
+
910
1054
private final JavaClass source , target ;
911
1055
private final String description ;
912
1056
private final DependencyType type ;
@@ -1043,15 +1187,32 @@ Violations isValidDependencyWithin(ApplicationModules modules) {
1043
1187
return violations ;
1044
1188
}
1045
1189
1190
+ if (originModule .containsTypeInAnyParent (target , modules )) {
1191
+ return violations ;
1192
+ }
1193
+
1046
1194
if (!targetModule .isExposed (target )) {
1047
1195
1048
- var violationText = "Module '%s' depends on non-exposed type %s within module '%s'!"
1196
+ var violationText = INTERNAL_REFERENCE
1049
1197
.formatted (originModule .getName (), target .getName (), targetModule .getName ());
1050
1198
1051
1199
return violations .and (new Violation (violationText + lineSeparator () + description ));
1052
1200
}
1053
1201
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 );
1055
1216
}
1056
1217
1057
1218
ApplicationModule getExistingModuleOf (JavaClass javaClass , ApplicationModules modules ) {
0 commit comments