GH-1039 - Revamp JavaPackage's sub-package traversal to retain empty intermediate packages.
This commit is contained in:
@@ -60,6 +60,7 @@ public class JavaPackage implements DescribedIterable<JavaClass>, Comparable<Jav
|
||||
private final PackageName name;
|
||||
private final Classes classes, packageClasses;
|
||||
private final Supplier<Set<JavaPackage>> directSubPackages;
|
||||
private final Supplier<JavaPackages> subPackages;
|
||||
|
||||
/**
|
||||
* Creates a new {@link JavaPackage} for the given {@link Classes}, name and whether to include all sub-packages.
|
||||
@@ -76,13 +77,13 @@ public class JavaPackage implements DescribedIterable<JavaClass>, Comparable<Jav
|
||||
this.packageClasses = classes
|
||||
.that(resideInAPackage(name.asFilter(includeSubPackages)));
|
||||
this.name = name;
|
||||
this.directSubPackages = SingletonSupplier.of(() -> packageClasses.stream() //
|
||||
.map(it -> it.getPackageName()) //
|
||||
.filter(Predicate.not(name::hasName)) //
|
||||
.map(it -> extractDirectSubPackage(it)) //
|
||||
.distinct() //
|
||||
.map(it -> of(classes, it)) //
|
||||
.collect(Collectors.toSet()));
|
||||
|
||||
this.directSubPackages = () -> detectSubPackages()
|
||||
.filter(this::isDirectParentOf)
|
||||
.collect(Collectors.toUnmodifiableSet());
|
||||
|
||||
this.subPackages = SingletonSupplier.of(() -> detectSubPackages()
|
||||
.collect(collectingAndThen(toUnmodifiableList(), JavaPackages::new)));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -93,7 +94,7 @@ public class JavaPackage implements DescribedIterable<JavaClass>, Comparable<Jav
|
||||
* @return
|
||||
*/
|
||||
public static JavaPackage of(Classes classes, String name) {
|
||||
return new JavaPackage(classes, new PackageName(name), true);
|
||||
return new JavaPackage(classes, PackageName.of(name), true);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -351,13 +352,7 @@ public class JavaPackage implements DescribedIterable<JavaClass>, Comparable<Jav
|
||||
* @since 1.3
|
||||
*/
|
||||
JavaPackages getSubPackages() {
|
||||
|
||||
return packageClasses.stream() //
|
||||
.map(JavaClass::getPackageName)
|
||||
.filter(Predicate.not(name::hasName))
|
||||
.distinct()
|
||||
.map(it -> new JavaPackage(classes, new PackageName(it), true))
|
||||
.collect(collectingAndThen(toUnmodifiableList(), JavaPackages::new));
|
||||
return subPackages.get();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -408,6 +403,21 @@ public class JavaPackage implements DescribedIterable<JavaClass>, Comparable<Jav
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the current {@link JavaPackage} is the direct parent of the given one.
|
||||
*
|
||||
* @param reference must not be {@literal null}.
|
||||
* @since 1.4
|
||||
*/
|
||||
boolean isDirectParentOf(JavaPackage reference) {
|
||||
|
||||
Assert.notNull(reference, "Reference JavaPackage must not be null!");
|
||||
|
||||
var name = reference.getPackageName();
|
||||
|
||||
return name.hasParent() && this.getPackageName().equals(name.getParent());
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see com.tngtech.archunit.base.HasDescription#getDescription()
|
||||
@@ -417,6 +427,17 @@ public class JavaPackage implements DescribedIterable<JavaClass>, Comparable<Jav
|
||||
return classes.getDescription();
|
||||
}
|
||||
|
||||
private Stream<JavaPackage> detectSubPackages() {
|
||||
|
||||
return packageClasses.stream() //
|
||||
.map(JavaClass::getPackageName)
|
||||
.filter(Predicate.not(name::hasName))
|
||||
.map(PackageName::of)
|
||||
.flatMap(name::expandUntil)
|
||||
.distinct()
|
||||
.map(it -> new JavaPackage(classes, it, true));
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see java.lang.Iterable#iterator()
|
||||
@@ -479,24 +500,6 @@ public class JavaPackage implements DescribedIterable<JavaClass>, Comparable<Jav
|
||||
return Objects.hash(classes, directSubPackages.get(), name, packageClasses);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the direct sub-package name of the given candidate.
|
||||
*
|
||||
* @param candidate
|
||||
* @return will never be {@literal null}.
|
||||
*/
|
||||
private String extractDirectSubPackage(String candidate) {
|
||||
|
||||
if (candidate.length() <= name.length()) {
|
||||
return candidate;
|
||||
}
|
||||
|
||||
int subSubPackageIndex = candidate.indexOf('.', name.length() + 1);
|
||||
int endIndex = subSubPackageIndex == -1 ? candidate.length() : subSubPackageIndex;
|
||||
|
||||
return candidate.substring(0, endIndex);
|
||||
}
|
||||
|
||||
static Comparator<JavaPackage> reverse() {
|
||||
return (left, right) -> -left.compareTo(right);
|
||||
}
|
||||
|
||||
@@ -456,7 +456,7 @@ public class NamedInterfaces implements Iterable<NamedInterface> {
|
||||
|
||||
return it -> {
|
||||
|
||||
var trailingName = new PackageName(basePackage.getTrailingName(it));
|
||||
var trailingName = PackageName.of(basePackage.getTrailingName(it));
|
||||
|
||||
return names.stream().anyMatch(trailingName::nameContainsOrMatches);
|
||||
};
|
||||
|
||||
@@ -15,8 +15,13 @@
|
||||
*/
|
||||
package org.springframework.modulith.core;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ClassUtils;
|
||||
|
||||
@@ -29,6 +34,8 @@ import org.springframework.util.ClassUtils;
|
||||
*/
|
||||
class PackageName implements Comparable<PackageName> {
|
||||
|
||||
private static final Map<String, PackageName> PACKAGE_NAMES = new HashMap<>();
|
||||
|
||||
private final String name;
|
||||
private final String[] segments;
|
||||
|
||||
@@ -37,12 +44,23 @@ class PackageName implements Comparable<PackageName> {
|
||||
*
|
||||
* @param name must not be {@literal null}.
|
||||
*/
|
||||
public PackageName(String name) {
|
||||
private PackageName(String name) {
|
||||
this(name, name.split("\\."));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new {@link PackageName} with the given name and segments.
|
||||
*
|
||||
* @param name must not be {@literal null}.
|
||||
* @param segments must not be {@literal null}.
|
||||
*/
|
||||
private PackageName(String name, String[] segments) {
|
||||
|
||||
Assert.notNull(name, "Name must not be null!");
|
||||
Assert.notNull(segments, "Segments must not be null!");
|
||||
|
||||
this.name = name;
|
||||
this.segments = name.split("\\.");
|
||||
this.segments = segments;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -51,11 +69,41 @@ class PackageName implements Comparable<PackageName> {
|
||||
* @param fullyQualifiedName must not be {@literal null} or empty.
|
||||
* @return will never be {@literal null}.
|
||||
*/
|
||||
public static PackageName ofType(String fullyQualifiedName) {
|
||||
static PackageName ofType(String fullyQualifiedName) {
|
||||
|
||||
Assert.notNull(fullyQualifiedName, "Type name must not be null!");
|
||||
|
||||
return new PackageName(ClassUtils.getPackageName(fullyQualifiedName));
|
||||
return PackageName.of(ClassUtils.getPackageName(fullyQualifiedName));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@link PackageName} with the given name.
|
||||
*
|
||||
* @param name must not be {@literal null}.
|
||||
* @return will never be {@literal null}.
|
||||
* @since 1.4
|
||||
*/
|
||||
static PackageName of(String name) {
|
||||
|
||||
Assert.notNull(name, "Name must not be null!");
|
||||
|
||||
return PACKAGE_NAMES.computeIfAbsent(name, PackageName::new);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@link PackageName} for the given segments.
|
||||
*
|
||||
* @param segments must not be {@literal null}.
|
||||
* @return will never be {@literal null}.
|
||||
* @since 1.4
|
||||
*/
|
||||
static PackageName of(String[] segments) {
|
||||
|
||||
Assert.notNull(segments, "Segments must not be null!");
|
||||
|
||||
var name = Stream.of(segments).collect(Collectors.joining("."));
|
||||
|
||||
return PACKAGE_NAMES.computeIfAbsent(name, it -> new PackageName(name, segments));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -134,6 +182,10 @@ class PackageName implements Comparable<PackageName> {
|
||||
return reference.name.startsWith(name + ".");
|
||||
}
|
||||
|
||||
boolean isDirectParentOf(PackageName reference) {
|
||||
return this.equals(getParent());
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
@@ -208,6 +260,48 @@ class PackageName implements Comparable<PackageName> {
|
||||
return segments.length - o.segments.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the names of sub-packages of the current one until the given reference {@link PackageName}.
|
||||
*
|
||||
* @param reference must not be {@literal null}.
|
||||
* @return will never be {@literal null}.
|
||||
* @since 1.4
|
||||
*/
|
||||
Stream<PackageName> expandUntil(PackageName reference) {
|
||||
|
||||
Assert.notNull(reference, "Reference must not be null!");
|
||||
|
||||
if (!reference.isSubPackageOf(this) || !reference.hasParent()) {
|
||||
return Stream.empty();
|
||||
}
|
||||
|
||||
if (isDirectParentOf(reference)) {
|
||||
return Stream.of(reference);
|
||||
}
|
||||
|
||||
return Stream.concat(expandUntil(reference.getParent()), Stream.of(reference));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the current {@link PackageName} has a parent.
|
||||
*
|
||||
* @since 1.4
|
||||
*/
|
||||
boolean hasParent() {
|
||||
return segments.length > 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the parent {@link PackageName}.
|
||||
*
|
||||
* @return can be {@literal null}.
|
||||
* @since 1.4
|
||||
*/
|
||||
@Nullable
|
||||
PackageName getParent() {
|
||||
return PackageName.of(Arrays.copyOf(segments, segments.length - 1));
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see java.lang.Object#toString()
|
||||
|
||||
@@ -116,4 +116,16 @@ class JavaPackageUnitTests {
|
||||
assertThat(first.equals(second)).isTrue();
|
||||
assertThat(second.equals(first)).isTrue();
|
||||
}
|
||||
|
||||
@Test // GH-1039
|
||||
void detectsIntermediateSubPackages() {
|
||||
|
||||
var packages = TestUtils.getPackage("with").getSubPackages();
|
||||
|
||||
assertThat(packages)
|
||||
.extracting(JavaPackage::getName)
|
||||
.containsExactly("with.many",
|
||||
"with.many.intermediate",
|
||||
"with.many.intermediate.packages");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,12 +31,12 @@ class PackageNameUnitTests {
|
||||
@Test // GH-578
|
||||
void sortsPackagesByNameAndDepth() {
|
||||
|
||||
var comAcme = new PackageName("com.acme");
|
||||
var comAcmeA = new PackageName("com.acme.a");
|
||||
var comAcmeAFirst = new PackageName("com.acme.a.first");
|
||||
var comAcmeAFirstOne = new PackageName("com.acme.a.first.one");
|
||||
var comAcmeASecond = new PackageName("com.acme.a.second");
|
||||
var comAcmeB = new PackageName("com.acme.b");
|
||||
var comAcme = PackageName.of("com.acme");
|
||||
var comAcmeA = PackageName.of("com.acme.a");
|
||||
var comAcmeAFirst = PackageName.of("com.acme.a.first");
|
||||
var comAcmeAFirstOne = PackageName.of("com.acme.a.first.one");
|
||||
var comAcmeASecond = PackageName.of("com.acme.a.second");
|
||||
var comAcmeB = PackageName.of("com.acme.b");
|
||||
|
||||
assertThat(List.of(comAcmeAFirstOne, comAcmeB, comAcmeASecond, comAcmeAFirst, comAcme, comAcmeA)
|
||||
.stream()
|
||||
@@ -48,8 +48,8 @@ class PackageNameUnitTests {
|
||||
@Test // GH-802
|
||||
void caculatesNestingCorrectly() {
|
||||
|
||||
var comAcme = new PackageName("com.acme");
|
||||
var comAcmeA = new PackageName("com.acme.a");
|
||||
var comAcme = PackageName.of("com.acme");
|
||||
var comAcmeA = PackageName.of("com.acme.a");
|
||||
|
||||
assertThat(comAcme.contains(comAcme)).isTrue();
|
||||
assertThat(comAcme.contains(comAcmeA)).isTrue();
|
||||
@@ -59,7 +59,7 @@ class PackageNameUnitTests {
|
||||
@Test
|
||||
void findsMatchingSegments() {
|
||||
|
||||
var source = new PackageName("com.acme.foo");
|
||||
var source = PackageName.of("com.acme.foo");
|
||||
|
||||
assertThat(source.nameContainsOrMatches("acme")).isTrue();
|
||||
assertThat(source.nameContainsOrMatches("*me")).isTrue();
|
||||
|
||||
@@ -85,10 +85,22 @@ public class TestUtils {
|
||||
.that(resideInAPackage(packageType.getPackage().getName() + "..")));
|
||||
}
|
||||
|
||||
public static Classes getClasses(String packageName) {
|
||||
|
||||
Assert.hasText(packageName, "Package name must not be null or empty!");
|
||||
|
||||
return Classes.of(new ClassFileImporter()
|
||||
.importPackages(packageName));
|
||||
}
|
||||
|
||||
public static JavaPackage getPackage(Class<?> packageType) {
|
||||
return JavaPackage.of(TestUtils.getClasses(packageType), packageType.getPackageName());
|
||||
}
|
||||
|
||||
public static JavaPackage getPackage(String name) {
|
||||
return JavaPackage.of(getClasses(name), name);
|
||||
}
|
||||
|
||||
public static ApplicationModule getApplicationModule(String packageName) {
|
||||
|
||||
var pkg = getPackage(packageName);
|
||||
@@ -97,18 +109,6 @@ public class TestUtils {
|
||||
return new ApplicationModule(source);
|
||||
}
|
||||
|
||||
private static JavaPackage getPackage(String name) {
|
||||
return JavaPackage.of(getClasses(name), name);
|
||||
}
|
||||
|
||||
private static Classes getClasses(String packageName) {
|
||||
|
||||
Assert.hasText(packageName, "Package name must not be null or empty!");
|
||||
|
||||
return Classes.of(new ClassFileImporter()
|
||||
.importPackages(packageName));
|
||||
}
|
||||
|
||||
private static ApplicationModules of(ModulithMetadata metadata, DescribedPredicate<JavaClass> ignores) {
|
||||
return new ApplicationModules(metadata, ignores, new ImportOption.OnlyIncludeTests()) {};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
/*
|
||||
* Copyright 2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package with.many.intermediate.packages;
|
||||
|
||||
/**
|
||||
* @author Oliver Drotbohm
|
||||
*/
|
||||
class Marker {}
|
||||
Reference in New Issue
Block a user