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
/**
@@ -77,25 +82,31 @@ public class ApplicationModule {
77
82
private final Supplier <List <JavaClass >> valueTypes ;
78
83
private final Supplier <List <EventType >> publishedEvents ;
79
84
85
+ ApplicationModule (JavaPackage basePackage , boolean useFullyQualifiedModuleNames ) {
86
+ this (basePackage , JavaPackages .NONE , useFullyQualifiedModuleNames );
87
+ }
88
+
80
89
/**
81
90
* Creates a new {@link ApplicationModule} for the given base package and whether to use fully-qualified module names.
82
91
*
83
92
* @param basePackage must not be {@literal null}.
84
93
* @param useFullyQualifiedModuleNames
85
94
*/
86
- ApplicationModule (JavaPackage basePackage , boolean useFullyQualifiedModuleNames ) {
95
+ ApplicationModule (JavaPackage basePackage , JavaPackages exclusions , boolean useFullyQualifiedModuleNames ) {
87
96
88
97
Assert .notNull (basePackage , "Base package must not be null!" );
89
98
90
99
this .basePackage = basePackage ;
100
+ this .exclusions = exclusions ;
101
+ this .classes = basePackage .getClasses (exclusions );
91
102
this .information = ApplicationModuleInformation .of (basePackage );
92
103
this .namedInterfaces = isOpen ()
93
104
? NamedInterfaces .forOpen (basePackage )
94
105
: NamedInterfaces .discoverNamedInterfaces (basePackage );
95
106
this .useFullyQualifiedModuleNames = useFullyQualifiedModuleNames ;
96
107
97
- this .springBeans = SingletonSupplier .of (() -> filterSpringBeans (basePackage ));
98
- this .aggregateRoots = SingletonSupplier .of (() -> findAggregateRoots (basePackage ));
108
+ this .springBeans = SingletonSupplier .of (() -> filterSpringBeans (classes ));
109
+ this .aggregateRoots = SingletonSupplier .of (() -> findAggregateRoots (classes ));
99
110
this .valueTypes = SingletonSupplier
100
111
.of (() -> findArchitecturallyEvidentType (ArchitecturallyEvidentType ::isValueObject ));
101
112
this .publishedEvents = SingletonSupplier .of (() -> findPublishedEvents ());
@@ -275,7 +286,7 @@ public ArchitecturallyEvidentType getArchitecturallyEvidentType(Class<?> type) {
275
286
}
276
287
277
288
public boolean contains (JavaClass type ) {
278
- return basePackage .contains (type );
289
+ return classes .contains (type );
279
290
}
280
291
281
292
public boolean contains (@ Nullable Class <?> type ) {
@@ -292,7 +303,7 @@ public Optional<JavaClass> getType(String candidate) {
292
303
293
304
Assert .hasText (candidate , "Candidate must not be null or emtpy!" );
294
305
295
- return basePackage .stream ()
306
+ return classes .stream ()
296
307
.filter (hasSimpleOrFullyQualifiedName (candidate ))
297
308
.findFirst ();
298
309
}
@@ -366,6 +377,19 @@ public String toString(@Nullable ApplicationModules modules) {
366
377
builder .append ("> Logical name: " ).append (getName ()).append ('\n' );
367
378
builder .append ("> Base package: " ).append (basePackage .getName ()).append ('\n' );
368
379
380
+ builder .append ("> Excluded packages: " );
381
+
382
+ if (!exclusions .iterator ().hasNext ()) {
383
+ builder .append ("none" ).append ('\n' );
384
+ } else {
385
+
386
+ builder .append ('\n' );
387
+
388
+ exclusions .stream ().forEach (it -> {
389
+ builder .append (" - " ).append (it .getName ()).append ('\n' );
390
+ });
391
+ }
392
+
369
393
if (namedInterfaces .hasExplicitInterfaces ()) {
370
394
371
395
builder .append ("> Named interfaces:\n " );
@@ -510,12 +534,21 @@ public int hashCode() {
510
534
useFullyQualifiedModuleNames , valueTypes );
511
535
}
512
536
537
+ /*
538
+ * (non-Javadoc)
539
+ * @see java.lang.Comparable#compareTo(java.lang.Object)
540
+ */
541
+ @ Override
542
+ public int compareTo (ApplicationModule o ) {
543
+ return getBasePackage ().compareTo (o .getBasePackage ());
544
+ }
545
+
513
546
private List <EventType > findPublishedEvents () {
514
547
515
548
DescribedPredicate <JavaClass > isEvent = implement (JMoleculesTypes .DOMAIN_EVENT ) //
516
549
.or (isAnnotatedWith (JMoleculesTypes .AT_DOMAIN_EVENT ));
517
550
518
- return basePackage .that (isEvent ).stream () //
551
+ return classes .that (isEvent ).stream () //
519
552
.map (EventType ::new ).toList ();
520
553
}
521
554
@@ -537,7 +570,7 @@ private Stream<JavaClass> resolveModuleSuperTypes(JavaClass type) {
537
570
538
571
private Stream <QualifiedDependency > getAllModuleDependencies (ApplicationModules modules ) {
539
572
540
- return basePackage .stream () //
573
+ return classes .stream () //
541
574
.flatMap (it -> getModuleDependenciesOf (it , modules ));
542
575
}
543
576
@@ -590,7 +623,7 @@ private boolean isDependencyToOtherModule(JavaClass dependency, ApplicationModul
590
623
return modules .contains (dependency ) && !contains (dependency );
591
624
}
592
625
593
- private Classes findAggregateRoots (JavaPackage source ) {
626
+ private Classes findAggregateRoots (Classes source ) {
594
627
595
628
return source .stream () //
596
629
.map (it -> ArchitecturallyEvidentType .of (it , getSpringBeansInternal ()))
@@ -599,11 +632,71 @@ private Classes findAggregateRoots(JavaPackage source) {
599
632
.collect (Classes .toClasses ());
600
633
}
601
634
635
+ /**
636
+ * Returns the current module's immediate parent module, if present.
637
+ *
638
+ * @param modules must not be {@literal null}.
639
+ * @return will never be {@literal null}.
640
+ * @since 1.3
641
+ */
642
+ Optional <ApplicationModule > getParentModule (ApplicationModules modules ) {
643
+
644
+ Assert .notNull (modules , "ApplicationModules must not be null!" );
645
+
646
+ var byPackageDepth = comparing (ApplicationModule ::getBasePackage , JavaPackage .reverse ());
647
+
648
+ return modules .stream ()
649
+ .filter (it -> basePackage .isSubPackageOf (it .getBasePackage ()))
650
+ .sorted (byPackageDepth )
651
+ .findFirst ();
652
+ }
653
+
654
+ /**
655
+ * Returns the {@link ApplicationModule}s directly nested inside the current one.
656
+ *
657
+ * @param modules must not be {@literal null}.
658
+ * @return will never be {@literal null}.
659
+ * @since 1.3
660
+ */
661
+ Collection <ApplicationModule > getDirectlyNestedModules (ApplicationModules modules ) {
662
+
663
+ Assert .notNull (modules , "ApplicationModules must not be null!" );
664
+
665
+ return doGetNestedModules (modules , false );
666
+ }
667
+
668
+ /**
669
+ * Returns all of the current {@link ApplicationModule}'s nested {@link ApplicationModule}s including ones contained
670
+ * in nested modules in turn.
671
+ *
672
+ * @param modules must not be {@literal null}.
673
+ * @return will never be {@literal null}.
674
+ * @since 1.3
675
+ */
676
+ Collection <ApplicationModule > getNestedModules (ApplicationModules modules ) {
677
+
678
+ Assert .notNull (modules , "ApplicationModules must not be null!" );
679
+
680
+ return doGetNestedModules (modules , true );
681
+ }
682
+
602
683
private String getQualifiedName (NamedInterface namedInterface ) {
603
684
return namedInterface .getQualifiedName (getName ());
604
685
}
605
686
606
- private static Classes filterSpringBeans (JavaPackage source ) {
687
+ private Collection <ApplicationModule > doGetNestedModules (ApplicationModules modules , boolean recursive ) {
688
+
689
+ var result = modules .stream ()
690
+ .filter (it -> it .getParentModule (modules ).filter (this ::equals ).isPresent ());
691
+
692
+ if (recursive ) {
693
+ result = result .flatMap (it -> Stream .concat (Stream .of (it ), it .getNestedModules (modules ).stream ()));
694
+ }
695
+
696
+ return result .toList ();
697
+ }
698
+
699
+ private static Classes filterSpringBeans (Classes source ) {
607
700
608
701
Map <Boolean , List <JavaClass >> collect = source .that (isConfiguration ()).stream () //
609
702
.flatMap (it -> it .getMethods ().stream ()) //
@@ -632,7 +725,7 @@ private List<JavaClass> findArchitecturallyEvidentType(Predicate<Architecturally
632
725
633
726
var springBeansInternal = getSpringBeansInternal ();
634
727
635
- return basePackage .stream ()
728
+ return classes .stream ()
636
729
.map (it -> ArchitecturallyEvidentType .of (it , springBeansInternal ))
637
730
.filter (selector )
638
731
.map (ArchitecturallyEvidentType ::getType )
@@ -907,6 +1000,9 @@ static class QualifiedDependency {
907
1000
908
1001
private static final List <String > INJECTION_TYPES = Arrays .asList (AT_AUTOWIRED , AT_RESOURCE , AT_INJECT );
909
1002
1003
+ private static final String INVALID_SUB_MODULE_REFERENCE = "Invalid sub-module reference from module '%s' to module '%s' (via %s -> %s)!" ;
1004
+ private static final String INTERNAL_REFERENCE = "Module '%s' depends on non-exposed type %s within module '%s'!" ;
1005
+
910
1006
private final JavaClass source , target ;
911
1007
private final String description ;
912
1008
private final DependencyType type ;
@@ -1045,13 +1141,41 @@ Violations isValidDependencyWithin(ApplicationModules modules) {
1045
1141
1046
1142
if (!targetModule .isExposed (target )) {
1047
1143
1048
- var violationText = "Module '%s' depends on non-exposed type %s within module '%s'!"
1144
+ var violationText = INTERNAL_REFERENCE
1049
1145
.formatted (originModule .getName (), target .getName (), targetModule .getName ());
1050
1146
1051
1147
return violations .and (new Violation (violationText + lineSeparator () + description ));
1052
1148
}
1053
1149
1054
- return violations ;
1150
+ // Parent child relationships
1151
+
1152
+ var targetParent = targetModule .getParentModule (modules );
1153
+
1154
+ if (targetParent .isEmpty ()) {
1155
+ return violations ;
1156
+ }
1157
+
1158
+ var originParent = originModule .getParentModule (modules );
1159
+
1160
+ if (targetParent .isPresent ()) {
1161
+
1162
+ var resolved = targetParent .get ();
1163
+
1164
+ if (resolved .equals (originModule )) {
1165
+ return violations ;
1166
+ }
1167
+
1168
+ if (originParent .isPresent () && originParent .get ().equals (targetModule )) {
1169
+ return violations ;
1170
+ }
1171
+ }
1172
+
1173
+ var violationText = INVALID_SUB_MODULE_REFERENCE
1174
+ .formatted (originModule .getName (), targetModule .getName (),
1175
+ FormatableType .of (source ).getAbbreviatedFullName (originModule ),
1176
+ FormatableType .of (target ).getAbbreviatedFullName (targetModule ));
1177
+
1178
+ return violations .and (new Violation (violationText ));
1055
1179
}
1056
1180
1057
1181
ApplicationModule getExistingModuleOf (JavaClass javaClass , ApplicationModules modules ) {
0 commit comments