diff --git a/spring-modulith-core/src/main/java/org/springframework/modulith/core/ApplicationModule.java b/spring-modulith-core/src/main/java/org/springframework/modulith/core/ApplicationModule.java index 4f742128..afd97bd0 100644 --- a/spring-modulith-core/src/main/java/org/springframework/modulith/core/ApplicationModule.java +++ b/spring-modulith-core/src/main/java/org/springframework/modulith/core/ApplicationModule.java @@ -109,9 +109,7 @@ public class ApplicationModule implements Comparable { 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)); diff --git a/spring-modulith-core/src/main/java/org/springframework/modulith/core/ApplicationModuleDetectionStrategy.java b/spring-modulith-core/src/main/java/org/springframework/modulith/core/ApplicationModuleDetectionStrategy.java index 067c0afd..890d2f21 100644 --- a/spring-modulith-core/src/main/java/org/springframework/modulith/core/ApplicationModuleDetectionStrategy.java +++ b/spring-modulith-core/src/main/java/org/springframework/modulith/core/ApplicationModuleDetectionStrategy.java @@ -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 getModuleBasePackages(JavaPackage basePackage); + Stream 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 diff --git a/spring-modulith-core/src/main/java/org/springframework/modulith/core/ApplicationModuleSource.java b/spring-modulith-core/src/main/java/org/springframework/modulith/core/ApplicationModuleSource.java index 630a904f..5de7c927 100644 --- a/spring-modulith-core/src/main/java/org/springframework/modulith/core/ApplicationModuleSource.java +++ b/spring-modulith-core/src/main/java/org/springframework/modulith/core/ApplicationModuleSource.java @@ -45,19 +45,25 @@ public class ApplicationModuleSource { private final JavaPackage moduleBasePackage; private final ApplicationModuleIdentifier identifier; + private final Function 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 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) diff --git a/spring-modulith-core/src/main/java/org/springframework/modulith/core/JavaPackage.java b/spring-modulith-core/src/main/java/org/springframework/modulith/core/JavaPackage.java index 0f91ff2c..4ccbf4ca 100644 --- a/spring-modulith-core/src/main/java/org/springframework/modulith/core/JavaPackage.java +++ b/spring-modulith-core/src/main/java/org/springframework/modulith/core/JavaPackage.java @@ -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, Comparable 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 getSubPackagesMatching(BiPredicate 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}. * diff --git a/spring-modulith-core/src/main/java/org/springframework/modulith/core/NamedInterface.java b/spring-modulith-core/src/main/java/org/springframework/modulith/core/NamedInterface.java index cb84f638..3a5cad4d 100644 --- a/spring-modulith-core/src/main/java/org/springframework/modulith/core/NamedInterface.java +++ b/spring-modulith-core/src/main/java/org/springframework/modulith/core/NamedInterface.java @@ -69,8 +69,9 @@ public class NamedInterface implements Iterable { * * @param javaPackage must not be {@literal null}. * @return will never be {@literal null}. + * @since 1.4 (previously package protected) */ - static List of(JavaPackage javaPackage) { + public static List 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 { * @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(); diff --git a/spring-modulith-core/src/main/java/org/springframework/modulith/core/NamedInterfaces.java b/spring-modulith-core/src/main/java/org/springframework/modulith/core/NamedInterfaces.java index 5c10be27..222363ad 100644 --- a/spring-modulith-core/src/main/java/org/springframework/modulith/core/NamedInterfaces.java +++ b/spring-modulith-core/src/main/java/org/springframework/modulith/core/NamedInterfaces.java @@ -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 { .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 { * * @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 { * * @param others must not be {@literal null}. * @return will never be {@literal null}. + * @since 1.4 (previously package protected) */ - NamedInterfaces and(Iterable others) { + public NamedInterfaces and(Iterable others) { Assert.notNull(others, "Other NamedInterfaces must not be null!"); @@ -265,4 +294,172 @@ public class NamedInterfaces implements Iterable { .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 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 inclusions, Predicate 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}. + *

+ * 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}. + *

+ * 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 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 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 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 exclusions) { + return new Builder(basePackage, recursive, inclusions, exclusions); + } + + /** + * Creates a {@link NamedInterfaces} instance according to the builder setup. Will always 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 matchesTrailingName(Collection names) { + + return it -> { + + var trailingName = new PackageName(basePackage.getTrailingName(it)); + + return names.stream().anyMatch(trailingName::nameContainsOrMatches); + }; + } + } } diff --git a/spring-modulith-core/src/main/java/org/springframework/modulith/core/PackageName.java b/spring-modulith-core/src/main/java/org/springframework/modulith/core/PackageName.java index f12ecb0c..3a4e2050 100644 --- a/spring-modulith-core/src/main/java/org/springframework/modulith/core/PackageName.java +++ b/spring-modulith-core/src/main/java/org/springframework/modulith/core/PackageName.java @@ -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 { 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) diff --git a/spring-modulith-core/src/test/java/org/springframework/modulith/core/CustomApplicationModuleDetectionStrategy.java b/spring-modulith-core/src/test/java/org/springframework/modulith/core/CustomApplicationModuleDetectionStrategy.java new file mode 100644 index 00000000..658bc717 --- /dev/null +++ b/spring-modulith-core/src/test/java/org/springframework/modulith/core/CustomApplicationModuleDetectionStrategy.java @@ -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 NAMED_INTERFACE_PACKAGE_NAMES = List.of("mapper", "model", "repository", "service", + "web"); + + private static final List INTERNAL_PACKAGE_NAME = List.of("internal"); + + @Override + public Stream 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(); + } +} diff --git a/spring-modulith-core/src/test/java/org/springframework/modulith/core/NamedInterfacesUnitTests.java b/spring-modulith-core/src/test/java/org/springframework/modulith/core/NamedInterfacesUnitTests.java index 2dec5abe..3c977646 100644 --- a/spring-modulith-core/src/test/java/org/springframework/modulith/core/NamedInterfacesUnitTests.java +++ b/spring-modulith-core/src/test/java/org/springframework/modulith/core/NamedInterfacesUnitTests.java @@ -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); diff --git a/spring-modulith-core/src/test/java/org/springframework/modulith/core/PackageNameUnitTests.java b/spring-modulith-core/src/test/java/org/springframework/modulith/core/PackageNameUnitTests.java index 71a6cb58..6c9fa3f3 100644 --- a/spring-modulith-core/src/test/java/org/springframework/modulith/core/PackageNameUnitTests.java +++ b/spring-modulith-core/src/test/java/org/springframework/modulith/core/PackageNameUnitTests.java @@ -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(); + + } } diff --git a/src/docs/antora/modules/ROOT/pages/fundamentals.adoc b/src/docs/antora/modules/ROOT/pages/fundamentals.adoc index 90412bde..7595fe40 100644 --- a/src/docs/antora/modules/ROOT/pages/fundamentals.adoc +++ b/src/docs/antora/modules/ROOT/pages/fundamentals.adoc @@ -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 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 { + // 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`. +