Skip to content

Commit b3ac620

Browse files
committed
Introduce fluent API to build named interfaces.
Primary selection criterion is the trailing package name and a flag to switch between
1 parent faf4532 commit b3ac620

File tree

6 files changed

+185
-0
lines changed

6 files changed

+185
-0
lines changed

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

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import java.util.Objects;
2929
import java.util.Optional;
3030
import java.util.Set;
31+
import java.util.function.BiPredicate;
3132
import java.util.function.Predicate;
3233
import java.util.function.Supplier;
3334
import java.util.stream.Collectors;
@@ -197,6 +198,23 @@ public Stream<JavaPackage> getSubPackagesAnnotatedWith(Class<? extends Annotatio
197198
.map(it -> of(classes, it));
198199
}
199200

201+
/**
202+
* Returns all sub-packages that match the given {@link BiPredicate} for the canidate package and its trailing name
203+
* relative to the current one.
204+
*
205+
* @param filter must not be {@literal null}.
206+
* @return will never be {@literal null}.
207+
* @see #getTrailingName(JavaPackage)
208+
* @since 1.4
209+
*/
210+
public Stream<JavaPackage> getSubPackagesMatching(BiPredicate<JavaPackage, String> filter) {
211+
212+
Assert.notNull(filter, "Filter must not be null!");
213+
214+
return getSubPackages().stream()
215+
.filter(it -> filter.test(it, this.getTrailingName(it)));
216+
}
217+
200218
/**
201219
* Returns all {@link Classes} that match the given {@link DescribedPredicate}.
202220
*

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

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,13 @@
1616
package org.springframework.modulith.core;
1717

1818
import java.util.ArrayList;
19+
import java.util.Collection;
1920
import java.util.Collections;
2021
import java.util.Comparator;
2122
import java.util.Iterator;
2223
import java.util.List;
2324
import java.util.Optional;
25+
import java.util.function.Predicate;
2426
import java.util.stream.Collectors;
2527
import java.util.stream.Stream;
2628

@@ -82,6 +84,10 @@ public static NamedInterfaces discoverNamedInterfaces(JavaPackage basePackage) {
8284
.and(ofAnnotatedTypes(basePackage.getClasses()));
8385
}
8486

87+
public static Builder builder(JavaPackage basePackage) {
88+
return new Builder(basePackage, false, __ -> false, __ -> false);
89+
}
90+
8591
/**
8692
* Creates a new {@link NamedInterfaces} for the given {@link NamedInterface}s.
8793
*
@@ -280,4 +286,64 @@ private static List<NamedInterface> ofAnnotatedTypes(Classes classes) {
280286
.map(entry -> NamedInterface.of(entry.getKey(), Classes.of(entry.getValue()))) //
281287
.toList();
282288
}
289+
290+
public static class Builder {
291+
292+
private final JavaPackage basePackage;
293+
private final boolean recursive;
294+
private final Predicate<JavaPackage> inclusions, exclusions;
295+
296+
private Builder(JavaPackage basePackage, boolean recursive, Predicate<JavaPackage> predicate,
297+
Predicate<JavaPackage> exclusions) {
298+
299+
this.basePackage = basePackage;
300+
this.recursive = recursive;
301+
this.inclusions = predicate;
302+
this.exclusions = exclusions;
303+
}
304+
305+
public Builder recursive() {
306+
return new Builder(basePackage, true, inclusions, exclusions);
307+
}
308+
309+
public Builder matching(String... names) {
310+
return matching(List.of(names));
311+
}
312+
313+
public Builder matching(Collection<String> names) {
314+
return matching(matchesTrailingName(names));
315+
}
316+
317+
public Builder matching(Predicate<JavaPackage> predicate) {
318+
return new Builder(basePackage, recursive, predicate, exclusions);
319+
}
320+
321+
public Builder excluding(String... names) {
322+
323+
var exclusions = Predicate.not(matchesTrailingName(List.of(names)));
324+
325+
return new Builder(basePackage, recursive, inclusions, exclusions);
326+
}
327+
328+
private Predicate<JavaPackage> matchesTrailingName(Collection<String> names) {
329+
330+
return it -> {
331+
332+
var trailingName = new PackageName(basePackage.getTrailingName(it));
333+
334+
return names.stream().anyMatch(trailingName::nameContainsOrMatches);
335+
};
336+
}
337+
338+
public NamedInterfaces build() {
339+
340+
var packages = recursive
341+
? basePackage.getSubPackages().stream()
342+
: basePackage.getDirectSubPackages().stream();
343+
344+
return packages.filter(inclusions)
345+
.map(it -> NamedInterface.of(basePackage.getTrailingName(it), it.getClasses()))
346+
.collect(Collectors.collectingAndThen(Collectors.toUnmodifiableList(), NamedInterfaces::new));
347+
}
348+
}
283349
}

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

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

18+
import java.util.stream.Stream;
19+
1820
import org.springframework.util.Assert;
1921
import org.springframework.util.ClassUtils;
2022

@@ -162,6 +164,27 @@ boolean isEmpty() {
162164
return length() == 0;
163165
}
164166

167+
/**
168+
* Returns whether the package name contains a segment with the given candidate or matches the given expression
169+
* entirely. The latter is tested for if the expression contains a dot, indicating a multi-package match is requested.
170+
* Expressions generally support single character ({@literal ?}) and multi-character ({@literal *}) wildcards.
171+
*
172+
* @param candidate must not be {@literal null} or empty.
173+
* @since 1.4
174+
*/
175+
boolean nameContainsOrMatches(String candidate) {
176+
177+
Assert.hasText(candidate, "Expression must not be null or empty!");
178+
179+
var regex = candidate.replace(".", "\\.")
180+
.replace("*", ".*")
181+
.replace("?", ".");
182+
183+
return candidate.contains(".")
184+
? name.matches(regex)
185+
: Stream.of(segments).anyMatch(it -> it.matches(regex));
186+
}
187+
165188
/*
166189
* (non-Javadoc)
167190
* @see java.lang.Comparable#compareTo(java.lang.Object)
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package org.springframework.modulith.core;
2+
3+
import java.util.List;
4+
import java.util.stream.Stream;
5+
6+
class CustomApplicationModuleDetectionStrategy implements ApplicationModuleDetectionStrategy {
7+
8+
private static final List<String> NAMED_INTERFACE_PACKAGE_NAMES = List.of("mapper", "model", "repository", "service",
9+
"web");
10+
11+
private static final List<String> INTERNAL_PACKAGE_NAME = List.of("internal");
12+
13+
@Override
14+
public Stream<JavaPackage> getModuleBasePackages(JavaPackage basePackage) {
15+
16+
var allExclusions = Stream.concat(NAMED_INTERFACE_PACKAGE_NAMES.stream(), INTERNAL_PACKAGE_NAME.stream());
17+
18+
// New method to be introduced on JavaPackage
19+
return basePackage.getSubPackagesMatching((pkg, trailingName) -> allExclusions.noneMatch(trailingName::contains));
20+
}
21+
22+
@Override
23+
public NamedInterfaces detectNamedInterfaces(JavaPackage basePackage, ApplicationModuleInformation information) {
24+
25+
return NamedInterfaces.builder(basePackage)
26+
.matching(NAMED_INTERFACE_PACKAGE_NAMES)
27+
.recursive()
28+
.build();
29+
}
30+
}

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

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,37 @@ void detectsNamedInterfacesATypeIsContainedIn() {
9898
.containsExactlyInAnyOrder("spi", "kpi");
9999
}
100100

101+
@Test
102+
void createsNamedInterfacesFromBuilder() {
103+
104+
JavaPackage pkg = TestUtils.getPackage(RootType.class);
105+
106+
var result = NamedInterfaces.builder(pkg)
107+
.excluding("internal")
108+
.matching("nested")
109+
.build();
110+
111+
assertThat(result).hasSize(1)
112+
.extracting(NamedInterface::getName)
113+
.containsExactlyInAnyOrder("nested");
114+
}
115+
116+
@Test
117+
void createsNamedInterfacesFromRecursiveBuilder() {
118+
119+
JavaPackage pkg = TestUtils.getPackage(RootType.class);
120+
121+
var result = NamedInterfaces.builder(pkg)
122+
.excluding("internal")
123+
.matching("nested")
124+
.recursive()
125+
.build();
126+
127+
assertThat(result).hasSize(5)
128+
.extracting(NamedInterface::getName)
129+
.containsExactlyInAnyOrder("nested", "nested.a", "nested.b", "nested.b.first", "nested.b.second");
130+
}
131+
101132
private static void assertInterfaceContains(NamedInterfaces interfaces, String name, Class<?>... types) {
102133

103134
var classNames = Arrays.stream(types).map(Class::getName).toArray(String[]::new);

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

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,4 +55,21 @@ void caculatesNestingCorrectly() {
5555
assertThat(comAcme.contains(comAcmeA)).isTrue();
5656
assertThat(comAcmeA.contains(comAcme)).isFalse();
5757
}
58+
59+
@Test
60+
void findsMatchingSegments() {
61+
62+
var source = new PackageName("com.acme.foo");
63+
64+
assertThat(source.nameContainsOrMatches("acme")).isTrue();
65+
assertThat(source.nameContainsOrMatches("*me")).isTrue();
66+
assertThat(source.nameContainsOrMatches("ac*")).isTrue();
67+
assertThat(source.nameContainsOrMatches("*m.acme.foo")).isTrue();
68+
assertThat(source.nameContainsOrMatches("*m.acme.?oo")).isTrue();
69+
assertThat(source.nameContainsOrMatches("*m.ac*")).isTrue();
70+
assertThat(source.nameContainsOrMatches("*m.*.fo*")).isTrue();
71+
72+
assertThat(source.nameContainsOrMatches("cm")).isFalse();
73+
74+
}
5875
}

0 commit comments

Comments
 (0)