Skip to content

Commit

Permalink
GH-1009 - Allow customizing the detection of NamedInterfaces via Appl…
Browse files Browse the repository at this point in the history
…icationModuleDetectionStrategy.

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.
  • Loading branch information
odrotbohm committed Jan 16, 2025
1 parent f941756 commit 2e31447
Show file tree
Hide file tree
Showing 11 changed files with 458 additions and 18 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,30 @@
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 rootPackage will never be {@literal null}.
* @return must not be {@literal null}.
*/
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
*/
Stream<JavaPackage> getModuleBasePackages(JavaPackage basePackage);
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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

/**
Expand All @@ -83,7 +89,7 @@ public static Stream<ApplicationModuleSource> from(JavaPackage rootPackage,
.orElseGet(() -> ApplicationModuleIdentifier.of(
fullyQualifiedModuleNames ? it.getName() : rootPackage.getTrailingName(it)));

return new ApplicationModuleSource(it, id);
return new ApplicationModuleSource(it, id, (info) -> strategy.detectNamedInterfaces(it, info));
});
}

Expand All @@ -95,7 +101,8 @@ public static Stream<ApplicationModuleSource> from(JavaPackage rootPackage,
* @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));
}

/**
Expand All @@ -116,6 +123,20 @@ public ApplicationModuleIdentifier getIdentifier() {
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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
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;
Expand Down Expand Up @@ -197,6 +198,23 @@ public Stream<JavaPackage> getSubPackagesAnnotatedWith(Class<? extends Annotatio
.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}.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,9 @@ private NamedInterface(String name, Classes classes) {
*
* @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) //
Expand All @@ -91,18 +92,44 @@ static List<NamedInterface> of(JavaPackage javaPackage) {
* @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();

Expand Down
Loading

0 comments on commit 2e31447

Please sign in to comment.