GH-11, GH-66, GH-67, GH-68 - Improve module canvas.

Overhauled the module canvas in various ways. By default, we now skip "empty" rows. In other words, if we do not find any published events for example, we skip the entire row. CanvasOptions.revealEmptyRows() can be used to keep those empty rows around. We now also expose all value types and Spring bean references.

Refactored the dependency lookup APIs on ApplicationModule. Both DependencyType and DependencyDepth are now top-level types. Also, ApplicationModuleDependency and ApplicationModuleDependencies were introduced as explicit types. In the course of that, ApplicationModule.getDependencies(…) has got its return type changed from List<ApplicationModule> to ApplicationModuleDependencies. The internal ModuleDependency type has been renamed to QualifiedDependency.
This commit is contained in:
Oliver Drotbohm
2022-11-11 13:52:42 +01:00
parent 62ac231ce4
commit d96263fe30
14 changed files with 784 additions and 373 deletions

View File

@@ -28,15 +28,14 @@ import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import lombok.ToString;
import lombok.Value;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@@ -67,15 +66,30 @@ import com.tngtech.archunit.thirdparty.com.google.common.base.Suppliers;
@EqualsAndHashCode(doNotUseGetters = true)
public class ApplicationModule {
/**
* The base package of the {@link ApplicationModule}.
*/
private final @Getter JavaPackage basePackage;
private final ApplicationModuleInformation information;
/**
* All {@link NamedInterfaces} of the {@link ApplicationModule} either declared explicitly via {@link NamedInterface}
* or implicitly.
*/
private final @Getter NamedInterfaces namedInterfaces;
private final boolean useFullyQualifiedModuleNames;
private final Supplier<Classes> springBeans;
private final Supplier<Classes> entities;
private final Supplier<List<JavaClass>> valueTypes;
private final Supplier<List<EventType>> publishedEvents;
/**
* 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
*/
ApplicationModule(JavaPackage basePackage, boolean useFullyQualifiedModuleNames) {
this.basePackage = basePackage;
@@ -85,6 +99,8 @@ public class ApplicationModule {
this.springBeans = Suppliers.memoize(() -> filterSpringBeans(basePackage));
this.entities = Suppliers.memoize(() -> findEntities(basePackage));
this.valueTypes = Suppliers
.memoize(() -> findArchitecturallyEvidentType(ArchitecturallyEvidentType::isValueObject));
this.publishedEvents = Suppliers.memoize(() -> findPublishedEvents());
}
@@ -108,14 +124,25 @@ public class ApplicationModule {
.orElseGet(() -> getName());
}
public List<ApplicationModule> getDependencies(ApplicationModules modules, DependencyType... type) {
/**
* Returns {@link DeclaredDependencies} of the current {@link ApplicationModule}.
*
* @param modules must not be {@literal null}.
* @param type must not be {@literal null}.
* @return will never be {@literal null}.
*/
public ApplicationModuleDependencies getDependencies(ApplicationModules modules, DependencyType... type) {
return getAllModuleDependencies(modules) //
Assert.notNull(modules, "ApplicationModules must not be null!");
Assert.notNull(type, "DependencyTypes must not be null!");
var foo = getAllModuleDependencies(modules) //
.filter(it -> type.length == 0 ? true : Arrays.stream(type).anyMatch(it::hasType)) //
.map(it -> modules.getModuleByType(it.target)) //
.distinct() //
.flatMap(it -> it.map(Stream::of).orElseGet(Stream::empty)) //
.collect(Collectors.toList());
.<ApplicationModuleDependency> flatMap(it -> DefaultApplicationModuleDependency.of(it, modules)) //
.toList();
return ApplicationModuleDependencies.of(foo, modules);
}
/**
@@ -130,8 +157,8 @@ public class ApplicationModule {
return getAllModuleDependencies(modules) //
.filter(it -> it.type == DependencyType.EVENT_LISTENER) //
.map(ModuleDependency::getTarget) //
.collect(Collectors.toList());
.map(QualifiedDependency::getTarget) //
.toList();
}
/**
@@ -143,6 +170,15 @@ public class ApplicationModule {
return publishedEvents.get();
}
/**
* Returns all value types contained in the module.
*
* @return will never be {@literal null}.
*/
public List<JavaClass> getValueTypes() {
return valueTypes.get();
}
/**
* Returns all types that are considered aggregate roots.
*
@@ -156,7 +192,7 @@ public class ApplicationModule {
.map(ArchitecturallyEvidentType::getType) //
.flatMap(this::resolveModuleSuperTypes) //
.distinct() //
.collect(Collectors.toList());
.toList();
}
/**
@@ -192,20 +228,22 @@ public class ApplicationModule {
Assert.notNull(modules, "Modules must not be null!");
Assert.notNull(depth, "Dependency depth must not be null!");
Stream<ApplicationModule> dependencies = streamBootstrapDependencies(modules, depth);
var dependencies = streamBootstrapDependencies(modules, depth);
return Stream.concat(Stream.of(this), dependencies) //
.map(ApplicationModule::getBasePackage);
}
/**
* Returns all {@link SpringBean}s contained in the module.
*
* @return will never be {@literal null}.
*/
public List<SpringBean> getSpringBeans() {
return getSpringBeansInternal().stream() //
.map(it -> SpringBean.of(it, this)) //
.collect(Collectors.toList());
}
Classes getSpringBeansInternal() {
return springBeans.get();
.toList();
}
public boolean contains(JavaClass type) {
@@ -267,7 +305,8 @@ public class ApplicationModule {
public String toString(@Nullable ApplicationModules modules) {
StringBuilder builder = new StringBuilder("## ").append(getDisplayName()).append(" ##\n");
var builder = new StringBuilder("## ").append(getDisplayName()).append(" ##\n");
builder.append("> Logical name: ").append(getName()).append('\n');
builder.append("> Base package: ").append(basePackage.getName()).append('\n');
@@ -282,7 +321,7 @@ public class ApplicationModule {
if (modules != null) {
List<ApplicationModule> dependencies = getBootstrapDependencies(modules).collect(Collectors.toList());
List<ApplicationModule> dependencies = getBootstrapDependencies(modules).toList();
builder.append("> Direct module dependencies: ");
builder.append(dependencies.isEmpty() ? "none"
@@ -307,6 +346,10 @@ public class ApplicationModule {
return builder.toString();
}
Classes getSpringBeansInternal() {
return springBeans.get();
}
/**
* Returns all allowed module dependencies, either explicitly declared or defined as shared on the given
* {@link ApplicationModules} instance.
@@ -314,25 +357,25 @@ public class ApplicationModule {
* @param modules must not be {@literal null}.
* @return
*/
ApplicationModuleDependencies getAllowedDependencies(ApplicationModules modules) {
DeclaredDependencies getAllowedDependencies(ApplicationModules modules) {
Assert.notNull(modules, "Modules must not be null!");
var allowedDependencyNames = information.getAllowedDependencies();
if (allowedDependencyNames.isEmpty()) {
return new ApplicationModuleDependencies(Collections.emptyList());
return new DeclaredDependencies(Collections.emptyList());
}
var explicitlyDeclaredModules = allowedDependencyNames.stream() //
.map(it -> ApplicationModuleDependency.of(it, this, modules));
.map(it -> DeclaredDependency.of(it, this, modules));
var sharedDependencies = modules.getSharedModules().stream()
.map(ApplicationModuleDependency::to);
.map(DeclaredDependency::to);
return Stream.concat(explicitlyDeclaredModules, sharedDependencies) //
.distinct() //
.collect(Collectors.collectingAndThen(Collectors.toList(), ApplicationModuleDependencies::new));
.collect(Collectors.collectingAndThen(Collectors.toList(), DeclaredDependencies::new));
}
/**
@@ -354,8 +397,7 @@ public class ApplicationModule {
.or(isAnnotatedWith(JMoleculesTypes.AT_DOMAIN_EVENT));
return basePackage.that(isEvent).stream() //
.map(EventType::new)
.collect(Collectors.toList());
.map(EventType::new).toList();
}
/**
@@ -374,7 +416,7 @@ public class ApplicationModule {
Stream.of(type));
}
private Stream<ModuleDependency> getAllModuleDependencies(ApplicationModules modules) {
private Stream<QualifiedDependency> getAllModuleDependencies(ApplicationModules modules) {
return basePackage.stream() //
.flatMap(it -> getModuleDependenciesOf(it, modules));
@@ -382,18 +424,19 @@ public class ApplicationModule {
private Stream<ApplicationModule> streamBootstrapDependencies(ApplicationModules modules, DependencyDepth depth) {
switch (depth) {
return switch (depth) {
case NONE -> Stream.empty();
case IMMEDIATE -> getDirectModuleBootstrapDependencies(modules);
case ALL -> getAllBootstrapDependencies(modules);
default -> getAllBootstrapDependencies(modules);
};
}
case NONE:
return Stream.empty();
case IMMEDIATE:
return getDirectModuleBootstrapDependencies(modules);
case ALL:
default:
return getDirectModuleBootstrapDependencies(modules) //
.flatMap(it -> Stream.concat(Stream.of(it), it.streamBootstrapDependencies(modules, DependencyDepth.ALL))) //
.distinct();
}
private Stream<ApplicationModule> getAllBootstrapDependencies(ApplicationModules modules) {
return getDirectModuleBootstrapDependencies(modules) //
.flatMap(it -> Stream.concat(Stream.of(it), it.streamBootstrapDependencies(modules, DependencyDepth.ALL))) //
.distinct();
}
private Stream<ApplicationModule> getDirectModuleBootstrapDependencies(ApplicationModules modules) {
@@ -402,7 +445,7 @@ public class ApplicationModule {
return beans.stream() //
.map(it -> ArchitecturallyEvidentType.of(it, beans)) //
.flatMap(it -> ModuleDependency.fromType(it)) //
.flatMap(it -> QualifiedDependency.fromType(it)) //
.filter(it -> isDependencyToOtherModule(it.target, modules)) //
.filter(it -> it.hasType(DependencyType.USES_COMPONENT)) //
.map(it -> modules.getModuleByType(it.target)) //
@@ -410,16 +453,16 @@ public class ApplicationModule {
.flatMap(it -> it.map(Stream::of).orElseGet(Stream::empty));
}
private Stream<ModuleDependency> getModuleDependenciesOf(JavaClass type, ApplicationModules modules) {
private Stream<QualifiedDependency> getModuleDependenciesOf(JavaClass type, ApplicationModules modules) {
var evidentType = ArchitecturallyEvidentType.of(type, getSpringBeansInternal());
var injections = ModuleDependency.fromType(evidentType) //
var injections = QualifiedDependency.fromType(evidentType) //
.filter(it -> isDependencyToOtherModule(it.getTarget(), modules)); //
var directDependencies = type.getDirectDependenciesFromSelf().stream() //
.filter(it -> isDependencyToOtherModule(it.getTargetClass(), modules)) //
.map(ModuleDependency::new);
.map(QualifiedDependency::new);
return Stream.concat(injections, directDependencies).distinct();
}
@@ -459,18 +502,20 @@ public class ApplicationModule {
return it -> it.getSimpleName().equals(candidate) || it.getFullName().equals(candidate);
}
public enum DependencyDepth {
private List<JavaClass> findArchitecturallyEvidentType(Predicate<ArchitecturallyEvidentType> selector) {
NONE,
var springBeansInternal = getSpringBeansInternal();
IMMEDIATE,
ALL;
return basePackage.stream()
.map(it -> ArchitecturallyEvidentType.of(it, springBeansInternal))
.filter(selector)
.map(ArchitecturallyEvidentType::getType)
.toList();
}
@Value
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
static class ApplicationModuleDependency {
static class DeclaredDependency {
private static final String INVALID_EXPLICIT_MODULE_DEPENDENCY = "Invalid explicit module dependency in %s! No module found with name '%s'.";
private static final String INVALID_NAMED_INTERFACE_DECLARATION = "No named interface named '%s' found! Original dependency declaration: %s -> %s.";
@@ -479,7 +524,7 @@ public class ApplicationModule {
@NonNull NamedInterface namedInterface;
/**
* Creates an {@link ApplicationModuleDependency} to the module and optionally named interface defined by the given
* Creates an {@link DeclaredDependency} to the module and optionally named interface defined by the given
* identifier.
*
* @param identifier must not be {@literal null} or empty. Follows the
@@ -490,7 +535,7 @@ public class ApplicationModule {
* @throws IllegalArgumentException in case the given identifier is invalid, i.e. does not refer to an existing
* module or named interface.
*/
public static ApplicationModuleDependency of(String identifier, ApplicationModule source,
public static DeclaredDependency of(String identifier, ApplicationModule source,
ApplicationModules modules) {
Assert.hasText(identifier, "Module dependency identifier must not be null or empty!");
@@ -509,42 +554,42 @@ public class ApplicationModule {
.orElseThrow(() -> new IllegalArgumentException(
INVALID_NAMED_INTERFACE_DECLARATION.formatted(namedInterfacename, source.getName(), identifier)));
return new ApplicationModuleDependency(target, namedInterface);
return new DeclaredDependency(target, namedInterface);
}
/**
* Creates a new {@link ApplicationModuleDependency} to the unnamed interface of the given
* {@link ApplicationModule}.
* Creates a new {@link DeclaredDependency} to the unnamed interface of the given {@link ApplicationModule}.
*
* @param module must not be {@literal null}.
* @return
*/
public static ApplicationModuleDependency to(ApplicationModule module) {
return new ApplicationModuleDependency(module, module.getNamedInterfaces().getUnnamedInterface());
public static DeclaredDependency to(ApplicationModule module) {
return new DeclaredDependency(module, module.getNamedInterfaces().getUnnamedInterface());
}
public boolean contains(JavaClass type) {
return namedInterface.contains(type);
}
/*
* (non-Javadoc)
* @see java.lang.Object#toString()
*/
@Override
public String toString() {
return namedInterface.isUnnamed() //
? target.getName() //
: target.getName() + "::" + namedInterface.getName();
return namedInterface.isUnnamed() ? target.getName() : target.getName() + "::" + namedInterface.getName();
}
}
/**
* A collection wrapper for {@link ApplicationModuleDependency} instances.
* A collection wrapper for {@link DeclaredDependency} instances.
*
* @author Oliver Drotbohm
*/
@Value
static class ApplicationModuleDependencies {
static class DeclaredDependencies {
List<ApplicationModuleDependency> dependencies;
List<DeclaredDependency> dependencies;
/**
* Returns whether any of the dependencies contains the given {@link JavaClass}.
@@ -568,23 +613,23 @@ public class ApplicationModule {
public String toString() {
return dependencies.stream() //
.map(ApplicationModuleDependency::toString)
.map(DeclaredDependency::toString)
.collect(Collectors.joining(", "));
}
}
@EqualsAndHashCode
@RequiredArgsConstructor
static class ModuleDependency {
static class QualifiedDependency {
private static final List<String> INJECTION_TYPES = Arrays.asList(//
AT_AUTOWIRED, AT_RESOURCE, AT_INJECT);
private final @NonNull @Getter JavaClass origin, target;
private final @NonNull @Getter JavaClass source, target;
private final @NonNull String description;
private final @NonNull DependencyType type;
ModuleDependency(Dependency dependency) {
QualifiedDependency(Dependency dependency) {
this(dependency.getOriginClass(), //
dependency.getTargetClass(), //
dependency.getDescription(), //
@@ -597,10 +642,10 @@ public class ApplicationModule {
Violations isValidDependencyWithin(ApplicationModules modules) {
var originModule = getExistingModuleOf(origin, modules);
var originModule = getExistingModuleOf(source, modules);
var targetModule = getExistingModuleOf(target, modules);
ApplicationModuleDependencies allowedTargets = originModule.getAllowedDependencies(modules);
DeclaredDependencies allowedTargets = originModule.getAllowedDependencies(modules);
Violations violations = Violations.NONE;
// Check explicitly defined allowed targets
@@ -608,7 +653,7 @@ public class ApplicationModule {
if (!allowedTargets.isEmpty() && !allowedTargets.contains(target)) {
var message = "Module '%s' depends on module '%s' via %s -> %s. Allowed targets: %s." //
.formatted(originModule.getName(), targetModule.getName(), origin.getName(), target.getName(),
.formatted(originModule.getName(), targetModule.getName(), source.getName(), target.getName(),
allowedTargets.toString());
return violations.and(new IllegalStateException(message));
@@ -629,120 +674,115 @@ public class ApplicationModule {
ApplicationModule getExistingModuleOf(JavaClass javaClass, ApplicationModules modules) {
Optional<ApplicationModule> module = modules.getModuleByType(javaClass);
return module.orElseThrow(() -> new IllegalStateException(
String.format("Origin/Target of a %s should always be within a module, but %s is not",
return modules.getModuleByType(javaClass).orElseThrow(() -> new IllegalStateException(
String.format("Source/Target of a %s should always be within a module, but %s is not",
getClass().getSimpleName(), javaClass.getName())));
}
static ModuleDependency fromCodeUnitParameter(JavaCodeUnit codeUnit, JavaClass parameter) {
String description = createDescription(codeUnit, parameter, "parameter");
DependencyType type = DependencyType.forCodeUnit(codeUnit) //
.or(() -> DependencyType.forParameter(parameter));
return new ModuleDependency(codeUnit.getOwner(), parameter, description, type);
}
static ModuleDependency fromCodeUnitReturnType(JavaCodeUnit codeUnit) {
String description = createDescription(codeUnit, codeUnit.getRawReturnType(), "return type");
return new ModuleDependency(codeUnit.getOwner(), codeUnit.getRawReturnType(), description,
DependencyType.DEFAULT);
}
static Stream<ModuleDependency> fromType(ArchitecturallyEvidentType type) {
JavaClass source = type.getType();
return Stream.concat(Stream.concat(fromConstructorOf(type), fromMethodsOf(source)), fromFieldsOf(source));
}
private static Stream<ModuleDependency> fromConstructorOf(ArchitecturallyEvidentType source) {
JavaClass type = source.getType();
Set<JavaConstructor> constructors = type.getConstructors();
return constructors.stream() //
.filter(it -> constructors.size() == 1 || isInjectionPoint(it)) //
.flatMap(it -> it.getRawParameterTypes().stream() //
.map(parameter -> {
return source.isConfigurationProperties()
? new ModuleDependency(type, parameter, createDescription(it, parameter, "parameter"),
DependencyType.DEFAULT)
: new InjectionModuleDependency(type, parameter, it);
}));
}
private static Stream<ModuleDependency> fromFieldsOf(JavaClass source) {
Stream<ModuleDependency> fieldInjections = source.getAllFields().stream() //
.filter(ModuleDependency::isInjectionPoint) //
.map(field -> new InjectionModuleDependency(source, field.getRawType(), field));
return fieldInjections;
}
private static Stream<ModuleDependency> fromMethodsOf(JavaClass source) {
Set<JavaMethod> methods = source.getAllMethods().stream() //
.filter(it -> !it.getOwner().isEquivalentTo(Object.class)) //
.collect(Collectors.toSet());
if (methods.isEmpty()) {
return Stream.empty();
}
Stream<ModuleDependency> returnTypes = methods.stream() //
.filter(it -> !it.getRawReturnType().isPrimitive()) //
.filter(it -> !it.getRawReturnType().getPackageName().startsWith("java")) //
.map(it -> fromCodeUnitReturnType(it));
Set<JavaMethod> injectionMethods = methods.stream() //
.filter(ModuleDependency::isInjectionPoint) //
.collect(Collectors.toSet());
Stream<ModuleDependency> methodInjections = injectionMethods.stream() //
.flatMap(it -> it.getRawParameterTypes().stream() //
.map(parameter -> new InjectionModuleDependency(source, parameter, it)));
Stream<ModuleDependency> otherMethods = methods.stream() //
.filter(it -> !injectionMethods.contains(it)) //
.flatMap(it -> it.getRawParameterTypes().stream() //
.map(parameter -> fromCodeUnitParameter(it, parameter)));
return Stream.concat(Stream.concat(methodInjections, otherMethods), returnTypes);
}
static Stream<ModuleDependency> allFrom(JavaCodeUnit codeUnit) {
Stream<ModuleDependency> parameterDependencies = codeUnit.getRawParameterTypes()//
.stream() //
.map(it -> fromCodeUnitParameter(codeUnit, it));
Stream<ModuleDependency> returnType = Stream.of(fromCodeUnitReturnType(codeUnit));
return Stream.concat(parameterDependencies, returnType);
}
/*
* (non-Javadoc)
* @see java.lang.Object#toString()
*/
@Override
public String toString() {
return type.format(FormatableJavaClass.of(origin), FormatableJavaClass.of(target));
return type.format(FormatableJavaClass.of(source), FormatableJavaClass.of(target));
}
static QualifiedDependency fromCodeUnitParameter(JavaCodeUnit codeUnit, JavaClass parameter) {
var description = createDescription(codeUnit, parameter, "parameter");
var type = DependencyType.forCodeUnit(codeUnit) //
.defaultOr(() -> DependencyType.forParameter(parameter));
return new QualifiedDependency(codeUnit.getOwner(), parameter, description, type);
}
static QualifiedDependency fromCodeUnitReturnType(JavaCodeUnit codeUnit) {
var description = createDescription(codeUnit, codeUnit.getRawReturnType(), "return type");
return new QualifiedDependency(codeUnit.getOwner(), codeUnit.getRawReturnType(), description,
DependencyType.DEFAULT);
}
static Stream<QualifiedDependency> fromType(ArchitecturallyEvidentType type) {
var source = type.getType();
return Stream.concat(Stream.concat(fromConstructorOf(type), fromMethodsOf(source)), fromFieldsOf(source));
}
static Stream<QualifiedDependency> allFrom(JavaCodeUnit codeUnit) {
var parameterDependencies = codeUnit.getRawParameterTypes()//
.stream() //
.map(it -> fromCodeUnitParameter(codeUnit, it));
var returnType = Stream.of(fromCodeUnitReturnType(codeUnit));
return Stream.concat(parameterDependencies, returnType);
}
private static Stream<QualifiedDependency> fromConstructorOf(ArchitecturallyEvidentType source) {
var type = source.getType();
var constructors = type.getConstructors();
return constructors.stream() //
.filter(it -> constructors.size() == 1 || isInjectionPoint(it)) //
.flatMap(it -> it.getRawParameterTypes().stream() //
.map(parameter -> {
return source.isInjectable() && !source.isConfigurationProperties()
? new InjectionDependency(it, parameter)
: new QualifiedDependency(type, parameter, createDescription(it, parameter, "parameter"),
DependencyType.DEFAULT);
}));
}
private static Stream<QualifiedDependency> fromFieldsOf(JavaClass source) {
return source.getAllFields().stream() //
.filter(QualifiedDependency::isInjectionPoint) //
.map(field -> new InjectionDependency(field, field.getRawType()));
}
private static Stream<QualifiedDependency> fromMethodsOf(JavaClass source) {
var methods = source.getAllMethods().stream() //
.filter(it -> !it.getOwner().isEquivalentTo(Object.class)) //
.collect(Collectors.toSet());
if (methods.isEmpty()) {
return Stream.empty();
}
var returnTypes = methods.stream() //
.filter(it -> !it.getRawReturnType().isPrimitive()) //
.filter(it -> !it.getRawReturnType().getPackageName().startsWith("java")) //
.map(it -> fromCodeUnitReturnType(it));
var injectionMethods = methods.stream() //
.filter(QualifiedDependency::isInjectionPoint) //
.collect(Collectors.toSet());
var methodInjections = injectionMethods.stream() //
.flatMap(it -> it.getRawParameterTypes().stream() //
.map(parameter -> new InjectionDependency(it, parameter)));
var otherMethods = methods.stream() //
.filter(it -> !injectionMethods.contains(it)) //
.flatMap(it -> it.getRawParameterTypes().stream() //
.map(parameter -> fromCodeUnitParameter(it, parameter)));
return Stream.concat(Stream.concat(methodInjections, otherMethods), returnTypes);
}
private static String createDescription(JavaMember codeUnit, JavaClass declaringElement,
String declarationDescription) {
String type = declaringElement.getSimpleName();
var type = declaringElement.getSimpleName();
String codeUnitDescription = JavaConstructor.class.isInstance(codeUnit) //
var codeUnitDescription = JavaConstructor.class.isInstance(codeUnit) //
? String.format("%s", declaringElement.getSimpleName()) //
: String.format("%s.%s", declaringElement.getSimpleName(), codeUnit.getName());
@@ -753,15 +793,15 @@ public class ApplicationModule {
.collect(Collectors.joining(", ")));
}
String annotations = codeUnit.getAnnotations().stream() //
var annotations = codeUnit.getAnnotations().stream() //
.filter(it -> INJECTION_TYPES.contains(it.getRawType().getName())) //
.map(it -> "@" + it.getRawType().getSimpleName()) //
.collect(Collectors.joining(" ", "", " "));
annotations = StringUtils.hasText(annotations) ? annotations : "";
String declaration = declarationDescription + " " + annotations + codeUnitDescription;
String location = SourceCodeLocation.of(codeUnit.getOwner(), 0).toString();
var declaration = declarationDescription + " " + annotations + codeUnitDescription;
var location = SourceCodeLocation.of(codeUnit.getOwner(), 0).toString();
return String.format("%s declares %s in %s", type, declaration, location);
}
@@ -771,23 +811,27 @@ public class ApplicationModule {
}
}
private static class InjectionModuleDependency extends ModuleDependency {
private static class InjectionDependency extends QualifiedDependency {
private final JavaMember member;
private final JavaMember source;
private final boolean isConfigurationClass;
/**
* @param origin
* @param target
* @param member
* Creates a new {@link InjectionDependency} for the given source, target and originating member.
*
* @param source must not be {@literal null}.
* @param target must not be {@literal null}.
*/
public InjectionModuleDependency(JavaClass origin, JavaClass target, JavaMember member) {
public InjectionDependency(JavaMember source, JavaClass target) {
super(origin, target, ModuleDependency.createDescription(member, origin, getDescriptionFor(member)),
super(source.getOwner(), target,
QualifiedDependency.createDescription(source, source.getOwner(), getDescriptionFor(source)),
DependencyType.USES_COMPONENT);
this.member = member;
this.isConfigurationClass = isConfiguration().test(origin);
Assert.notNull(source, "Originating member must not be null!");
this.source = source;
this.isConfigurationClass = isConfiguration().test(source.getOwner());
}
/*
@@ -799,13 +843,13 @@ public class ApplicationModule {
Violations violations = super.isValidDependencyWithin(modules);
if (JavaField.class.isInstance(member) && !isConfigurationClass) {
if (JavaField.class.isInstance(source) && !isConfigurationClass) {
ApplicationModule module = getExistingModuleOf(member.getOwner(), modules);
ApplicationModule module = getExistingModuleOf(source.getOwner(), modules);
violations = violations.and(new IllegalStateException(
String.format("Module %s uses field injection in %s. Prefer constructor injection instead!",
module.getDisplayName(), member.getFullName())));
module.getDisplayName(), source.getFullName())));
}
return violations;
@@ -825,130 +869,54 @@ public class ApplicationModule {
}
}
public enum DependencyType {
@ToString
@EqualsAndHashCode
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
private static class DefaultApplicationModuleDependency implements ApplicationModuleDependency {
/**
* Indicates that the module depends on the other one by a component dependency, i.e. that other module needs to be
* bootstrapped to run the source module.
private final QualifiedDependency dependency;
private final ApplicationModule target;
static Stream<DefaultApplicationModuleDependency> of(QualifiedDependency dependency, ApplicationModules modules) {
return modules.getModuleByType(dependency.getTarget()).stream()
.map(it -> new DefaultApplicationModuleDependency(dependency, it));
}
/*
* (non-Javadoc)
* @see org.springframework.modulith.model.MaterializedDependency#getSourceType()
*/
USES_COMPONENT {
@Override
public JavaClass getSourceType() {
return dependency.source;
}
/*
* (non-Javadoc)
* @see org.springframework.modulith.model.Module.DependencyType#format(org.springframework.modulith.model.FormatableJavaClass, org.springframework.modulith.model.FormatableJavaClass)
*/
@Override
public String format(FormatableJavaClass source, FormatableJavaClass target) {
return String.format("Component %s using %s", source.getAbbreviatedFullName(), target.getAbbreviatedFullName());
}
},
/**
* Indicates that the module refers to an entity of the other.
/*
* (non-Javadoc)
* @see org.springframework.modulith.model.MaterializedDependency#getTargetType()
*/
ENTITY {
@Override
public JavaClass getTargetType() {
return dependency.target;
}
/*
* (non-Javadoc)
* @see org.springframework.modulith.model.Module.DependencyType#format(org.springframework.modulith.model.FormatableJavaClass, org.springframework.modulith.model.FormatableJavaClass)
*/
@Override
public String format(FormatableJavaClass source, FormatableJavaClass target) {
return String.format("Entity %s depending on %s", source.getAbbreviatedFullName(),
target.getAbbreviatedFullName());
}
},
/**
* Indicates that the module depends on the other by declaring an event listener for an event exposed by the other
* module. Thus, the target module does not have to be bootstrapped to run the source one.
/*
* (non-Javadoc)
* @see org.springframework.modulith.model.MaterializedDependency#getDependencyType()
*/
EVENT_LISTENER {
/*
* (non-Javadoc)
* @see org.springframework.modulith.model.Module.DependencyType#format(org.springframework.modulith.model.FormatableJavaClass, org.springframework.modulith.model.FormatableJavaClass)
*/
@Override
public String format(FormatableJavaClass source, FormatableJavaClass target) {
return String.format("%s listening to events of type %s", source.getAbbreviatedFullName(),
target.getAbbreviatedFullName());
}
},
DEFAULT {
/*
* (non-Javadoc)
* @see org.springframework.modulith.model.Module.DependencyType#or(com.tngtech.archunit.thirdparty.com.google.common.base.Supplier)
*/
@Override
public DependencyType or(Supplier<DependencyType> supplier) {
return supplier.get();
}
/*
* (non-Javadoc)
* @see org.springframework.modulith.model.Module.DependencyType#format(org.springframework.modulith.model.FormatableJavaClass, org.springframework.modulith.model.FormatableJavaClass)
*/
@Override
public String format(FormatableJavaClass source, FormatableJavaClass target) {
return String.format("%s depending on %s", source.getAbbreviatedFullName(), target.getAbbreviatedFullName());
}
};
public static DependencyType forParameter(JavaClass type) {
return type.isAnnotatedWith("javax.persistence.Entity") ? ENTITY : DEFAULT;
@Override
public DependencyType getDependencyType() {
return dependency.type;
}
public static DependencyType forCodeUnit(JavaCodeUnit codeUnit) {
return Types.isAnnotatedWith(SpringTypes.AT_EVENT_LISTENER).test(codeUnit) //
|| Types.isAnnotatedWith(JMoleculesTypes.AT_DOMAIN_EVENT_HANDLER).test(codeUnit) //
? EVENT_LISTENER
: DEFAULT;
}
public static DependencyType forDependency(Dependency dependency) {
return forParameter(dependency.getTargetClass());
}
public abstract String format(FormatableJavaClass source, FormatableJavaClass target);
public DependencyType or(Supplier<DependencyType> supplier) {
return this;
}
/**
* Returns all {@link DependencyType}s except the given ones.
*
* @param types must not be {@literal null}.
* @return
/*
* (non-Javadoc)
* @see org.springframework.modulith.model.MaterializedDependency#getTargetModule()
*/
public static Stream<DependencyType> allBut(Collection<DependencyType> types) {
Assert.notNull(types, "Types must not be null!");
Predicate<DependencyType> isIncluded = types::contains;
return Arrays.stream(values()) //
.filter(isIncluded.negate());
}
public static Stream<DependencyType> allBut(Stream<DependencyType> types) {
return allBut(types.collect(Collectors.toList()));
}
/**
* Returns all {@link DependencyType}s except the given ones.
*
* @param types must not be {@literal null}.
* @return
*/
public static Stream<DependencyType> allBut(DependencyType... types) {
Assert.notNull(types, "Types must not be null!");
return allBut(Arrays.asList(types));
@Override
public ApplicationModule getTargetModule() {
return target;
}
}
}

View File

@@ -0,0 +1,93 @@
/*
* Copyright 2022 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.model;
import lombok.RequiredArgsConstructor;
import java.util.List;
import java.util.stream.Stream;
import org.springframework.util.Assert;
/**
* The materialized, in other words actually present dependencies of the current module towards other modules.
*
* @author Oliver Drotbohm
*/
@RequiredArgsConstructor(staticName = "of")
public class ApplicationModuleDependencies {
private final List<ApplicationModuleDependency> dependencies;
private final ApplicationModules modules;
/**
* Returns whether the dependencies contain the given {@link ApplicationModule}.
*
* @param module must not be {@literal null}.
* @return will never be {@literal null}.
*/
public boolean contains(ApplicationModule module) {
Assert.notNull(module, "ApplicationModule must not be null!");
return dependencies.stream()
.map(ApplicationModuleDependency::getTargetModule)
.anyMatch(module::equals);
}
/**
* Returns whether the dependencies contain the {@link ApplicationModule} with the given name.
*
* @param name must not be {@literal null} or empty.
* @return will never be {@literal null}.
*/
public boolean containsModuleNamed(String name) {
Assert.hasText(name, "Module name must not be null or empty!");
return dependencies.stream()
.map(ApplicationModuleDependency::getTargetModule)
.map(ApplicationModule::getName)
.anyMatch(name::equals);
}
/**
* Returns all {@link DefaultMaterializedDependency} instances as {@link Stream}.
*
* @return will never be {@literal null}.
*/
public Stream<ApplicationModuleDependency> stream() {
return dependencies.stream();
}
public ApplicationModuleDependencies withType(DependencyType type) {
var filtered = dependencies.stream()
.filter(it -> it.getDependencyType().equals(type))
.toList();
return ApplicationModuleDependencies.of(filtered, modules);
}
/**
* Returns whether there are any dependencies at all.
*
* @return will never be {@literal null}.
*/
public boolean isEmpty() {
return dependencies.isEmpty();
}
}

View File

@@ -0,0 +1,54 @@
/*
* Copyright 2022 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.model;
import com.tngtech.archunit.core.domain.JavaClass;
/**
* A dependency between two {@link ApplicationModule}s.
*
* @author Oliver Drotbohm
*/
public interface ApplicationModuleDependency {
/**
* The source java type establishing the dependency.
*
* @return will never be {@literal null}.
*/
JavaClass getSourceType();
/**
* The dependency target type.
*
* @return will never be {@literal null}.
*/
JavaClass getTargetType();
/**
* The type of the dependency.
*
* @return will never be {@literal null}.
*/
DependencyType getDependencyType();
/**
* The target {@link ApplicationModule}.
*
* @return will never be {@literal null}.
*/
ApplicationModule getTargetModule();
}

View File

@@ -32,7 +32,6 @@ import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.springframework.data.repository.core.RepositoryMetadata;
@@ -81,7 +80,7 @@ public abstract class ArchitecturallyEvidentType {
delegates.add(new SpringDataAwareArchitecturallyEvidentType(type, beanTypes));
}
delegates.add(new SpringAwareArchitecturallyEvidentType(type));
delegates.add(new SpringAwareArchitecturallyEvidentType(type, beanTypes));
return DelegatingType.of(type, delegates);
});
@@ -135,6 +134,14 @@ public abstract class ArchitecturallyEvidentType {
return false;
}
public boolean isInjectable() {
return isService() || isController() || isEventListener() || isConfigurationProperties();
}
public boolean isValueObject() {
return false;
}
/**
* Returns other types that are interesting in the context of the current {@link ArchitecturallyEvidentType}. For
* example, for an event listener this might be the event types the particular listener is interested in.
@@ -195,8 +202,13 @@ public abstract class ArchitecturallyEvidentType {
private static final Predicate<JavaMethod> IS_EVENT_LISTENER = IS_ANNOTATED_EVENT_LISTENER
.or(IS_IMPLEMENTING_EVENT_LISTENER);
public SpringAwareArchitecturallyEvidentType(JavaClass type) {
private final boolean injectable;
public SpringAwareArchitecturallyEvidentType(JavaClass type, Classes beanTypes) {
super(type);
this.injectable = beanTypes.contains(type);
}
/*
@@ -253,6 +265,15 @@ public abstract class ArchitecturallyEvidentType {
return Types.isAnnotatedWith(SpringTypes.AT_CONFIGURATION_PROPERTIES).test(getType());
}
/*
* (non-Javadoc)
* @see org.springframework.modulith.model.ArchitecturallyEvidentType#isInjectable()
*/
@Override
public boolean isInjectable() {
return injectable || SpringTypes.isComponent().test(getType()) || super.isInjectable();
}
/*
* (non-Javadoc)
* @see org.springframework.modulith.model.ArchitecturallyEvidentType#getOtherTypeReferences()
@@ -411,13 +432,26 @@ public abstract class ArchitecturallyEvidentType {
public boolean isEventListener() {
return getType().getMethods().stream().anyMatch(IS_ANNOTATED_EVENT_LISTENER);
}
/*
* (non-Javadoc)
* @see org.springframework.modulith.model.ArchitecturallyEvidentType#isValueObject()
*/
@Override
public boolean isValueObject() {
JavaClass type = getType();
return Types.isAnnotatedWith(org.jmolecules.ddd.annotation.ValueObject.class).test(type)
|| type.isAssignableTo(org.jmolecules.ddd.types.ValueObject.class);
}
}
static class DelegatingType extends ArchitecturallyEvidentType {
private final Supplier<Boolean> isAggregateRoot, isRepository, isEntity, isService, isController,
isEventListener,
isConfigurationProperties;
isEventListener, isConfigurationProperties, isInjectable, isValueObject;
private final Supplier<Collection<JavaClass>> referenceTypes;
private final Supplier<Collection<ReferenceMethod>> referenceMethods;
@@ -425,6 +459,8 @@ public abstract class ArchitecturallyEvidentType {
Supplier<Boolean> isRepository, Supplier<Boolean> isEntity, Supplier<Boolean> isService,
Supplier<Boolean> isController, Supplier<Boolean> isEventListener,
Supplier<Boolean> isConfigurationProperties,
Supplier<Boolean> isInjectable,
Supplier<Boolean> isValueObject,
Supplier<Collection<JavaClass>> referenceTypes,
Supplier<Collection<ReferenceMethod>> referenceMethods) {
@@ -437,43 +473,49 @@ public abstract class ArchitecturallyEvidentType {
this.isController = isController;
this.isEventListener = isEventListener;
this.isConfigurationProperties = isConfigurationProperties;
this.isInjectable = isInjectable;
this.isValueObject = isValueObject;
this.referenceTypes = referenceTypes;
this.referenceMethods = referenceMethods;
}
public static DelegatingType of(JavaClass type, List<ArchitecturallyEvidentType> types) {
Supplier<Boolean> isAggregateRoot = Suppliers
var isAggregateRoot = Suppliers
.memoize(() -> types.stream().anyMatch(ArchitecturallyEvidentType::isAggregateRoot));
Supplier<Boolean> isRepository = Suppliers
var isRepository = Suppliers
.memoize(() -> types.stream().anyMatch(ArchitecturallyEvidentType::isRepository));
Supplier<Boolean> isEntity = Suppliers
var isEntity = Suppliers
.memoize(() -> types.stream().anyMatch(ArchitecturallyEvidentType::isEntity));
Supplier<Boolean> isService = Suppliers
var isService = Suppliers
.memoize(() -> types.stream().anyMatch(ArchitecturallyEvidentType::isService));
Supplier<Boolean> isController = Suppliers
var isController = Suppliers
.memoize(() -> types.stream().anyMatch(ArchitecturallyEvidentType::isController));
Supplier<Boolean> isEventListener = Suppliers
var isEventListener = Suppliers
.memoize(() -> types.stream().anyMatch(ArchitecturallyEvidentType::isEventListener));
Supplier<Boolean> isConfigurationProperties = Suppliers
var isConfigurationProperties = Suppliers
.memoize(() -> types.stream().anyMatch(ArchitecturallyEvidentType::isConfigurationProperties));
var isInjectable = Suppliers.memoize(() -> types.stream().anyMatch(ArchitecturallyEvidentType::isInjectable));
var isValueObject = Suppliers.memoize(() -> types.stream().anyMatch(ArchitecturallyEvidentType::isValueObject));
Supplier<Collection<JavaClass>> referenceTypes = Suppliers.memoize(() -> types.stream() //
.flatMap(ArchitecturallyEvidentType::getReferenceTypes) //
.collect(Collectors.toList()));
.toList());
Supplier<Collection<ReferenceMethod>> referenceMethods = Suppliers.memoize(() -> types.stream() //
.flatMap(ArchitecturallyEvidentType::getReferenceMethods) //
.collect(Collectors.toList()));
.toList());
return new DelegatingType(type, isAggregateRoot, isRepository, isEntity, isService, isController,
isEventListener, isConfigurationProperties, referenceTypes, referenceMethods);
isEventListener, isConfigurationProperties, isInjectable, isValueObject, referenceTypes, referenceMethods);
}
/*
@@ -541,6 +583,24 @@ public abstract class ArchitecturallyEvidentType {
return isConfigurationProperties.get();
}
/*
* (non-Javadoc)
* @see org.springframework.modulith.model.ArchitecturallyEvidentType#isInjectable()
*/
@Override
public boolean isInjectable() {
return isInjectable.get();
}
/*
* (non-Javadoc)
* @see org.springframework.modulith.model.ArchitecturallyEvidentType#isValueObject()
*/
@Override
public boolean isValueObject() {
return isValueObject.get();
}
/*
* (non-Javadoc)
* @see org.springframework.modulith.model.ArchitecturallyEvidentType#getOtherTypeReferences()

View File

@@ -0,0 +1,25 @@
/*
* Copyright 2022 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.model;
public enum DependencyDepth {
NONE,
IMMEDIATE,
ALL;
}

View File

@@ -0,0 +1,172 @@
/*
* Copyright 2022 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.model;
import java.util.Arrays;
import java.util.Collection;
import java.util.function.Predicate;
import java.util.stream.Stream;
import org.springframework.modulith.model.Types.JMoleculesTypes;
import org.springframework.modulith.model.Types.SpringTypes;
import org.springframework.util.Assert;
import com.tngtech.archunit.core.domain.Dependency;
import com.tngtech.archunit.core.domain.JavaClass;
import com.tngtech.archunit.core.domain.JavaCodeUnit;
import com.tngtech.archunit.thirdparty.com.google.common.base.Supplier;
/**
* The type of dependency between {@link ApplicationModule}s.
*
* @author Oliver Drotbohm
*/
public enum DependencyType {
/**
* Indicates that the module depends on the other one by a component dependency, i.e. that other module needs to be
* bootstrapped to run the source module.
*/
USES_COMPONENT {
/*
* (non-Javadoc)
* @see org.springframework.modulith.model.Module.DependencyType#format(org.springframework.modulith.model.FormatableJavaClass, org.springframework.modulith.model.FormatableJavaClass)
*/
@Override
public String format(FormatableJavaClass source, FormatableJavaClass target) {
return String.format("Component %s using %s", source.getAbbreviatedFullName(), target.getAbbreviatedFullName());
}
},
/**
* Indicates that the module refers to an entity of the other.
*/
ENTITY {
/*
* (non-Javadoc)
* @see org.springframework.modulith.model.Module.DependencyType#format(org.springframework.modulith.model.FormatableJavaClass, org.springframework.modulith.model.FormatableJavaClass)
*/
@Override
public String format(FormatableJavaClass source, FormatableJavaClass target) {
return String.format("Entity %s depending on %s", source.getAbbreviatedFullName(),
target.getAbbreviatedFullName());
}
},
/**
* Indicates that the module depends on the other by declaring an event listener for an event exposed by the other
* module. Thus, the target module does not have to be bootstrapped to run the source one.
*/
EVENT_LISTENER {
/*
* (non-Javadoc)
* @see org.springframework.modulith.model.Module.DependencyType#format(org.springframework.modulith.model.FormatableJavaClass, org.springframework.modulith.model.FormatableJavaClass)
*/
@Override
public String format(FormatableJavaClass source, FormatableJavaClass target) {
return String.format("%s listening to events of type %s", source.getAbbreviatedFullName(),
target.getAbbreviatedFullName());
}
},
DEFAULT {
/*
* (non-Javadoc)
* @see org.springframework.modulith.model.Module.DependencyType#or(com.tngtech.archunit.thirdparty.com.google.common.base.Supplier)
*/
@Override
public DependencyType defaultOr(Supplier<DependencyType> supplier) {
return supplier.get();
}
/*
* (non-Javadoc)
* @see org.springframework.modulith.model.Module.DependencyType#format(org.springframework.modulith.model.FormatableJavaClass, org.springframework.modulith.model.FormatableJavaClass)
*/
@Override
public String format(FormatableJavaClass source, FormatableJavaClass target) {
return String.format("%s depending on %s", source.getAbbreviatedFullName(), target.getAbbreviatedFullName());
}
};
/**
* Returns the current {@link DependencyType} or obtains the one provided by the given supplier if the current one is
* {@link DependencyType#DEFAULT}.
*
* @param supplier must not be {@literal null}.
* @return
*/
DependencyType defaultOr(Supplier<DependencyType> supplier) {
Assert.notNull(supplier, "The fallback supplier must not be null!");
return this;
}
static DependencyType forParameter(JavaClass type) {
return type.isAnnotatedWith("javax.persistence.Entity") ? ENTITY : DEFAULT;
}
static DependencyType forCodeUnit(JavaCodeUnit codeUnit) {
return Types.isAnnotatedWith(SpringTypes.AT_EVENT_LISTENER).test(codeUnit) //
|| Types.isAnnotatedWith(JMoleculesTypes.AT_DOMAIN_EVENT_HANDLER).test(codeUnit) //
? EVENT_LISTENER
: DEFAULT;
}
static DependencyType forDependency(Dependency dependency) {
return forParameter(dependency.getTargetClass());
}
public abstract String format(FormatableJavaClass source, FormatableJavaClass target);
/**
* Returns all {@link DependencyType}s except the given ones.
*
* @param types must not be {@literal null}.
* @return
*/
public static Stream<DependencyType> allBut(Collection<DependencyType> types) {
Assert.notNull(types, "Types must not be null!");
Predicate<DependencyType> isIncluded = types::contains;
return Arrays.stream(values()) //
.filter(isIncluded.negate());
}
public static Stream<DependencyType> allBut(Stream<DependencyType> types) {
return allBut(types.toList());
}
/**
* Returns all {@link DependencyType}s except the given ones.
*
* @param types must not be {@literal null}.
* @return
*/
public static Stream<DependencyType> allBut(DependencyType... types) {
Assert.notNull(types, "Types must not be null!");
return allBut(Arrays.asList(types));
}
}

View File

@@ -71,7 +71,7 @@ class ArchitecturallyEvidentTypeUnitTest {
void detectsSpringAnnotatedRepositories() {
ArchitecturallyEvidentType type = new SpringAwareArchitecturallyEvidentType(
classes.getRequiredClass(SpringRepository.class));
classes.getRequiredClass(SpringRepository.class), Classes.NONE);
assertThat(type.isRepository()).isTrue();
}
@@ -80,7 +80,7 @@ class ArchitecturallyEvidentTypeUnitTest {
void doesNotConsiderEntityAggregateRoot() {
ArchitecturallyEvidentType type = new SpringAwareArchitecturallyEvidentType(
classes.getRequiredClass(SampleEntity.class));
classes.getRequiredClass(SampleEntity.class), Classes.NONE);
assertThat(type.isEntity()).isTrue();
assertThat(type.isAggregateRoot()).isFalse();

View File

@@ -21,13 +21,13 @@ import java.util.stream.Stream;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.modulith.model.ApplicationModule.ModuleDependency;
import org.springframework.modulith.model.ApplicationModule.QualifiedDependency;
import com.tngtech.archunit.core.domain.JavaClass;
import com.tngtech.archunit.core.importer.ClassFileImporter;
/**
* Unit tests for {@link ModuleDependency}.
* Unit tests for {@link QualifiedDependency}.
*
* @author Oliver Drotbohm
*/
@@ -61,8 +61,8 @@ class ModuleDependencyUnitTest {
var imported = importer.importClass(type);
var evidentType = ArchitecturallyEvidentType.of(imported, Classes.NONE);
return ModuleDependency.fromType(evidentType) //
.map(ModuleDependency::getTarget) //
return QualifiedDependency.fromType(evidentType) //
.map(QualifiedDependency::getTarget) //
.map(JavaClass::reflect);
}

View File

@@ -31,6 +31,7 @@ import org.springframework.modulith.docs.Documenter.CanvasOptions.Groupings;
import org.springframework.modulith.model.ApplicationModule;
import org.springframework.modulith.model.ApplicationModules;
import org.springframework.modulith.model.ArchitecturallyEvidentType;
import org.springframework.modulith.model.DependencyType;
import org.springframework.modulith.model.EventType;
import org.springframework.modulith.model.FormatableJavaClass;
import org.springframework.modulith.model.Source;
@@ -129,7 +130,7 @@ class Asciidoctor {
return String.format("%s (via %s)", interfacesAsString, base);
}
public String renderSpringBeans(CanvasOptions options, ApplicationModule module) {
public String renderSpringBeans(ApplicationModule module, CanvasOptions options) {
StringBuilder builder = new StringBuilder();
Groupings groupings = options.groupBeans(module);
@@ -355,4 +356,18 @@ class Asciidoctor {
return source;
}
/**
* @param module
* @return
*/
public String renderBeanReferences(ApplicationModule module) {
var bullets = module.getDependencies(modules, DependencyType.USES_COMPONENT).stream()
.map(it -> "%s (in %s)".formatted(toInlineCode(it.getTargetType()), it.getTargetModule().getDisplayName()))
.map(this::toBulletPoint)
.collect(Collectors.joining("\n"));
return bullets.isBlank() ? "None" : bullets;
}
}

View File

@@ -44,9 +44,10 @@ import java.util.stream.Stream;
import org.springframework.lang.Nullable;
import org.springframework.modulith.model.ApplicationModule;
import org.springframework.modulith.model.ApplicationModule.DependencyDepth;
import org.springframework.modulith.model.ApplicationModule.DependencyType;
import org.springframework.modulith.model.ApplicationModuleDependency;
import org.springframework.modulith.model.ApplicationModules;
import org.springframework.modulith.model.DependencyDepth;
import org.springframework.modulith.model.DependencyType;
import org.springframework.modulith.model.SpringBean;
import org.springframework.util.Assert;
import org.springframework.util.LinkedMultiValueMap;
@@ -315,28 +316,47 @@ public class Documenter {
String toModuleCanvas(ApplicationModule module, CanvasOptions options) {
Asciidoctor asciidoctor = Asciidoctor.withJavadocBase(modules, options.getApiBase());
var asciidoctor = Asciidoctor.withJavadocBase(modules, options.getApiBase());
var filter = options.hideInternalFilter(module);
var aggregates = module.getAggregateRoots().stream().filter(filter).toList();
var valueTypes = module.getValueTypes().stream().filter(filter).toList();
Function<List<JavaClass>, String> mapper = asciidoctor::typesToBulletPoints;
List<JavaClass> aggregates = options.hideInternals
? module.getAggregateRoots().stream().filter(module::isExposed).collect(Collectors.toList())
: module.getAggregateRoots();
StringBuilder builder = new StringBuilder();
builder.append(startTable("%autowidth.stretch, cols=\"h,a\""));
builder.append(writeTableRow("Base package", asciidoctor.toInlineCode(module.getBasePackage().getName())));
builder.append(writeTableRow("Spring components", asciidoctor.renderSpringBeans(options, module)));
builder.append(addTableRow(aggregates, "Aggregate roots", mapper));
builder.append(writeTableRow("Published events", asciidoctor.renderEvents(module)));
builder.append(addTableRow(module.getEventsListenedTo(modules), "Events listened to", mapper));
builder.append(writeTableRow("Properties",
asciidoctor.renderConfigurationProperties(module, properties.getModuleProperties(module))));
builder.append(startOrEndTable());
return new StringBuilder() //
return builder.toString();
.append(startTable("%autowidth.stretch, cols=\"h,a\""))
.append(addTableRow("Base package", asciidoctor.toInlineCode(module.getBasePackage().getName()), options))
// Spring components
.append(addTableRow("Spring components", asciidoctor.renderSpringBeans(module, options), options)) //
.append(addTableRow("Bean references", asciidoctor.renderBeanReferences(module), options)) //
// Aggregates
.append(addTableRow(aggregates, "Aggregate roots", mapper, options)) //
.append(addTableRow(valueTypes, "Value types", mapper, options)) //
// Events
.append(addTableRow("Published events", asciidoctor.renderEvents(module), options)) //
.append(addTableRow(module.getEventsListenedTo(modules), "Events listened to", mapper, options)) //
// Properties
.append(addTableRow("Properties",
asciidoctor.renderConfigurationProperties(module, properties.getModuleProperties(module)), options)) //
.append(startOrEndTable())
.toString();
}
private <T> String addTableRow(List<T> types, String header, Function<List<T>, String> mapper) {
return types.isEmpty() ? "" : writeTableRow(header, mapper.apply(types));
private static String addTableRow(String title, String content, CanvasOptions options) {
return options.hideEmptyLines && (content.isBlank() || content.equalsIgnoreCase("none"))
? ""
: writeTableRow(title, content);
}
private static <T> String addTableRow(List<T> types, String header, Function<List<T>, String> mapper,
CanvasOptions options) {
return options.hideEmptyLines && types.isEmpty() ? "" : writeTableRow(header, mapper.apply(types));
}
String toPlantUml() {
@@ -348,20 +368,17 @@ public class Documenter {
DEPENDENCY_DESCRIPTIONS.entrySet().stream().forEach(entry -> {
module.getDependencies(modules, entry.getKey()).stream() //
.map(ApplicationModuleDependency::getTargetModule) //
.map(it -> getComponents(options).get(it)) //
// .filter(it -> !component.hasEfferentRelationshipWith(it)) //
.forEach(it -> {
Relationship relationship = component.uses(it, entry.getValue());
relationship.addTags(entry.getKey().toString());
});
.map(it -> component.uses(it, entry.getValue())) //
.filter(it -> it != null) //
.forEach(it -> it.addTags(entry.getKey().toString()));
});
module.getBootstrapDependencies(modules) //
.forEach(it -> {
Relationship relationship = component.uses(getComponents(options).get(it), "uses");
relationship.addTags(DependencyType.USES_COMPONENT.toString());
});
module.getBootstrapDependencies(modules)
.map(it -> component.uses(getComponents(options).get(it), "uses"))
.filter(it -> it != null)
.forEach(it -> it.addTags(DependencyType.USES_COMPONENT.toString()));
}
private Map<ApplicationModule, Component> getComponents(DiagramOptions options) {
@@ -383,7 +400,8 @@ public class Documenter {
Supplier<Stream<ApplicationModule>> bootstrapDependencies = () -> module.getBootstrapDependencies(modules,
options.getDependencyDepth());
Supplier<Stream<ApplicationModule>> otherDependencies = () -> options.getDependencyTypes()
.flatMap(it -> module.getDependencies(modules, it).stream());
.flatMap(it -> module.getDependencies(modules, it).stream()
.map(ApplicationModuleDependency::getTargetModule));
Supplier<Stream<ApplicationModule>> dependencies = () -> Stream.concat(bootstrapDependencies.get(),
otherDependencies.get());
@@ -592,8 +610,7 @@ public class Documenter {
private final @With Predicate<ApplicationModule> exclusions;
/**
* A {@link Predicate} to define which Structurizr {@link Component}s to be included in the diagram to be
* created.
* A {@link Predicate} to define which Structurizr {@link Component}s to be included in the diagram to be created.
*/
private final @With Predicate<Component> componentFilter;
@@ -616,8 +633,8 @@ public class Documenter {
private final @With Function<ApplicationModule, Optional<String>> colorSelector;
/**
* A callback to return a default display names for a given {@link ApplicationModule}. Default implementation
* just forwards to {@link ApplicationModule#getDisplayName()}.
* A callback to return a default display names for a given {@link ApplicationModule}. Default implementation just
* forwards to {@link ApplicationModule#getDisplayName()}.
*/
private final @With Function<ApplicationModule, String> defaultDisplayName;
@@ -629,8 +646,8 @@ public class Documenter {
/**
* Configuration setting to define whether modules that do not have a relationship to any other module shall be
* retained in the diagrams created. The default is {@value ElementsWithoutRelationships#HIDDEN}. See
* {@link DiagramOptions#withExclusions(Predicate)} for a more fine-grained way of defining which modules to
* exclude in case you flip this to {@link ElementsWithoutRelationships#VISIBLE}.
* {@link DiagramOptions#withExclusions(Predicate)} for a more fine-grained way of defining which modules to exclude
* in case you flip this to {@link ElementsWithoutRelationships#VISIBLE}.
*
* @see #withExclusions(Predicate)
*/
@@ -701,8 +718,8 @@ public class Documenter {
/**
* Configuration setting to define whether modules that do not have a relationship to any other module shall be
* retained in the diagrams created. The default is {@value ElementsWithoutRelationships#HIDDEN}. See
* {@link DiagramOptions#withExclusions(Predicate)} for a more fine-grained way of defining which modules to
* exclude in case you flip this to {@link ElementsWithoutRelationships#VISIBLE}.
* {@link DiagramOptions#withExclusions(Predicate)} for a more fine-grained way of defining which modules to exclude
* in case you flip this to {@link ElementsWithoutRelationships#VISIBLE}.
*
* @author Oliver Drotbohm
* @see DiagramOptions#withExclusions(Predicate)
@@ -722,6 +739,7 @@ public class Documenter {
private final @With @Getter @Nullable String apiBase;
private final @With @Nullable String targetFileName;
private final boolean hideInternals;
private final boolean hideEmptyLines;
public static CanvasOptions defaults() {
@@ -735,7 +753,7 @@ public class Documenter {
}
public static CanvasOptions withoutDefaultGroupings() {
return new CanvasOptions(new ArrayList<>(), null, null, true);
return new CanvasOptions(new ArrayList<>(), null, null, true, true);
}
public CanvasOptions groupingBy(Grouping... groupings) {
@@ -743,7 +761,7 @@ public class Documenter {
List<Grouping> result = new ArrayList<>(groupers);
result.addAll(Arrays.asList(groupings));
return new CanvasOptions(result, apiBase, targetFileName, hideInternals);
return new CanvasOptions(result, apiBase, targetFileName, hideInternals, hideEmptyLines);
}
public CanvasOptions groupingBy(String name, Predicate<SpringBean> filter) {
@@ -756,7 +774,11 @@ public class Documenter {
* @return will never be {@literal null}.
*/
public CanvasOptions revealInternals() {
return new CanvasOptions(groupers, apiBase, targetFileName, false);
return new CanvasOptions(groupers, apiBase, targetFileName, false, hideEmptyLines);
}
public CanvasOptions revealEmptyLines() {
return new CanvasOptions(groupers, apiBase, targetFileName, hideInternals, false);
}
Groupings groupBeans(ApplicationModule module) {
@@ -785,6 +807,10 @@ public class Documenter {
return Groupings.of(result);
}
Predicate<JavaClass> hideInternalFilter(ApplicationModule module) {
return hideInternals ? module::isExposed : __ -> true;
}
private Optional<String> getTargetFileName() {
return Optional.ofNullable(targetFileName);
}

View File

@@ -19,9 +19,10 @@ import static org.assertj.core.api.Assertions.*;
import org.junit.jupiter.api.Test;
import org.springframework.modulith.model.ApplicationModule;
import org.springframework.modulith.model.ApplicationModule.DependencyType;
import org.springframework.modulith.model.ApplicationModuleDependencies;
import org.springframework.modulith.model.ApplicationModules;
import org.springframework.modulith.model.ApplicationModules.Filters;
import org.springframework.modulith.model.DependencyType;
import org.springframework.modulith.model.Violations;
import com.acme.myproject.invalid.InvalidComponent;
@@ -43,14 +44,14 @@ class ModulithTest {
@Test
void verifyModules() {
String componentName = InternalComponentB.class.getSimpleName();
String componentName = InternalComponentB.class.getName();
assertThatExceptionOfType(Violations.class) //
.isThrownBy(() -> ApplicationModules.of(Application.class, DEFAULT_EXCLUSIONS).verify()) //
.withMessageContaining(String.format("Module '%s' depends on non-exposed type %s within module 'moduleB'",
"invalid", InternalComponentB.class.getName()))
.withMessageContaining(String.format("%s declares constructor %s(%s)", InvalidComponent.class.getSimpleName(),
InvalidComponent.class.getSimpleName(), componentName));
.withMessageContaining(
String.format("Constructor <%s.<init>(%s)>", InvalidComponent.class.getName(), componentName));
}
@Test
@@ -94,11 +95,10 @@ class ModulithTest {
assertThat(modules.getModuleByName("moduleD")).hasValueSatisfying(it -> {
assertThat(it.getDependencies(modules, DependencyType.DEFAULT))
.map(ApplicationModule::getName)
.contains("moduleC"); // ConfigurationProperties -> Value
.matches(inner -> inner.containsModuleNamed("moduleC"));
assertThat(it.getDependencies(modules, DependencyType.USES_COMPONENT))
.isEmpty();
.matches(ApplicationModuleDependencies::isEmpty);
});
}
}

View File

@@ -28,7 +28,7 @@ import java.util.Optional;
import org.junit.jupiter.api.Test;
import org.springframework.modulith.docs.Documenter.DiagramOptions;
import org.springframework.modulith.model.ApplicationModule;
import org.springframework.modulith.model.ApplicationModule.DependencyType;
import org.springframework.modulith.model.DependencyType;
import com.acme.myproject.Application;

View File

@@ -23,7 +23,6 @@ import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.junit.jupiter.api.Test;
import org.springframework.modulith.model.ApplicationModule.DependencyType;
import com.acme.myproject.Application;
import com.acme.myproject.complex.internal.FirstTypeBasedPort;
@@ -117,8 +116,7 @@ class ModulesIntegrationTest {
ApplicationModule moduleA = modules.getModuleByName("moduleA").orElseThrow(IllegalStateException::new);
assertThat(module).hasValueSatisfying(it -> {
assertThat(it.getDependencies(modules, DependencyType.EVENT_LISTENER)) //
.contains(moduleA);
assertThat(it.getDependencies(modules, DependencyType.EVENT_LISTENER).contains(moduleA)).isTrue();
});
}

View File

@@ -28,7 +28,7 @@ import org.springframework.boot.autoconfigure.ImportAutoConfiguration;
import org.springframework.boot.test.autoconfigure.filter.TypeExcludeFilters;
import org.springframework.boot.test.context.SpringBootTestContextBootstrapper;
import org.springframework.core.annotation.AliasFor;
import org.springframework.modulith.model.ApplicationModule.DependencyDepth;
import org.springframework.modulith.model.DependencyDepth;
import org.springframework.test.context.BootstrapWith;
import org.springframework.test.context.TestConstructor;
import org.springframework.test.context.TestConstructor.AutowireMode;