GH-14 - Remove Lombok from production sources.

Polished a lot of Javadoc.
This commit is contained in:
Oliver Drotbohm
2023-01-12 00:54:04 +01:00
parent 20554c3af3
commit 9ce6bf23ae
87 changed files with 3546 additions and 1061 deletions

View File

@@ -15,9 +15,6 @@
*/
package org.springframework.modulith.model;
import lombok.AccessLevel;
import lombok.RequiredArgsConstructor;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
@@ -33,12 +30,26 @@ import org.springframework.util.StringUtils;
*
* @author Oliver Drotbohm
*/
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
class AnnotationModulithMetadata implements ModulithMetadata {
private final Class<?> modulithType;
private final Modulithic annotation;
/**
* Creates a new {@link AnnotationModulithMetadata} for the given type and annotation.
*
* @param modulithType must not be {@literal null}.
* @param annotation must not be {@literal null}.
*/
private AnnotationModulithMetadata(Class<?> modulithType, Modulithic annotation) {
Assert.notNull(modulithType, "Type must not be null!");
Assert.notNull(annotation, "Annotation must not be null!");
this.modulithType = modulithType;
this.annotation = annotation;
}
/**
* Creates a {@link ModulithMetadata} inspecting {@link Modulithic} annotation or return {@link Optional#empty()} if
* the type given does not carry the annotation.
@@ -62,6 +73,15 @@ class AnnotationModulithMetadata implements ModulithMetadata {
*/
@Override
public Object getModulithSource() {
return getSource();
}
/*
* (non-Javadoc)
* @see org.springframework.modulith.model.ModulithMetadata#getModulithSource()
*/
@Override
public Object getSource() {
return modulithType;
}

View File

@@ -23,18 +23,11 @@ import static org.springframework.modulith.model.Types.JavaXTypes.*;
import static org.springframework.modulith.model.Types.SpringDataTypes.*;
import static org.springframework.modulith.model.Types.SpringTypes.*;
import lombok.AccessLevel;
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.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Predicate;
import java.util.stream.Collectors;
@@ -63,20 +56,19 @@ import com.tngtech.archunit.thirdparty.com.google.common.base.Suppliers;
*
* @author Oliver Drotbohm
*/
@EqualsAndHashCode(doNotUseGetters = true)
public class ApplicationModule {
/**
* The base package of the {@link ApplicationModule}.
*/
private final @Getter JavaPackage basePackage;
private final 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 NamedInterfaces namedInterfaces;
private final boolean useFullyQualifiedModuleNames;
private final Supplier<Classes> springBeans;
@@ -104,6 +96,24 @@ public class ApplicationModule {
this.publishedEvents = Suppliers.memoize(() -> findPublishedEvents());
}
/**
* Returns the module's base package.
*
* @return the basePackage
*/
public JavaPackage getBasePackage() {
return basePackage;
}
/**
* Returns all {@link NamedInterfaces} exposed by the module.
*
* @return the namedInterfaces will never be {@literal null}.
*/
public NamedInterfaces getNamedInterfaces() {
return namedInterfaces;
}
/**
* Returns the logical name of the module.
*
@@ -407,6 +417,41 @@ public class ApplicationModule {
return getType(candidate).isPresent();
}
/*
* (non-Javadoc)
* @see java.lang.Object#equals(java.lang.Object)
*/
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (!(obj instanceof ApplicationModule that)) {
return false;
}
return Objects.equals(this.basePackage, that.basePackage) //
&& Objects.equals(this.entities, that.entities) //
&& 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);
}
/*
* (non-Javadoc)
* @see java.lang.Object#hashCode()
*/
@Override
public int hashCode() {
return Objects.hash(basePackage, entities, information, namedInterfaces, publishedEvents, springBeans,
useFullyQualifiedModuleNames, valueTypes);
}
private List<EventType> findPublishedEvents() {
DescribedPredicate<JavaClass> isEvent = implement(JMoleculesTypes.DOMAIN_EVENT) //
@@ -529,15 +574,28 @@ public class ApplicationModule {
.toList();
}
@Value
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
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.";
@NonNull ApplicationModule target;
@NonNull NamedInterface namedInterface;
private final ApplicationModule target;
private final NamedInterface namedInterface;
/**
* Creates a new {@link DeclaredDependency} for the given {@link ApplicationModule} and {@link NamedInterface}.
*
* @param target must not be {@literal null}.
* @param namedInterface must not be {@literal null}.
*/
private DeclaredDependency(ApplicationModule target, NamedInterface namedInterface) {
Assert.notNull(target, "Target ApplicationModule must not be null!");
Assert.notNull(namedInterface, "NamedInterface must not be null!");
this.target = target;
this.namedInterface = namedInterface;
}
/**
* Creates an {@link DeclaredDependency} to the module and optionally named interface defined by the given
@@ -580,10 +638,22 @@ public class ApplicationModule {
* @return
*/
public static DeclaredDependency to(ApplicationModule module) {
Assert.notNull(module, "ApplicationModule must not be null!");
return new DeclaredDependency(module, module.getNamedInterfaces().getUnnamedInterface());
}
/**
* Returns whether the {@link DeclaredDependency} contains the given {@link JavaClass}.
*
* @param type must not be {@literal null}.
* @return
*/
public boolean contains(JavaClass type) {
Assert.notNull(type, "Type must not be null!");
return namedInterface.contains(type);
}
@@ -595,6 +665,35 @@ public class ApplicationModule {
public String toString() {
return namedInterface.isUnnamed() ? target.getName() : target.getName() + "::" + namedInterface.getName();
}
/*
* (non-Javadoc)
* @see java.lang.Object#equals(java.lang.Object)
*/
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (!(obj instanceof DeclaredDependency that)) {
return false;
}
return Objects.equals(this.target, that.target) //
&& Objects.equals(this.namedInterface, that.namedInterface);
}
/*
* (non-Javadoc)
* @see java.lang.Object#hashCode()
*/
@Override
public int hashCode() {
return Objects.hash(target, namedInterface);
}
}
/**
@@ -602,16 +701,26 @@ public class ApplicationModule {
*
* @author Oliver Drotbohm
*/
@Value
static class DeclaredDependencies {
List<DeclaredDependency> dependencies;
private final List<DeclaredDependency> dependencies;
/**
* Creates a new {@link DeclaredDependencies} for the given {@link List} of {@link DeclaredDependency}.
*
* @param dependencies must not be {@literal null}.
*/
public DeclaredDependencies(List<DeclaredDependency> dependencies) {
Assert.notNull(dependencies, "Dependencies must not be null!");
this.dependencies = dependencies;
}
/**
* Returns whether any of the dependencies contains the given {@link JavaClass}.
*
* @param type must not be {@literal null}.
* @return
*/
public boolean contains(JavaClass type) {
@@ -621,10 +730,17 @@ public class ApplicationModule {
.anyMatch(it -> it.contains(type));
}
/**
* Returns whether the {@link DeclaredDependencies} are empty.
*/
public boolean isEmpty() {
return dependencies.isEmpty();
}
/*
* (non-Javadoc)
* @see java.lang.Object#toString()
*/
@Override
public String toString() {
@@ -632,18 +748,64 @@ public class ApplicationModule {
.map(DeclaredDependency::toString)
.collect(Collectors.joining(", "));
}
/*
* (non-Javadoc)
* @see java.lang.Object#equals(java.lang.Object)
*/
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (!(obj instanceof DeclaredDependencies that)) {
return false;
}
return Objects.equals(this.dependencies, that.dependencies);
}
/*
* (non-Javadoc)
* @see java.lang.Object#hashCode()
*/
@Override
public int hashCode() {
return Objects.hash(dependencies);
}
}
@EqualsAndHashCode
@RequiredArgsConstructor
static class QualifiedDependency {
private static final List<String> INJECTION_TYPES = Arrays.asList(//
AT_AUTOWIRED, AT_RESOURCE, AT_INJECT);
private static final List<String> INJECTION_TYPES = Arrays.asList(AT_AUTOWIRED, AT_RESOURCE, AT_INJECT);
private final @NonNull @Getter JavaClass source, target;
private final @NonNull String description;
private final @NonNull DependencyType type;
private final JavaClass source, target;
private final String description;
private final DependencyType type;
/**
* Creates a new {@link QualifiedDependency} from the given source and target {@link JavaClass}, description and
* {@link DependencyType}.
*
* @param source must not be {@literal null}.
* @param target must not be {@literal null}.
* @param description must not be {@literal null}.
* @param type must not be {@literal null}.
*/
public QualifiedDependency(JavaClass source, JavaClass target, String description, DependencyType type) {
Assert.notNull(source, "Source JavaClass must not be null!");
Assert.notNull(target, "Target JavaClass must not be null!");
Assert.notNull(description, "Description must not be null!");
Assert.notNull(type, "DependencyType must not be null!");
this.source = source;
this.target = target;
this.description = description;
this.type = type;
}
QualifiedDependency(Dependency dependency) {
this(dependency.getOriginClass(), //
@@ -652,6 +814,65 @@ public class ApplicationModule {
DependencyType.forDependency(dependency));
}
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);
}
/**
* Returns the source {@link JavaClass}.
*
* @return the source will never be {@literal null}.
*/
public JavaClass getSource() {
return source;
}
/**
* Returns the target {@link JavaClass}.
*
* @return the target must not be {@literal null}.
*/
public JavaClass getTarget() {
return target;
}
/**
* Returns whether the {@link QualifiedDependency} has the given {@link DependencyType}.
*
* @param type must not be {@literal null}.
* @return
*/
boolean hasType(DependencyType type) {
return this.type.equals(type);
}
@@ -704,39 +925,34 @@ public class ApplicationModule {
return type.format(FormatableType.of(source), FormatableType.of(target));
}
static QualifiedDependency fromCodeUnitParameter(JavaCodeUnit codeUnit, JavaClass parameter) {
/*
* (non-Javadoc)
* @see java.lang.Object#equals(java.lang.Object)
*/
@Override
public boolean equals(Object obj) {
var description = createDescription(codeUnit, parameter, "parameter");
var type = DependencyType.forCodeUnit(codeUnit) //
.defaultOr(() -> DependencyType.forParameter(parameter));
if (this == obj) {
return true;
}
return new QualifiedDependency(codeUnit.getOwner(), parameter, description, type);
if (!(obj instanceof QualifiedDependency other)) {
return false;
}
return Objects.equals(this.source, other.source) //
&& Objects.equals(this.target, other.target) //
&& Objects.equals(this.description, other.description) //
&& Objects.equals(this.type, other.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);
/*
* (non-Javadoc)
* @see java.lang.Object#hashCode()
*/
@Override
public int hashCode() {
return Objects.hash(source, target, description, type);
}
private static Stream<QualifiedDependency> fromConstructorOf(ArchitecturallyEvidentType source) {
@@ -885,15 +1101,35 @@ public class ApplicationModule {
}
}
@ToString
@EqualsAndHashCode
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
private static class DefaultApplicationModuleDependency implements ApplicationModuleDependency {
private final QualifiedDependency dependency;
private final ApplicationModule target;
static Stream<DefaultApplicationModuleDependency> of(QualifiedDependency dependency, ApplicationModules modules) {
/**
* Creates a new {@link ApplicationModuleDependency} for the given {@link QualifiedDependency} and
* {@link ApplicationModules}.
*
* @param dependency must not be {@literal null}.
* @param target must not be {@literal null}.
*/
private DefaultApplicationModuleDependency(QualifiedDependency dependency, ApplicationModule target) {
Assert.notNull(dependency, "QualifiedDependency must not be null!");
Assert.notNull(target, "Target ApplicationModule must not be null!");
this.dependency = dependency;
this.target = target;
}
/**
* Creates a new {@link Stream} of {@link ApplicationModuleDependency} for the given {@link QualifiedDependency} and
* {@link ApplicationModules}.
*
* @param dependency must not be {@literal null}.
* @param modules must not be {@literal null}.
*/
static Stream<ApplicationModuleDependency> of(QualifiedDependency dependency, ApplicationModules modules) {
return modules.getModuleByType(dependency.getTarget()).stream()
.map(it -> new DefaultApplicationModuleDependency(dependency, it));
@@ -934,5 +1170,42 @@ public class ApplicationModule {
public ApplicationModule getTargetModule() {
return target;
}
/*
* (non-Javadoc)
* @see java.lang.Object#toString()
*/
@Override
public String toString() {
return "DefaultApplicationModuleDependency [dependency=" + dependency + ", target=" + target + "]";
}
/*
* (non-Javadoc)
* @see java.lang.Object#equals(java.lang.Object)
*/
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (!(obj instanceof DefaultApplicationModuleDependency other)) {
return false;
}
return Objects.equals(this.target, other.target) //
&& Objects.equals(this.dependency, other.dependency);
}
/*
* (non-Javadoc)
* @see java.lang.Object#hashCode()
*/
@Override
public int hashCode() {
return Objects.hash(target, dependency);
}
}
}

View File

@@ -15,8 +15,6 @@
*/
package org.springframework.modulith.model;
import lombok.RequiredArgsConstructor;
import java.util.HashSet;
import java.util.List;
import java.util.function.Function;
@@ -25,16 +23,45 @@ 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.
* 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;
/**
* Creates a new {@link ApplicationModuleDependencies} for the given {@link List} of
* {@link ApplicationModuleDependency} and {@link ApplicationModules}.
*
* @param dependencies must not be {@literal null}.
* @param modules must not be {@literal null}.
*/
private ApplicationModuleDependencies(List<ApplicationModuleDependency> dependencies, ApplicationModules modules) {
Assert.notNull(dependencies, "ApplicationModuleDependency list must not be null!");
Assert.notNull(modules, "ApplicationModules must not be null!");
this.dependencies = dependencies;
this.modules = modules;
}
/**
* Creates a new {@link ApplicationModuleDependencies} for the given {@link List} of
* {@link ApplicationModuleDependency} and {@link ApplicationModules}.
*
* @param dependencies must not be {@literal null}.
* @param modules must not be {@literal null}.
* @return will never be {@literal null}.
*/
static ApplicationModuleDependencies of(List<ApplicationModuleDependency> dependencies,
ApplicationModules modules) {
return new ApplicationModuleDependencies(dependencies, modules);
}
/**
* Returns whether the dependencies contain the given {@link ApplicationModule}.
*

View File

@@ -19,12 +19,6 @@ import static com.tngtech.archunit.base.DescribedPredicate.*;
import static com.tngtech.archunit.core.domain.JavaClass.Predicates.*;
import static java.util.stream.Collectors.*;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Value;
import lombok.With;
import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;
@@ -60,7 +54,6 @@ import com.tngtech.archunit.library.dependencies.SlicesRuleDefinition;
* @author Oliver Drotbohm
* @author Peter Gafert
*/
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public class ApplicationModules implements Iterable<ApplicationModule> {
private static final Map<CacheKey, ApplicationModules> CACHE = new HashMap<>();
@@ -90,7 +83,7 @@ public class ApplicationModules implements Iterable<ApplicationModule> {
private final Map<String, ApplicationModule> modules;
private final JavaClasses allClasses;
private final List<JavaPackage> rootPackages;
private final @With(AccessLevel.PRIVATE) @Getter Set<ApplicationModule> sharedModules;
private final Set<ApplicationModule> sharedModules;
private final List<String> orderedNames;
private boolean verified;
@@ -123,6 +116,39 @@ public class ApplicationModules implements Iterable<ApplicationModule> {
: modules.values().stream().map(ApplicationModule::getName).toList();
}
/**
* Creates a new {@link ApplicationModules} for the given {@link ModulithMetadata}, {@link ApplicationModule}s,
* {@link JavaClasses}, {@link JavaPackage}s, shared {@link ApplicationModule}s, ordered module names and verified
* flag.
*
* @param metadata must not be {@literal null}.
* @param modules must not be {@literal null}.
* @param allClasses must not be {@literal null}.
* @param rootPackages must not be {@literal null}.
* @param sharedModules must not be {@literal null}.
* @param orderedNames must not be {@literal null}.
* @param verified
*/
private ApplicationModules(ModulithMetadata metadata, Map<String, ApplicationModule> modules, JavaClasses classes,
List<JavaPackage> rootPackages, Set<ApplicationModule> sharedModules, List<String> orderedNames,
boolean verified) {
Assert.notNull(metadata, "ModulithMetadata must not be null!");
Assert.notNull(modules, "Application modules must not be null!");
Assert.notNull(classes, "JavaClasses must not be null!");
Assert.notNull(rootPackages, "Root JavaPackages must not be null!");
Assert.notNull(sharedModules, "Shared ApplicationModules must not be null!");
Assert.notNull(orderedNames, "Ordered application module names must not be null!");
this.metadata = metadata;
this.modules = modules;
this.allClasses = classes;
this.rootPackages = rootPackages;
this.sharedModules = sharedModules;
this.orderedNames = orderedNames;
this.verified = verified;
}
/**
* Creates a new {@link ApplicationModules} relative to the given modulith type. Will inspect the {@link Modulith}
* annotation on the class given for advanced customizations of the module setup.
@@ -147,7 +173,7 @@ public class ApplicationModules implements Iterable<ApplicationModule> {
*/
public static ApplicationModules of(Class<?> modulithType, DescribedPredicate<JavaClass> ignored) {
CacheKey key = TypeKey.of(modulithType, ignored);
CacheKey key = new TypeKey(modulithType, ignored);
return CACHE.computeIfAbsent(key, it -> {
@@ -177,7 +203,7 @@ public class ApplicationModules implements Iterable<ApplicationModule> {
*/
public static ApplicationModules of(String javaPackage, DescribedPredicate<JavaClass> ignored) {
CacheKey key = PackageKey.of(javaPackage, ignored);
CacheKey key = new PackageKey(javaPackage, ignored);
return CACHE.computeIfAbsent(key, it -> {
@@ -189,33 +215,32 @@ public class ApplicationModules implements Iterable<ApplicationModule> {
}
/**
* Creates a new {@link ApplicationModules} instance for the given {@link CacheKey}.
* Returns the source of the {@link ApplicationModules}. Either a main application class or a package name.
*
* @param key must not be {@literal null}.
* @return will never be {@literal null}.
* @deprecated use {@link #getSource()} instead
*/
private static ApplicationModules of(CacheKey key) {
Assert.notNull(key, "Cache key must not be null!");
ModulithMetadata metadata = key.getMetadata();
Set<String> basePackages = new HashSet<>();
basePackages.add(key.getBasePackage());
basePackages.addAll(metadata.getAdditionalPackages());
ApplicationModules modules = new ApplicationModules(metadata, basePackages, key.getIgnored(),
metadata.useFullyQualifiedModuleNames(), IMPORT_OPTION);
Set<ApplicationModule> sharedModules = metadata.getSharedModuleNames() //
.map(modules::getRequiredModule) //
.collect(Collectors.toSet());
return modules.withSharedModules(sharedModules);
@Deprecated(forRemoval = true)
public Object getModulithSource() {
return metadata.getSource();
}
public Object getModulithSource() {
return metadata.getModulithSource();
/**
* Returns the source of the {@link ApplicationModules}. Either a main application class or a package name.
*
* @return will never be {@literal null}.
*/
public Object getSource() {
return metadata.getSource();
}
/**
* Returns all {@link ApplicationModule}s registered as shared ones.
*
* @return will never be {@literal null}.
*/
public Set<ApplicationModule> getSharedModules() {
return sharedModules;
}
/**
@@ -421,9 +446,13 @@ public class ApplicationModules implements Iterable<ApplicationModule> {
return this.stream().map(ApplicationModule::toString).collect(Collectors.joining("\n"));
}
private ApplicationModules withSharedModules(Set<ApplicationModule> sharedModules) {
return new ApplicationModules(metadata, modules, allClasses, rootPackages, sharedModules, orderedNames, verified);
}
private FailureReport assertNoCyclesFor(JavaPackage rootPackage) {
EvaluationResult result = SlicesRuleDefinition.slices() //
var result = SlicesRuleDefinition.slices() //
.matching(rootPackage.getName().concat(".(*)..")) //
.should().beFreeOfCycles() //
.evaluate(allClasses.that(resideInAPackage(rootPackage.getName().concat(".."))));
@@ -457,7 +486,7 @@ public class ApplicationModules implements Iterable<ApplicationModule> {
*/
private ApplicationModule getRequiredModule(String moduleName) {
ApplicationModule module = modules.get(moduleName);
var module = modules.get(moduleName);
if (module == null) {
throw new IllegalArgumentException(String.format("Module %s does not exist!", moduleName));
@@ -466,6 +495,32 @@ public class ApplicationModules implements Iterable<ApplicationModule> {
return module;
}
/**
* Creates a new {@link ApplicationModules} instance for the given {@link CacheKey}.
*
* @param key must not be {@literal null}.
* @return will never be {@literal null}.
*/
private static ApplicationModules of(CacheKey key) {
Assert.notNull(key, "Cache key must not be null!");
var metadata = key.getMetadata();
var basePackages = new HashSet<String>();
basePackages.add(key.getBasePackage());
basePackages.addAll(metadata.getAdditionalPackages());
var modules = new ApplicationModules(metadata, basePackages, key.getIgnored(),
metadata.useFullyQualifiedModuleNames(), IMPORT_OPTION);
var sharedModules = metadata.getSharedModuleNames() //
.map(modules::getRequiredModule) //
.collect(Collectors.toSet());
return modules.withSharedModules(sharedModules);
}
public static class Filters {
public static DescribedPredicate<JavaClass> withoutModules(String... names) {
@@ -489,11 +544,22 @@ public class ApplicationModules implements Iterable<ApplicationModule> {
ModulithMetadata getMetadata();
}
@Value(staticConstructor = "of")
private static final class TypeKey implements CacheKey {
Class<?> type;
DescribedPredicate<JavaClass> ignored;
private final Class<?> type;
private final DescribedPredicate<JavaClass> ignored;
/**
* Creates a new {@link TypeKey} for the given type and {@link DescribedPredicate} of ignored {@link JavaClass}es.
*
* @param type must not be {@literal null}.
* @param ignored must not be {@literal null}.
*/
TypeKey(Class<?> type, DescribedPredicate<JavaClass> ignored) {
this.type = type;
this.ignored = ignored;
}
/*
* (non-Javadoc)
@@ -512,13 +578,79 @@ public class ApplicationModules implements Iterable<ApplicationModule> {
public ModulithMetadata getMetadata() {
return ModulithMetadata.of(type);
}
/*
* (non-Javadoc)
* @see org.springframework.modulith.model.ApplicationModules.CacheKey#getIgnored()
*/
@Override
public DescribedPredicate<JavaClass> getIgnored() {
return ignored;
}
/*
* (non-Javadoc)
* @see java.lang.Object#equals(java.lang.Object)
*/
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (!(obj instanceof TypeKey other)) {
return false;
}
return Objects.equals(this.type, other.type) //
&& Objects.equals(this.ignored, other.ignored);
}
/*
* (non-Javadoc)
* @see java.lang.Object#hashCode()
*/
@Override
public int hashCode() {
return Objects.hash(type, ignored);
}
}
@Value(staticConstructor = "of")
private static final class PackageKey implements CacheKey {
String basePackage;
DescribedPredicate<JavaClass> ignored;
private final String basePackage;
private final DescribedPredicate<JavaClass> ignored;
/**
* Creates a new {@link PackageKey} for the given base package and {@link DescribedPredicate} of ignored
* {@link JavaClass}es.
*
* @param basePackage must not be {@literal null}.
* @param ignored must not be {@literal null}.
*/
PackageKey(String basePackage, DescribedPredicate<JavaClass> ignored) {
this.basePackage = basePackage;
this.ignored = ignored;
}
/*
* (non-Javadoc)
* @see org.springframework.modulith.model.ApplicationModules.CacheKey#getBasePackage()
*/
@Override
public String getBasePackage() {
return basePackage;
}
/*
* (non-Javadoc)
* @see org.springframework.modulith.model.ApplicationModules.CacheKey#getIgnored()
*/
public DescribedPredicate<JavaClass> getIgnored() {
return ignored;
}
/*
* (non-Javadoc)
@@ -528,6 +660,34 @@ public class ApplicationModules implements Iterable<ApplicationModule> {
public ModulithMetadata getMetadata() {
return ModulithMetadata.of(basePackage);
}
/*
* (non-Javadoc)
* @see java.lang.Object#equals(java.lang.Object)
*/
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (!(obj instanceof PackageKey that)) {
return false;
}
return Objects.equals(this.basePackage, that.basePackage) //
&& Objects.equals(this.ignored, that.ignored);
}
/*
* (non-Javadoc)
* @see java.lang.Object#hashCode()
*/
@Override
public int hashCode() {
return Objects.hash(basePackage, ignored);
}
}
/**

View File

@@ -17,11 +17,6 @@ package org.springframework.modulith.model;
import static org.springframework.modulith.model.Types.JavaXTypes.*;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.Value;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
@@ -51,12 +46,15 @@ import com.tngtech.archunit.thirdparty.com.google.common.base.Suppliers;
*
* @author Oliver Drotbohm
*/
@RequiredArgsConstructor(access = AccessLevel.PROTECTED)
public abstract class ArchitecturallyEvidentType {
private static Map<Key, ArchitecturallyEvidentType> CACHE = new HashMap<>();
private final @Getter JavaClass type;
private final JavaClass type;
protected ArchitecturallyEvidentType(JavaClass type) {
this.type = type;
}
/**
* Creates a new {@link ArchitecturallyEvidentType} for the given {@link JavaType} and {@link Classes} of Spring
@@ -68,7 +66,7 @@ public abstract class ArchitecturallyEvidentType {
*/
public static ArchitecturallyEvidentType of(JavaClass type, Classes beanTypes) {
return CACHE.computeIfAbsent(Key.of(type, beanTypes), it -> {
return CACHE.computeIfAbsent(new Key(type, beanTypes), it -> {
List<ArchitecturallyEvidentType> delegates = new ArrayList<>();
@@ -86,6 +84,15 @@ public abstract class ArchitecturallyEvidentType {
});
}
/**
* Returns the {@link JavaClass} backing the {@link ArchitecturallyEvidentType}.
*
* @return the type wnn
*/
public JavaClass getType() {
return type;
}
/**
* Returns the abbreviated (i.e. every package fragment reduced to its first character) full name.
*
@@ -620,18 +627,20 @@ public abstract class ArchitecturallyEvidentType {
}
}
@Value(staticConstructor = "of")
private static class Key {
private static record Key(JavaClass type, Classes beanTypes) {}
JavaClass type;
Classes beanTypes;
}
@Value
public final class ReferenceMethod {
public static class ReferenceMethod {
private final JavaMethod method;
public ReferenceMethod(JavaMethod method) {
this.method = method;
}
public JavaMethod getMethod() {
return method;
}
public boolean isAsync() {
return method.isAnnotatedWith(SpringTypes.AT_ASYNC) || method.isMetaAnnotatedWith(SpringTypes.AT_ASYNC);
}

View File

@@ -15,15 +15,13 @@
*/
package org.springframework.modulith.model;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Predicate;
import java.util.stream.Collector;
@@ -46,8 +44,6 @@ import com.tngtech.archunit.core.domain.properties.HasName;
/**
* @author Oliver Drotbohm
*/
@ToString
@EqualsAndHashCode
class Classes implements DescribedIterable<JavaClass> {
public static Classes NONE = Classes.of(Collections.emptyList());
@@ -65,7 +61,7 @@ class Classes implements DescribedIterable<JavaClass> {
this.classes = classes.stream() //
.sorted(Comparator.comparing(JavaClass::getName)) //
.collect(Collectors.collectingAndThen(Collectors.toList(), Collections::unmodifiableList));
.toList();
}
/**
@@ -190,35 +186,73 @@ class Classes implements DescribedIterable<JavaClass> {
return classes.iterator();
}
/*
* (non-Javadoc)
* @see java.lang.Object#toString()
*/
@Override
public String toString() {
return "Classes [classes=" + classes + "]";
}
/*
* (non-Javadoc)
* @see java.lang.Object#equals(java.lang.Object)
*/
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (!(obj instanceof Classes that)) {
return false;
}
return Objects.equals(classes, that.classes);
}
/*
* (non-Javadoc)
* @see java.lang.Object#hashCode()
*/
@Override
public int hashCode() {
return Objects.hash(classes);
}
String format() {
return classes.stream() //
.map(Classes::format) //
.collect(Collectors.joining("\n"));
}
String format(String basePackage) {
return classes.stream() //
.map(it -> Classes.format(it, basePackage)) //
.collect(Collectors.joining("\n"));
}
private static String format(JavaClass type) {
return format(type, "");
}
static String format(JavaClass type, String basePackage) {
Assert.notNull(type, "Type must not be null!");
Assert.notNull(basePackage, "Base package must not be null!");
String prefix = type.getModifiers().contains(JavaModifier.PUBLIC) ? "+" : "o";
String name = StringUtils.hasText(basePackage) //
var prefix = type.getModifiers().contains(JavaModifier.PUBLIC) ? "+" : "o";
var name = StringUtils.hasText(basePackage) //
? type.getName().replace(basePackage, "") //
: type.getName();
return String.format("%s %s", prefix, name);
}
private static String format(JavaClass type) {
return format(type, "");
}
private static class SameClass extends DescribedPredicate<JavaClass> {
private final JavaClass reference;

View File

@@ -15,10 +15,6 @@
*/
package org.springframework.modulith.model;
import lombok.AccessLevel;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import java.lang.annotation.Annotation;
import java.util.Collections;
import java.util.List;
@@ -26,6 +22,7 @@ import java.util.Optional;
import java.util.stream.Stream;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.lang.NonNull;
import org.springframework.modulith.Modulith;
import org.springframework.modulith.Modulithic;
import org.springframework.modulith.model.Types.SpringTypes;
@@ -37,13 +34,24 @@ import org.springframework.util.Assert;
*
* @author Oliver Drotbohm
*/
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
class DefaultModulithMetadata implements ModulithMetadata {
private static final Class<? extends Annotation> AT_SPRING_BOOT_APPLICATION = Types
.loadIfPresent(SpringTypes.AT_SPRING_BOOT_APPLICATION);
private final @NonNull Object modulithSource;
private final @NonNull Object source;
/**
* Creates a new {@link DefaultModulithMetadata} for the given source.
*
* @param source must not be {@literal null}.
*/
private DefaultModulithMetadata(Object source) {
Assert.notNull(source, "Source must not be null!");
this.source = source;
}
/**
* Creates a new {@link ModulithMetadata} representing the defaults of a class annotated but not customized with
@@ -80,7 +88,16 @@ class DefaultModulithMetadata implements ModulithMetadata {
*/
@Override
public Object getModulithSource() {
return modulithSource;
return getSource();
}
/*
* (non-Javadoc)
* @see org.springframework.modulith.model.ModulithMetadata#getModulithSource()
*/
@Override
public Object getSource() {
return source;
}
/*

View File

@@ -15,9 +15,8 @@
*/
package org.springframework.modulith.model;
import lombok.Value;
import java.util.List;
import java.util.Objects;
import java.util.stream.Stream;
import org.springframework.util.Assert;
@@ -30,15 +29,9 @@ import com.tngtech.archunit.core.domain.JavaModifier;
*
* @author Oliver Drotbohm
*/
@Value
public class EventType {
private final JavaClass type;
/**
* The sources that create that event. Includes static factory methods that return an instance of the event type
* itself as well as constructor invocations, except ones from the factory methods.
*/
private final List<Source> sources;
/**
@@ -66,7 +59,68 @@ public class EventType {
.toList();
}
/**
* The {@link JavaClass} of the {@link EventType}.
*
* @return will never be {@literal null}.
*/
public JavaClass getType() {
return type;
}
/**
* The sources that create that event. Includes static factory methods that return an instance of the event type
* itself as well as constructor invocations, except ones from the factory methods.
*
* @return will never be {@literal null}.
*/
public List<Source> getSources() {
return sources;
}
/**
* Whether any sources exist at all.
*
* @see #getSources()
*/
public boolean hasSources() {
return !this.sources.isEmpty();
}
/*
* (non-Javadoc)
* @see java.lang.Object#toString()
*/
@Override
public String toString() {
return "EventType [type=" + type + ", sources=" + sources + "]";
}
/*
* (non-Javadoc)
* @see java.lang.Object#equals(java.lang.Object)
*/
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (!(obj instanceof EventType thaType)) {
return false;
}
return Objects.equals(sources, thaType.sources) //
&& Objects.equals(type, thaType.type);
}
/*
* (non-Javadoc)
* @see java.lang.Object#hashCode()
*/
@Override
public int hashCode() {
return Objects.hash(sources, type);
}
}

View File

@@ -15,9 +15,6 @@
*/
package org.springframework.modulith.model;
import lombok.AccessLevel;
import lombok.RequiredArgsConstructor;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
@@ -37,7 +34,6 @@ import com.tngtech.archunit.thirdparty.com.google.common.base.Suppliers;
*
* @author Oliver Drotbohm
*/
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
public class FormatableType {
private static final Map<String, FormatableType> CACHE = new ConcurrentHashMap<>();
@@ -45,6 +41,21 @@ public class FormatableType {
private final String type;
private final Supplier<String> abbreviatedName;
/**
* Creates a new {@link FormatableType} for the given source {@link String} and lazily computed abbreviated name.
*
* @param type must not be {@literal null} or empty.
* @param abbreviatedName must not be {@literal null}.
*/
private FormatableType(String type, Supplier<String> abbreviatedName) {
Assert.hasText(type, "Type string must not be null or empty!");
Assert.notNull(abbreviatedName, "Computed abbreviated name must not be null!");
this.type = type;
this.abbreviatedName = abbreviatedName;
}
/**
* Creates a new {@link FormatableType} for the given {@link JavaClass}.
*

View File

@@ -17,20 +17,17 @@ package org.springframework.modulith.model;
import static com.tngtech.archunit.core.domain.JavaClass.Predicates.*;
import lombok.AccessLevel;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import java.lang.annotation.Annotation;
import java.util.Collection;
import java.util.Iterator;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.util.Assert;
import com.tngtech.archunit.base.DescribedIterable;
import com.tngtech.archunit.base.DescribedPredicate;
@@ -42,19 +39,27 @@ import com.tngtech.archunit.thirdparty.com.google.common.base.Suppliers;
/**
* @author Oliver Drotbohm
*/
@EqualsAndHashCode
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
public class JavaPackage implements DescribedIterable<JavaClass> {
private static final String PACKAGE_INFO_NAME = "package-info";
private final @Getter String name;
private final String name;
private final Classes classes;
private final 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 includeSubPackages
*/
private JavaPackage(Classes classes, String 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.name = name;
@@ -67,22 +72,61 @@ public class JavaPackage implements DescribedIterable<JavaClass> {
.collect(Collectors.toSet()));
}
/**
* Creates a new {@link JavaPackage} for the given classes and name.
*
* @param classes must not be {@literal null}.
* @param name must not be {@literal null} or empty.
* @return
*/
public static JavaPackage of(Classes classes, String name) {
return new JavaPackage(classes, name, true);
}
/**
* Returns whether the given type is the {@code package-info.java} one.
*
* @param type must not be {@literal null}.
*/
public static boolean isPackageInfoType(JavaClass type) {
Assert.notNull(type, "Type must not be null!");
return type.getSimpleName().equals(PACKAGE_INFO_NAME);
}
/**
* Returns the name of the package.
*
* @return will never be {@literal null}.
*/
public String getName() {
return name;
}
/**
* Reduces the {@link JavaPackage} to only its base package.
*
* @return will never be {@literal null}.
*/
public JavaPackage toSingle() {
return new JavaPackage(classes, name, false);
}
/**
* Returns the local name of the package, i.e. the last segment of the qualified package name.
*
* @return will never be {@literal null}.
*/
public String getLocalName() {
return name.substring(name.lastIndexOf(".") + 1);
}
/**
* Returns all direct sub-packages of the current one.
*
* @return will never be {@literal null}.
*/
public Collection<JavaPackage> getDirectSubPackages() {
return directSubPackages.get();
}
@@ -91,32 +135,22 @@ public class JavaPackage implements DescribedIterable<JavaClass> {
* Returns all classes residing in the current package and potentially in sub-packages if the current package was
* created to include them.
*
* @return
* @return will never be {@literal null}.
*/
public Classes getClasses() {
return packageClasses;
}
/**
* Extract the direct sub-package name of the given candidate.
* Returns all sub-packages that carry the given annotation type.
*
* @param candidate
* @return
* @param annotation must not be {@literal null}.
* @return will never be {@literal null}.
*/
private String extractDirectSubPackage(String candidate) {
if (candidate.length() <= name.length()) {
return candidate;
}
int subSubPackageIndex = candidate.indexOf('.', name.length() + 1);
int endIndex = subSubPackageIndex == -1 ? candidate.length() : subSubPackageIndex;
return candidate.substring(0, endIndex);
}
public Stream<JavaPackage> getSubPackagesAnnotatedWith(Class<? extends Annotation> annotation) {
Assert.notNull(annotation, "Annotation must not be null!");
return packageClasses.that(JavaClass.Predicates.simpleName(PACKAGE_INFO_NAME) //
.and(CanBeAnnotated.Predicates.annotatedWith(annotation))).stream() //
.map(JavaClass::getPackageName) //
@@ -124,22 +158,59 @@ public class JavaPackage implements DescribedIterable<JavaClass> {
.map(it -> of(classes, it));
}
/**
* Returns all {@link Classes} that match the given {@link DescribedPredicate}.
*
* @param predicate must not be {@literal null}.
* @return
*/
public Classes that(DescribedPredicate<? super JavaClass> predicate) {
Assert.notNull(predicate, "Predicate must not be null!");
return packageClasses.that(predicate);
}
/**
* Return whether the {@link JavaPackage} contains the given type.
*
* @param type must not be {@literal null}.
*/
public boolean contains(JavaClass type) {
Assert.notNull(type, "Type must not be null!");
return packageClasses.contains(type);
}
public boolean contains(String className) {
return packageClasses.contains(className);
/**
* Returns whether the {@link JavaPackage} contains the type with the given name.
*
* @param typeName must not be {@literal null} or empty.
*/
public boolean contains(String typeName) {
Assert.hasText(typeName, "Type name must not be null or empty!");
return packageClasses.contains(typeName);
}
/**
* Returns a {@link Stream} of all {@link JavaClass}es contained in the {@link JavaPackage}.
*
* @return will never be {@literal null}.
*/
public Stream<JavaClass> stream() {
return packageClasses.stream();
}
/**
* Return the annotation of the given type declared on the package.
*
* @param <A> the annotation type.
* @param annotationType the annotation type to be found.
* @return will never be {@literal null}.
*/
public <A extends Annotation> Optional<A> getAnnotation(Class<A> annotationType) {
return packageClasses.that(JavaClass.Predicates.simpleName(PACKAGE_INFO_NAME) //
@@ -180,4 +251,52 @@ public class JavaPackage implements DescribedIterable<JavaClass> {
.append('\n') //
.toString();
}
/*
* (non-Javadoc)
* @see java.lang.Object#equals(java.lang.Object)
*/
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (!(obj instanceof JavaPackage that)) {
return false;
}
return Objects.equals(this.classes, that.classes) //
&& Objects.equals(this.getDirectSubPackages(), that.getDirectSubPackages()) //
&& Objects.equals(this.name, that.name) //
&& Objects.equals(this.packageClasses, that.packageClasses);
}
/*
* (non-Javadoc)
* @see java.lang.Object#hashCode()
*/
@Override
public int hashCode() {
return Objects.hash(classes, directSubPackages, name, packageClasses);
}
/**
* Extract the direct sub-package name of the given candidate.
*
* @param candidate
* @return will never be {@literal null}.
*/
private String extractDirectSubPackage(String candidate) {
if (candidate.length() <= name.length()) {
return candidate;
}
int subSubPackageIndex = candidate.indexOf('.', name.length() + 1);
int endIndex = subSubPackageIndex == -1 ? candidate.length() : subSubPackageIndex;
return candidate.substring(0, endIndex);
}
}

View File

@@ -61,11 +61,20 @@ public interface ModulithMetadata {
}
/**
* Returns the source of the Moduliths setup. Either a type or a package.
* Returns the source of the Spring Modulith setup. Either a type or a package.
*
* @return will never be {@literal null}.
* @deprecated use {@link #getSource()} instead.
*/
@Deprecated(forRemoval = true)
Object getModulithSource();
/**
* Returns the source of the Spring Modulith setup. Either a type or a package.
*
* @return will never be {@literal null}.
*/
Object getModulithSource();
Object getSource();
/**
* Returns the names of the packages that are supposed to be considered modulith base packages, i.e. for which to

View File

@@ -15,10 +15,6 @@
*/
package org.springframework.modulith.model;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
@@ -32,21 +28,40 @@ import com.tngtech.archunit.core.domain.JavaModifier;
import com.tngtech.archunit.core.domain.properties.HasModifiers;
/**
* A named interface into an {@link ApplicationModule}. This can either be a package, explicitly annotated with
* {@link org.springframework.modulith.NamedInterface} or a set of types annotated with the same annotation. Other
* {@link ApplicationModules} can define allowed dependencies to particular named interfaces via the
* {@code $moduleName::$namedInterfaceName} syntax.
*
* @author Oliver Drotbohm
* @see org.springframework.modulith.ApplicationModule#allowedDependencies()
*/
@RequiredArgsConstructor(access = AccessLevel.PROTECTED)
public abstract class NamedInterface implements Iterable<JavaClass> {
private static final String UNNAMED_NAME = "<<UNNAMED>>";
private static final String PACKAGE_INFO_NAME = "package-info";
protected final @Getter String name;
protected final String name;
static NamedInterface unnamed(JavaPackage javaPackage) {
return new PackageBasedNamedInterface(UNNAMED_NAME, javaPackage);
/**
* Creates a new {@link NamedInterface} with the given name.
*
* @param name must not be {@literal null} or empty.
*/
protected NamedInterface(String name) {
Assert.hasText(name, "Name must not be null or empty!");
this.name = name;
}
public static List<PackageBasedNamedInterface> of(JavaPackage javaPackage) {
/**
* Returns all {@link PackageBasedNamedInterface}s for the given {@link JavaPackage}.
*
* @param javaPackage must not be {@literal null}.
* @return will never be {@literal null}.
*/
public static List<NamedInterface> of(JavaPackage javaPackage) {
String[] name = javaPackage.getAnnotation(org.springframework.modulith.NamedInterface.class) //
.map(it -> it.name()) //
@@ -54,33 +69,81 @@ public abstract class NamedInterface implements Iterable<JavaClass> {
String.format("Couldn't find NamedInterface annotation on package %s!", javaPackage)));
return Arrays.stream(name) //
.map(it -> new PackageBasedNamedInterface(it, javaPackage)) //
.<NamedInterface> map(it -> new PackageBasedNamedInterface(it, javaPackage)) //
.toList();
}
/**
* Returns a {@link TypeBasedNamedInterface} with the given name, {@link Classes} and base {@link JavaPackage}.
*
* @param name must not be {@literal null} or empty.
* @param classes must not be {@literal null}.
* @param basePackage must not be {@literal null}.
* @return will never be {@literal null}.
*/
public static TypeBasedNamedInterface of(String name, Classes classes, JavaPackage basePackage) {
return new TypeBasedNamedInterface(name, classes, basePackage);
}
/**
* Creates an unnamed {@link NamedInterface} for the given {@link JavaPackage}.
*
* @param javaPackage must not be {@literal null}.
* @return will never be {@literal null}.
*/
static NamedInterface unnamed(JavaPackage javaPackage) {
return new PackageBasedNamedInterface(UNNAMED_NAME, javaPackage);
}
/**
* Returns the {@link NamedInterface}'s name.
*
* @return will never be {@literal null} or empty.
*/
public String getName() {
return name;
}
/**
* Returns whether this is the unnamed (implicit) {@link NamedInterface}.
*/
public boolean isUnnamed() {
return name.equals(UNNAMED_NAME);
}
/**
* Returns whether the {@link NamedInterface} contains the given {@link JavaClass}.
*
* @param type must not be {@literal null}.
*/
public boolean contains(JavaClass type) {
Assert.notNull(type, "JavaClass must not be null!");
return getClasses().contains(type);
}
/**
* Returns whether the {@link NamedInterface} contains the given type.
*
* @param type must not be {@literal null}.
*/
public boolean contains(Class<?> type) {
Assert.notNull(type, "Type must not be null!");
return !getClasses().that(Predicates.equivalentTo(type)).isEmpty();
}
/**
* Returns whether the given {@link NamedInterface} has the same name as the current one.
*
* @param other
* @return
* @param other must not be {@literal null}.
*/
boolean hasSameNameAs(NamedInterface other) {
Assert.notNull(other, "NamedInterface must not be null!");
return this.name.equals(other.name);
}
@@ -93,13 +156,24 @@ public abstract class NamedInterface implements Iterable<JavaClass> {
return getClasses().iterator();
}
/**
* Returns all {@link Classes} making up this {@link NamedInterface}.
*
* @return will never be {@literal null}.
*/
protected abstract Classes getClasses();
/**
* Merges the current {@link NamedInterface} with the given {@link TypeBasedNamedInterface}.
*
* @param other must not be {@literal null}.
* @return will never be {@literal null}.
*/
public abstract NamedInterface merge(TypeBasedNamedInterface other);
static class PackageBasedNamedInterface extends NamedInterface {
private static class PackageBasedNamedInterface extends NamedInterface {
private final @Getter Classes classes;
private final Classes classes;
private final JavaPackage javaPackage;
public PackageBasedNamedInterface(String name, JavaPackage pkg) {
@@ -123,6 +197,15 @@ public abstract class NamedInterface implements Iterable<JavaClass> {
this.javaPackage = pkg;
}
/*
* (non-Javadoc)
* @see org.springframework.modulith.model.NamedInterface#getClasses()
*/
@Override
public Classes getClasses() {
return classes;
}
/*
* (non-Javadoc)
* @see org.springframework.modulith.model.NamedInterface#merge(org.springframework.modulith.model.NamedInterface.TypeBasedNamedInterface)
@@ -143,18 +226,38 @@ public abstract class NamedInterface implements Iterable<JavaClass> {
}
}
static class TypeBasedNamedInterface extends NamedInterface {
public static class TypeBasedNamedInterface extends NamedInterface {
private final @Getter Classes classes;
private final Classes classes;
private final JavaPackage pkg;
/**
* Creates a new {@link TypeBasedNamedInterface} with the given name, {@link Classes} and {@link JavaPackage}.
*
* @param name must not be {@literal null} or empty.
* @param types must not be {@literal null}.
* @param pkg must not be {@literal null}.
*/
public TypeBasedNamedInterface(String name, Classes types, JavaPackage pkg) {
super(name);
Assert.notNull(types, "Classes must not be null!");
Assert.notNull(pkg, "JavaPackage must not be null!");
this.classes = types;
this.pkg = pkg;
}
/*
* (non-Javadoc)
* @see org.springframework.modulith.model.NamedInterface#getClasses()
*/
@Override
public Classes getClasses() {
return classes;
}
/*
* (non-Javadoc)
* @see org.springframework.modulith.model.NamedInterface#merge(org.springframework.modulith.model.NamedInterface.TypeBasedNamedInterface)

View File

@@ -15,9 +15,6 @@
*/
package org.springframework.modulith.model;
import lombok.AccessLevel;
import lombok.RequiredArgsConstructor;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
@@ -28,112 +25,101 @@ import java.util.stream.Stream;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.modulith.model.NamedInterface.TypeBasedNamedInterface;
import org.springframework.util.Assert;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import com.tngtech.archunit.core.domain.JavaClass;
/**
* A collection of {@link NamedInterface}s.
*
* @author Oliver Drotbohm
*/
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
public class NamedInterfaces implements Iterable<NamedInterface> {
public static final NamedInterfaces NONE = new NamedInterfaces(Collections.emptyList());
private final List<NamedInterface> namedInterfaces;
public static NamedInterfaces discoverNamedInterfaces(JavaPackage basePackage) {
/**
* Creates a new {@link NamedInterfaces} for all {@link NamedInterface}s.
*
* @param namedInterfaces must not be {@literal null}.
*/
private NamedInterfaces(List<NamedInterface> namedInterfaces) {
Assert.notNull(namedInterfaces, "Named interfaces must not be null!");
this.namedInterfaces = namedInterfaces;
}
/**
* Discovers all {@link NamedInterfaces} declared for the given {@link JavaPackage}.
*
* @param basePackage must not be {@literal null}.
* @return will never be {@literal null}.
*/
static NamedInterfaces discoverNamedInterfaces(JavaPackage basePackage) {
return NamedInterfaces.ofAnnotatedPackages(basePackage) //
.and(NamedInterfaces.ofAnnotatedTypes(basePackage)) //
.and(NamedInterface.unnamed(basePackage));
}
public static NamedInterfaces of(List<NamedInterface> interfaces) {
/**
* Creates a new {@link NamedInterfaces} for the given {@link NamedInterface}s.
*
* @param interfaces must not be {@literal null}.
* @return will never be {@literal null}.
*/
static NamedInterfaces of(List<NamedInterface> interfaces) {
return interfaces.isEmpty() ? NONE : new NamedInterfaces(interfaces);
}
/**
* Creates a new {@link NamedInterfaces} for the given base {@link JavaPackage}.
*
* @param basePackage must not be {@literal null}.
* @return will never be {@literal null}.
*/
static NamedInterfaces ofAnnotatedPackages(JavaPackage basePackage) {
Assert.notNull(basePackage, "Base package must not be null!");
return basePackage //
.getSubPackagesAnnotatedWith(org.springframework.modulith.NamedInterface.class) //
.flatMap(it -> NamedInterface.of(it).stream()) //
.collect(Collectors.collectingAndThen(Collectors.toList(), NamedInterfaces::of));
}
private static List<TypeBasedNamedInterface> ofAnnotatedTypes(JavaPackage basePackage) {
MultiValueMap<String, JavaClass> mappings = new LinkedMultiValueMap<>();
basePackage.stream() //
.filter(it -> !JavaPackage.isPackageInfoType(it)) //
.forEach(it -> {
if (!it.isAnnotatedWith(org.springframework.modulith.NamedInterface.class)) {
return;
}
org.springframework.modulith.NamedInterface annotation = AnnotatedElementUtils
.getMergedAnnotation(it.reflect(), org.springframework.modulith.NamedInterface.class);
for (String name : annotation.name()) {
mappings.add(name, it);
}
});
return mappings.entrySet().stream() //
.map(entry -> NamedInterface.of(entry.getKey(), Classes.of(entry.getValue()), basePackage)) //
.toList();
}
private NamedInterfaces and(NamedInterface namedInterface) {
List<NamedInterface> result = new ArrayList<>(namedInterfaces.size() + 1);
result.addAll(namedInterfaces);
result.add(namedInterface);
return new NamedInterfaces(result);
}
/**
* Returns whether at least one explicit {@link NamedInterface} is declared.
*
* @return will never be {@literal null}.
*/
public boolean hasExplicitInterfaces() {
return namedInterfaces.size() > 1 || !namedInterfaces.get(0).isUnnamed();
}
/**
* Create a {@link Stream} of {@link NamedInterface}s.
*
* @return will never be {@literal null}.
*/
public Stream<NamedInterface> stream() {
return namedInterfaces.stream();
}
public NamedInterfaces and(List<TypeBasedNamedInterface> others) {
List<NamedInterface> namedInterfaces = new ArrayList<>();
List<NamedInterface> unmergedInterface = this.namedInterfaces;
for (TypeBasedNamedInterface candidate : others) {
Optional<NamedInterface> existing = namedInterfaces.stream() //
.filter(it -> it.hasSameNameAs(candidate)) //
.findFirst();
// Merge existing with new and add to result
existing.ifPresent(it -> {
namedInterfaces.add(it.merge(candidate));
namedInterfaces.add(it);
unmergedInterface.remove(it);
});
// Simply add candidate
if (!existing.isPresent()) {
namedInterfaces.add(candidate);
}
}
namedInterfaces.addAll(unmergedInterface);
return new NamedInterfaces(namedInterfaces);
}
/**
* Returns the {@link NamedInterface} with the given name if present.
*
* @param name must not be {@literal null} or empty.
* @return will never be {@literal null}.
*/
public Optional<NamedInterface> getByName(String name) {
Assert.hasText(name, "Named interface name must not be null or empty!");
return namedInterfaces.stream().filter(it -> it.getName().equals(name)).findFirst();
}
@@ -158,4 +144,79 @@ public class NamedInterfaces implements Iterable<NamedInterface> {
public Iterator<NamedInterface> iterator() {
return namedInterfaces.iterator();
}
/**
* Creates a new {@link NamedInterfaces} instance with the given {@link TypeBasedNamedInterface}s added.
*
* @param others must not be {@literal null}.
* @return will never be {@literal null}.
*/
NamedInterfaces and(List<TypeBasedNamedInterface> others) {
Assert.notNull(others, "Other TypeBasedNamedInterfaces must not be null!");
var namedInterfaces = new ArrayList<NamedInterface>();
var unmergedInterface = this.namedInterfaces;
if (others.isEmpty()) {
return this;
}
for (TypeBasedNamedInterface candidate : others) {
var existing = namedInterfaces.stream() //
.filter(it -> it.hasSameNameAs(candidate)) //
.findFirst();
// Merge existing with new and add to result
existing.ifPresent(it -> {
namedInterfaces.add(it.merge(candidate));
namedInterfaces.add(it);
unmergedInterface.remove(it);
});
// Simply add candidate
if (!existing.isPresent()) {
namedInterfaces.add(candidate);
}
}
namedInterfaces.addAll(unmergedInterface);
return new NamedInterfaces(namedInterfaces);
}
private NamedInterfaces and(NamedInterface namedInterface) {
var result = new ArrayList<NamedInterface>(namedInterfaces.size() + 1);
result.addAll(namedInterfaces);
result.add(namedInterface);
return new NamedInterfaces(result);
}
private static List<TypeBasedNamedInterface> ofAnnotatedTypes(JavaPackage basePackage) {
var mappings = new LinkedMultiValueMap<String, JavaClass>();
basePackage.stream() //
.filter(it -> !JavaPackage.isPackageInfoType(it)) //
.forEach(it -> {
if (!it.isAnnotatedWith(org.springframework.modulith.NamedInterface.class)) {
return;
}
var annotation = AnnotatedElementUtils.getMergedAnnotation(it.reflect(),
org.springframework.modulith.NamedInterface.class);
for (String name : annotation.name()) {
mappings.add(name, it);
}
});
return mappings.entrySet().stream() //
.map(entry -> NamedInterface.of(entry.getKey(), Classes.of(entry.getValue()), basePackage)) //
.toList();
}
}

View File

@@ -15,12 +15,10 @@
*/
package org.springframework.modulith.model;
import lombok.AccessLevel;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import java.util.List;
import java.util.Objects;
import org.springframework.util.Assert;
import com.tngtech.archunit.core.domain.JavaClass;
@@ -29,17 +27,49 @@ import com.tngtech.archunit.core.domain.JavaClass;
*
* @author Oliver Drotbohm
*/
@EqualsAndHashCode
@RequiredArgsConstructor(staticName = "of", access = AccessLevel.PACKAGE)
public class SpringBean {
private final @Getter JavaClass type;
private final JavaClass type;
private final ApplicationModule module;
/**
* Creates a new {@link SpringBean} for the given {@link JavaClass} and {@link ApplicationModule}.
*
* @param type must not be {@literal null}.
* @param module must not be {@literal null}.
*/
private SpringBean(JavaClass type, ApplicationModule module) {
Assert.notNull(type, "JavaClass must not be null!");
Assert.notNull(module, "ApplicationModule must not be null!");
this.type = type;
this.module = module;
}
/**
* Creates a new {@link SpringBean} for the given {@link JavaClass} and {@link ApplicationModule}.
*
* @param type must not be {@literal null}.
* @param module must not be {@literal null}.
*/
static SpringBean of(JavaClass type, ApplicationModule module) {
return new SpringBean(type, module);
}
/**
* Returns the {@link JavaClass} of the {@link SpringBean}.
*
* @return will never be {@literal null}.
*/
public JavaClass getType() {
return type;
}
/**
* Returns the fully-qualified name of the Spring bean type.
*
* @return
* @return will never be {@literal null} or empty.
*/
public String getFullyQualifiedTypeName() {
return type.getFullName();
@@ -56,9 +86,9 @@ public class SpringBean {
}
/**
* Returns all interfaces implemented by the bean that are part of the same module.
* Returns all interfaces implemented by the bean that are part of the same application module.
*
* @return
* @return will never be {@literal null}.
*/
public List<JavaClass> getInterfacesWithinModule() {
@@ -67,11 +97,40 @@ public class SpringBean {
.toList();
}
public boolean isAnnotatedWith(Class<?> type) {
return Types.isAnnotatedWith(type).test(this.type);
}
/**
* Creates a new {@link ArchitecturallyEvidentType} from the current {@link SpringBean}.
*
* @return
*/
public ArchitecturallyEvidentType toArchitecturallyEvidentType() {
return ArchitecturallyEvidentType.of(type, module.getSpringBeansInternal());
}
/*
* (non-Javadoc)
* @see java.lang.Object#equals(java.lang.Object)
*/
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (!(obj instanceof SpringBean that)) {
return false;
}
return Objects.equals(this.module, that.module) //
&& Objects.equals(this.type, that.type);
}
/*
* (non-Javadoc)
* @see java.lang.Object#hashCode()
*/
@Override
public int hashCode() {
return Objects.hash(type, module);
}
}

View File

@@ -17,8 +17,6 @@ package org.springframework.modulith.model;
import static com.tngtech.archunit.core.domain.JavaClass.Predicates.*;
import lombok.experimental.UtilityClass;
import java.lang.annotation.Annotation;
import org.springframework.lang.Nullable;
@@ -33,12 +31,11 @@ import com.tngtech.archunit.core.domain.properties.CanBeAnnotated.Predicates;
/**
* @author Oliver Drotbohm
*/
@UtilityClass
class Types {
@Nullable
@SuppressWarnings("unchecked")
<T> Class<T> loadIfPresent(String name) {
static <T> Class<T> loadIfPresent(String name) {
ClassLoader loader = Types.class.getClassLoader();
@@ -79,7 +76,6 @@ class Types {
}
}
@UtilityClass
static class JavaXTypes {
private static final String BASE_PACKAGE = "jakarta";
@@ -93,7 +89,6 @@ class Types {
}
}
@UtilityClass
static class SpringTypes {
private static final String BASE_PACKAGE = "org.springframework";
@@ -129,7 +124,6 @@ class Types {
}
}
@UtilityClass
static class SpringDataTypes {
private static final String BASE_PACKAGE = SpringTypes.BASE_PACKAGE + ".data";
@@ -147,11 +141,11 @@ class Types {
}
}
DescribedPredicate<CanBeAnnotated> isAnnotatedWith(Class<?> type) {
static DescribedPredicate<CanBeAnnotated> isAnnotatedWith(Class<?> type) {
return isAnnotatedWith(type.getName());
}
DescribedPredicate<CanBeAnnotated> isAnnotatedWith(String type) {
static DescribedPredicate<CanBeAnnotated> isAnnotatedWith(String type) {
return Predicates.annotatedWith(type) //
.or(Predicates.metaAnnotatedWith(type));
}

View File

@@ -15,9 +15,6 @@
*/
package org.springframework.modulith.model;
import lombok.AccessLevel;
import lombok.RequiredArgsConstructor;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
@@ -32,7 +29,6 @@ import org.springframework.util.Assert;
*
* @author Oliver Drotbohm
*/
@RequiredArgsConstructor(staticName = "of", access = AccessLevel.PRIVATE)
public class Violations extends RuntimeException {
private static final long serialVersionUID = 6863781504675034691L;
@@ -41,13 +37,25 @@ public class Violations extends RuntimeException {
private final List<RuntimeException> exceptions;
/**
* Creates a new {@link Violations} from the given {@link RuntimeException}s.
*
* @param exceptions must not be {@literal null}.
*/
private Violations(List<RuntimeException> exceptions) {
Assert.notNull(exceptions, "Exceptions must not be null!");
this.exceptions = exceptions;
}
/**
* A {@link Collector} to turn a {@link Stream} of {@link RuntimeException}s into a {@link Violations} instance.
*
* @return will never be {@literal null}.
*/
static Collector<RuntimeException, ?, Violations> toViolations() {
return Collectors.collectingAndThen(Collectors.toList(), Violations::of);
return Collectors.collectingAndThen(Collectors.toList(), Violations::new);
}
/*

View File

@@ -19,7 +19,6 @@ import static org.assertj.core.api.Assertions.*;
import org.junit.jupiter.api.Test;
import com.tngtech.archunit.core.domain.JavaClasses;
import com.tngtech.archunit.core.importer.ClassFileImporter;
import com.tngtech.archunit.core.importer.ImportOption;
@@ -47,11 +46,11 @@ class ModuleDetectionStrategyUnitTest {
@Test
void detectsJMoleculesAnnotatedModule() {
JavaClasses classes = new ClassFileImporter() //
var classes = new ClassFileImporter() //
.withImportOption(new ImportOption.OnlyIncludeTests()) //
.importPackages("jmolecules");
JavaPackage javaPackage = JavaPackage.of(Classes.of(classes), "jmolecules");
var javaPackage = JavaPackage.of(Classes.of(classes), "jmolecules");
assertThat(ApplicationModuleDetectionStrategy.explictlyAnnotated().getModuleBasePackages(javaPackage))
.containsExactly(javaPackage);