GH-1009 - Allow customizing the detection of NamedInterfaces via ApplicationModuleDetectionStrategy.
The detection of NamedInterfaces for an application module can now be customized by implementing ApplicationModuleDetectionStrategy.detectNamedInterfaces(…). The default implementation of that methods uses the previously default NamedInterfaces.of(…) lookup. NamedInterfaces and NamedInterface now expose factory methods to be able to construct and combine (through NamedInterfaces.and(…)) instances manually. In particular, the newly introduced NamedInterfaces.builder() method allows setting up a detection configuration that allows picking up packages by naming conventions.
This commit is contained in:
@@ -109,9 +109,7 @@ public class ApplicationModule implements Comparable<ApplicationModule> {
|
||||
this.exclusions = exclusions;
|
||||
this.classes = basePackage.getClasses(exclusions);
|
||||
this.information = ApplicationModuleInformation.of(basePackage);
|
||||
this.namedInterfaces = isOpen()
|
||||
? NamedInterfaces.forOpen(basePackage)
|
||||
: NamedInterfaces.discoverNamedInterfaces(basePackage);
|
||||
this.namedInterfaces = source.getNamedInterfaces(information);
|
||||
|
||||
this.springBeans = SingletonSupplier.of(() -> filterSpringBeans(classes));
|
||||
this.aggregateRoots = SingletonSupplier.of(() -> findAggregateRoots(classes));
|
||||
|
||||
@@ -29,13 +29,30 @@ import org.springframework.modulith.core.Types.JMoleculesTypes;
|
||||
public interface ApplicationModuleDetectionStrategy {
|
||||
|
||||
/**
|
||||
* Given the {@link JavaPackage} that Moduliths was initialized with, return the base packages for all modules in the
|
||||
* system.
|
||||
* Given the {@link JavaPackage} that Spring Modulith was initialized with, return the base packages that are supposed
|
||||
* to be considered base packages for {@link ApplicationModule}s.
|
||||
*
|
||||
* @param basePackage will never be {@literal null}.
|
||||
* @param rootPackage will never be {@literal null}.
|
||||
* @return must not be {@literal null}.
|
||||
*/
|
||||
Stream<JavaPackage> getModuleBasePackages(JavaPackage basePackage);
|
||||
Stream<JavaPackage> getModuleBasePackages(JavaPackage rootPackage);
|
||||
|
||||
/**
|
||||
* Optionally customize the detection of {@link NamedInterfaces} for a module with the given base package and the
|
||||
* pre-obtained {@link ApplicationModuleInformation}. Defaults to
|
||||
* {@link NamedInterfaces#of(JavaPackage, ApplicationModuleInformation)}. {@link NamedInterfaces} exposes a
|
||||
* {@link NamedInterfaces.Builder} API to define the selection of packages to be considered named interfaces.
|
||||
*
|
||||
* @param basePackage will never be {@literal null}.
|
||||
* @param information will never be {@literal null}.
|
||||
* @return must not be {@literal null}.
|
||||
* @see NamedInterfaces#of(JavaPackage, ApplicationModuleInformation)
|
||||
* @see NamedInterfaces#builder(JavaPackage)
|
||||
* @since 1.4
|
||||
*/
|
||||
default NamedInterfaces detectNamedInterfaces(JavaPackage basePackage, ApplicationModuleInformation information) {
|
||||
return NamedInterfaces.of(basePackage, information);
|
||||
}
|
||||
|
||||
/**
|
||||
* A {@link ApplicationModuleDetectionStrategy} that considers all direct sub-packages of the Moduliths base package
|
||||
|
||||
@@ -45,19 +45,25 @@ public class ApplicationModuleSource {
|
||||
|
||||
private final JavaPackage moduleBasePackage;
|
||||
private final ApplicationModuleIdentifier identifier;
|
||||
private final Function<ApplicationModuleInformation, NamedInterfaces> namedInterfacesFactory;
|
||||
|
||||
/**
|
||||
* Creates a new {@link ApplicationModuleSource} for the given module base package and module name.
|
||||
*
|
||||
* @param moduleBasePackage must not be {@literal null}.
|
||||
* @param moduleName must not be {@literal null} or empty.
|
||||
* @param namedInterfacesFactory must not be {@literal null}.
|
||||
*/
|
||||
private ApplicationModuleSource(JavaPackage moduleBasePackage, ApplicationModuleIdentifier identifier) {
|
||||
private ApplicationModuleSource(JavaPackage moduleBasePackage, ApplicationModuleIdentifier identifier,
|
||||
Function<ApplicationModuleInformation, NamedInterfaces> namedInterfacesFactory) {
|
||||
|
||||
Assert.notNull(moduleBasePackage, "JavaPackage must not be null!");
|
||||
Assert.notNull(identifier, "ApplicationModuleIdentifier must not be null!");
|
||||
Assert.notNull(namedInterfacesFactory, "NamedInterfaces factory must not be null!");
|
||||
|
||||
this.moduleBasePackage = moduleBasePackage;
|
||||
this.identifier = identifier;
|
||||
this.namedInterfacesFactory = namedInterfacesFactory;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -83,7 +89,7 @@ public class ApplicationModuleSource {
|
||||
.orElseGet(() -> ApplicationModuleIdentifier.of(
|
||||
fullyQualifiedModuleNames ? it.getName() : rootPackage.getTrailingName(it)));
|
||||
|
||||
return new ApplicationModuleSource(it, id);
|
||||
return new ApplicationModuleSource(it, id, (info) -> strategy.detectNamedInterfaces(it, info));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -95,7 +101,8 @@ public class ApplicationModuleSource {
|
||||
* @return will never be {@literal null}.
|
||||
*/
|
||||
static ApplicationModuleSource from(JavaPackage pkg, String identifier) {
|
||||
return new ApplicationModuleSource(pkg, ApplicationModuleIdentifier.of(identifier));
|
||||
return new ApplicationModuleSource(pkg, ApplicationModuleIdentifier.of(identifier),
|
||||
(info) -> NamedInterfaces.of(pkg, info));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -116,6 +123,20 @@ public class ApplicationModuleSource {
|
||||
return identifier;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all {@link NamedInterfaces} for the given {@link ApplicationModuleInformation}.
|
||||
*
|
||||
* @param information must not be {@literal null}.
|
||||
* @return will never be {@literal null}.
|
||||
* @since 1.4
|
||||
*/
|
||||
public NamedInterfaces getNamedInterfaces(ApplicationModuleInformation information) {
|
||||
|
||||
Assert.notNull(information, "ApplicationModuleInformation must not be null!");
|
||||
|
||||
return namedInterfacesFactory.apply(information);
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see java.lang.Object#equals(java.lang.Object)
|
||||
|
||||
@@ -28,6 +28,7 @@ import java.util.Iterator;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.function.BiPredicate;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Collectors;
|
||||
@@ -197,6 +198,23 @@ public class JavaPackage implements DescribedIterable<JavaClass>, Comparable<Jav
|
||||
.map(it -> of(classes, it));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all sub-packages that match the given {@link BiPredicate} for the canidate package and its trailing name
|
||||
* relative to the current one.
|
||||
*
|
||||
* @param filter must not be {@literal null}.
|
||||
* @return will never be {@literal null}.
|
||||
* @see #getTrailingName(JavaPackage)
|
||||
* @since 1.4
|
||||
*/
|
||||
public Stream<JavaPackage> getSubPackagesMatching(BiPredicate<JavaPackage, String> filter) {
|
||||
|
||||
Assert.notNull(filter, "Filter must not be null!");
|
||||
|
||||
return getSubPackages().stream()
|
||||
.filter(it -> filter.test(it, this.getTrailingName(it)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all {@link Classes} that match the given {@link DescribedPredicate}.
|
||||
*
|
||||
|
||||
@@ -69,8 +69,9 @@ public class NamedInterface implements Iterable<JavaClass> {
|
||||
*
|
||||
* @param javaPackage must not be {@literal null}.
|
||||
* @return will never be {@literal null}.
|
||||
* @since 1.4 (previously package protected)
|
||||
*/
|
||||
static List<NamedInterface> of(JavaPackage javaPackage) {
|
||||
public static List<NamedInterface> of(JavaPackage javaPackage) {
|
||||
|
||||
var basePackage = javaPackage.toSingle();
|
||||
var names = basePackage.findAnnotation(org.springframework.modulith.NamedInterface.class) //
|
||||
@@ -91,18 +92,44 @@ public class NamedInterface implements Iterable<JavaClass> {
|
||||
* @param name must not be {@literal null} or empty.
|
||||
* @param classes must not be {@literal null}.
|
||||
* @return will never be {@literal null}.
|
||||
* @since 1.4 (previously package protected)
|
||||
*/
|
||||
static NamedInterface of(String name, Classes classes) {
|
||||
public static NamedInterface of(String name, Classes classes) {
|
||||
return new NamedInterface(name, classes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the unnamed interface for the given package, excluding any sub-packages by convention.
|
||||
*
|
||||
* @param javaPackage must not be {@literal null}.
|
||||
* @return will never be {@literal null}.
|
||||
* @since 1.4
|
||||
*/
|
||||
public static NamedInterface unnamed(JavaPackage javaPackage) {
|
||||
return unnamed(javaPackage, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an unnamed interface for the given package applying open module semantics, i.e. including all sub-packages
|
||||
* of the given one.
|
||||
*
|
||||
* @param javaPackage must not be {@literal null}.
|
||||
* @return will never be {@literal null}.
|
||||
* @since 1.4
|
||||
*/
|
||||
public static NamedInterface open(JavaPackage javaPackage) {
|
||||
return unnamed(javaPackage, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an unnamed {@link NamedInterface} for the given {@link JavaPackage}.
|
||||
*
|
||||
* @param javaPackage must not be {@literal null}.
|
||||
* @return will never be {@literal null}.
|
||||
*/
|
||||
static NamedInterface unnamed(JavaPackage javaPackage, boolean flatten) {
|
||||
private static NamedInterface unnamed(JavaPackage javaPackage, boolean flatten) {
|
||||
|
||||
Assert.notNull(javaPackage, "Java package must not be null!");
|
||||
|
||||
var basePackageClasses = (flatten ? javaPackage.toSingle() : javaPackage).getExposedClasses();
|
||||
|
||||
|
||||
@@ -15,12 +15,16 @@
|
||||
*/
|
||||
package org.springframework.modulith.core;
|
||||
|
||||
import static java.util.stream.Collectors.*;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
@@ -55,19 +59,43 @@ public class NamedInterfaces implements Iterable<NamedInterface> {
|
||||
.toList();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param basePackage must not be {@literal null}.
|
||||
* @param information must not be {@literal null}.
|
||||
* @return
|
||||
* @since 1.4
|
||||
*/
|
||||
public static NamedInterfaces of(JavaPackage basePackage, ApplicationModuleInformation information) {
|
||||
|
||||
return information.isOpen()
|
||||
? NamedInterfaces.forOpen(basePackage)
|
||||
: NamedInterfaces.discoverNamedInterfaces(basePackage);
|
||||
}
|
||||
|
||||
/**
|
||||
* Discovers all {@link NamedInterfaces} declared for the given {@link JavaPackage}.
|
||||
*
|
||||
* @param basePackage must not be {@literal null}.
|
||||
* @return will never be {@literal null}.
|
||||
* @since 1.4 (previously package protected)
|
||||
*/
|
||||
static NamedInterfaces discoverNamedInterfaces(JavaPackage basePackage) {
|
||||
public static NamedInterfaces discoverNamedInterfaces(JavaPackage basePackage) {
|
||||
|
||||
return NamedInterfaces.of(NamedInterface.unnamed(basePackage, true))
|
||||
return NamedInterfaces.of(NamedInterface.unnamed(basePackage))
|
||||
.and(ofAnnotatedPackages(basePackage))
|
||||
.and(ofAnnotatedTypes(basePackage.getClasses()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new {@link Builder} to eventually create {@link NamedInterfaces} for the given base package.
|
||||
*
|
||||
* @param basePackage must not be {@literal null}.
|
||||
* @return will never be {@literal null}.
|
||||
*/
|
||||
public static Builder builder(JavaPackage basePackage) {
|
||||
return new Builder(basePackage);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new {@link NamedInterfaces} for the given {@link NamedInterface}s.
|
||||
*
|
||||
@@ -100,11 +128,11 @@ public class NamedInterfaces implements Iterable<NamedInterface> {
|
||||
*
|
||||
* @param basePackage must not be {@literal null}.
|
||||
* @return will never be {@literal null}.
|
||||
* @since 1.2
|
||||
* @since 1.2, public since 1.4
|
||||
*/
|
||||
static NamedInterfaces forOpen(JavaPackage basePackage) {
|
||||
public static NamedInterfaces forOpen(JavaPackage basePackage) {
|
||||
|
||||
return NamedInterfaces.of(NamedInterface.unnamed(basePackage, false))
|
||||
return NamedInterfaces.of(NamedInterface.open(basePackage))
|
||||
.and(ofAnnotatedPackages(basePackage))
|
||||
.and(ofAnnotatedTypes(basePackage.getClasses()));
|
||||
}
|
||||
@@ -195,8 +223,9 @@ public class NamedInterfaces implements Iterable<NamedInterface> {
|
||||
*
|
||||
* @param others must not be {@literal null}.
|
||||
* @return will never be {@literal null}.
|
||||
* @since 1.4 (previously package protected)
|
||||
*/
|
||||
NamedInterfaces and(Iterable<NamedInterface> others) {
|
||||
public NamedInterfaces and(Iterable<NamedInterface> others) {
|
||||
|
||||
Assert.notNull(others, "Other NamedInterfaces must not be null!");
|
||||
|
||||
@@ -265,4 +294,172 @@ public class NamedInterfaces implements Iterable<NamedInterface> {
|
||||
.map(entry -> NamedInterface.of(entry.getKey(), Classes.of(entry.getValue()))) //
|
||||
.toList();
|
||||
}
|
||||
|
||||
/**
|
||||
* A builder API to manually construct {@link NamedInterfaces} instances. Allows selecting packages to create
|
||||
* {@link NamedInterface} instances for based on excluding and including predicates, name matches etc. Will always
|
||||
* include the unnamed named interface as it's required for a valid application module.
|
||||
*
|
||||
* @author Oliver Drotbohm
|
||||
* @since 1.4
|
||||
*/
|
||||
public static class Builder {
|
||||
|
||||
private final JavaPackage basePackage;
|
||||
private final boolean recursive;
|
||||
private final Predicate<JavaPackage> inclusions, exclusions;
|
||||
|
||||
/**
|
||||
* Creates a new {@link Builder} for the given {@link JavaPackage}.
|
||||
*
|
||||
* @param basePackage must not be {@literal null}.
|
||||
*/
|
||||
private Builder(JavaPackage basePackage) {
|
||||
this(basePackage, false, __ -> false, __ -> false);
|
||||
}
|
||||
|
||||
private Builder(JavaPackage basePackage, boolean recursive,
|
||||
Predicate<JavaPackage> inclusions, Predicate<JavaPackage> exclusions) {
|
||||
|
||||
Assert.notNull(basePackage, "Base package must not be null!");
|
||||
Assert.notNull(inclusions, "Inclusions must not be null!");
|
||||
Assert.notNull(exclusions, "Exclusions must not be null!");
|
||||
|
||||
this.basePackage = basePackage;
|
||||
this.recursive = recursive;
|
||||
this.inclusions = inclusions;
|
||||
this.exclusions = exclusions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures the builder to not only consider directly nested packages but also ones nested in those.
|
||||
*
|
||||
* @return will never be {@literal null}.
|
||||
*/
|
||||
public Builder recursive() {
|
||||
return new Builder(basePackage, true, inclusions, exclusions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds all packages with the trailing name relative to the base package matching the given names or expressions,
|
||||
* unless set up to be excluded (see {@link #excluding(Predicate)} and overloads).. For a base package
|
||||
* {@code com.acme}, the trailing name of package {@code com.acme.foo} would be {@code foo}. For
|
||||
* {@code com.acme.foo.bar} it would be {@code foo.bar}.
|
||||
* <p>
|
||||
* Expressions can use wildcards, such as {@literal *} (for multi-character matches) and {@literal ?} (for
|
||||
* single-character ones). As soon as an expression contains a dot ({@literal .}), the expression is applied to the
|
||||
* entire trailing name. Expressions with a dot are applied to all name segments individually. In other words, an
|
||||
* expression {@code foo} would match both the trailing names {@code foo}, {@code foo.bar}, and {@code bar.foo}.
|
||||
*
|
||||
* @param names must not be {@literal null}.
|
||||
* @return will never be {@literal null}.
|
||||
* @see #excluding(String...)
|
||||
*/
|
||||
public Builder matching(String... names) {
|
||||
return matching(List.of(names));
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds all packages with the trailing name relative to the base package matching the given names or expressions,
|
||||
* unless set up to be excluded (see {@link #excluding(Predicate)} and overloads). For a base package
|
||||
* {@code com.acme}, the trailing name of package {@code com.acme.foo} would be {@code foo}. For
|
||||
* {@code com.acme.foo.bar} it would be {@code foo.bar}.
|
||||
* <p>
|
||||
* Expressions can use wildcards, such as {@literal *} (for multi-character matches) and {@literal ?} (for
|
||||
* single-character ones). As soon as an expression contains a dot ({@literal .}), the expression is applied to the
|
||||
* entire trailing name. Expressions with a dot are applied to all name segments individually. In other words, an
|
||||
* expression {@code foo} would match both the trailing names {@code foo}, {@code foo.bar}, and {@code bar.foo}.
|
||||
*
|
||||
* @param names must not be {@literal null}.
|
||||
* @return will never be {@literal null}.
|
||||
* @see #excluding(Collection)
|
||||
*/
|
||||
public Builder matching(Collection<String> names) {
|
||||
return including(matchesTrailingName(names));
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds all packages matching the given predicate as named interface.
|
||||
*
|
||||
* @param inclusions must not be {@literal null}.
|
||||
* @return will never be {@literal null}.
|
||||
* @see #excluding(Predicate)
|
||||
*/
|
||||
public Builder including(Predicate<JavaPackage> inclusions) {
|
||||
|
||||
Assert.notNull(inclusions, "Inclusions must not be null!");
|
||||
|
||||
return new Builder(basePackage, recursive, inclusions, exclusions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Excludes the packages with the given name expressions from being considered as named interface. See
|
||||
* {@link #matching(String...)} for details on matching expressions.
|
||||
*
|
||||
* @param expressions must not be {@literal null}.
|
||||
* @return will never be {@literal null}.
|
||||
* @see #matching(String...)
|
||||
*/
|
||||
public Builder excluding(String... expressions) {
|
||||
|
||||
Assert.notNull(expressions, "Expressions must not be null!");
|
||||
|
||||
return excluding(List.of(expressions));
|
||||
}
|
||||
|
||||
/**
|
||||
* Excludes the packages with the given name expressions from being considered as named interface. See
|
||||
* {@link #matching(Collection)} for details on matching expressions.
|
||||
*
|
||||
* @param expressions must not be {@literal null}.
|
||||
* @return will never be {@literal null}.
|
||||
* @see #matching(Collection)
|
||||
*/
|
||||
public Builder excluding(Collection<String> expressions) {
|
||||
return excluding(Predicate.not(matchesTrailingName(expressions)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Excludes the packages matching the given {@link Predicate} from being considered as named interface.
|
||||
*
|
||||
* @param exclusions must not be {@literal null}.
|
||||
* @return will never be {@literal null}.
|
||||
* @see #including(Predicate)
|
||||
*/
|
||||
public Builder excluding(Predicate<JavaPackage> exclusions) {
|
||||
return new Builder(basePackage, recursive, inclusions, exclusions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a {@link NamedInterfaces} instance according to the builder setup. Will <em>always</em> include the
|
||||
* unnamed interface established by the {@link JavaPackage} the {@link Builder} was set up for originally, as it's a
|
||||
* required abstraction for every application module.
|
||||
*
|
||||
* @return will never be {@literal null}.
|
||||
*/
|
||||
public NamedInterfaces build() {
|
||||
|
||||
var packages = recursive
|
||||
? basePackage.getSubPackages().stream()
|
||||
: basePackage.getDirectSubPackages().stream();
|
||||
|
||||
var result = packages
|
||||
.filter(exclusions)
|
||||
.filter(inclusions)
|
||||
.map(it -> NamedInterface.of(basePackage.getTrailingName(it), it.getClasses()))
|
||||
.collect(collectingAndThen(toUnmodifiableList(), NamedInterfaces::new));
|
||||
|
||||
return result.and(List.of(NamedInterface.unnamed(basePackage)));
|
||||
}
|
||||
|
||||
private Predicate<JavaPackage> matchesTrailingName(Collection<String> names) {
|
||||
|
||||
return it -> {
|
||||
|
||||
var trailingName = new PackageName(basePackage.getTrailingName(it));
|
||||
|
||||
return names.stream().anyMatch(trailingName::nameContainsOrMatches);
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,8 @@
|
||||
*/
|
||||
package org.springframework.modulith.core;
|
||||
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ClassUtils;
|
||||
|
||||
@@ -162,6 +164,27 @@ class PackageName implements Comparable<PackageName> {
|
||||
return length() == 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the package name contains a segment with the given candidate or matches the given expression
|
||||
* entirely. The latter is tested for if the expression contains a dot, indicating a multi-package match is requested.
|
||||
* Expressions generally support single character ({@literal ?}) and multi-character ({@literal *}) wildcards.
|
||||
*
|
||||
* @param candidate must not be {@literal null} or empty.
|
||||
* @since 1.4
|
||||
*/
|
||||
boolean nameContainsOrMatches(String candidate) {
|
||||
|
||||
Assert.hasText(candidate, "Expression must not be null or empty!");
|
||||
|
||||
var regex = candidate.replace(".", "\\.")
|
||||
.replace("*", ".*")
|
||||
.replace("?", ".");
|
||||
|
||||
return candidate.contains(".")
|
||||
? name.matches(regex)
|
||||
: Stream.of(segments).anyMatch(it -> it.matches(regex));
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see java.lang.Comparable#compareTo(java.lang.Object)
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
package org.springframework.modulith.core;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
class CustomApplicationModuleDetectionStrategy implements ApplicationModuleDetectionStrategy {
|
||||
|
||||
private static final List<String> NAMED_INTERFACE_PACKAGE_NAMES = List.of("mapper", "model", "repository", "service",
|
||||
"web");
|
||||
|
||||
private static final List<String> INTERNAL_PACKAGE_NAME = List.of("internal");
|
||||
|
||||
@Override
|
||||
public Stream<JavaPackage> getModuleBasePackages(JavaPackage basePackage) {
|
||||
|
||||
var allExclusions = Stream.concat(NAMED_INTERFACE_PACKAGE_NAMES.stream(), INTERNAL_PACKAGE_NAME.stream());
|
||||
|
||||
// New method to be introduced on JavaPackage
|
||||
return basePackage.getSubPackagesMatching((pkg, trailingName) -> allExclusions.noneMatch(trailingName::contains));
|
||||
}
|
||||
|
||||
@Override
|
||||
public NamedInterfaces detectNamedInterfaces(JavaPackage basePackage, ApplicationModuleInformation information) {
|
||||
|
||||
return NamedInterfaces.builder(basePackage)
|
||||
.matching(NAMED_INTERFACE_PACKAGE_NAMES)
|
||||
.recursive()
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@@ -98,6 +98,38 @@ class NamedInterfacesUnitTests {
|
||||
.containsExactlyInAnyOrder("spi", "kpi");
|
||||
}
|
||||
|
||||
@Test
|
||||
void createsNamedInterfacesFromBuilder() {
|
||||
|
||||
JavaPackage pkg = TestUtils.getPackage(RootType.class);
|
||||
|
||||
var result = NamedInterfaces.builder(pkg)
|
||||
.excluding("internal")
|
||||
.matching("nested")
|
||||
.build();
|
||||
|
||||
assertThat(result).hasSize(2)
|
||||
.extracting(NamedInterface::getName)
|
||||
.containsExactlyInAnyOrder(NamedInterface.UNNAMED_NAME, "nested");
|
||||
}
|
||||
|
||||
@Test
|
||||
void createsNamedInterfacesFromRecursiveBuilder() {
|
||||
|
||||
JavaPackage pkg = TestUtils.getPackage(RootType.class);
|
||||
|
||||
var result = NamedInterfaces.builder(pkg)
|
||||
.excluding("internal")
|
||||
.matching("nested", "internal")
|
||||
.recursive()
|
||||
.build();
|
||||
|
||||
assertThat(result).hasSize(6)
|
||||
.extracting(NamedInterface::getName)
|
||||
.containsExactlyInAnyOrder(NamedInterface.UNNAMED_NAME, "nested", "nested.a", "nested.b", "nested.b.first",
|
||||
"nested.b.second");
|
||||
}
|
||||
|
||||
private static void assertInterfaceContains(NamedInterfaces interfaces, String name, Class<?>... types) {
|
||||
|
||||
var classNames = Arrays.stream(types).map(Class::getName).toArray(String[]::new);
|
||||
|
||||
@@ -55,4 +55,21 @@ class PackageNameUnitTests {
|
||||
assertThat(comAcme.contains(comAcmeA)).isTrue();
|
||||
assertThat(comAcmeA.contains(comAcme)).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void findsMatchingSegments() {
|
||||
|
||||
var source = new PackageName("com.acme.foo");
|
||||
|
||||
assertThat(source.nameContainsOrMatches("acme")).isTrue();
|
||||
assertThat(source.nameContainsOrMatches("*me")).isTrue();
|
||||
assertThat(source.nameContainsOrMatches("ac*")).isTrue();
|
||||
assertThat(source.nameContainsOrMatches("*m.acme.foo")).isTrue();
|
||||
assertThat(source.nameContainsOrMatches("*m.acme.?oo")).isTrue();
|
||||
assertThat(source.nameContainsOrMatches("*m.ac*")).isTrue();
|
||||
assertThat(source.nameContainsOrMatches("*m.*.fo*")).isTrue();
|
||||
|
||||
assertThat(source.nameContainsOrMatches("cm")).isFalse();
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -394,6 +394,8 @@ package example.inventory
|
||||
----
|
||||
======
|
||||
|
||||
If you require more generic control about the named interfaces of an application module, check out xref:fundamentals.adoc#customizing-named-interfaces[the customization section].
|
||||
|
||||
[[customizing-modules-arrangement]]
|
||||
== Customizing the Application Modules Arrangement
|
||||
|
||||
@@ -581,3 +583,61 @@ public class CustomApplicationModuleSourceFactory implements ApplicationModuleSo
|
||||
The above example would use `com.acme.toscan` to detect xref:fundamentals.adoc#customizing-modules[explicitly declared modules] within that and also create an application module from `com.acme.module`.
|
||||
The package names returned from these will subsequently be translated into ``ApplicationModuleSource``s via the corresponding `getApplicationModuleSource(…)` flavors exposed in `ApplicationModuleDetectionStrategy`.
|
||||
|
||||
[[customizing-named-interfaces]]
|
||||
=== Customizing Named Interface detection
|
||||
|
||||
If you would like to programatically describe the named interfaces of an application module, register an `ApplicationModuleDetectionStrategy` as described xref:fundamentals.adoc#customizing-modules[here] and use the `detectNamedInterfaces(JavaPackage, ApplicationModuleInformation)` to implement a custom discovery algorithm.
|
||||
|
||||
.Customizing the named interface detection using a custom `ApplicationModuleDetectionStrategy`
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source, java, role="primary"]
|
||||
----
|
||||
package example;
|
||||
|
||||
class CustomApplicationModuleDetectionStrategy implements ApplicationModuleDetectionStrategy {
|
||||
|
||||
@Override
|
||||
public Stream<JavaPackage> getModuleBasePackages(JavaPackage basePackage) {
|
||||
// Your module detection goes here
|
||||
}
|
||||
|
||||
@Override
|
||||
NamedInterfaces detectNamedInterfaces(JavaPackage basePackage, ApplicationModuleInformation information) {
|
||||
return NamedInterfaces.builder()
|
||||
.recursive()
|
||||
.matching("api")
|
||||
.build();
|
||||
}
|
||||
}
|
||||
----
|
||||
Kotlin::
|
||||
+
|
||||
[source, kotlin, role="secondary"]
|
||||
----
|
||||
package example
|
||||
|
||||
class CustomApplicationModuleDetectionStrategy : ApplicationModuleDetectionStrategy {
|
||||
|
||||
override fun getModuleBasePackages(basePackage: JavaPackage): Stream<JavaPackage> {
|
||||
// Your module detection goes here
|
||||
}
|
||||
|
||||
override fun detectNamedInterfaces(basePackage: JavaPackage, information: ApplicationModuleInformation): NamedInterfaces {
|
||||
return NamedInterfaces.builder()
|
||||
.recursive()
|
||||
.matching("api")
|
||||
.build()
|
||||
}
|
||||
}
|
||||
----
|
||||
======
|
||||
|
||||
In the `detectNamedInterfaces(…)` implementation shown above, we build up a `NamedInterfaces` instance for all packages named `api` underneath the given application module's base package.
|
||||
The `Builder` API exposes additional methods to select packages as named interfaces or explicitly exclude them from that.
|
||||
Note, that the builder will always include the unnamed named interface containing all public methods located in the application module's base package as that interface is required for application modules.
|
||||
|
||||
For a more manual setup of a `NamedInterfaces`, be sure to check out its factory methods and the ones exposed by `NamedInterface`.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user