GH-578 - Support for nested application modules.
The ApplicationModules bootstrap now triggers the module base package detection, followed by a new, additional pass of detecting nested application module packages. Those packages are now added to the ones we create ApplicationModule instances for and also handed into the module instance creation step as exclusions to make sure that parent modules do not include code residing in sub-modules. The bootstrap of ApplicationModules now uses a dedicated ApplicationModuleSource to allow calculating a default module name relative to the application base package. Each module now operates on the Classes instance obtained from the JavaPackage instance that constitutes the module's base package but filtered by the given exclusions.
This commit is contained in:
committed by
Oliver Drotbohm
parent
b927bf1211
commit
7838204c25
10
pom.xml
10
pom.xml
@@ -299,11 +299,11 @@ limitations under the License.
|
||||
<extensions>true</extensions>
|
||||
<configuration>
|
||||
<packages>
|
||||
<package>@antora/atlas-extension@1.0.0-alpha.1</package>
|
||||
<package>@antora/collector-extension@1.0.0-alpha.3</package>
|
||||
<package>@asciidoctor/tabs@1.0.0-beta.3</package>
|
||||
<package>@springio/antora-extensions@1.5.0</package>
|
||||
<package>@springio/asciidoctor-extensions@1.0.0-alpha.9</package>
|
||||
<package>@antora/atlas-extension@1.0.0-alpha.2</package>
|
||||
<package>@antora/collector-extension@1.0.0-beta.2</package>
|
||||
<package>@asciidoctor/tabs@1.0.0-beta.6</package>
|
||||
<package>@springio/antora-extensions@1.13.1</package>
|
||||
<package>@springio/asciidoctor-extensions@1.0.0-alpha.12</package>
|
||||
<package>asciidoctor-kroki</package>
|
||||
</packages>
|
||||
<playbook>../src/docs/antora/antora-playbook.yml</playbook>
|
||||
|
||||
@@ -18,12 +18,14 @@ package org.springframework.modulith.core;
|
||||
import static com.tngtech.archunit.base.DescribedPredicate.*;
|
||||
import static com.tngtech.archunit.core.domain.JavaClass.Predicates.*;
|
||||
import static java.lang.System.*;
|
||||
import static java.util.Comparator.*;
|
||||
import static org.springframework.modulith.core.SyntacticSugar.*;
|
||||
import static org.springframework.modulith.core.Types.JavaXTypes.*;
|
||||
import static org.springframework.modulith.core.Types.SpringDataTypes.*;
|
||||
import static org.springframework.modulith.core.Types.SpringTypes.*;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@@ -57,12 +59,15 @@ import com.tngtech.archunit.core.domain.SourceCodeLocation;
|
||||
*
|
||||
* @author Oliver Drotbohm
|
||||
*/
|
||||
public class ApplicationModule {
|
||||
public class ApplicationModule implements Comparable<ApplicationModule> {
|
||||
|
||||
/**
|
||||
* The base package of the {@link ApplicationModule}.
|
||||
*/
|
||||
private final JavaPackage basePackage;
|
||||
private final Classes classes;
|
||||
private final JavaPackages exclusions;
|
||||
|
||||
private final ApplicationModuleInformation information;
|
||||
|
||||
/**
|
||||
@@ -70,32 +75,46 @@ public class ApplicationModule {
|
||||
* or implicitly.
|
||||
*/
|
||||
private final NamedInterfaces namedInterfaces;
|
||||
private final boolean useFullyQualifiedModuleNames;
|
||||
private final ApplicationModuleSource source;
|
||||
|
||||
private final Supplier<Classes> springBeans;
|
||||
private final Supplier<Classes> aggregateRoots;
|
||||
private final Supplier<List<JavaClass>> valueTypes;
|
||||
private final Supplier<List<EventType>> publishedEvents;
|
||||
|
||||
/**
|
||||
* Creates a new {@link ApplicationModule} from the given {@link ApplicationModuleSource}.
|
||||
*
|
||||
* @param source must not be {@literal null}.
|
||||
*/
|
||||
ApplicationModule(ApplicationModuleSource source) {
|
||||
this(source, JavaPackages.NONE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new {@link ApplicationModule} for the given base package and whether to use fully-qualified module names.
|
||||
*
|
||||
* @param basePackage must not be {@literal null}.
|
||||
* @param useFullyQualifiedModuleNames
|
||||
* @param source must not be {@literal null}.
|
||||
* @param exclusions must not be {@literal null}.
|
||||
*/
|
||||
ApplicationModule(JavaPackage basePackage, boolean useFullyQualifiedModuleNames) {
|
||||
ApplicationModule(ApplicationModuleSource source, JavaPackages exclusions) {
|
||||
|
||||
Assert.notNull(basePackage, "Base package must not be null!");
|
||||
Assert.notNull(source, "Base package must not be null!");
|
||||
Assert.notNull(exclusions, "Exclusions must not be null!");
|
||||
|
||||
JavaPackage basePackage = source.moduleBasePackage();
|
||||
|
||||
this.source = source;
|
||||
this.basePackage = basePackage;
|
||||
this.exclusions = exclusions;
|
||||
this.classes = basePackage.getClasses(exclusions);
|
||||
this.information = ApplicationModuleInformation.of(basePackage);
|
||||
this.namedInterfaces = isOpen()
|
||||
? NamedInterfaces.forOpen(basePackage)
|
||||
: NamedInterfaces.discoverNamedInterfaces(basePackage);
|
||||
this.useFullyQualifiedModuleNames = useFullyQualifiedModuleNames;
|
||||
|
||||
this.springBeans = SingletonSupplier.of(() -> filterSpringBeans(basePackage));
|
||||
this.aggregateRoots = SingletonSupplier.of(() -> findAggregateRoots(basePackage));
|
||||
this.springBeans = SingletonSupplier.of(() -> filterSpringBeans(classes));
|
||||
this.aggregateRoots = SingletonSupplier.of(() -> findAggregateRoots(classes));
|
||||
this.valueTypes = SingletonSupplier
|
||||
.of(() -> findArchitecturallyEvidentType(ArchitecturallyEvidentType::isValueObject));
|
||||
this.publishedEvents = SingletonSupplier.of(() -> findPublishedEvents());
|
||||
@@ -125,7 +144,7 @@ public class ApplicationModule {
|
||||
* @return will never be {@literal null} or empty.
|
||||
*/
|
||||
public String getName() {
|
||||
return useFullyQualifiedModuleNames ? basePackage.getName() : basePackage.getLocalName();
|
||||
return source.moduleName();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -274,10 +293,20 @@ public class ApplicationModule {
|
||||
FormatableType.of(type).getAbbreviatedFullName(this), getName())));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the current module contains the given type.
|
||||
*
|
||||
* @param type must not be {@literal null}.
|
||||
*/
|
||||
public boolean contains(JavaClass type) {
|
||||
return basePackage.contains(type);
|
||||
return classes.contains(type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the current module contains the given type.
|
||||
*
|
||||
* @param type can be {@literal null}.
|
||||
*/
|
||||
public boolean contains(@Nullable Class<?> type) {
|
||||
return type != null && getType(type.getName()).isPresent();
|
||||
}
|
||||
@@ -292,7 +321,7 @@ public class ApplicationModule {
|
||||
|
||||
Assert.hasText(candidate, "Candidate must not be null or emtpy!");
|
||||
|
||||
return basePackage.stream()
|
||||
return classes.stream()
|
||||
.filter(hasSimpleOrFullyQualifiedName(candidate))
|
||||
.findFirst();
|
||||
}
|
||||
@@ -363,9 +392,28 @@ public class ApplicationModule {
|
||||
|
||||
builder.append("\n");
|
||||
|
||||
if (modules != null) {
|
||||
modules.getParentOf(this).ifPresent(it -> {
|
||||
builder.append("> Parent module: ").append(it.getName()).append("\n");
|
||||
});
|
||||
}
|
||||
|
||||
builder.append("> Logical name: ").append(getName()).append('\n');
|
||||
builder.append("> Base package: ").append(basePackage.getName()).append('\n');
|
||||
|
||||
builder.append("> Excluded packages: ");
|
||||
|
||||
if (!exclusions.iterator().hasNext()) {
|
||||
builder.append("none").append('\n');
|
||||
} else {
|
||||
|
||||
builder.append('\n');
|
||||
|
||||
exclusions.stream().forEach(it -> {
|
||||
builder.append(" - ").append(it.getName()).append('\n');
|
||||
});
|
||||
}
|
||||
|
||||
if (namedInterfaces.hasExplicitInterfaces()) {
|
||||
|
||||
builder.append("> Named interfaces:\n");
|
||||
@@ -475,6 +523,23 @@ public class ApplicationModule {
|
||||
return information.isOpen();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the given type is contained in any of the parent modules of the current one.
|
||||
*
|
||||
* @param type must not be {@literal null}.
|
||||
* @param modules must not be {@literal null}.
|
||||
* @since 1.3
|
||||
*/
|
||||
boolean containsTypeInAnyParent(JavaClass type, ApplicationModules modules) {
|
||||
|
||||
Assert.notNull(type, "Type must not be null!");
|
||||
Assert.notNull(modules, "ApplicationModules must not be null!");
|
||||
|
||||
return modules.getParentOf(this)
|
||||
.filter(it -> it.contains(type) || it.containsTypeInAnyParent(type, modules))
|
||||
.isPresent();
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see java.lang.Object#equals(java.lang.Object)
|
||||
@@ -490,13 +555,13 @@ public class ApplicationModule {
|
||||
return false;
|
||||
}
|
||||
|
||||
return Objects.equals(this.basePackage, that.basePackage) //
|
||||
return Objects.equals(this.source, that.source)
|
||||
&& Objects.equals(this.basePackage, that.basePackage) //
|
||||
&& Objects.equals(this.aggregateRoots, that.aggregateRoots) //
|
||||
&& Objects.equals(this.information, that.information) //
|
||||
&& Objects.equals(this.namedInterfaces, that.namedInterfaces) //
|
||||
&& Objects.equals(this.publishedEvents, that.publishedEvents) //
|
||||
&& Objects.equals(this.springBeans, that.springBeans) //
|
||||
&& Objects.equals(this.useFullyQualifiedModuleNames, that.useFullyQualifiedModuleNames) //
|
||||
&& Objects.equals(this.valueTypes, that.valueTypes);
|
||||
}
|
||||
|
||||
@@ -506,8 +571,17 @@ public class ApplicationModule {
|
||||
*/
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(basePackage, aggregateRoots, information, namedInterfaces, publishedEvents, springBeans,
|
||||
useFullyQualifiedModuleNames, valueTypes);
|
||||
return Objects.hash(source, basePackage, aggregateRoots, information, namedInterfaces, publishedEvents, springBeans,
|
||||
valueTypes);
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see java.lang.Comparable#compareTo(java.lang.Object)
|
||||
*/
|
||||
@Override
|
||||
public int compareTo(ApplicationModule o) {
|
||||
return getBasePackage().compareTo(o.getBasePackage());
|
||||
}
|
||||
|
||||
private List<EventType> findPublishedEvents() {
|
||||
@@ -515,7 +589,7 @@ public class ApplicationModule {
|
||||
DescribedPredicate<JavaClass> isEvent = implement(JMoleculesTypes.DOMAIN_EVENT) //
|
||||
.or(isAnnotatedWith(JMoleculesTypes.AT_DOMAIN_EVENT));
|
||||
|
||||
return basePackage.that(isEvent).stream() //
|
||||
return classes.that(isEvent).stream() //
|
||||
.map(EventType::new).toList();
|
||||
}
|
||||
|
||||
@@ -537,7 +611,7 @@ public class ApplicationModule {
|
||||
|
||||
private Stream<QualifiedDependency> getAllModuleDependencies(ApplicationModules modules) {
|
||||
|
||||
return basePackage.stream() //
|
||||
return classes.stream() //
|
||||
.flatMap(it -> getModuleDependenciesOf(it, modules));
|
||||
}
|
||||
|
||||
@@ -590,7 +664,7 @@ public class ApplicationModule {
|
||||
return modules.contains(dependency) && !contains(dependency);
|
||||
}
|
||||
|
||||
private Classes findAggregateRoots(JavaPackage source) {
|
||||
private Classes findAggregateRoots(Classes source) {
|
||||
|
||||
return source.stream() //
|
||||
.map(it -> ArchitecturallyEvidentType.of(it, getSpringBeansInternal()))
|
||||
@@ -599,11 +673,78 @@ public class ApplicationModule {
|
||||
.collect(Classes.toClasses());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current module's immediate parent module, if present.
|
||||
*
|
||||
* @param modules must not be {@literal null}.
|
||||
* @return will never be {@literal null}.
|
||||
* @since 1.3
|
||||
*/
|
||||
Optional<ApplicationModule> getParentModule(ApplicationModules modules) {
|
||||
|
||||
Assert.notNull(modules, "ApplicationModules must not be null!");
|
||||
|
||||
var byPackageDepth = comparing(ApplicationModule::getBasePackage, JavaPackage.reverse());
|
||||
|
||||
return modules.stream()
|
||||
.filter(it -> basePackage.isSubPackageOf(it.getBasePackage()))
|
||||
.sorted(byPackageDepth)
|
||||
.findFirst();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@link ApplicationModule}s directly nested inside the current one.
|
||||
*
|
||||
* @param modules must not be {@literal null}.
|
||||
* @return will never be {@literal null}.
|
||||
* @since 1.3
|
||||
*/
|
||||
Collection<ApplicationModule> getDirectlyNestedModules(ApplicationModules modules) {
|
||||
|
||||
Assert.notNull(modules, "ApplicationModules must not be null!");
|
||||
|
||||
return doGetNestedModules(modules, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all of the current {@link ApplicationModule}'s nested {@link ApplicationModule}s including ones contained
|
||||
* in nested modules in turn.
|
||||
*
|
||||
* @param modules must not be {@literal null}.
|
||||
* @return will never be {@literal null}.
|
||||
* @since 1.3
|
||||
*/
|
||||
Collection<ApplicationModule> getNestedModules(ApplicationModules modules) {
|
||||
|
||||
Assert.notNull(modules, "ApplicationModules must not be null!");
|
||||
|
||||
return doGetNestedModules(modules, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the classes
|
||||
*/
|
||||
Classes getClasses() {
|
||||
return classes;
|
||||
}
|
||||
|
||||
private String getQualifiedName(NamedInterface namedInterface) {
|
||||
return namedInterface.getQualifiedName(getName());
|
||||
}
|
||||
|
||||
private static Classes filterSpringBeans(JavaPackage source) {
|
||||
private Collection<ApplicationModule> doGetNestedModules(ApplicationModules modules, boolean recursive) {
|
||||
|
||||
var result = modules.stream()
|
||||
.filter(it -> it.getParentModule(modules).filter(this::equals).isPresent());
|
||||
|
||||
if (recursive) {
|
||||
result = result.flatMap(it -> Stream.concat(Stream.of(it), it.getNestedModules(modules).stream()));
|
||||
}
|
||||
|
||||
return result.toList();
|
||||
}
|
||||
|
||||
private static Classes filterSpringBeans(Classes source) {
|
||||
|
||||
Map<Boolean, List<JavaClass>> collect = source.that(isConfiguration()).stream() //
|
||||
.flatMap(it -> it.getMethods().stream()) //
|
||||
@@ -632,7 +773,7 @@ public class ApplicationModule {
|
||||
|
||||
var springBeansInternal = getSpringBeansInternal();
|
||||
|
||||
return basePackage.stream()
|
||||
return classes.stream()
|
||||
.map(it -> ArchitecturallyEvidentType.of(it, springBeansInternal))
|
||||
.filter(selector)
|
||||
.map(ArchitecturallyEvidentType::getType)
|
||||
@@ -907,6 +1048,9 @@ public class ApplicationModule {
|
||||
|
||||
private static final List<String> INJECTION_TYPES = Arrays.asList(AT_AUTOWIRED, AT_RESOURCE, AT_INJECT);
|
||||
|
||||
private static final String INVALID_SUB_MODULE_REFERENCE = "Invalid sub-module reference from module '%s' to module '%s' (via %s -> %s)!";
|
||||
private static final String INTERNAL_REFERENCE = "Module '%s' depends on non-exposed type %s within module '%s'!";
|
||||
|
||||
private final JavaClass source, target;
|
||||
private final String description;
|
||||
private final DependencyType type;
|
||||
@@ -1043,15 +1187,32 @@ public class ApplicationModule {
|
||||
return violations;
|
||||
}
|
||||
|
||||
if (originModule.containsTypeInAnyParent(target, modules)) {
|
||||
return violations;
|
||||
}
|
||||
|
||||
if (!targetModule.isExposed(target)) {
|
||||
|
||||
var violationText = "Module '%s' depends on non-exposed type %s within module '%s'!"
|
||||
var violationText = INTERNAL_REFERENCE
|
||||
.formatted(originModule.getName(), target.getName(), targetModule.getName());
|
||||
|
||||
return violations.and(new Violation(violationText + lineSeparator() + description));
|
||||
}
|
||||
|
||||
return violations;
|
||||
// Parent child relationships
|
||||
|
||||
return targetModule.getParentModule(modules)
|
||||
.filter(it -> !it.equals(originModule))
|
||||
.map(__ -> {
|
||||
|
||||
var violationText = INVALID_SUB_MODULE_REFERENCE
|
||||
.formatted(originModule.getName(), targetModule.getName(),
|
||||
FormatableType.of(source).getAbbreviatedFullName(originModule),
|
||||
FormatableType.of(target).getAbbreviatedFullName(targetModule));
|
||||
|
||||
return violations.and(new Violation(violationText));
|
||||
})
|
||||
.orElse(violations);
|
||||
}
|
||||
|
||||
ApplicationModule getExistingModuleOf(JavaClass javaClass, ApplicationModules modules) {
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
* Copyright 2024 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 org.springframework.modulith.core;
|
||||
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* The source of an {@link ApplicationModule}. Essentially a {@link JavaPackage} and associated naming strategy for the
|
||||
* module. This will be used when constructing sources from a base package and an
|
||||
* {@link ApplicationModuleDetectionStrategy} so that the names of the module to be created for the detected packages
|
||||
* become the trailing name underneath the base package. For example, scanning from {@code com.acme}, an
|
||||
* {@link ApplicationModule} located in {@code com.acme.foo.bar} would be named {@code foo.bar}.
|
||||
*
|
||||
* @author Oliver Drotbohm
|
||||
* @since 1.3
|
||||
*/
|
||||
record ApplicationModuleSource(
|
||||
JavaPackage moduleBasePackage,
|
||||
String moduleName) {
|
||||
|
||||
/**
|
||||
* Returns a {@link Stream} of {@link ApplicationModuleSource}s by applying the given
|
||||
* {@link ApplicationModuleDetectionStrategy} to the given base package.
|
||||
*
|
||||
* @param pkg must not be {@literal null}.
|
||||
* @param strategy must not be {@literal null}.
|
||||
* @param fullyQualifiedModuleNames whether to use fully qualified module names.
|
||||
* @return will never be {@literal null}.
|
||||
*/
|
||||
public static Stream<ApplicationModuleSource> from(JavaPackage pkg, ApplicationModuleDetectionStrategy strategy,
|
||||
boolean fullyQualifiedModuleNames) {
|
||||
|
||||
Assert.notNull(pkg, "Base package must not be null!");
|
||||
Assert.notNull(strategy, "ApplicationModuleDetectionStrategy must not be null!");
|
||||
|
||||
return strategy.getModuleBasePackages(pkg)
|
||||
.flatMap(it -> it.andSubPackagesAnnotatedWith(org.springframework.modulith.ApplicationModule.class))
|
||||
.map(it -> new ApplicationModuleSource(it, fullyQualifiedModuleNames ? it.getName() : pkg.getTrailingName(it)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new {@link ApplicationModuleSource} for the given {@link JavaPackage} and name.
|
||||
*
|
||||
* @param pkg must not be {@literal null}.
|
||||
* @param name must not be {@literal null} or empty.
|
||||
* @return will never be {@literal null}.
|
||||
*/
|
||||
public static ApplicationModuleSource from(JavaPackage pkg, String name) {
|
||||
|
||||
Assert.hasText(name, "Name must not be null or empty!");
|
||||
|
||||
return new ApplicationModuleSource(pkg, name);
|
||||
}
|
||||
}
|
||||
@@ -138,10 +138,19 @@ public class ApplicationModules implements Iterable<ApplicationModule> {
|
||||
Classes classes = Classes.of(allClasses);
|
||||
var strategy = ApplicationModuleDetectionStrategyLookup.getStrategy();
|
||||
|
||||
this.modules = packages.stream() //
|
||||
var sources = packages.stream() //
|
||||
.map(it -> JavaPackage.of(classes, it))
|
||||
.flatMap(strategy::getModuleBasePackages) //
|
||||
.map(it -> new ApplicationModule(it, useFullyQualifiedModuleNames)) //
|
||||
.flatMap(it -> ApplicationModuleSource.from(it, strategy, useFullyQualifiedModuleNames))
|
||||
.distinct()
|
||||
.collect(Collectors.toUnmodifiableSet());
|
||||
|
||||
this.modules = sources.stream() //
|
||||
.map(it -> {
|
||||
|
||||
return new ApplicationModule(it,
|
||||
JavaPackages.onlySubPackagesOf(it.moduleBasePackage(),
|
||||
sources.stream().map(ApplicationModuleSource::moduleBasePackage).toList())); //
|
||||
})
|
||||
.collect(toMap(ApplicationModule::getName, Function.identity()));
|
||||
|
||||
this.rootPackages = packages.stream() //
|
||||
@@ -154,9 +163,13 @@ public class ApplicationModules implements Iterable<ApplicationModule> {
|
||||
|
||||
this.sharedModules = Collections.emptySet();
|
||||
|
||||
this.orderedNames = JGRAPHT_PRESENT //
|
||||
? TopologicalSorter.topologicallySortModules(this) //
|
||||
: modules.values().stream().map(ApplicationModule::getName).toList();
|
||||
Supplier<List<String>> fallback = () -> modules.values().stream()
|
||||
.map(ApplicationModule::getName)
|
||||
.sorted()
|
||||
.toList();
|
||||
|
||||
this.orderedNames = Optional.ofNullable(JGRAPHT_PRESENT ? TopologicalSorter.topologicallySortModules(this) : null)
|
||||
.orElseGet(fallback);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -511,6 +524,30 @@ public class ApplicationModules implements Iterable<ApplicationModule> {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the parent {@link ApplicationModule} if the given one has one.
|
||||
*
|
||||
* @param module must not be {@literal null}.
|
||||
* @return will never be {@literal null}.
|
||||
* @since 1.3
|
||||
*/
|
||||
public Optional<ApplicationModule> getParentOf(ApplicationModule module) {
|
||||
|
||||
Assert.notNull(module, "ApplicationModule must not be null!");
|
||||
|
||||
return module.getParentModule(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the given {@link ApplicationModule} has a parent one.
|
||||
*
|
||||
* @param module must not be {@literal null}.
|
||||
* @since 1.3
|
||||
*/
|
||||
public boolean hasParent(ApplicationModule module) {
|
||||
return getParentOf(module).isPresent();
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see java.lang.Iterable#iterator()
|
||||
@@ -528,6 +565,7 @@ public class ApplicationModules implements Iterable<ApplicationModule> {
|
||||
public String toString() {
|
||||
|
||||
return this.stream()
|
||||
.sorted()
|
||||
.map(it -> it.toString(this))
|
||||
.collect(Collectors.joining("\n"));
|
||||
}
|
||||
@@ -542,7 +580,7 @@ public class ApplicationModules implements Iterable<ApplicationModule> {
|
||||
var result = SlicesRuleDefinition.slices() //
|
||||
.assignedFrom(new ApplicationModulesSliceAssignment())
|
||||
.should().beFreeOfCycles() //
|
||||
.evaluate(allClasses.that(resideInAPackage(rootPackage.getName().concat(".."))));
|
||||
.evaluate(allClasses.that(resideInAPackage(rootPackage.asFilter())));
|
||||
|
||||
return result.getFailureReport();
|
||||
}
|
||||
@@ -623,14 +661,11 @@ public class ApplicationModules implements Iterable<ApplicationModule> {
|
||||
* @return will never be {@literal null}.
|
||||
* @since 1.1
|
||||
*/
|
||||
private static ApplicationModule rootModuleFor(JavaPackage javaPackage) {
|
||||
private static ApplicationModule rootModuleFor(JavaPackage pkg) {
|
||||
|
||||
return new ApplicationModule(javaPackage, true) {
|
||||
var source = ApplicationModuleSource.from(pkg, "root:" + pkg.getName());
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "root:" + super.getName();
|
||||
}
|
||||
return new ApplicationModule(source, JavaPackages.NONE) {
|
||||
|
||||
@Override
|
||||
public boolean isRootModule() {
|
||||
@@ -726,21 +761,25 @@ public class ApplicationModules implements Iterable<ApplicationModule> {
|
||||
*/
|
||||
private static class TopologicalSorter {
|
||||
|
||||
@Nullable
|
||||
private static List<String> topologicallySortModules(ApplicationModules modules) {
|
||||
|
||||
Graph<ApplicationModule, DefaultEdge> graph = new DefaultDirectedGraph<>(DefaultEdge.class);
|
||||
|
||||
modules.modules.forEach((__, project) -> {
|
||||
modules.modules.values()
|
||||
.stream()
|
||||
.sorted()
|
||||
.forEach(project -> {
|
||||
|
||||
graph.addVertex(project);
|
||||
graph.addVertex(project);
|
||||
|
||||
project.getDependencies(modules).stream() //
|
||||
.map(ApplicationModuleDependency::getTargetModule) //
|
||||
.forEach(dependency -> {
|
||||
graph.addVertex(dependency);
|
||||
graph.addEdge(project, dependency);
|
||||
});
|
||||
});
|
||||
project.getDependencies(modules).stream() //
|
||||
.map(ApplicationModuleDependency::getTargetModule) //
|
||||
.forEach(dependency -> {
|
||||
graph.addVertex(dependency);
|
||||
graph.addEdge(project, dependency);
|
||||
});
|
||||
});
|
||||
|
||||
var names = new ArrayList<String>();
|
||||
var iterator = new TopologicalOrderIterator<>(graph);
|
||||
@@ -751,7 +790,7 @@ public class ApplicationModules implements Iterable<ApplicationModule> {
|
||||
return names;
|
||||
|
||||
} catch (IllegalArgumentException o_O) {
|
||||
return modules.modules.values().stream().map(ApplicationModule::getName).toList();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,17 +18,21 @@ package org.springframework.modulith.core;
|
||||
import static com.tngtech.archunit.core.domain.JavaClass.Predicates.*;
|
||||
import static com.tngtech.archunit.core.domain.properties.CanBeAnnotated.Predicates.*;
|
||||
import static com.tngtech.archunit.core.domain.properties.HasModifiers.Predicates.*;
|
||||
import static java.util.stream.Collectors.*;
|
||||
import static org.springframework.modulith.core.SyntacticSugar.*;
|
||||
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.util.Collection;
|
||||
import java.util.Comparator;
|
||||
import java.util.Iterator;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
import java.util.stream.StreamSupport;
|
||||
|
||||
import org.springframework.core.annotation.AnnotatedElementUtils;
|
||||
import org.springframework.modulith.PackageInfo;
|
||||
@@ -45,36 +49,35 @@ import com.tngtech.archunit.core.domain.JavaModifier;
|
||||
*
|
||||
* @author Oliver Drotbohm
|
||||
*/
|
||||
public class JavaPackage implements DescribedIterable<JavaClass> {
|
||||
public class JavaPackage implements DescribedIterable<JavaClass>, Comparable<JavaPackage> {
|
||||
|
||||
private static final String PACKAGE_INFO_NAME = "package-info";
|
||||
private static final String MULTIPLE_TYPES_ANNOTATED_WITH = "Expected maximum of one type in package %s to be annotated with %s, but got %s!";
|
||||
private static final DescribedPredicate<JavaClass> ARE_PACKAGE_INFOS = //
|
||||
has(simpleName(PACKAGE_INFO_NAME)).or(is(metaAnnotatedWith(PackageInfo.class)));
|
||||
|
||||
private final String name;
|
||||
private final Classes classes;
|
||||
private final Classes packageClasses;
|
||||
private final PackageName name;
|
||||
private final Classes classes, packageClasses;
|
||||
private final Supplier<Set<JavaPackage>> directSubPackages;
|
||||
|
||||
/**
|
||||
* Creates a new {@link JavaPackage} for the given {@link Classes}, name and whether to include all sub-packages.
|
||||
*
|
||||
* @param classes must not be {@literal null}.
|
||||
* @param name must not be {@literal null} or empty.
|
||||
* @param name must not be {@literal null}.
|
||||
* @param includeSubPackages
|
||||
*/
|
||||
private JavaPackage(Classes classes, String name, boolean includeSubPackages) {
|
||||
private JavaPackage(Classes classes, PackageName name, boolean includeSubPackages) {
|
||||
|
||||
Assert.notNull(classes, "Classes must not be null!");
|
||||
Assert.hasText(name, "Name must not be null or empty!");
|
||||
|
||||
this.classes = classes;
|
||||
this.packageClasses = classes.that(resideInAPackage(includeSubPackages ? name.concat("..") : name));
|
||||
this.packageClasses = classes
|
||||
.that(resideInAPackage(name.asFilter(includeSubPackages)));
|
||||
this.name = name;
|
||||
this.directSubPackages = SingletonSupplier.of(() -> packageClasses.stream() //
|
||||
.map(it -> it.getPackageName()) //
|
||||
.filter(it -> !it.equals(name)) //
|
||||
.filter(Predicate.not(name::hasName)) //
|
||||
.map(it -> extractDirectSubPackage(it)) //
|
||||
.distinct() //
|
||||
.map(it -> of(classes, it)) //
|
||||
@@ -89,7 +92,7 @@ public class JavaPackage implements DescribedIterable<JavaClass> {
|
||||
* @return
|
||||
*/
|
||||
public static JavaPackage of(Classes classes, String name) {
|
||||
return new JavaPackage(classes, name, true);
|
||||
return new JavaPackage(classes, new PackageName(name), true);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -110,7 +113,22 @@ public class JavaPackage implements DescribedIterable<JavaClass> {
|
||||
* @return will never be {@literal null}.
|
||||
*/
|
||||
public String getName() {
|
||||
return name;
|
||||
return name.getName();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the name of the given {@link JavaPackage} with regards to the current one.
|
||||
*
|
||||
* @param pkg must not be {@literal null}.
|
||||
* @return will never be {@literal null}.
|
||||
* @since 1.3
|
||||
*/
|
||||
public String getTrailingName(JavaPackage pkg) {
|
||||
|
||||
Assert.notNull(pkg, "JavaPackage must not be null!");
|
||||
Assert.isTrue(pkg.isSubPackageOf(this), "Given package must be a sub-package of the current one!");
|
||||
|
||||
return pkg.getName().substring(getName().length() + 1);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -128,7 +146,7 @@ public class JavaPackage implements DescribedIterable<JavaClass> {
|
||||
* @return will never be {@literal null}.
|
||||
*/
|
||||
public String getLocalName() {
|
||||
return name.substring(name.lastIndexOf('.') + 1);
|
||||
return name.getLocalName();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -174,6 +192,7 @@ public class JavaPackage implements DescribedIterable<JavaClass> {
|
||||
|
||||
return packageClasses.that(ARE_PACKAGE_INFOS.and(are(metaAnnotatedWith(annotation)))).stream() //
|
||||
.map(JavaClass::getPackageName) //
|
||||
.filter(Predicate.not(name::hasName))
|
||||
.distinct() //
|
||||
.map(it -> of(classes, it));
|
||||
}
|
||||
@@ -182,7 +201,7 @@ public class JavaPackage implements DescribedIterable<JavaClass> {
|
||||
* Returns all {@link Classes} that match the given {@link DescribedPredicate}.
|
||||
*
|
||||
* @param predicate must not be {@literal null}.
|
||||
* @return
|
||||
* @return will never be {@literal null}.
|
||||
*/
|
||||
public Classes that(DescribedPredicate<? super JavaClass> predicate) {
|
||||
|
||||
@@ -240,6 +259,105 @@ public class JavaPackage implements DescribedIterable<JavaClass> {
|
||||
.map(it -> AnnotatedElementUtils.getMergedAnnotation(it, annotationType));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the name of the package.
|
||||
*
|
||||
* @return will never be {@literal null}.
|
||||
* @since 1.3
|
||||
*/
|
||||
PackageName getPackageName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a filter expression including all types within that package and any nested package.
|
||||
*
|
||||
* @return will never be {@literal null}.
|
||||
* @since 1.3
|
||||
*/
|
||||
String asFilter() {
|
||||
return name.asFilter(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new {@link Stream} of the current package and all its sub-packages annotated with the given annotation
|
||||
* type.
|
||||
*
|
||||
* @param type must not be {@literal null}.
|
||||
* @return will never be {@literal null}.
|
||||
* @since 1.3
|
||||
*/
|
||||
Stream<JavaPackage> andSubPackagesAnnotatedWith(Class<? extends Annotation> type) {
|
||||
|
||||
Assert.notNull(type, "Annotation type must not be null!");
|
||||
|
||||
return Stream.concat(Stream.of(this), getSubPackagesAnnotatedWith(type));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the given reference package is a sub-package of the current one.
|
||||
*
|
||||
* @param reference must not be {@literal null}.
|
||||
* @return will never be {@literal null}.
|
||||
* @since 1.3
|
||||
*/
|
||||
boolean isSubPackageOf(JavaPackage reference) {
|
||||
|
||||
Assert.notNull(reference, "Reference package must not be null!");
|
||||
|
||||
return name.isSubPackageOf(reference.name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all Classes residing in the current package but not in any of the given sub-packages.
|
||||
*
|
||||
* @param exclusions must not be {@literal null}.
|
||||
* @return will never be {@literal null}.
|
||||
* @since 1.3
|
||||
*/
|
||||
Classes getClasses(Iterable<JavaPackage> exclusions) {
|
||||
|
||||
Assert.notNull(exclusions, "Object must not be null!");
|
||||
|
||||
var excludedPackages = StreamSupport.stream(exclusions.spliterator(), false)
|
||||
.map(JavaPackage::asFilter)
|
||||
.toArray(String[]::new);
|
||||
|
||||
return packageClasses.that(resideOutsideOfPackages(excludedPackages));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a {@link JavaPackages} instance representing all sub-packages.
|
||||
*
|
||||
* @return will never be {@literal null}.
|
||||
* @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));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the sub-package with the given local name.
|
||||
*
|
||||
* @param localName must not be {@literal null} or empty.
|
||||
* @return will never be {@literal null}.
|
||||
* @since 1.3
|
||||
*/
|
||||
Optional<JavaPackage> getSubPackage(String localName) {
|
||||
|
||||
Assert.hasText(localName, "Local name must not be null or empty!");
|
||||
|
||||
return getSubPackages().stream()
|
||||
.filter(it -> it.getLocalName().equals(localName))
|
||||
.findFirst();
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the annotation of the given type declared on the package itself or any type located the direct package's
|
||||
* types .
|
||||
@@ -290,6 +408,15 @@ public class JavaPackage implements DescribedIterable<JavaClass> {
|
||||
return classes.iterator();
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see java.lang.Comparable#compareTo(java.lang.Object)
|
||||
*/
|
||||
@Override
|
||||
public int compareTo(JavaPackage o) {
|
||||
return name.compareTo(o.name);
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see java.lang.Object#toString()
|
||||
@@ -297,9 +424,9 @@ public class JavaPackage implements DescribedIterable<JavaClass> {
|
||||
@Override
|
||||
public String toString() {
|
||||
|
||||
return new StringBuilder(name) //
|
||||
return new StringBuilder(name.toString()) //
|
||||
.append("\n") //
|
||||
.append(getClasses().format(name)) //
|
||||
.append(getClasses().format(name.toString())) //
|
||||
.append('\n') //
|
||||
.toString();
|
||||
}
|
||||
@@ -331,7 +458,7 @@ public class JavaPackage implements DescribedIterable<JavaClass> {
|
||||
*/
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(classes, directSubPackages, name, packageClasses);
|
||||
return Objects.hash(classes, directSubPackages.get(), name, packageClasses);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -351,4 +478,8 @@ public class JavaPackage implements DescribedIterable<JavaClass> {
|
||||
|
||||
return candidate.substring(0, endIndex);
|
||||
}
|
||||
|
||||
static Comparator<JavaPackage> reverse() {
|
||||
return (left, right) -> -left.compareTo(right);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
/*
|
||||
* Copyright 2023-2024 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 org.springframework.modulith.core;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* A collection of {@link JavaPackage}s.
|
||||
*
|
||||
* @author Oliver Drotbohm
|
||||
* @since 1.3
|
||||
*/
|
||||
class JavaPackages implements Iterable<JavaPackage> {
|
||||
|
||||
public static JavaPackages NONE = new JavaPackages(Collections.emptyList());
|
||||
|
||||
private final List<JavaPackage> packages;
|
||||
|
||||
/**
|
||||
* Creates a new {@link JavaPackages} instance for the given {@link JavaPackage}s.
|
||||
*
|
||||
* @param packages must not be {@literal null}.
|
||||
*/
|
||||
JavaPackages(Collection<JavaPackage> packages) {
|
||||
|
||||
Assert.notNull(packages, "Packages must not be null!");
|
||||
|
||||
this.packages = packages.stream().sorted().toList();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new {@link JavaPackages} containing all sub-packages of the given reference package contained in the
|
||||
* given packages.
|
||||
*
|
||||
* @param reference must not be {@literal null}.
|
||||
* @param packages must not be {@literal null}.
|
||||
* @return will never be {@literal null}.
|
||||
*/
|
||||
public static JavaPackages onlySubPackagesOf(JavaPackage reference, Collection<JavaPackage> packages) {
|
||||
|
||||
Assert.notNull(reference, "Reference package must not be null!");
|
||||
Assert.notNull(packages, "Packages must not be null!");
|
||||
|
||||
var subPackages = packages.stream()
|
||||
.filter(it -> it.isSubPackageOf(reference))
|
||||
.toList();
|
||||
|
||||
return subPackages.isEmpty() ? NONE : new JavaPackages(subPackages).flatten();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a {@link JavaPackages} instance that only contains the primary packages contained in the current
|
||||
* {@link JavaPackages}. Any package that's a sub-package of any other package will get dropped.
|
||||
* <p>
|
||||
* In other words for a list of {code com.foo}, {@code com.bar}, and {@code com.foo.bar}, only {@code com.foo} and
|
||||
* {@code com.bar} will be retained.
|
||||
*
|
||||
* @return will never be {@literal null}.
|
||||
*/
|
||||
JavaPackages flatten() {
|
||||
return packages.isEmpty() ? this : new JavaPackages(removeSubPackages(packages));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a stream of {@link JavaPackage}s.
|
||||
*
|
||||
* @return will never be {@literal null}.
|
||||
*/
|
||||
Stream<JavaPackage> stream() {
|
||||
return packages.stream();
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see java.lang.Iterable#iterator()
|
||||
*/
|
||||
@Override
|
||||
public Iterator<JavaPackage> iterator() {
|
||||
return packages.iterator();
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all sub-packages from the given list of packages.
|
||||
*
|
||||
* @param packages must not be {@literal null}.
|
||||
* @return will never be {@literal null}.
|
||||
*/
|
||||
private static List<JavaPackage> removeSubPackages(List<JavaPackage> packages) {
|
||||
|
||||
Assert.notNull(packages, "Packages must not be null!");
|
||||
|
||||
if (packages.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
var result = new ArrayList<JavaPackage>();
|
||||
|
||||
for (JavaPackage candidate : packages) {
|
||||
if (result.stream().noneMatch(candidate::isSubPackageOf)) {
|
||||
result.add(candidate);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -65,7 +65,7 @@ public class NamedInterfaces implements Iterable<NamedInterface> {
|
||||
|
||||
return NamedInterfaces.of(NamedInterface.unnamed(basePackage, true))
|
||||
.and(ofAnnotatedPackages(basePackage))
|
||||
.and(ofAnnotatedTypes(basePackage));
|
||||
.and(ofAnnotatedTypes(basePackage.getClasses()));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -106,7 +106,7 @@ public class NamedInterfaces implements Iterable<NamedInterface> {
|
||||
|
||||
return NamedInterfaces.of(NamedInterface.unnamed(basePackage, false))
|
||||
.and(ofAnnotatedPackages(basePackage))
|
||||
.and(ofAnnotatedTypes(basePackage));
|
||||
.and(ofAnnotatedTypes(basePackage.getClasses()));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -242,11 +242,11 @@ public class NamedInterfaces implements Iterable<NamedInterface> {
|
||||
return new NamedInterfaces(List.of(interfaces));
|
||||
}
|
||||
|
||||
private static List<NamedInterface> ofAnnotatedTypes(JavaPackage basePackage) {
|
||||
private static List<NamedInterface> ofAnnotatedTypes(Classes classes) {
|
||||
|
||||
var mappings = new LinkedMultiValueMap<String, JavaClass>();
|
||||
|
||||
basePackage.stream() //
|
||||
classes.stream() //
|
||||
.filter(it -> !JavaPackage.isPackageInfoType(it)) //
|
||||
.forEach(it -> {
|
||||
|
||||
|
||||
@@ -0,0 +1,192 @@
|
||||
/*
|
||||
* Copyright 2023 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 org.springframework.modulith.core;
|
||||
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* The name of a Java package. Packages are sortable comparing their individual segments and deeper packages sorted
|
||||
* last.
|
||||
*
|
||||
* @author Oliver Drotbohm
|
||||
* @since 1.2
|
||||
*/
|
||||
class PackageName implements Comparable<PackageName> {
|
||||
|
||||
private final String name;
|
||||
private final String[] segments;
|
||||
|
||||
/**
|
||||
* Creates a new {@link PackageName} with the given name.
|
||||
*
|
||||
* @param name must not be {@literal null} or empty.
|
||||
*/
|
||||
public PackageName(String name) {
|
||||
|
||||
Assert.hasText(name, "Name must not be null or empty!");
|
||||
|
||||
this.name = name;
|
||||
this.segments = name.split("\\.");
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the length of the package name.
|
||||
*
|
||||
* @return will never be {@literal null}.
|
||||
*/
|
||||
int length() {
|
||||
return name.length();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the raw name.
|
||||
*
|
||||
* @return will never be {@literal null}.
|
||||
*/
|
||||
String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the {@link PackageName} has the given {@link String} name.
|
||||
*
|
||||
* @param name must not be {@literal null} or empty.
|
||||
*/
|
||||
boolean hasName(String name) {
|
||||
|
||||
Assert.hasText(name, "Name must not be null or empty!");
|
||||
|
||||
return this.name.equals(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the last segment of a package name.
|
||||
*
|
||||
* @return will never be {@literal null}.
|
||||
*/
|
||||
String getLocalName() {
|
||||
return segments[segments.length - 1];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the nested name in reference to the given base.
|
||||
*
|
||||
* @param base must not be {@literal null} or empty.
|
||||
* @return will never be {@literal null}.
|
||||
*/
|
||||
String getLocalName(String base) {
|
||||
|
||||
Assert.hasText(base, "Base must not be null or empty!");
|
||||
Assert.isTrue(name.startsWith(base + "."),
|
||||
() -> "Given name %s is not a parent of the current package %s!".formatted(base, name));
|
||||
|
||||
return name.substring(base.length() + 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the filter expression to include all types including from nested packages.
|
||||
*
|
||||
* @return will never be {@literal null}.
|
||||
*/
|
||||
String asFilter(boolean includeNested) {
|
||||
return includeNested ? name.concat("..") : name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the current {@link PackageName} is the name of a parent package of the given one.
|
||||
*
|
||||
* @param reference must not be {@literal null}.
|
||||
* @return will never be {@literal null}.
|
||||
*/
|
||||
boolean isParentPackageOf(PackageName reference) {
|
||||
|
||||
Assert.notNull(reference, "Reference package name must not be null!");
|
||||
|
||||
return reference.name.startsWith(name + ".");
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the current {@link PackageName} is the name of a sub-package with the given name.
|
||||
*
|
||||
* @param reference must not be {@literal null}.
|
||||
* @return will never be {@literal null}.
|
||||
*/
|
||||
boolean isSubPackageOf(PackageName reference) {
|
||||
|
||||
Assert.notNull(reference, "Reference package name must not be null!");
|
||||
|
||||
return name.startsWith(reference.name + ".");
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see java.lang.Comparable#compareTo(java.lang.Object)
|
||||
*/
|
||||
@Override
|
||||
public int compareTo(PackageName o) {
|
||||
|
||||
for (var i = 0; i < segments.length; i++) {
|
||||
|
||||
if (o.segments.length <= i) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
var segCompare = segments[i].compareTo(o.segments[i]);
|
||||
|
||||
if (segCompare != 0) {
|
||||
return segCompare;
|
||||
}
|
||||
}
|
||||
|
||||
return segments.length - o.segments.length;
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see java.lang.Object#toString()
|
||||
*/
|
||||
@Override
|
||||
public String toString() {
|
||||
return name;
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see java.lang.Object#equals(java.lang.Object)
|
||||
*/
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
|
||||
if (obj == this) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!(obj instanceof PackageName that)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return this.name.equals(that.name);
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see java.lang.Object#hashCode()
|
||||
*/
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return name.hashCode();
|
||||
}
|
||||
}
|
||||
@@ -112,41 +112,37 @@ public class Violations extends RuntimeException {
|
||||
|
||||
Assert.notNull(violation, "Exception must not be null!");
|
||||
|
||||
if (violations.isEmpty()) {
|
||||
return new Violations(List.of(violation));
|
||||
}
|
||||
|
||||
if (violations.contains(violation)) {
|
||||
return this;
|
||||
}
|
||||
|
||||
List<Violation> newExceptions = new ArrayList<>(violations.size() + 1);
|
||||
newExceptions.addAll(violations);
|
||||
newExceptions.add(violation);
|
||||
|
||||
return new Violations(newExceptions);
|
||||
return new Violations(unionByMessage(violations, List.of(violation)));
|
||||
}
|
||||
|
||||
Violations and(Violations other) {
|
||||
|
||||
if (violations.isEmpty()) {
|
||||
return new Violations(other.violations);
|
||||
}
|
||||
|
||||
var newExceptions = new ArrayList<>(violations);
|
||||
|
||||
for (Violation candidate : other.violations) {
|
||||
if (!violations.contains(candidate)) {
|
||||
newExceptions.add(candidate);
|
||||
}
|
||||
}
|
||||
|
||||
return new Violations(newExceptions);
|
||||
return new Violations(unionByMessage(violations, other.violations));
|
||||
}
|
||||
|
||||
Violations and(String violation) {
|
||||
return and(new Violation(violation));
|
||||
}
|
||||
|
||||
private List<Violation> unionByMessage(List<Violation> left, List<Violation> right) {
|
||||
|
||||
if (left.isEmpty()) {
|
||||
return right;
|
||||
}
|
||||
|
||||
if (right.isEmpty()) {
|
||||
return left;
|
||||
}
|
||||
|
||||
var result = new ArrayList<>(left);
|
||||
|
||||
var messages = left.stream().map(Violation::message).toList();
|
||||
|
||||
right.stream()
|
||||
.filter(it -> !messages.contains(it.message()))
|
||||
.forEach(result::add);
|
||||
|
||||
return result.size() == left.size() ? left : result;
|
||||
}
|
||||
|
||||
static record Violation(String message) {}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package example.empty;
|
||||
package empty;
|
||||
|
||||
import org.springframework.modulith.Modulithic;
|
||||
|
||||
@@ -21,6 +21,4 @@ import org.springframework.modulith.Modulithic;
|
||||
* @author Oliver Drotbohm
|
||||
*/
|
||||
@Modulithic
|
||||
public class EmptyApplication {
|
||||
|
||||
}
|
||||
public class EmptyApplication {}
|
||||
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* Copyright 2023 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 example.invalid;
|
||||
|
||||
import example.ni.nested.InNested;
|
||||
import example.ni.nested.b.first.InNestedBFirst;
|
||||
|
||||
/**
|
||||
* @author Oliver Drotbohm
|
||||
*/
|
||||
public class Invalid {
|
||||
InNested invalid;
|
||||
InNestedBFirst invalidToo;
|
||||
}
|
||||
@@ -1,2 +1,2 @@
|
||||
@org.jmolecules.ddd.annotation.Module
|
||||
package jmolecules;
|
||||
package example.jmolecules;
|
||||
@@ -15,7 +15,17 @@
|
||||
*/
|
||||
package example.ni;
|
||||
|
||||
import example.ni.nested.b.first.InNestedBFirst;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* @author Oliver Drotbohm
|
||||
*/
|
||||
public interface RootType {}
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class RootType {
|
||||
|
||||
final InNestedBFirst inNestedBFirst;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
/*
|
||||
* Copyright 2023 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 example.ni.nested;
|
||||
|
||||
/**
|
||||
* @author Oliver Drotbohm
|
||||
*/
|
||||
public class InNested {}
|
||||
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* Copyright 2023 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 example.ni.nested.a;
|
||||
|
||||
import example.ni.internal.Internal;
|
||||
|
||||
/**
|
||||
* @author Oliver Drotbohm
|
||||
*/
|
||||
public class InNestedA {
|
||||
Internal internal;
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* Copyright 2023 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 example.ni.nested.b;
|
||||
|
||||
import example.ni.RootType;
|
||||
import example.ni.nested.b.first.InNestedBFirst;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* @author Oliver Drotbohm
|
||||
*/
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class InNestedB {
|
||||
|
||||
final InNestedBFirst downward;
|
||||
final RootType upward;
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* Copyright 2023 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 example.ni.nested.b.first;
|
||||
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* @author Oliver Drotbohm
|
||||
*/
|
||||
@Component
|
||||
public class InNestedBFirst {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
@org.springframework.modulith.ApplicationModule
|
||||
package example.ni.nested.b.first;
|
||||
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* Copyright 2023 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 example.ni.nested.b.second;
|
||||
|
||||
import example.ni.nested.InNested;
|
||||
|
||||
/**
|
||||
* @author Oliver Drotbohm
|
||||
*/
|
||||
public class InNestedBSecond {
|
||||
InNested inNested;
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
@org.springframework.modulith.ApplicationModule
|
||||
package example.ni.nested.b.second;
|
||||
@@ -0,0 +1,2 @@
|
||||
@org.springframework.modulith.ApplicationModule
|
||||
package example.ni.nested;
|
||||
@@ -0,0 +1,79 @@
|
||||
/*
|
||||
* Copyright 2023 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 org.springframework.modulith.core;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link ApplicationModules}.
|
||||
*
|
||||
* @author Oliver Drotbohm
|
||||
*/
|
||||
class ApplicationModulesUnitTests {
|
||||
|
||||
ApplicationModules modules = TestUtils.of("example", "example.ninvalid");
|
||||
|
||||
@Test // GH 578
|
||||
void discoversComplexModuleArrangement() {
|
||||
|
||||
assertThat(modules)
|
||||
.extracting(ApplicationModule::getName)
|
||||
.containsExactlyInAnyOrder(
|
||||
"invalid",
|
||||
"jmolecules",
|
||||
"ni",
|
||||
"ni.nested",
|
||||
"ni.nested.b.first",
|
||||
"ni.nested.b.second",
|
||||
"springbean");
|
||||
}
|
||||
|
||||
@Test // GH 578
|
||||
void detectsModuleNesting() {
|
||||
|
||||
var ni = modules.getModuleByName("ni").orElseThrow();
|
||||
var nested = modules.getModuleByName("ni.nested").orElseThrow();
|
||||
var inner = modules.getModuleByName("ni.nested.b.first").orElseThrow();
|
||||
|
||||
assertThat(inner.getParentModule(modules)).hasValue(nested);
|
||||
assertThat(nested.getParentModule(modules)).hasValue(ni);
|
||||
assertThat(ni.getParentModule(modules)).isEmpty();
|
||||
|
||||
assertThat(ni.getDirectlyNestedModules(modules))
|
||||
.extracting(ApplicationModule::getName)
|
||||
.containsExactlyInAnyOrder("ni.nested");
|
||||
|
||||
assertThat(ni.getNestedModules(modules))
|
||||
.extracting(ApplicationModule::getName)
|
||||
.containsExactlyInAnyOrder("ni.nested", "ni.nested.b.first", "ni.nested.b.second");
|
||||
}
|
||||
|
||||
@Test // GH 578
|
||||
void detectsInvalidReferenceToNestedModule() {
|
||||
|
||||
var violations = modules.detectViolations();
|
||||
var messages = violations.getMessages();
|
||||
|
||||
assertThat(messages)
|
||||
.hasSize(3)
|
||||
.satisfiesExactlyInAnyOrder(
|
||||
it -> assertThat(it).contains("Invalid", "'invalid'", "'ni.nested'"),
|
||||
it -> assertThat(it).contains("Invalid", "'invalid'", "'ni.nested.b.first'"),
|
||||
it -> assertThat(it).contains("Invalid", "'ni'", "'ni.nested.b.first'"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
/*
|
||||
* Copyright 2024 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 org.springframework.modulith.core;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
|
||||
import example.ni.nested.a.InNestedA;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import com.tngtech.archunit.core.domain.JavaClass;
|
||||
import com.tngtech.archunit.core.domain.JavaClasses;
|
||||
import com.tngtech.archunit.core.importer.ClassFileImporter;
|
||||
import com.tngtech.archunit.core.importer.ImportOption;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link JavaPackage}.
|
||||
*
|
||||
* @author Oliver Drotbohm
|
||||
*/
|
||||
class JavaPackageUnitTests {
|
||||
|
||||
static final JavaClasses ALL_CLASSES = new ClassFileImporter() //
|
||||
.withImportOption(ImportOption.Predefined.ONLY_INCLUDE_TESTS)
|
||||
.importPackages("example");
|
||||
|
||||
Classes classes = Classes.of(ALL_CLASSES);
|
||||
JavaPackage pkg = JavaPackage.of(classes, "example.ni");
|
||||
|
||||
@Test // GH-578
|
||||
void detectsDirectSubPackages() throws Exception {
|
||||
|
||||
assertThat(pkg.getLocalName()).isEqualTo("ni");
|
||||
assertThat(pkg.getDirectSubPackages()) //
|
||||
.extracting(JavaPackage::getLocalName) //
|
||||
.containsExactlyInAnyOrder("api", "internal", "nested", "ontype", "spi");
|
||||
}
|
||||
|
||||
@Test // GH-578
|
||||
void detectsAllSubPackages() {
|
||||
|
||||
var subPackages = pkg.getSubPackages();
|
||||
|
||||
assertThat(subPackages.stream()
|
||||
.map(JavaPackage::getPackageName)
|
||||
.map(it -> it.getLocalName("example.ni")))
|
||||
.containsExactly(
|
||||
"api",
|
||||
"internal",
|
||||
"nested",
|
||||
"nested.a",
|
||||
"nested.b",
|
||||
"nested.b.first",
|
||||
"nested.b.second",
|
||||
"ontype",
|
||||
"spi");
|
||||
}
|
||||
|
||||
@Test // GH-578
|
||||
void detectsFlattenedSubPackages() {
|
||||
|
||||
var subPackages = pkg.getSubPackages();
|
||||
|
||||
assertThat(subPackages.flatten().stream()
|
||||
.map(JavaPackage::getPackageName)
|
||||
.map(it -> it.getLocalName("example.ni")))
|
||||
.containsExactlyInAnyOrder("api", "internal", "nested", "ontype", "spi");
|
||||
}
|
||||
|
||||
@Test // GH-578
|
||||
void considersExclusionsForClassesLookup() {
|
||||
|
||||
assertThat(pkg.contains(InNestedA.class.getName()));
|
||||
|
||||
var nestedA = JavaPackage.of(classes, "example.ni.nested.a");
|
||||
assertThat(nestedA.contains(InNestedA.class.getName()));
|
||||
|
||||
assertThat(pkg.getClasses(List.of(nestedA)).stream().map(JavaClass::getName))
|
||||
.doesNotContain(InNestedA.class.getName());
|
||||
}
|
||||
|
||||
@Test // GH-578
|
||||
void detectsSubPackage() {
|
||||
|
||||
var classes = Classes.of(ALL_CLASSES);
|
||||
var root = JavaPackage.of(classes, "example.ni");
|
||||
var direct = JavaPackage.of(classes, "example.ni.internal");
|
||||
var nested = JavaPackage.of(classes, "example.ni.internal.nested");
|
||||
|
||||
assertThat(direct.isSubPackageOf(root)).isTrue();
|
||||
assertThat(nested.isSubPackageOf(root)).isTrue();
|
||||
assertThat(nested.isSubPackageOf(direct)).isTrue();
|
||||
}
|
||||
|
||||
@Test // GH-578
|
||||
void samePackagesConsideredEqual() {
|
||||
|
||||
var first = JavaPackage.of(classes, "example.ni");
|
||||
var second = JavaPackage.of(classes, "example.ni");
|
||||
|
||||
assertThat(first.equals(second)).isTrue();
|
||||
assertThat(second.equals(first)).isTrue();
|
||||
}
|
||||
}
|
||||
@@ -34,11 +34,12 @@ class ModuleDetectionStrategyUnitTest {
|
||||
|
||||
var classes = new ClassFileImporter() //
|
||||
.withImportOption(new ImportOption.OnlyIncludeTests()) //
|
||||
.importPackages("jmolecules");
|
||||
.importPackages("example.jmolecules");
|
||||
|
||||
var javaPackage = JavaPackage.of(Classes.of(classes), "jmolecules");
|
||||
var javaPackage = JavaPackage.of(Classes.of(classes), "example");
|
||||
var expected = javaPackage.getSubPackage("jmolecules").orElseThrow();
|
||||
|
||||
assertThat(ApplicationModuleDetectionStrategy.explicitlyAnnotated().getModuleBasePackages(javaPackage))
|
||||
.containsExactly(javaPackage);
|
||||
.containsExactly(expected);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,8 +33,6 @@ import com.acme.withatbean.SampleAggregate;
|
||||
import com.acme.withatbean.TestEvents.JMoleculesAnnotated;
|
||||
import com.acme.withatbean.TestEvents.JMoleculesImplementing;
|
||||
import com.tngtech.archunit.core.domain.JavaClass;
|
||||
import com.tngtech.archunit.core.domain.JavaClasses;
|
||||
import com.tngtech.archunit.core.importer.ClassFileImporter;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link ApplicationModule}.
|
||||
@@ -45,10 +43,7 @@ import com.tngtech.archunit.core.importer.ClassFileImporter;
|
||||
class ModuleUnitTest {
|
||||
|
||||
String packageName = "com.acme.withatbean";
|
||||
JavaClasses classes = new ClassFileImporter().importPackages(packageName);
|
||||
JavaPackage javaPackage = JavaPackage.of(Classes.of(classes), packageName);
|
||||
|
||||
ApplicationModule module = new ApplicationModule(javaPackage, false);
|
||||
ApplicationModule module = TestUtils.getApplicationModule(packageName);
|
||||
|
||||
@Test
|
||||
public void considersExternalSpringBeans() {
|
||||
@@ -61,8 +56,10 @@ class ModuleUnitTest {
|
||||
@Test
|
||||
void discoversPublishedEvents() {
|
||||
|
||||
JavaClass jMoleculesAnnotated = classes.get(JMoleculesAnnotated.class);
|
||||
JavaClass jMoleculesImplementing = classes.get(JMoleculesImplementing.class);
|
||||
var classes = module.getClasses();
|
||||
|
||||
JavaClass jMoleculesAnnotated = classes.getRequiredClass(JMoleculesAnnotated.class);
|
||||
JavaClass jMoleculesImplementing = classes.getRequiredClass(JMoleculesImplementing.class);
|
||||
|
||||
List<EventType> events = module.getPublishedEvents();
|
||||
|
||||
|
||||
@@ -23,6 +23,11 @@ import example.ni.api.ApiType;
|
||||
import example.ni.internal.AdditionalSpiType;
|
||||
import example.ni.internal.DefaultedNamedInterfaceType;
|
||||
import example.ni.internal.Internal;
|
||||
import example.ni.nested.InNested;
|
||||
import example.ni.nested.a.InNestedA;
|
||||
import example.ni.nested.b.InNestedB;
|
||||
import example.ni.nested.b.first.InNestedBFirst;
|
||||
import example.ni.nested.b.second.InNestedBSecond;
|
||||
import example.ni.ontype.Exposed;
|
||||
import example.ni.spi.SpiType;
|
||||
import example.ninvalid.InvalidDefaultNamedInterface;
|
||||
@@ -62,7 +67,8 @@ class NamedInterfacesUnitTests {
|
||||
|
||||
var javaPackage = TestUtils.getPackage(InvalidDefaultNamedInterface.class);
|
||||
|
||||
assertThatIllegalStateException().isThrownBy(() -> NamedInterfaces.discoverNamedInterfaces(javaPackage))
|
||||
assertThatIllegalStateException()
|
||||
.isThrownBy(() -> NamedInterfaces.discoverNamedInterfaces(javaPackage))
|
||||
.withMessageContaining("named interface defaulting")
|
||||
.withMessageContaining(InvalidDefaultNamedInterface.class.getSimpleName());
|
||||
}
|
||||
@@ -76,7 +82,9 @@ class NamedInterfacesUnitTests {
|
||||
assertThat(interfaces).map(NamedInterface::getName)
|
||||
.containsExactlyInAnyOrder(NamedInterface.UNNAMED_NAME, "api", "spi", "kpi", "internal", "ontype");
|
||||
|
||||
assertInterfaceContains(interfaces, NamedInterface.UNNAMED_NAME, RootType.class, Internal.class);
|
||||
assertInterfaceContains(interfaces, NamedInterface.UNNAMED_NAME,
|
||||
RootType.class, Internal.class, InNested.class, InNestedA.class, InNestedB.class, InNestedBFirst.class,
|
||||
InNestedBSecond.class);
|
||||
}
|
||||
|
||||
@Test // GH-595
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* Copyright 2023 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 org.springframework.modulith.core;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link PackageName}.
|
||||
*
|
||||
* @author Oliver Drotbohm
|
||||
*/
|
||||
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");
|
||||
|
||||
assertThat(List.of(comAcmeAFirstOne, comAcmeB, comAcmeASecond, comAcmeAFirst, comAcme, comAcmeA)
|
||||
.stream()
|
||||
.sorted((l, r) -> -l.compareTo(r))
|
||||
.map(it -> it.getLocalName("com")))
|
||||
.containsExactly("acme.b", "acme.a.second", "acme.a.first.one", "acme.a.first", "acme.a", "acme");
|
||||
}
|
||||
}
|
||||
@@ -46,6 +46,17 @@ public class TestUtils {
|
||||
private static Supplier<Classes> classes = SingletonSupplier
|
||||
.of(() -> Classes.of(imported.get()).that(IS_MODULE_TYPE));
|
||||
|
||||
/**
|
||||
* Creates an {@link ApplicationModules} instance from the given package but only inspecting the test code.
|
||||
*
|
||||
* @param basePackage must not be {@literal null} or empty.
|
||||
* @return will never be {@literal null}.
|
||||
* @since 1.3
|
||||
*/
|
||||
public static ApplicationModules of(String basePackage, String... ignoredPackages) {
|
||||
return of(ModulithMetadata.of(basePackage), JavaClass.Predicates.resideInAnyPackage(ignoredPackages));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all {@link Classes} of this module.
|
||||
*
|
||||
@@ -78,12 +89,12 @@ public class TestUtils {
|
||||
return JavaPackage.of(TestUtils.getClasses(packageType), packageType.getPackageName());
|
||||
}
|
||||
|
||||
public static ApplicationModules of(String basePackage, String... ignoredPackages) {
|
||||
return of(ModulithMetadata.of(basePackage), JavaClass.Predicates.resideInAnyPackage(ignoredPackages));
|
||||
}
|
||||
|
||||
public static ApplicationModule getApplicationModule(String packageName) {
|
||||
return new ApplicationModule(getPackage(packageName), false);
|
||||
|
||||
var pkg = getPackage(packageName);
|
||||
var source = ApplicationModuleSource.from(pkg, pkg.getLocalName());
|
||||
|
||||
return new ApplicationModule(source);
|
||||
}
|
||||
|
||||
private static JavaPackage getPackage(String name) {
|
||||
|
||||
@@ -575,7 +575,8 @@ public class Documenter {
|
||||
ComponentView componentView = createComponentView(options);
|
||||
componentView.setTitle(getDefaultedSystemName());
|
||||
|
||||
addComponentsToView(() -> modules.stream(), componentView, options, it -> {});
|
||||
addComponentsToView(() -> modules.stream().filter(Predicate.not(modules::hasParent)), componentView,
|
||||
options, it -> {});
|
||||
|
||||
return render(componentView, options);
|
||||
}
|
||||
|
||||
@@ -1,11 +1,6 @@
|
||||
antora:
|
||||
extensions:
|
||||
- '@springio/antora-extensions/partial-build-extension'
|
||||
- require: '@springio/antora-extensions/latest-version-extension'
|
||||
- require: '@springio/antora-extensions/inject-collector-cache-config-extension'
|
||||
- '@antora/collector-extension'
|
||||
- '@antora/atlas-extension'
|
||||
- require: '@springio/antora-extensions/root-component-extension'
|
||||
- require: '@springio/antora-extensions'
|
||||
root_component_name: 'modulith'
|
||||
site:
|
||||
title: Spring Modulith
|
||||
@@ -36,4 +31,4 @@ runtime:
|
||||
format: pretty
|
||||
ui:
|
||||
bundle:
|
||||
url: https://github.com/spring-io/antora-ui-spring/releases/download/v0.3.7/ui-bundle.zip
|
||||
url: https://github.com/spring-io/antora-ui-spring/releases/download/v0.4.16/ui-bundle.zip
|
||||
|
||||
@@ -158,7 +158,7 @@ icon:cubes[] Example
|
||||
├─ **icon:cube[] example.order**
|
||||
| └─ icon:plus-circle[role=green] OrderManagement.java
|
||||
└─ icon:cube[] example.order.internal
|
||||
└─ icon:plus-circle[role=green] SomethingOrderInternal.java
|
||||
└─ icon:plus-circle[role=red] SomethingOrderInternal.java
|
||||
----
|
||||
|
||||
In such an arrangement, the `order` package is considered an API package.
|
||||
@@ -169,8 +169,47 @@ Note how `SomethingOrderInternal` is a public type, likely because `OrderManagem
|
||||
This unfortunately means that it can also be referred to from other packages such as the `inventory` one.
|
||||
In this case, the Java compiler is not of much use to prevent these illegal references.
|
||||
|
||||
[[modules.advanced.open]]
|
||||
==== Open Application Modules
|
||||
[[modules.nested]]
|
||||
=== Nested Application Modules
|
||||
|
||||
As of version 1.3, Spring Modulith application modules can contain nested modules.
|
||||
This allows governing the internal structure in case a module contains parts to be logically separated in turn.
|
||||
To define nested application modules, explicitly annotate packages that are supposed to constitute with `@ApplicationModule`.
|
||||
|
||||
[source, subs="macros, quotes"]
|
||||
----
|
||||
icon:cubes[] Example
|
||||
└─ icon:folder[] src/main/java
|
||||
├─ icon:cube[] example
|
||||
| └─ icon:plus-circle[role=green] Application.java
|
||||
├─ icon:cube[] example.inventory
|
||||
| ├─ icon:plus-circle[role=green] InventoryManagement.java
|
||||
| └─ icon:minus-circle[role=red] SomethingInventoryInternal.java
|
||||
├─ icon:cube[] example.inventory.internal
|
||||
| └─ icon:plus-circle[role=red] SomethingInventoryInternal.java
|
||||
├─ icon:cube[] example.inventory.nested
|
||||
| ├─ icon:coffee[] package-info.java // @ApplicationModule
|
||||
| └─ icon:plus-circle[role=yellow] NestedApi.java
|
||||
├─ icon:cube[] example.inventory.nested.internal
|
||||
| └─ icon:minus-circle[role=red] NestedInternal.java
|
||||
└─ icon:cube[] example.order
|
||||
├─ icon:plus-circle[role=green] OrderManagement.java
|
||||
└─ icon:minus-circle[role=red] SomethingOrderInternal.java
|
||||
|
||||
----
|
||||
|
||||
In this example `inventory` is an application module as described xref:fundamentals.adoc#modules.simple[above].
|
||||
The `@ApplicationModule` annotation on the `nested` package caused that to become a nested application module in turn.
|
||||
In that arrangement, the following access rules apply:
|
||||
|
||||
* The code in _Nested_ is only available from _Inventory_, i.e. only any of the `SomethingInventoryInternal` types can access `NestedApi`. `NestedInternal` is only accessible from `NestedApi`.
|
||||
* Any code in the _Nested_ module can access code in parent modules, even internal.
|
||||
I.e., both `NestedApi` and `NestedInternal` can access `inventory.internal.SomethingInventoryInternal`.
|
||||
* Code from nested modules can also access exposed types by top-level application modules.
|
||||
Any code in `nested` (or any sub-packages) can access `OrderManagement`.
|
||||
|
||||
[[modules.open, modules.advanced.open]]
|
||||
=== Open Application Modules
|
||||
|
||||
The arrangement described xref:fundamentals.adoc#modules.advanced[above] are considered closed as they only expose types to other modules that are actively selected for exposure.
|
||||
When applying Spring Modulith to legacy applications, hiding all types located in nested packages from other modules might be inadequate or require marking all those packages for exposure, too.
|
||||
|
||||
Reference in New Issue
Block a user