Skip to content

Commit 1cc5719

Browse files
committed
GH-1041 - Revamp JavaPackage's sub-package traversal to retain empty intermediate packages.
1 parent 003c428 commit 1cc5719

File tree

6 files changed

+189
-57
lines changed

6 files changed

+189
-57
lines changed

spring-modulith-core/src/main/java/org/springframework/modulith/core/JavaPackage.java

Lines changed: 36 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ public class JavaPackage implements DescribedIterable<JavaClass>, Comparable<Jav
5959
private final PackageName name;
6060
private final Classes classes, packageClasses;
6161
private final Supplier<Set<JavaPackage>> directSubPackages;
62+
private final Supplier<JavaPackages> subPackages;
6263

6364
/**
6465
* Creates a new {@link JavaPackage} for the given {@link Classes}, name and whether to include all sub-packages.
@@ -75,13 +76,13 @@ private JavaPackage(Classes classes, PackageName name, boolean includeSubPackage
7576
this.packageClasses = classes
7677
.that(resideInAPackage(name.asFilter(includeSubPackages)));
7778
this.name = name;
78-
this.directSubPackages = SingletonSupplier.of(() -> packageClasses.stream() //
79-
.map(it -> it.getPackageName()) //
80-
.filter(Predicate.not(name::hasName)) //
81-
.map(it -> extractDirectSubPackage(it)) //
82-
.distinct() //
83-
.map(it -> of(classes, it)) //
84-
.collect(Collectors.toSet()));
79+
80+
this.directSubPackages = () -> detectSubPackages()
81+
.filter(this::isDirectParentOf)
82+
.collect(Collectors.toUnmodifiableSet());
83+
84+
this.subPackages = SingletonSupplier.of(() -> detectSubPackages()
85+
.collect(collectingAndThen(toUnmodifiableList(), JavaPackages::new)));
8586
}
8687

8788
/**
@@ -92,7 +93,7 @@ private JavaPackage(Classes classes, PackageName name, boolean includeSubPackage
9293
* @return
9394
*/
9495
public static JavaPackage of(Classes classes, String name) {
95-
return new JavaPackage(classes, new PackageName(name), true);
96+
return new JavaPackage(classes, PackageName.of(name), true);
9697
}
9798

9899
/**
@@ -333,13 +334,7 @@ Classes getClasses(Iterable<JavaPackage> exclusions) {
333334
* @since 1.3
334335
*/
335336
JavaPackages getSubPackages() {
336-
337-
return packageClasses.stream() //
338-
.map(JavaClass::getPackageName)
339-
.filter(Predicate.not(name::hasName))
340-
.distinct()
341-
.map(it -> new JavaPackage(classes, new PackageName(it), true))
342-
.collect(collectingAndThen(toUnmodifiableList(), JavaPackages::new));
337+
return subPackages.get();
343338
}
344339

345340
/**
@@ -390,6 +385,21 @@ public <A extends Annotation> Optional<A> findAnnotation(Class<A> annotationType
390385
});
391386
}
392387

388+
/**
389+
* Returns whether the current {@link JavaPackage} is the direct parent of the given one.
390+
*
391+
* @param reference must not be {@literal null}.
392+
* @since 1.4
393+
*/
394+
boolean isDirectParentOf(JavaPackage reference) {
395+
396+
Assert.notNull(reference, "Reference JavaPackage must not be null!");
397+
398+
var name = reference.getPackageName();
399+
400+
return name.hasParent() && this.getPackageName().equals(name.getParent());
401+
}
402+
393403
/*
394404
* (non-Javadoc)
395405
* @see com.tngtech.archunit.base.HasDescription#getDescription()
@@ -399,6 +409,17 @@ public String getDescription() {
399409
return classes.getDescription();
400410
}
401411

412+
private Stream<JavaPackage> detectSubPackages() {
413+
414+
return packageClasses.stream() //
415+
.map(JavaClass::getPackageName)
416+
.filter(Predicate.not(name::hasName))
417+
.map(PackageName::of)
418+
.flatMap(name::expandUntil)
419+
.distinct()
420+
.map(it -> new JavaPackage(classes, it, true));
421+
}
422+
402423
/*
403424
* (non-Javadoc)
404425
* @see java.lang.Iterable#iterator()
@@ -461,24 +482,6 @@ public int hashCode() {
461482
return Objects.hash(classes, directSubPackages.get(), name, packageClasses);
462483
}
463484

464-
/**
465-
* Extract the direct sub-package name of the given candidate.
466-
*
467-
* @param candidate
468-
* @return will never be {@literal null}.
469-
*/
470-
private String extractDirectSubPackage(String candidate) {
471-
472-
if (candidate.length() <= name.length()) {
473-
return candidate;
474-
}
475-
476-
int subSubPackageIndex = candidate.indexOf('.', name.length() + 1);
477-
int endIndex = subSubPackageIndex == -1 ? candidate.length() : subSubPackageIndex;
478-
479-
return candidate.substring(0, endIndex);
480-
}
481-
482485
static Comparator<JavaPackage> reverse() {
483486
return (left, right) -> -left.compareTo(right);
484487
}

spring-modulith-core/src/main/java/org/springframework/modulith/core/PackageName.java

Lines changed: 100 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,13 @@
1515
*/
1616
package org.springframework.modulith.core;
1717

18+
import java.util.Arrays;
19+
import java.util.HashMap;
20+
import java.util.Map;
21+
import java.util.stream.Collectors;
22+
import java.util.stream.Stream;
23+
24+
import org.springframework.lang.Nullable;
1825
import org.springframework.util.Assert;
1926
import org.springframework.util.ClassUtils;
2027

@@ -27,6 +34,8 @@
2734
*/
2835
class PackageName implements Comparable<PackageName> {
2936

37+
private static final Map<String, PackageName> PACKAGE_NAMES = new HashMap<>();
38+
3039
private final String name;
3140
private final String[] segments;
3241

@@ -35,12 +44,23 @@ class PackageName implements Comparable<PackageName> {
3544
*
3645
* @param name must not be {@literal null}.
3746
*/
38-
public PackageName(String name) {
47+
private PackageName(String name) {
48+
this(name, name.split("\\."));
49+
}
50+
51+
/**
52+
* Creates a new {@link PackageName} with the given name and segments.
53+
*
54+
* @param name must not be {@literal null}.
55+
* @param segments must not be {@literal null}.
56+
*/
57+
private PackageName(String name, String[] segments) {
3958

4059
Assert.notNull(name, "Name must not be null!");
60+
Assert.notNull(segments, "Segments must not be null!");
4161

4262
this.name = name;
43-
this.segments = name.split("\\.");
63+
this.segments = segments;
4464
}
4565

4666
/**
@@ -49,11 +69,41 @@ public PackageName(String name) {
4969
* @param fullyQualifiedName must not be {@literal null} or empty.
5070
* @return will never be {@literal null}.
5171
*/
52-
public static PackageName ofType(String fullyQualifiedName) {
72+
static PackageName ofType(String fullyQualifiedName) {
5373

5474
Assert.notNull(fullyQualifiedName, "Type name must not be null!");
5575

56-
return new PackageName(ClassUtils.getPackageName(fullyQualifiedName));
76+
return PackageName.of(ClassUtils.getPackageName(fullyQualifiedName));
77+
}
78+
79+
/**
80+
* Returns the {@link PackageName} with the given name.
81+
*
82+
* @param name must not be {@literal null}.
83+
* @return will never be {@literal null}.
84+
* @since 1.4
85+
*/
86+
static PackageName of(String name) {
87+
88+
Assert.notNull(name, "Name must not be null!");
89+
90+
return PACKAGE_NAMES.computeIfAbsent(name, PackageName::new);
91+
}
92+
93+
/**
94+
* Returns the {@link PackageName} for the given segments.
95+
*
96+
* @param segments must not be {@literal null}.
97+
* @return will never be {@literal null}.
98+
* @since 1.4
99+
*/
100+
static PackageName of(String[] segments) {
101+
102+
Assert.notNull(segments, "Segments must not be null!");
103+
104+
var name = Stream.of(segments).collect(Collectors.joining("."));
105+
106+
return PACKAGE_NAMES.computeIfAbsent(name, it -> new PackageName(name, segments));
57107
}
58108

59109
/**
@@ -132,6 +182,10 @@ boolean isParentPackageOf(PackageName reference) {
132182
return reference.name.startsWith(name + ".");
133183
}
134184

185+
boolean isDirectParentOf(PackageName reference) {
186+
return this.equals(getParent());
187+
}
188+
135189
/**
136190
* Returns whether the package name contains the given one, i.e. if the given one either is the current one or a
137191
* sub-package of it.
@@ -185,6 +239,48 @@ public int compareTo(PackageName o) {
185239
return segments.length - o.segments.length;
186240
}
187241

242+
/**
243+
* Returns the names of sub-packages of the current one until the given reference {@link PackageName}.
244+
*
245+
* @param reference must not be {@literal null}.
246+
* @return will never be {@literal null}.
247+
* @since 1.4
248+
*/
249+
Stream<PackageName> expandUntil(PackageName reference) {
250+
251+
Assert.notNull(reference, "Reference must not be null!");
252+
253+
if (!reference.isSubPackageOf(this) || !reference.hasParent()) {
254+
return Stream.empty();
255+
}
256+
257+
if (isDirectParentOf(reference)) {
258+
return Stream.of(reference);
259+
}
260+
261+
return Stream.concat(expandUntil(reference.getParent()), Stream.of(reference));
262+
}
263+
264+
/**
265+
* Returns whether the current {@link PackageName} has a parent.
266+
*
267+
* @since 1.4
268+
*/
269+
boolean hasParent() {
270+
return segments.length > 1;
271+
}
272+
273+
/**
274+
* Returns the parent {@link PackageName}.
275+
*
276+
* @return can be {@literal null}.
277+
* @since 1.4
278+
*/
279+
@Nullable
280+
PackageName getParent() {
281+
return PackageName.of(Arrays.copyOf(segments, segments.length - 1));
282+
}
283+
188284
/*
189285
* (non-Javadoc)
190286
* @see java.lang.Object#toString()

spring-modulith-core/src/test/java/org/springframework/modulith/core/JavaPackageUnitTests.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,4 +116,16 @@ void samePackagesConsideredEqual() {
116116
assertThat(first.equals(second)).isTrue();
117117
assertThat(second.equals(first)).isTrue();
118118
}
119+
120+
@Test // GH-1039
121+
void detectsIntermediateSubPackages() {
122+
123+
var packages = TestUtils.getPackage("with").getSubPackages();
124+
125+
assertThat(packages)
126+
.extracting(JavaPackage::getName)
127+
.containsExactly("with.many",
128+
"with.many.intermediate",
129+
"with.many.intermediate.packages");
130+
}
119131
}

spring-modulith-core/src/test/java/org/springframework/modulith/core/PackageNameUnitTests.java

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -31,12 +31,12 @@ class PackageNameUnitTests {
3131
@Test // GH-578
3232
void sortsPackagesByNameAndDepth() {
3333

34-
var comAcme = new PackageName("com.acme");
35-
var comAcmeA = new PackageName("com.acme.a");
36-
var comAcmeAFirst = new PackageName("com.acme.a.first");
37-
var comAcmeAFirstOne = new PackageName("com.acme.a.first.one");
38-
var comAcmeASecond = new PackageName("com.acme.a.second");
39-
var comAcmeB = new PackageName("com.acme.b");
34+
var comAcme = PackageName.of("com.acme");
35+
var comAcmeA = PackageName.of("com.acme.a");
36+
var comAcmeAFirst = PackageName.of("com.acme.a.first");
37+
var comAcmeAFirstOne = PackageName.of("com.acme.a.first.one");
38+
var comAcmeASecond = PackageName.of("com.acme.a.second");
39+
var comAcmeB = PackageName.of("com.acme.b");
4040

4141
assertThat(List.of(comAcmeAFirstOne, comAcmeB, comAcmeASecond, comAcmeAFirst, comAcme, comAcmeA)
4242
.stream()
@@ -48,8 +48,8 @@ void sortsPackagesByNameAndDepth() {
4848
@Test // GH-802
4949
void caculatesNestingCorrectly() {
5050

51-
var comAcme = new PackageName("com.acme");
52-
var comAcmeA = new PackageName("com.acme.a");
51+
var comAcme = PackageName.of("com.acme");
52+
var comAcmeA = PackageName.of("com.acme.a");
5353

5454
assertThat(comAcme.contains(comAcme)).isTrue();
5555
assertThat(comAcme.contains(comAcmeA)).isTrue();

spring-modulith-core/src/test/java/org/springframework/modulith/core/TestUtils.java

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -85,28 +85,28 @@ public static Classes getClasses(Class<?> packageType) {
8585
.that(resideInAPackage(packageType.getPackage().getName() + "..")));
8686
}
8787

88-
public static JavaPackage getPackage(Class<?> packageType) {
89-
return JavaPackage.of(TestUtils.getClasses(packageType), packageType.getPackageName());
90-
}
88+
public static Classes getClasses(String packageName) {
9189

92-
public static ApplicationModule getApplicationModule(String packageName) {
90+
Assert.hasText(packageName, "Package name must not be null or empty!");
9391

94-
var pkg = getPackage(packageName);
95-
var source = ApplicationModuleSource.from(pkg, pkg.getLocalName());
92+
return Classes.of(new ClassFileImporter()
93+
.importPackages(packageName));
94+
}
9695

97-
return new ApplicationModule(source);
96+
public static JavaPackage getPackage(Class<?> packageType) {
97+
return JavaPackage.of(TestUtils.getClasses(packageType), packageType.getPackageName());
9898
}
9999

100-
private static JavaPackage getPackage(String name) {
100+
public static JavaPackage getPackage(String name) {
101101
return JavaPackage.of(getClasses(name), name);
102102
}
103103

104-
private static Classes getClasses(String packageName) {
104+
public static ApplicationModule getApplicationModule(String packageName) {
105105

106-
Assert.hasText(packageName, "Package name must not be null or empty!");
106+
var pkg = getPackage(packageName);
107+
var source = ApplicationModuleSource.from(pkg, pkg.getLocalName());
107108

108-
return Classes.of(new ClassFileImporter()
109-
.importPackages(packageName));
109+
return new ApplicationModule(source);
110110
}
111111

112112
private static ApplicationModules of(ModulithMetadata metadata, DescribedPredicate<JavaClass> ignores) {
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/*
2+
* Copyright 2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package with.many.intermediate.packages;
17+
18+
/**
19+
* @author Oliver Drotbohm
20+
*/
21+
class Marker {}

0 commit comments

Comments
 (0)