diff --git a/pom.xml b/pom.xml index 36a7289d..5816b8d9 100644 --- a/pom.xml +++ b/pom.xml @@ -120,24 +120,6 @@ limitations under the License. - - org.projectlombok - lombok-maven-plugin - 1.18.20.0 - - false - ${project.basedir}/src/main/java - - - - generate-sources - - delombok - - - - - org.apache.maven.plugins maven-javadoc-plugin @@ -154,7 +136,6 @@ limitations under the License. none true package - target/generated-sources/delombok @@ -433,7 +414,7 @@ limitations under the License. org.projectlombok lombok - provided + test diff --git a/spring-modulith-actuator/src/main/java/org/springframework/modulith/actuator/ApplicationModulesEndpoint.java b/spring-modulith-actuator/src/main/java/org/springframework/modulith/actuator/ApplicationModulesEndpoint.java index f62e1c42..b8835285 100644 --- a/spring-modulith-actuator/src/main/java/org/springframework/modulith/actuator/ApplicationModulesEndpoint.java +++ b/spring-modulith-actuator/src/main/java/org/springframework/modulith/actuator/ApplicationModulesEndpoint.java @@ -17,8 +17,6 @@ package org.springframework.modulith.actuator; import static java.util.stream.Collectors.*; -import lombok.extern.slf4j.Slf4j; - import java.util.LinkedHashMap; import java.util.Map; import java.util.Map.Entry; @@ -28,6 +26,8 @@ import java.util.function.Supplier; import java.util.stream.Collector; import java.util.stream.Collectors; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.boot.actuate.endpoint.annotation.Endpoint; import org.springframework.boot.actuate.endpoint.annotation.ReadOperation; import org.springframework.modulith.model.ApplicationModule; @@ -41,10 +41,11 @@ import org.springframework.util.Assert; * * @author Oliver Drotbohm */ -@Slf4j @Endpoint(id = "applicationmodules") public class ApplicationModulesEndpoint { + private static final Logger LOGGER = LoggerFactory.getLogger(ApplicationModulesEndpoint.class); + private static final Function, Set> REMOVE_DEFAULT_DEPENDENCY_TYPE_IF_OTHERS_PRESENT = it -> { if (it.stream().anyMatch(type -> type != DependencyType.DEFAULT)) { @@ -69,7 +70,7 @@ public class ApplicationModulesEndpoint { Assert.notNull(runtime, "ModulesRuntime must not be null!"); - LOG.debug("Activating Spring Modulith actuator."); + LOGGER.debug("Activating Spring Modulith actuator."); this.runtime = runtime; } diff --git a/spring-modulith-api/src/main/java/org/springframework/modulith/ApplicationModule.java b/spring-modulith-api/src/main/java/org/springframework/modulith/ApplicationModule.java index 69e13867..1919f90b 100644 --- a/spring-modulith-api/src/main/java/org/springframework/modulith/ApplicationModule.java +++ b/spring-modulith-api/src/main/java/org/springframework/modulith/ApplicationModule.java @@ -32,7 +32,7 @@ public @interface ApplicationModule { /** * The human readable name of the module to be used for display and documentation purposes. * - * @return + * @return will never be {@literal null}. */ String displayName() default ""; @@ -43,7 +43,7 @@ public @interface ApplicationModule { * {@link NamedInterface}s need to be separated by a double colon {@code ::}, e.g. {@code module::API} if * {@code module} is the logical module name and {@code API} is the name of the named interface. * - * @return + * @return will never be {@literal null}. * @see NamedInterface */ String[] allowedDependencies() default {}; diff --git a/spring-modulith-core/src/main/java/org/springframework/modulith/model/AnnotationModulithMetadata.java b/spring-modulith-core/src/main/java/org/springframework/modulith/model/AnnotationModulithMetadata.java index 225dff3f..5174d880 100644 --- a/spring-modulith-core/src/main/java/org/springframework/modulith/model/AnnotationModulithMetadata.java +++ b/spring-modulith-core/src/main/java/org/springframework/modulith/model/AnnotationModulithMetadata.java @@ -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; } diff --git a/spring-modulith-core/src/main/java/org/springframework/modulith/model/ApplicationModule.java b/spring-modulith-core/src/main/java/org/springframework/modulith/model/ApplicationModule.java index 32a92aa4..fc7ee02f 100644 --- a/spring-modulith-core/src/main/java/org/springframework/modulith/model/ApplicationModule.java +++ b/spring-modulith-core/src/main/java/org/springframework/modulith/model/ApplicationModule.java @@ -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 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 findPublishedEvents() { DescribedPredicate 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 dependencies; + private final List dependencies; + + /** + * Creates a new {@link DeclaredDependencies} for the given {@link List} of {@link DeclaredDependency}. + * + * @param dependencies must not be {@literal null}. + */ + public DeclaredDependencies(List 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 INJECTION_TYPES = Arrays.asList(// - AT_AUTOWIRED, AT_RESOURCE, AT_INJECT); + private static final List 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 fromType(ArchitecturallyEvidentType type) { + + var source = type.getType(); + + return Stream.concat(Stream.concat(fromConstructorOf(type), fromMethodsOf(source)), fromFieldsOf(source)); + } + + static Stream 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 fromType(ArchitecturallyEvidentType type) { - - var source = type.getType(); - - return Stream.concat(Stream.concat(fromConstructorOf(type), fromMethodsOf(source)), fromFieldsOf(source)); - } - - static Stream 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 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 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 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); + } } } diff --git a/spring-modulith-core/src/main/java/org/springframework/modulith/model/ApplicationModuleDependencies.java b/spring-modulith-core/src/main/java/org/springframework/modulith/model/ApplicationModuleDependencies.java index 7dab79b0..ffc3a98b 100644 --- a/spring-modulith-core/src/main/java/org/springframework/modulith/model/ApplicationModuleDependencies.java +++ b/spring-modulith-core/src/main/java/org/springframework/modulith/model/ApplicationModuleDependencies.java @@ -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 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 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 dependencies, + ApplicationModules modules) { + + return new ApplicationModuleDependencies(dependencies, modules); + } + /** * Returns whether the dependencies contain the given {@link ApplicationModule}. * diff --git a/spring-modulith-core/src/main/java/org/springframework/modulith/model/ApplicationModules.java b/spring-modulith-core/src/main/java/org/springframework/modulith/model/ApplicationModules.java index 0c2b05ba..af8b09e7 100644 --- a/spring-modulith-core/src/main/java/org/springframework/modulith/model/ApplicationModules.java +++ b/spring-modulith-core/src/main/java/org/springframework/modulith/model/ApplicationModules.java @@ -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 { private static final Map CACHE = new HashMap<>(); @@ -90,7 +83,7 @@ public class ApplicationModules implements Iterable { private final Map modules; private final JavaClasses allClasses; private final List rootPackages; - private final @With(AccessLevel.PRIVATE) @Getter Set sharedModules; + private final Set sharedModules; private final List orderedNames; private boolean verified; @@ -123,6 +116,39 @@ public class ApplicationModules implements Iterable { : 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 modules, JavaClasses classes, + List rootPackages, Set sharedModules, List 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 { */ public static ApplicationModules of(Class modulithType, DescribedPredicate 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 { */ public static ApplicationModules of(String javaPackage, DescribedPredicate 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 { } /** - * 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 basePackages = new HashSet<>(); - basePackages.add(key.getBasePackage()); - basePackages.addAll(metadata.getAdditionalPackages()); - - ApplicationModules modules = new ApplicationModules(metadata, basePackages, key.getIgnored(), - metadata.useFullyQualifiedModuleNames(), IMPORT_OPTION); - - Set 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 getSharedModules() { + return sharedModules; } /** @@ -421,9 +446,13 @@ public class ApplicationModules implements Iterable { return this.stream().map(ApplicationModule::toString).collect(Collectors.joining("\n")); } + private ApplicationModules withSharedModules(Set 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 { */ 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 { 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(); + 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 withoutModules(String... names) { @@ -489,11 +544,22 @@ public class ApplicationModules implements Iterable { ModulithMetadata getMetadata(); } - @Value(staticConstructor = "of") private static final class TypeKey implements CacheKey { - Class type; - DescribedPredicate ignored; + private final Class type; + private final DescribedPredicate 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 ignored) { + + this.type = type; + this.ignored = ignored; + } /* * (non-Javadoc) @@ -512,13 +578,79 @@ public class ApplicationModules implements Iterable { public ModulithMetadata getMetadata() { return ModulithMetadata.of(type); } + + /* + * (non-Javadoc) + * @see org.springframework.modulith.model.ApplicationModules.CacheKey#getIgnored() + */ + @Override + public DescribedPredicate 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 ignored; + private final String basePackage; + private final DescribedPredicate 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 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 getIgnored() { + return ignored; + } /* * (non-Javadoc) @@ -528,6 +660,34 @@ public class ApplicationModules implements Iterable { 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); + } } /** diff --git a/spring-modulith-core/src/main/java/org/springframework/modulith/model/ArchitecturallyEvidentType.java b/spring-modulith-core/src/main/java/org/springframework/modulith/model/ArchitecturallyEvidentType.java index 1115d60f..43e999df 100644 --- a/spring-modulith-core/src/main/java/org/springframework/modulith/model/ArchitecturallyEvidentType.java +++ b/spring-modulith-core/src/main/java/org/springframework/modulith/model/ArchitecturallyEvidentType.java @@ -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 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 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); } diff --git a/spring-modulith-core/src/main/java/org/springframework/modulith/model/Classes.java b/spring-modulith-core/src/main/java/org/springframework/modulith/model/Classes.java index 9689bede..9f3597f8 100644 --- a/spring-modulith-core/src/main/java/org/springframework/modulith/model/Classes.java +++ b/spring-modulith-core/src/main/java/org/springframework/modulith/model/Classes.java @@ -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 { public static Classes NONE = Classes.of(Collections.emptyList()); @@ -65,7 +61,7 @@ class Classes implements DescribedIterable { 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 { 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 { private final JavaClass reference; diff --git a/spring-modulith-core/src/main/java/org/springframework/modulith/model/DefaultModulithMetadata.java b/spring-modulith-core/src/main/java/org/springframework/modulith/model/DefaultModulithMetadata.java index 3419de19..10502671 100644 --- a/spring-modulith-core/src/main/java/org/springframework/modulith/model/DefaultModulithMetadata.java +++ b/spring-modulith-core/src/main/java/org/springframework/modulith/model/DefaultModulithMetadata.java @@ -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 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; } /* diff --git a/spring-modulith-core/src/main/java/org/springframework/modulith/model/EventType.java b/spring-modulith-core/src/main/java/org/springframework/modulith/model/EventType.java index a4e14c8a..314c1f6a 100644 --- a/spring-modulith-core/src/main/java/org/springframework/modulith/model/EventType.java +++ b/spring-modulith-core/src/main/java/org/springframework/modulith/model/EventType.java @@ -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 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 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); + } } diff --git a/spring-modulith-core/src/main/java/org/springframework/modulith/model/FormatableType.java b/spring-modulith-core/src/main/java/org/springframework/modulith/model/FormatableType.java index adb66f7f..d167ef33 100644 --- a/spring-modulith-core/src/main/java/org/springframework/modulith/model/FormatableType.java +++ b/spring-modulith-core/src/main/java/org/springframework/modulith/model/FormatableType.java @@ -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 CACHE = new ConcurrentHashMap<>(); @@ -45,6 +41,21 @@ public class FormatableType { private final String type; private final Supplier 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 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}. * diff --git a/spring-modulith-core/src/main/java/org/springframework/modulith/model/JavaPackage.java b/spring-modulith-core/src/main/java/org/springframework/modulith/model/JavaPackage.java index 0dd9f8c8..69f5fa75 100644 --- a/spring-modulith-core/src/main/java/org/springframework/modulith/model/JavaPackage.java +++ b/spring-modulith-core/src/main/java/org/springframework/modulith/model/JavaPackage.java @@ -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 { 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> 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 { .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 getDirectSubPackages() { return directSubPackages.get(); } @@ -91,32 +135,22 @@ public class JavaPackage implements DescribedIterable { * 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 getSubPackagesAnnotatedWith(Class 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 { .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 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 stream() { return packageClasses.stream(); } + /** + * Return the annotation of the given type declared on the package. + * + * @param the annotation type. + * @param annotationType the annotation type to be found. + * @return will never be {@literal null}. + */ public Optional getAnnotation(Class annotationType) { return packageClasses.that(JavaClass.Predicates.simpleName(PACKAGE_INFO_NAME) // @@ -180,4 +251,52 @@ public class JavaPackage implements DescribedIterable { .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); + } } diff --git a/spring-modulith-core/src/main/java/org/springframework/modulith/model/ModulithMetadata.java b/spring-modulith-core/src/main/java/org/springframework/modulith/model/ModulithMetadata.java index 0a8af325..6c6a5e05 100644 --- a/spring-modulith-core/src/main/java/org/springframework/modulith/model/ModulithMetadata.java +++ b/spring-modulith-core/src/main/java/org/springframework/modulith/model/ModulithMetadata.java @@ -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 diff --git a/spring-modulith-core/src/main/java/org/springframework/modulith/model/NamedInterface.java b/spring-modulith-core/src/main/java/org/springframework/modulith/model/NamedInterface.java index f7f642d3..45677ecf 100644 --- a/spring-modulith-core/src/main/java/org/springframework/modulith/model/NamedInterface.java +++ b/spring-modulith-core/src/main/java/org/springframework/modulith/model/NamedInterface.java @@ -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 { private static final String UNNAMED_NAME = "<>"; 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 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 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 { String.format("Couldn't find NamedInterface annotation on package %s!", javaPackage))); return Arrays.stream(name) // - .map(it -> new PackageBasedNamedInterface(it, javaPackage)) // + . 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 { 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 { 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 { } } - 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) diff --git a/spring-modulith-core/src/main/java/org/springframework/modulith/model/NamedInterfaces.java b/spring-modulith-core/src/main/java/org/springframework/modulith/model/NamedInterfaces.java index 13cd0b46..2aaa0887 100644 --- a/spring-modulith-core/src/main/java/org/springframework/modulith/model/NamedInterfaces.java +++ b/spring-modulith-core/src/main/java/org/springframework/modulith/model/NamedInterfaces.java @@ -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 { public static final NamedInterfaces NONE = new NamedInterfaces(Collections.emptyList()); private final List 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 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 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 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 ofAnnotatedTypes(JavaPackage basePackage) { - - MultiValueMap 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 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 stream() { return namedInterfaces.stream(); } - public NamedInterfaces and(List others) { - - List namedInterfaces = new ArrayList<>(); - List unmergedInterface = this.namedInterfaces; - - for (TypeBasedNamedInterface candidate : others) { - - Optional 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 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 { public Iterator 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 others) { + + Assert.notNull(others, "Other TypeBasedNamedInterfaces must not be null!"); + + var namedInterfaces = new ArrayList(); + 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(namedInterfaces.size() + 1); + result.addAll(namedInterfaces); + result.add(namedInterface); + + return new NamedInterfaces(result); + } + + private static List ofAnnotatedTypes(JavaPackage basePackage) { + + var mappings = new LinkedMultiValueMap(); + + 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(); + } } diff --git a/spring-modulith-core/src/main/java/org/springframework/modulith/model/SpringBean.java b/spring-modulith-core/src/main/java/org/springframework/modulith/model/SpringBean.java index 5ac2237e..fb9cb3c5 100644 --- a/spring-modulith-core/src/main/java/org/springframework/modulith/model/SpringBean.java +++ b/spring-modulith-core/src/main/java/org/springframework/modulith/model/SpringBean.java @@ -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 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); + } } diff --git a/spring-modulith-core/src/main/java/org/springframework/modulith/model/Types.java b/spring-modulith-core/src/main/java/org/springframework/modulith/model/Types.java index 8350be7b..ae88b49e 100644 --- a/spring-modulith-core/src/main/java/org/springframework/modulith/model/Types.java +++ b/spring-modulith-core/src/main/java/org/springframework/modulith/model/Types.java @@ -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") - Class loadIfPresent(String name) { + static Class 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 isAnnotatedWith(Class type) { + static DescribedPredicate isAnnotatedWith(Class type) { return isAnnotatedWith(type.getName()); } - DescribedPredicate isAnnotatedWith(String type) { + static DescribedPredicate isAnnotatedWith(String type) { return Predicates.annotatedWith(type) // .or(Predicates.metaAnnotatedWith(type)); } diff --git a/spring-modulith-core/src/main/java/org/springframework/modulith/model/Violations.java b/spring-modulith-core/src/main/java/org/springframework/modulith/model/Violations.java index 58341b3c..11a7429a 100644 --- a/spring-modulith-core/src/main/java/org/springframework/modulith/model/Violations.java +++ b/spring-modulith-core/src/main/java/org/springframework/modulith/model/Violations.java @@ -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 exceptions; + /** + * Creates a new {@link Violations} from the given {@link RuntimeException}s. + * + * @param exceptions must not be {@literal null}. + */ + private Violations(List 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 toViolations() { - return Collectors.collectingAndThen(Collectors.toList(), Violations::of); + return Collectors.collectingAndThen(Collectors.toList(), Violations::new); } /* diff --git a/spring-modulith-core/src/test/java/org/springframework/modulith/model/ModuleDetectionStrategyUnitTest.java b/spring-modulith-core/src/test/java/org/springframework/modulith/model/ModuleDetectionStrategyUnitTest.java index e0690c2b..cb8e83a0 100644 --- a/spring-modulith-core/src/test/java/org/springframework/modulith/model/ModuleDetectionStrategyUnitTest.java +++ b/spring-modulith-core/src/test/java/org/springframework/modulith/model/ModuleDetectionStrategyUnitTest.java @@ -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); diff --git a/spring-modulith-docs/src/main/java/org/springframework/modulith/docs/Asciidoctor.java b/spring-modulith-docs/src/main/java/org/springframework/modulith/docs/Asciidoctor.java index 8a658410..f1ab6737 100644 --- a/spring-modulith-docs/src/main/java/org/springframework/modulith/docs/Asciidoctor.java +++ b/spring-modulith-docs/src/main/java/org/springframework/modulith/docs/Asciidoctor.java @@ -27,7 +27,6 @@ import java.util.stream.Stream; import org.springframework.lang.Nullable; import org.springframework.modulith.docs.ConfigurationProperties.ModuleProperty; import org.springframework.modulith.docs.Documenter.CanvasOptions; -import org.springframework.modulith.docs.Documenter.CanvasOptions.Groupings; import org.springframework.modulith.model.ApplicationModule; import org.springframework.modulith.model.ApplicationModuleDependency; import org.springframework.modulith.model.ApplicationModules; @@ -42,7 +41,6 @@ import org.springframework.util.ClassUtils; import org.springframework.util.StringUtils; import com.tngtech.archunit.core.domain.JavaClass; -import com.tngtech.archunit.core.domain.JavaMethod; import com.tngtech.archunit.core.domain.JavaModifier; /** @@ -99,10 +97,9 @@ class Asciidoctor { */ public String toInlineCode(String source) { - String[] parts = source.split("#"); - - String type = parts[0]; - Optional methodSignature = parts.length == 2 ? Optional.of(parts[1]) : Optional.empty(); + var parts = source.split("#"); + var type = parts[0]; + var methodSignature = parts.length == 2 ? Optional.of(parts[1]) : Optional. empty(); return modules.getModuleByType(type) .flatMap(it -> it.getType(type)) @@ -116,15 +113,14 @@ class Asciidoctor { public String toInlineCode(SpringBean bean) { - String base = toInlineCode(bean.toArchitecturallyEvidentType()); - - List interfaces = bean.getInterfacesWithinModule(); + var base = toInlineCode(bean.toArchitecturallyEvidentType()); + var interfaces = bean.getInterfacesWithinModule(); if (interfaces.isEmpty()) { return base; } - String interfacesAsString = interfaces.stream() // + var interfacesAsString = interfaces.stream() // .map(this::toInlineCode) // .collect(Collectors.joining(", ")); @@ -133,8 +129,8 @@ class Asciidoctor { public String renderSpringBeans(ApplicationModule module, CanvasOptions options) { - StringBuilder builder = new StringBuilder(); - Groupings groupings = options.groupBeans(module); + var builder = new StringBuilder(); + var groupings = options.groupBeans(module); if (groupings.hasOnlyFallbackGroup()) { return toBulletPoints(groupings.byGrouping(CanvasOptions.FALLBACK_GROUP)); @@ -166,13 +162,13 @@ class Asciidoctor { public String renderEvents(ApplicationModule module) { - List events = module.getPublishedEvents(); + var events = module.getPublishedEvents(); if (events.isEmpty()) { return "none"; } - StringBuilder builder = new StringBuilder(); + var builder = new StringBuilder(); for (EventType eventType : events) { @@ -205,12 +201,12 @@ class Asciidoctor { Stream stream = properties.stream() .map(it -> { - StringBuilder builder = new StringBuilder() - .append(toCode(it.getName())) + var builder = new StringBuilder() + .append(toCode(it.name())) .append(" -- ") - .append(toInlineCode(it.getType())); + .append(toInlineCode(it.type())); - String defaultValue = it.getDefaultValue(); + var defaultValue = it.defaultValue(); if (defaultValue != null && StringUtils.hasText(defaultValue)) { @@ -219,7 +215,7 @@ class Asciidoctor { .append(""); } - String description = it.getDescription(); + var description = it.description(); if (description != null && StringUtils.hasText(description)) { builder = builder.append(". ") @@ -257,8 +253,8 @@ class Asciidoctor { private String toOptionalLink(JavaClass source, Optional methodSignature) { - ApplicationModule module = modules.getModuleByType(source).orElse(null); - String typeAndMethod = toCode( + var module = modules.getModuleByType(source).orElse(null); + var typeAndMethod = toCode( toTypeAndMethod(FormatableType.of(source).getAbbreviatedFullName(module), methodSignature)); if (module == null @@ -267,7 +263,7 @@ class Asciidoctor { return typeAndMethod; } - String classPath = convertClassNameToResourcePath(source.getFullName()) // + var classPath = convertClassNameToResourcePath(source.getFullName()) // .replace('$', '.'); return Optional.ofNullable(javaDocBase == PLACEHOLDER ? null : javaDocBase) // @@ -288,7 +284,7 @@ class Asciidoctor { if (!docSource.isPresent()) { - Stream referenceTypes = type.getReferenceTypes(); + var referenceTypes = type.getReferenceTypes(); return String.format("%s listening to %s", // toInlineCode(type.getType()), // @@ -299,14 +295,14 @@ class Asciidoctor { return header + type.getReferenceMethods().map(it -> { - JavaMethod method = it.getMethod(); + var method = it.getMethod(); Assert.isTrue(method.getRawParameterTypes().size() > 0, () -> String.format("Method %s must have at least one parameter!", method)); - JavaClass parameterType = it.getMethod().getRawParameterTypes().get(0); - String isAsync = it.isAsync() ? "(async) " : ""; + var parameterType = method.getRawParameterTypes().get(0); + var isAsync = it.isAsync() ? "(async) " : ""; - return docSource.flatMap(source -> source.getDocumentation(it.getMethod())) + return docSource.flatMap(source -> source.getDocumentation(method)) .map(doc -> String.format("** %s %s-- %s", toInlineCode(parameterType), isAsync, doc)) .orElseGet(() -> String.format("** %s %s", toInlineCode(parameterType), isAsync)); diff --git a/spring-modulith-docs/src/main/java/org/springframework/modulith/docs/CodeReplacingDocumentationSource.java b/spring-modulith-docs/src/main/java/org/springframework/modulith/docs/CodeReplacingDocumentationSource.java index 7b38ddeb..874c3d71 100644 --- a/spring-modulith-docs/src/main/java/org/springframework/modulith/docs/CodeReplacingDocumentationSource.java +++ b/spring-modulith-docs/src/main/java/org/springframework/modulith/docs/CodeReplacingDocumentationSource.java @@ -15,10 +15,10 @@ */ package org.springframework.modulith.docs; -import lombok.RequiredArgsConstructor; - import java.util.Optional; +import org.springframework.util.Assert; + import com.tngtech.archunit.core.domain.JavaMethod; /** @@ -27,12 +27,27 @@ import com.tngtech.archunit.core.domain.JavaMethod; * * @author Oliver Drotbohm */ -@RequiredArgsConstructor class CodeReplacingDocumentationSource implements DocumentationSource { private final DocumentationSource delegate; private final Asciidoctor codeSource; + /** + * Creates a new {@link CodeReplacingDocumentationSource} for the given delegate {@link DocumentationSource} and + * {@link Asciidoctor} instance. + * + * @param delegate must not be {@literal null}. + * @param asciidoctor must not be {@literal null}. + */ + CodeReplacingDocumentationSource(DocumentationSource delegate, Asciidoctor asciidoctor) { + + Assert.notNull(delegate, "Delegate DocumentationSource must not be null!"); + Assert.notNull(asciidoctor, "Asciidoctor must not be null!"); + + this.delegate = delegate; + this.codeSource = asciidoctor; + } + /* * (non-Javadoc) * @see org.springframework.modulith.docs.DocumentationSource#getDocumentation(com.tngtech.archunit.core.domain.JavaMethod) diff --git a/spring-modulith-docs/src/main/java/org/springframework/modulith/docs/ConfigurationProperties.java b/spring-modulith-docs/src/main/java/org/springframework/modulith/docs/ConfigurationProperties.java index 71bf89e2..1ae9587a 100644 --- a/spring-modulith-docs/src/main/java/org/springframework/modulith/docs/ConfigurationProperties.java +++ b/spring-modulith-docs/src/main/java/org/springframework/modulith/docs/ConfigurationProperties.java @@ -15,15 +15,12 @@ */ package org.springframework.modulith.docs; -import lombok.Value; - import java.io.IOException; import java.io.InputStream; import java.util.Arrays; import java.util.Iterator; import java.util.List; import java.util.Map; -import java.util.stream.Collectors; import java.util.stream.Stream; import org.springframework.core.io.Resource; @@ -34,7 +31,6 @@ import org.springframework.modulith.model.ApplicationModule; import org.springframework.util.Assert; import org.springframework.util.StringUtils; -import com.jayway.jsonpath.DocumentContext; import com.jayway.jsonpath.JsonPath; import com.tngtech.archunit.core.domain.JavaType; @@ -55,10 +51,11 @@ class ConfigurationProperties implements Iterable { */ ConfigurationProperties() { - PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver(); + var resolver = new PathMatchingResourcePatternResolver(); try { - Resource[] resources = resolver.getResources(METADATA_PATH); + + var resources = resolver.getResources(METADATA_PATH); this.properties = Arrays.stream(resources) .flatMap(ConfigurationProperties::parseProperties) @@ -96,9 +93,9 @@ class ConfigurationProperties implements Iterable { private Stream getModuleProperty(ApplicationModule module, ConfigurationProperty property) { - return module.getType(property.getSourceType()) - .map(it -> new ModuleProperty(property.getName(), property.getDescription(), property.getType(), it, - property.getDefaultValue())) + return module.getType(property.sourceType) + .map(it -> new ModuleProperty(property.name(), property.description(), property.type(), it, + property.defaultValue())) .map(Stream::of) .orElseGet(Stream::empty); } @@ -112,7 +109,7 @@ class ConfigurationProperties implements Iterable { try (InputStream stream = source.getInputStream()) { - DocumentContext context = JsonPath.parse(stream); + var context = JsonPath.parse(stream); List read = context.read(PATH, List.class); return read.stream() @@ -124,13 +121,8 @@ class ConfigurationProperties implements Iterable { } } - @Value - static class ConfigurationProperty { - - String name; - @Nullable String description; - String type, sourceType; - @Nullable String defaultValue; + static record ConfigurationProperty(String name, @Nullable String description, String type, String sourceType, + @Nullable String defaultValue) { @SuppressWarnings("null") static Stream of(Map source) { @@ -162,12 +154,6 @@ class ConfigurationProperties implements Iterable { } } - @Value - static class ModuleProperty { - String name; - @Nullable String description; - String type; - JavaType sourceType; - @Nullable String defaultValue; - } + static record ModuleProperty(String name, @Nullable String description, String type, JavaType sourceType, + @Nullable String defaultValue) {} } diff --git a/spring-modulith-docs/src/main/java/org/springframework/modulith/docs/Documenter.java b/spring-modulith-docs/src/main/java/org/springframework/modulith/docs/Documenter.java index 502b02e0..efc7003b 100644 --- a/spring-modulith-docs/src/main/java/org/springframework/modulith/docs/Documenter.java +++ b/spring-modulith-docs/src/main/java/org/springframework/modulith/docs/Documenter.java @@ -17,13 +17,6 @@ package org.springframework.modulith.docs; import static org.springframework.modulith.docs.Asciidoctor.*; -import lombok.AccessLevel; -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.RequiredArgsConstructor; -import lombok.Value; -import lombok.With; - import java.io.File; import java.io.FileWriter; import java.io.IOException; @@ -77,11 +70,9 @@ import com.tngtech.archunit.core.domain.JavaClass; * * @author Oliver Drotbohm */ -@AllArgsConstructor(access = AccessLevel.PRIVATE) public class Documenter { private static final Map DEPENDENCY_DESCRIPTIONS = new LinkedHashMap<>(); - private static final String INVALID_FILE_NAME_PATTERN = "Configured file name pattern does not include a '%s' placeholder for the module name!"; static { @@ -89,7 +80,7 @@ public class Documenter { DEPENDENCY_DESCRIPTIONS.put(DependencyType.DEFAULT, "depends on"); } - private final @Getter ApplicationModules modules; + private final ApplicationModules modules; private final Workspace workspace; private final Container container; private final ConfigurationProperties properties; @@ -98,7 +89,8 @@ public class Documenter { private Map components; /** - * Creates a new {@link Documenter} for the {@link ApplicationModules} created for the given modulith type. + * Creates a new {@link Documenter} for the {@link ApplicationModules} created for the given modulith type in the + * default output folder ({@code spring-modulith-docs}). * * @param modulithType must not be {@literal null}. */ @@ -107,7 +99,8 @@ public class Documenter { } /** - * Creates a new {@link Documenter} for the given {@link ApplicationModules} instance. + * Creates a new {@link Documenter} for the given {@link ApplicationModules} instance in the default output folder + * ({@code spring-modulith-docs}). * * @param modules must not be {@literal null}. */ @@ -115,7 +108,13 @@ public class Documenter { this(modules, getDefaultOutputDirectory()); } - private Documenter(ApplicationModules modules, String outputFolder) { + /** + * Creates a new {@link Documenter} for the given {@link ApplicationModules} and output folder. + * + * @param modules must not be {@literal null}. + * @param outputFolder must not be {@literal null} or empty. + */ + public Documenter(ApplicationModules modules, String outputFolder) { Assert.notNull(modules, "Modules must not be null!"); Assert.hasText(outputFolder, "Output folder must not be null or empty!"); @@ -142,11 +141,12 @@ public class Documenter { * Customize the output folder to write the generated files to. Defaults to {@value #DEFAULT_LOCATION}. * * @param outputFolder must not be {@literal null} or empty. - * @return the current instance, will never be {@literal null}. - * @see #DEFAULT_LOCATION + * @return will never be {@literal null}. + * @deprecated use {@link Documenter(ApplicationModules, String)} directly. */ + @Deprecated(forRemoval = true) public Documenter withOutputFolder(String outputFolder) { - return new Documenter(modules, workspace, container, properties, outputFolder, components); + return new Documenter(modules, outputFolder); } /** @@ -258,12 +258,12 @@ public class Documenter { Assert.notNull(module, "Module must not be null!"); Assert.notNull(options, "Options must not be null!"); - ComponentView view = createComponentView(options, module); - view.setTitle(options.getDefaultDisplayName().apply(module)); + var view = createComponentView(options, module); + view.setTitle(options.defaultDisplayName.apply(module)); addComponentsToView(module, view, options); - String fileNamePattern = options.getTargetFileName().orElse("module-%s.uml"); + var fileNamePattern = options.getTargetFileName().orElse("module-%s.uml"); Assert.isTrue(fileNamePattern.contains("%s"), () -> String.format(INVALID_FILE_NAME_PATTERN, fileNamePattern)); @@ -291,8 +291,8 @@ public class Documenter { modules.forEach(module -> { - String filename = String.format(options.getTargetFileName().orElse("module-%s.adoc"), module.getName()); - Path file = recreateFile(filename); + var filename = options.getTargetFileName(module.getName()); + var file = recreateFile(filename); try (FileWriter writer = new FileWriter(file.toFile())) { @@ -347,16 +347,8 @@ public class Documenter { .toString(); } - private static String addTableRow(String title, String content, CanvasOptions options) { - - return options.hideEmptyLines && (content.isBlank() || content.equalsIgnoreCase("none")) - ? "" - : writeTableRow(title, content); - } - - private static String addTableRow(List types, String header, Function, String> mapper, - CanvasOptions options) { - return options.hideEmptyLines && types.isEmpty() ? "" : writeTableRow(header, mapper.apply(types)); + ApplicationModules getModules() { + return modules; } String toPlantUml() { @@ -387,7 +379,7 @@ public class Documenter { this.components = modules.stream() // .collect(Collectors.toMap(Function.identity(), - it -> container.addComponent(options.getDefaultDisplayName().apply(it), "", "Module"))); + it -> container.addComponent(options.defaultDisplayName.apply(it), "", "Module"))); this.components.forEach((key, value) -> addDependencies(key, value, options)); } @@ -398,7 +390,7 @@ public class Documenter { private void addComponentsToView(ApplicationModule module, ComponentView view, DiagramOptions options) { Supplier> bootstrapDependencies = () -> module.getBootstrapDependencies(modules, - options.getDependencyDepth()); + options.dependencyDepth); Supplier> otherDependencies = () -> options.getDependencyTypes() .flatMap(it -> module.getDependencies(modules, it).stream() .map(ApplicationModuleDependency::getTargetModule)); @@ -413,14 +405,14 @@ public class Documenter { DiagramOptions options, Consumer afterCleanup) { - Styles styles = view.getViewSet().getConfiguration().getStyles(); - Map components = getComponents(options); + var styles = view.getViewSet().getConfiguration().getStyles(); + var components = getComponents(options); modules.get() // .distinct() - .filter(options.getExclusions().negate()) // + .filter(options.exclusions.negate()) // .map(it -> applyBackgroundColor(it, components, options, styles)) // - .filter(options.getComponentFilter()) // + .filter(options.componentFilter) // .forEach(view::add); // Remove filtered dependency types @@ -431,7 +423,7 @@ public class Documenter { afterCleanup.accept(view); // Filter outgoing relationships of target-only modules - modules.get().filter(options.getTargetOnly()) // + modules.get().filter(options.targetOnly) // .forEach(module -> { Component component = components.get(module); @@ -467,31 +459,6 @@ public class Documenter { .findFirst().ifPresent(view::remove); } - private static Component applyBackgroundColor(ApplicationModule module, - Map components, - DiagramOptions options, - Styles styles) { - - Component component = components.get(module); - Function> selector = options.getColorSelector(); - - // Apply custom color if configured - selector.apply(module).ifPresent(color -> { - - String tag = module.getName() + "-" + color; - component.addTags(tag); - - // Add or update background color - styles.getElements().stream() - .filter(it -> it.getTag().equals(tag)) - .findFirst() - .orElseGet(() -> styles.addElementStyle(tag)) - .background(color); - }); - - return component; - } - private Documenter writeViewAsPlantUml(ComponentView view, String filename, DiagramOptions options) { Path file = recreateFile(filename); @@ -566,6 +533,43 @@ public class Documenter { } } + private static Component applyBackgroundColor(ApplicationModule module, + Map components, + DiagramOptions options, + Styles styles) { + + var component = components.get(module); + var selector = options.colorSelector; + + // Apply custom color if configured + selector.apply(module).ifPresent(color -> { + + var tag = module.getName() + "-" + color; + component.addTags(tag); + + // Add or update background color + styles.getElements().stream() + .filter(it -> it.getTag().equals(tag)) + .findFirst() + .orElseGet(() -> styles.addElementStyle(tag)) + .background(color); + }); + + return component; + } + + private static String addTableRow(String title, String content, CanvasOptions options) { + + return options.hideEmptyLines && (content.isBlank() || content.equalsIgnoreCase("none")) + ? "" + : writeTableRow(title, content); + } + + private static String addTableRow(List types, String header, Function, String> mapper, + CanvasOptions options) { + return options.hideEmptyLines && types.isEmpty() ? "" : writeTableRow(header, mapper.apply(types)); + } + /** * Returns the default output directory based on the detected build system. * @@ -575,11 +579,7 @@ public class Documenter { return (new File("pom.xml").exists() ? "target" : "build").concat("/spring-modulith-docs"); } - @Value - private static class Connection { - - Element source, target; - + private static record Connection(Element source, Element target) { public static Connection of(Relationship relationship) { return new Connection(relationship.getSource(), relationship.getDestination()); } @@ -590,58 +590,130 @@ public class Documenter { * * @author Oliver Drotbohm */ - @Getter(AccessLevel.PRIVATE) - @RequiredArgsConstructor(access = AccessLevel.PRIVATE) public static class DiagramOptions { private static final Set ALL_TYPES = Arrays.stream(DependencyType.values()) .collect(Collectors.toSet()); private final Set dependencyTypes; + private final DependencyDepth dependencyDepth; + private final Predicate exclusions; + private final Predicate componentFilter; + private final Predicate targetOnly; + private final @Nullable String targetFileName; + private final Function> colorSelector; + private final Function defaultDisplayName; + private final DiagramStyle style; + private final ElementsWithoutRelationships elementsWithoutRelationships; + + /** + * @param dependencyTypes must not be {@literal null}. + * @param dependencyDepth must not be {@literal null}. + * @param exclusions must not be {@literal null}. + * @param componentFilter must not be {@literal null}. + * @param targetOnly must not be {@literal null}. + * @param targetFileName can be {@literal null}. + * @param colorSelector must not be {@literal null}. + * @param defaultDisplayName must not be {@literal null}. + * @param style must not be {@literal null}. + * @param elementsWithoutRelationships must not be {@literal null}. + */ + DiagramOptions(Set dependencyTypes, DependencyDepth dependencyDepth, + Predicate exclusions, Predicate componentFilter, + Predicate targetOnly, @Nullable String targetFileName, + Function> colorSelector, + Function defaultDisplayName, DiagramStyle style, + ElementsWithoutRelationships elementsWithoutRelationships) { + + Assert.notNull(dependencyTypes, "Dependency types must not be null!"); + Assert.notNull(dependencyDepth, "Dependency depth must not be null!"); + Assert.notNull(exclusions, "Exclusions must not be null!"); + Assert.notNull(componentFilter, "Component filter must not be null!"); + Assert.notNull(targetOnly, "Target only must not be null!"); + Assert.notNull(colorSelector, "Color selector must not be null!"); + Assert.notNull(defaultDisplayName, "Default display name must not be null!"); + Assert.notNull(style, "DiagramStyle must not be null!"); + Assert.notNull(elementsWithoutRelationships, "ElementsWithoutRelationships must not be null!"); + + this.dependencyTypes = dependencyTypes; + this.dependencyDepth = dependencyDepth; + this.exclusions = exclusions; + this.componentFilter = componentFilter; + this.targetOnly = targetOnly; + this.targetFileName = targetFileName; + this.colorSelector = colorSelector; + this.defaultDisplayName = defaultDisplayName; + this.style = style; + this.elementsWithoutRelationships = elementsWithoutRelationships; + } /** * The {@link DependencyDepth} to define which other modules to be included in the diagram to be created. */ - private final @With DependencyDepth dependencyDepth; + public DiagramOptions withDependencyDepth(DependencyDepth dependencyDepth) { + return new DiagramOptions(dependencyTypes, dependencyDepth, exclusions, componentFilter, targetOnly, + targetFileName, colorSelector, defaultDisplayName, style, elementsWithoutRelationships); + } /** * A {@link Predicate} to define the which modules to exclude from the diagram to be created. */ - private final @With Predicate exclusions; + public DiagramOptions withExcusions(Predicate exclusions) { + return new DiagramOptions(dependencyTypes, dependencyDepth, exclusions, componentFilter, targetOnly, + targetFileName, colorSelector, defaultDisplayName, style, elementsWithoutRelationships); + } /** * A {@link Predicate} to define which Structurizr {@link Component}s to be included in the diagram to be created. */ - private final @With Predicate componentFilter; + public DiagramOptions withComponentFilter(Predicate componentFilter) { + return new DiagramOptions(dependencyTypes, dependencyDepth, exclusions, componentFilter, targetOnly, + targetFileName, colorSelector, defaultDisplayName, style, elementsWithoutRelationships); + } /** * A {@link Predicate} to define which of the modules shall only be considered targets, i.e. all efferent * relationships are going to be hidden from the rendered view. Modules that have no incoming relationships will * entirely be removed from the view. */ - private final @With Predicate targetOnly; + public DiagramOptions withTargetOnly(Predicate targetOnly) { + return new DiagramOptions(dependencyTypes, dependencyDepth, exclusions, componentFilter, targetOnly, + targetFileName, colorSelector, defaultDisplayName, style, elementsWithoutRelationships); + } /** * The target file name to be used for the diagram to be created. For individual module diagrams this needs to * include a {@code %s} placeholder for the module names. */ - private final @With @Nullable String targetFileName; + public DiagramOptions withTargetFileName(String targetFileName) { + return new DiagramOptions(dependencyTypes, dependencyDepth, exclusions, componentFilter, targetOnly, + targetFileName, colorSelector, defaultDisplayName, style, elementsWithoutRelationships); + } /** * A callback to return a hex-encoded color per {@link ApplicationModule}. */ - private final @With Function> colorSelector; + public DiagramOptions withColorSelector(Function> colorSelector) { + return new DiagramOptions(dependencyTypes, dependencyDepth, exclusions, componentFilter, targetOnly, + targetFileName, colorSelector, defaultDisplayName, style, elementsWithoutRelationships); + } /** * 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 defaultDisplayName; + public DiagramOptions withDefaultDisplayName(Function defaultDisplayName) { + return new DiagramOptions(dependencyTypes, dependencyDepth, exclusions, componentFilter, targetOnly, + targetFileName, colorSelector, defaultDisplayName, style, elementsWithoutRelationships); + } /** * Which style to render the diagram in. Defaults to {@value DiagramStyle#UML}. */ - private final @With DiagramStyle style; + public DiagramOptions withStyle(DiagramStyle style) { + return new DiagramOptions(dependencyTypes, dependencyDepth, exclusions, componentFilter, targetOnly, + targetFileName, colorSelector, defaultDisplayName, style, elementsWithoutRelationships); + } /** * Configuration setting to define whether modules that do not have a relationship to any other module shall be @@ -651,7 +723,10 @@ public class Documenter { * * @see #withExclusions(Predicate) */ - private final @With ElementsWithoutRelationships elementsWithoutRelationships; + public DiagramOptions withElementsWithoutRelationships(ElementsWithoutRelationships elementsWithoutRelationships) { + return new DiagramOptions(dependencyTypes, dependencyDepth, exclusions, componentFilter, targetOnly, + targetFileName, colorSelector, defaultDisplayName, style, elementsWithoutRelationships); + } /** * Creates a new default {@link DiagramOptions} instance configured to use all dependency types, list immediate @@ -729,17 +804,23 @@ public class Documenter { } } - // Prefix required for javac 🤔 - @lombok.RequiredArgsConstructor(access = AccessLevel.PACKAGE) public static class CanvasOptions { - static final Grouping FALLBACK_GROUP = Grouping.of("Others", null, __ -> true); + static final Grouping FALLBACK_GROUP = new Grouping("Others", __ -> true, null); private final List groupers; - private final @With @Getter @Nullable String apiBase; - private final @With @Nullable String targetFileName; - private final boolean hideInternals; - private final boolean hideEmptyLines; + private final @Nullable String apiBase, targetFileName; + private final boolean hideInternals, hideEmptyLines; + + CanvasOptions(List groupers, @Nullable String apiBase, @Nullable String targetFileName, + boolean hideInternals, boolean hideEmptyLines) { + + this.groupers = groupers; + this.apiBase = apiBase; + this.targetFileName = targetFileName; + this.hideInternals = hideInternals; + this.hideEmptyLines = hideEmptyLines; + } public static CanvasOptions defaults() { @@ -758,14 +839,38 @@ public class Documenter { public CanvasOptions groupingBy(Grouping... groupings) { - List result = new ArrayList<>(groupers); - result.addAll(Arrays.asList(groupings)); + var result = new ArrayList<>(groupers); + result.addAll(List.of(groupings)); return new CanvasOptions(result, apiBase, targetFileName, hideInternals, hideEmptyLines); } + /** + * Registers a component grouping with the given name and selecting filter. + * + * @param name must not be {@literal null} or empty. + * @param filter must not be {@literal null}. + * @return will never be {@literal null}. + */ public CanvasOptions groupingBy(String name, Predicate filter) { - return groupingBy(Grouping.of(name, null, filter)); + return groupingBy(Grouping.of(name, filter, null)); + } + + /** + * Registers a component grouping with the given name, selecting filter and description. + * + * @param name must not be {@literal null} or empty. + * @param filter must not be {@literal null}. + * @param description must not be {@literal null} or empty. + * @return will never be {@literal null}. + */ + public CanvasOptions groupingBy(String name, Predicate filter, String description) { + + Assert.hasText(name, "Name must not be null!"); + Assert.notNull(filter, "Filter must not be null!"); + Assert.hasText(description, "Description must not be null!"); + + return groupingBy(Grouping.of(name, filter, description)); } /** @@ -777,21 +882,55 @@ public class Documenter { return new CanvasOptions(groupers, apiBase, targetFileName, false, hideEmptyLines); } + /** + * Enables table rows not containing any values to be retained in the output. By default, no table rows for e.g. + * aggregates will be rendered if none are found in the {@link ApplicationModule}. + * + * @return will never be {@literal null}. + */ public CanvasOptions revealEmptyLines() { return new CanvasOptions(groupers, apiBase, targetFileName, hideInternals, false); } + /** + * Configures a URI string to act as the base of the Javadoc accessible for the types contained in the canvas. If + * set, the output will add links to the Javadoc for those types. + * + * @param apiBase must not be {@literal null} or empty. + * @return will never be {@literal null}. + */ + public CanvasOptions withApiBase(String apiBase) { + + Assert.hasText(apiBase, "API base must not be null or empty!"); + + return new CanvasOptions(groupers, apiBase, targetFileName, hideInternals, hideEmptyLines); + } + + /** + * Configures the target file name for the canvas to be written. Defaults to {@code module-$moduleName.adoc}. + * + * @param targetFileName must not be {@literal null} or empty. + * @return will never be {@literal null}. + */ + public CanvasOptions withTargetFileName(String targetFileName) { + return new CanvasOptions(groupers, apiBase, targetFileName, hideInternals, hideEmptyLines); + } + + String getApiBase() { + return apiBase; + } + Groupings groupBeans(ApplicationModule module) { - List sources = new ArrayList<>(groupers); + var sources = new ArrayList(groupers); sources.add(FALLBACK_GROUP); - MultiValueMap result = new LinkedMultiValueMap<>(); - List alreadyMapped = new ArrayList<>(); + var result = new LinkedMultiValueMap(); + var alreadyMapped = new ArrayList(); sources.forEach(it -> { - List matchingBeans = getMatchingBeans(module, it, alreadyMapped); + var matchingBeans = getMatchingBeans(module, it, alreadyMapped); result.addAll(it, matchingBeans); alreadyMapped.addAll(matchingBeans); @@ -804,15 +943,15 @@ public class Documenter { } }); - return Groupings.of(result); + return new Groupings(result); } Predicate hideInternalFilter(ApplicationModule module) { return hideInternals ? module::isExposed : __ -> true; } - private Optional getTargetFileName() { - return Optional.ofNullable(targetFileName); + private String getTargetFileName(String moduleName) { + return (targetFileName == null ? "module-%s.adoc" : targetFileName).formatted(moduleName); } private List getMatchingBeans(ApplicationModule module, Grouping filter, @@ -825,45 +964,149 @@ public class Documenter { .toList(); } - @Value(staticConstructor = "of") - @Getter(AccessLevel.PACKAGE) public static class Grouping { - String name; - @Nullable String description; - Predicate predicate; + private final String name; + private final Predicate predicate; + private final @Nullable String description; + /** + * Creates a new {@link Grouping} for the given {@link Predicate} and description. + * + * @param name must not be {@literal null} or empty. + * @param predicate must not be {@literal null}. + * @param description can be {@literal null}. + */ + private Grouping(String name, Predicate predicate, @Nullable String description) { + + Assert.hasText(name, "Name must not be null or empty!"); + Assert.notNull(predicate, "Predicate must not be null!"); + Assert.isTrue(description == null || !description.isBlank(), "Description must not be empty or null!"); + + this.name = name; + this.predicate = predicate; + this.description = description; + } + + /** + * Creates a {@link Grouping} with the given name. + * + * @param name must not be {@literal null} or empty. + * @return will never be {@literal null}. + * @deprecated no replacement as a name-only {@link Grouping} doesn't make any sense in the first place. + */ + @Deprecated public static Grouping of(String name) { - return new Grouping(name, null, __ -> false); + return new Grouping(name, __ -> false, null); } + /** + * Creates a {@link Grouping} with the given name and selecting {@link Predicate}. + * + * @param name must not be {@literal null} or empty. + * @param predicate must not be {@literal null}. + * @return will never be {@literal null}. + */ public static Grouping of(String name, Predicate predicate) { - return new Grouping(name, null, predicate); + return new Grouping(name, predicate, null); } - public boolean matches(SpringBean candidate) { - return predicate.test(candidate); + /** + * Creates a {@link Grouping} with the given name, selecting {@link Predicate} and description. + * + * @param name must not be {@literal null} or empty. + * @param predicate must not be {@literal null}. + * @param description must not be {@literal null} or empty. + * @return will never be {@literal null}. + */ + public static Grouping of(String name, Predicate predicate, String description) { + return new Grouping(name, predicate, description); } + /** + * Helper method to create a {@link Predicate} for {@link SpringBean}s matching the given name pattern. + * + * @param pattern must not be {@literal null} or empty. + * @return will never be {@literal null}. + */ public static Predicate nameMatching(String pattern) { + + Assert.hasText(pattern, "Pattern must not be null or empty!"); + return bean -> bean.getFullyQualifiedTypeName().matches(pattern); } + /** + * Helper method to create a {@link Predicate} for {@link SpringBean}s implementing the given interface. + * + * @param type must not be {@literal null}. + * @return will never be {@literal null}. + */ public static Predicate implementing(Class type) { + + Assert.notNull(type, "Type must not be null!"); + return bean -> bean.getType().isAssignableTo(type); } + /** + * Helper method to create a {@link Predicate} for {@link SpringBean}s that are a subtype of the given one. In + * other words, implement or extend it but are not the type itself. + * + * @param type must not be {@literal null}. + * @return will never be {@literal null}. + */ public static Predicate subtypeOf(Class type) { + + Assert.notNull(type, "Type must not be null!"); + return implementing(type) // .and(bean -> !bean.getType().isEquivalentTo(type)); } + + /** + * Returns the name of the {@link Grouping}. + * + * @return will never be {@literal null} or empty. + */ + public String getName() { + return name; + } + + /** + * Returns the description of the {@link Grouping}. + * + * @return can be {@literal null}. + */ + @Nullable + public String getDescription() { + return description; + } + + /** + * Returns whether the given {@link SpringBean} matches the {@link Grouping}. + * + * @param candidate must not be {@literal null}. + */ + public boolean matches(SpringBean candidate) { + + Assert.notNull(candidate, "Candidate Spring bean must not be null!"); + + return predicate.test(candidate); + } } - @RequiredArgsConstructor(access = AccessLevel.PACKAGE, staticName = "of") static class Groupings { private final MultiValueMap groupings; + Groupings(MultiValueMap groupings) { + + Assert.notNull(groupings, "Groupings must not be null!"); + + this.groupings = groupings; + } + Set keySet() { return groupings.keySet(); } @@ -873,7 +1116,7 @@ public class Documenter { } List byGroupName(String name) { - return byFilter(it -> it.getName().equals(name)); + return byFilter(it -> it.name.equals(name)); } void forEach(BiConsumer> consumer) { diff --git a/spring-modulith-events/spring-modulith-events-core/src/main/java/org/springframework/modulith/events/CompletableEventPublication.java b/spring-modulith-events/spring-modulith-events-core/src/main/java/org/springframework/modulith/events/CompletableEventPublication.java index 41169f22..3ee69225 100644 --- a/spring-modulith-events/spring-modulith-events-core/src/main/java/org/springframework/modulith/events/CompletableEventPublication.java +++ b/spring-modulith-events/spring-modulith-events-core/src/main/java/org/springframework/modulith/events/CompletableEventPublication.java @@ -56,6 +56,6 @@ public interface CompletableEventPublication extends EventPublication { * @return */ static CompletableEventPublication of(Object event, PublicationTargetIdentifier id) { - return DefaultEventPublication.of(event, id); + return new DefaultEventPublication(event, id); } } diff --git a/spring-modulith-events/spring-modulith-events-core/src/main/java/org/springframework/modulith/events/DefaultEventPublication.java b/spring-modulith-events/spring-modulith-events-core/src/main/java/org/springframework/modulith/events/DefaultEventPublication.java index fa8896a4..fbd706b9 100644 --- a/spring-modulith-events/spring-modulith-events-core/src/main/java/org/springframework/modulith/events/DefaultEventPublication.java +++ b/spring-modulith-events/spring-modulith-events-core/src/main/java/org/springframework/modulith/events/DefaultEventPublication.java @@ -15,36 +15,125 @@ */ package org.springframework.modulith.events; -import lombok.EqualsAndHashCode; -import lombok.Getter; -import lombok.NonNull; -import lombok.RequiredArgsConstructor; -import lombok.ToString; - import java.time.Instant; +import java.util.Objects; import java.util.Optional; +import org.springframework.util.Assert; + /** * Default {@link CompletableEventPublication} implementation. * * @author Oliver Drotbohm */ -@Getter -@RequiredArgsConstructor(staticName = "of") -@EqualsAndHashCode -@ToString class DefaultEventPublication implements CompletableEventPublication { - private final @NonNull Object event; - private final @NonNull PublicationTargetIdentifier targetIdentifier; - private final Instant publicationDate = Instant.now(); + private final Object event; + private final PublicationTargetIdentifier targetIdentifier; + private final Instant publicationDate; - private Optional completionDate = Optional.empty(); + private Optional completionDate; + /** + * Creates a new {@link DefaultEventPublication} for the given event and {@link PublicationTargetIdentifier}. + * + * @param event must not be {@literal null}. + * @param targetIdentifier must not be {@literal null}. + */ + DefaultEventPublication(Object event, PublicationTargetIdentifier targetIdentifier) { + + Assert.notNull(event, "Event must not be null!"); + Assert.notNull(targetIdentifier, "PublicationTargetIdentifier must not be null!"); + + this.event = event; + this.targetIdentifier = targetIdentifier; + this.publicationDate = Instant.now(); + this.completionDate = Optional.empty(); + } + + /* + * (non-Javadoc) + * @see org.springframework.modulith.events.EventPublication#getEvent() + */ + @Override + public Object getEvent() { + return event; + } + + /* + * (non-Javadoc) + * @see org.springframework.modulith.events.EventPublication#getTargetIdentifier() + */ + @Override + public PublicationTargetIdentifier getTargetIdentifier() { + return targetIdentifier; + } + + /* + * (non-Javadoc) + * @see org.springframework.modulith.events.EventPublication#getPublicationDate() + */ + public Instant getPublicationDate() { + return publicationDate; + } + + /* + * (non-Javadoc) + * @see org.springframework.modulith.events.CompletableEventPublication#getCompletionDate() + */ + public Optional getCompletionDate() { + return completionDate; + } + + /* + * (non-Javadoc) + * @see org.springframework.modulith.events.CompletableEventPublication#markCompleted() + */ @Override public CompletableEventPublication markCompleted() { this.completionDate = Optional.of(Instant.now()); return this; } + + /* + * (non-Javadoc) + * @see java.lang.Object#toString() + */ + @Override + public String toString() { + + return "DefaultEventPublication [event=" + event + ", targetIdentifier=" + targetIdentifier + ", publicationDate=" + + publicationDate + ", completionDate=" + completionDate + "]"; + } + + /* + * (non-Javadoc) + * @see java.lang.Object#equals(java.lang.Object) + */ + @Override + public boolean equals(Object obj) { + + if (this == obj) { + return true; + } + + if (!(obj instanceof DefaultEventPublication that)) { + return false; + } + + return Objects.equals(this.completionDate, that.completionDate) // + && Objects.equals(this.event, that.event) // + && Objects.equals(this.publicationDate, that.publicationDate) // + && Objects.equals(this.targetIdentifier, that.targetIdentifier); + } + + /* + * (non-Javadoc) + * @see java.lang.Object#hashCode() + */ + @Override + public int hashCode() { + return Objects.hash(completionDate, event, publicationDate, targetIdentifier); + } } diff --git a/spring-modulith-events/spring-modulith-events-core/src/main/java/org/springframework/modulith/events/DefaultEventPublicationRegistry.java b/spring-modulith-events/spring-modulith-events-core/src/main/java/org/springframework/modulith/events/DefaultEventPublicationRegistry.java index 90d9c41b..b03831ad 100644 --- a/spring-modulith-events/spring-modulith-events-core/src/main/java/org/springframework/modulith/events/DefaultEventPublicationRegistry.java +++ b/spring-modulith-events/spring-modulith-events-core/src/main/java/org/springframework/modulith/events/DefaultEventPublicationRegistry.java @@ -15,13 +15,11 @@ */ package org.springframework.modulith.events; -import lombok.NonNull; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; - import java.util.List; import java.util.stream.Stream; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.beans.factory.DisposableBean; import org.springframework.context.ApplicationListener; import org.springframework.transaction.annotation.Propagation; @@ -36,12 +34,28 @@ import org.springframework.util.Assert; * @author Björn Kieling * @author Dmitry Belyaev */ -@Slf4j -@RequiredArgsConstructor public class DefaultEventPublicationRegistry implements DisposableBean, EventPublicationRegistry { - private final @NonNull EventPublicationRepository events; + private static final Logger LOGGER = LoggerFactory.getLogger(DefaultEventPublicationRegistry.class); + private final EventPublicationRepository events; + + /** + * Creates a new {@link DefaultEventPublicationRegistry} for the given {@link EventPublicationRepository}. + * + * @param events must not be {@literal null}. + */ + public DefaultEventPublicationRegistry(EventPublicationRepository events) { + + Assert.notNull(events, "EventPublicationRepository must not be null!"); + + this.events = events; + } + + /* + * (non-Javadoc) + * @see org.springframework.modulith.events.EventPublicationRegistry#store(java.lang.Object, java.util.stream.Stream) + */ @Override public void store(Object event, Stream listeners) { @@ -49,11 +63,19 @@ public class DefaultEventPublicationRegistry implements DisposableBean, EventPub .forEach(events::create); } + /* + * (non-Javadoc) + * @see org.springframework.modulith.events.EventPublicationRegistry#findIncompletePublications() + */ @Override public Iterable findIncompletePublications() { return events.findIncompletePublications(); } + /* + * (non-Javadoc) + * @see org.springframework.modulith.events.EventPublicationRegistry#markCompleted(java.lang.Object, org.springframework.modulith.events.PublicationTargetIdentifier) + */ @Override @Transactional(propagation = Propagation.REQUIRES_NEW) public void markCompleted(Object event, PublicationTargetIdentifier targetIdentifier) { @@ -67,6 +89,10 @@ public class DefaultEventPublicationRegistry implements DisposableBean, EventPub .ifPresent(it -> events.update(it.markCompleted())); } + /* + * (non-Javadoc) + * @see org.springframework.beans.factory.DisposableBean#destroy() + */ @Override public void destroy() { @@ -74,18 +100,18 @@ public class DefaultEventPublicationRegistry implements DisposableBean, EventPub if (publications.isEmpty()) { - LOG.info("No publications outstanding!"); + LOGGER.info("No publications outstanding!"); return; } - LOG.info("Shutting down with the following publications left unfinished:"); + LOGGER.info("Shutting down with the following publications left unfinished:"); for (int i = 0; i < publications.size(); i++) { String prefix = i + 1 == publications.size() ? "└─" : "├─"; EventPublication it = publications.get(i); - LOG.info("{} {} - {}", prefix, it.getEvent().getClass().getName(), it.getTargetIdentifier().getValue()); + LOGGER.info("{} {} - {}", prefix, it.getEvent().getClass().getName(), it.getTargetIdentifier().getValue()); } } @@ -93,7 +119,7 @@ public class DefaultEventPublicationRegistry implements DisposableBean, EventPub EventPublication result = CompletableEventPublication.of(event, targetIdentifier); - LOG.debug("Registering publication of {} for {}.", // + LOGGER.debug("Registering publication of {} for {}.", // result.getEvent().getClass().getName(), result.getTargetIdentifier().getValue()); return result; @@ -101,9 +127,10 @@ public class DefaultEventPublicationRegistry implements DisposableBean, EventPub private static EventPublication logCompleted(EventPublication publication) { - LOG.debug("Marking publication of event {} to listener {} completed.", // + LOGGER.debug("Marking publication of event {} to listener {} completed.", // publication.getEvent().getClass().getName(), publication.getTargetIdentifier().getValue()); return publication; } + } diff --git a/spring-modulith-events/spring-modulith-events-core/src/main/java/org/springframework/modulith/events/EventPublication.java b/spring-modulith-events/spring-modulith-events-core/src/main/java/org/springframework/modulith/events/EventPublication.java index 10e488eb..56349911 100644 --- a/spring-modulith-events/spring-modulith-events-core/src/main/java/org/springframework/modulith/events/EventPublication.java +++ b/spring-modulith-events/spring-modulith-events-core/src/main/java/org/springframework/modulith/events/EventPublication.java @@ -79,6 +79,10 @@ public interface EventPublication extends Comparable { return this.getTargetIdentifier().equals(identifier); } + /* + * (non-Javadoc) + * @see java.lang.Comparable#compareTo(java.lang.Object) + */ @Override public default int compareTo(EventPublication that) { return this.getPublicationDate().compareTo(that.getPublicationDate()); diff --git a/spring-modulith-events/spring-modulith-events-core/src/main/java/org/springframework/modulith/events/PublicationTargetIdentifier.java b/spring-modulith-events/spring-modulith-events-core/src/main/java/org/springframework/modulith/events/PublicationTargetIdentifier.java index d53ac195..75dbc3d2 100644 --- a/spring-modulith-events/spring-modulith-events-core/src/main/java/org/springframework/modulith/events/PublicationTargetIdentifier.java +++ b/spring-modulith-events/spring-modulith-events-core/src/main/java/org/springframework/modulith/events/PublicationTargetIdentifier.java @@ -15,19 +15,49 @@ */ package org.springframework.modulith.events; -import lombok.RequiredArgsConstructor; -import lombok.Value; +import java.util.Objects; + +import org.springframework.util.Assert; /** * Identifier for a publication target. * * @author Oliver Drotbohm */ -@Value -@RequiredArgsConstructor(staticName = "of") public class PublicationTargetIdentifier { - String value; + private final String value; + + /** + * Creates a new {@link PublicationTargetIdentifier} for the given value. + * + * @param value must not be {@literal null}. + */ + private PublicationTargetIdentifier(String value) { + + Assert.hasText(value, "Value must not be null or empty!"); + + this.value = value; + } + + /** + * Returns the {@link PublicationTargetIdentifier} for the given value. + * + * @param value must not be {@literal null} or empty. + * @return will never be {@literal null}. + */ + public static PublicationTargetIdentifier of(String value) { + return new PublicationTargetIdentifier(value); + } + + /** + * Returns the raw String value of the identifier. + * + * @return the value + */ + public String getValue() { + return value; + } /* * (non-Javadoc) @@ -37,4 +67,32 @@ public class PublicationTargetIdentifier { public String toString() { return value; } + + /* + * (non-Javadoc) + * @see java.lang.Object#hashCode() + */ + @Override + public int hashCode() { + return Objects.hash(value); + } + + /* + * (non-Javadoc) + * @see java.lang.Object#equals(java.lang.Object) + */ + @Override + public boolean equals(Object obj) { + + if (this == obj) { + return true; + } + + if (!(obj instanceof PublicationTargetIdentifier that)) { + return false; + } + + return Objects.equals(value, that.value); + } + } diff --git a/spring-modulith-events/spring-modulith-events-core/src/main/java/org/springframework/modulith/events/config/EnablePersistentDomainEvents.java b/spring-modulith-events/spring-modulith-events-core/src/main/java/org/springframework/modulith/events/config/EnablePersistentDomainEvents.java index 476c9dd0..7cea23e4 100644 --- a/spring-modulith-events/spring-modulith-events-core/src/main/java/org/springframework/modulith/events/config/EnablePersistentDomainEvents.java +++ b/spring-modulith-events/spring-modulith-events-core/src/main/java/org/springframework/modulith/events/config/EnablePersistentDomainEvents.java @@ -17,8 +17,6 @@ package org.springframework.modulith.events.config; import static org.springframework.core.io.support.SpringFactoriesLoader.*; -import lombok.RequiredArgsConstructor; - import java.lang.annotation.Documented; import java.lang.annotation.ElementType; import java.lang.annotation.Inherited; @@ -45,12 +43,15 @@ import org.springframework.modulith.events.config.EnablePersistentDomainEvents.P @Import(PersistentDomainEventsImportSelector.class) public @interface EnablePersistentDomainEvents { - @RequiredArgsConstructor static class PersistentDomainEventsImportSelector implements ImportSelector, ResourceLoaderAware { private ResourceLoader resourceLoader; - /* + PersistentDomainEventsImportSelector(ResourceLoader resourceLoader) { + this.resourceLoader = resourceLoader; + } + + /* * (non-Javadoc) * @see org.springframework.context.ResourceLoaderAware#setResourceLoader(org.springframework.core.io.ResourceLoader) */ @@ -59,7 +60,7 @@ public @interface EnablePersistentDomainEvents { this.resourceLoader = resourceLoader; } - /* + /* * (non-Javadoc) * @see org.springframework.context.annotation.ImportSelector#selectImports(org.springframework.core.type.AnnotationMetadata) */ diff --git a/spring-modulith-events/spring-modulith-events-core/src/main/java/org/springframework/modulith/events/support/CompletionRegisteringBeanPostProcessor.java b/spring-modulith-events/spring-modulith-events-core/src/main/java/org/springframework/modulith/events/support/CompletionRegisteringBeanPostProcessor.java index 07b31203..7aa14f91 100644 --- a/spring-modulith-events/spring-modulith-events-core/src/main/java/org/springframework/modulith/events/support/CompletionRegisteringBeanPostProcessor.java +++ b/spring-modulith-events/spring-modulith-events-core/src/main/java/org/springframework/modulith/events/support/CompletionRegisteringBeanPostProcessor.java @@ -15,19 +15,14 @@ */ package org.springframework.modulith.events.support; -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.NonNull; -import lombok.RequiredArgsConstructor; -import lombok.Value; -import lombok.extern.slf4j.Slf4j; - import java.lang.reflect.Method; import java.util.function.Supplier; import org.aopalliance.aop.Advice; import org.aopalliance.intercept.MethodInterceptor; import org.aopalliance.intercept.MethodInvocation; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.aop.framework.Advised; import org.springframework.aop.framework.AopProxyUtils; import org.springframework.aop.framework.ProxyFactory; @@ -35,6 +30,7 @@ import org.springframework.beans.BeansException; import org.springframework.beans.factory.config.BeanPostProcessor; import org.springframework.core.Ordered; import org.springframework.core.annotation.AnnotatedElementUtils; +import org.springframework.lang.NonNull; import org.springframework.modulith.events.EventPublicationRegistry; import org.springframework.modulith.events.PublicationTargetIdentifier; import org.springframework.transaction.event.TransactionPhase; @@ -47,17 +43,28 @@ import org.springframework.util.ReflectionUtils.MethodCallback; /** * {@link BeanPostProcessor} that will add a - * {@link CompletionRegisteringBeanPostProcessor.ProxyCreatingMethodCallback.CompletionRegisteringMethodInterceptor} - * to the bean in case it carries a {@link TransactionalEventListener} annotation so that the successful invocation of + * {@link CompletionRegisteringBeanPostProcessor.ProxyCreatingMethodCallback.CompletionRegisteringMethodInterceptor} to + * the bean in case it carries a {@link TransactionalEventListener} annotation so that the successful invocation of * those methods mark the event publication to those listeners as completed. * * @author Oliver Drotbohm */ -@RequiredArgsConstructor public class CompletionRegisteringBeanPostProcessor implements BeanPostProcessor { private final Supplier registry; + /** + * Creates a new {@link CompletionRegisteringBeanPostProcessor} for the given {@link EventPublicationRegistry}. + * + * @param registry must not be {@literal null}. + */ + public CompletionRegisteringBeanPostProcessor(Supplier registry) { + + Assert.notNull(registry, "EventPublicationRegistry must not be null!"); + + this.registry = registry; + } + /* * (non-Javadoc) * @see org.springframework.beans.factory.config.BeanPostProcessor#postProcessAfterInitialization(java.lang.Object, java.lang.String) @@ -65,11 +72,11 @@ public class CompletionRegisteringBeanPostProcessor implements BeanPostProcessor @Override public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { - ProxyCreatingMethodCallback callback = new ProxyCreatingMethodCallback(registry, beanName, bean, false); + ProxyCreatingMethodCallback callback = new ProxyCreatingMethodCallback(registry, beanName, bean); ReflectionUtils.doWithMethods(AopProxyUtils.ultimateTargetClass(bean), callback); - return callback.methodFound ? callback.getBean() : bean; + return callback.methodFound ? callback.bean : bean; } @@ -79,14 +86,33 @@ public class CompletionRegisteringBeanPostProcessor implements BeanPostProcessor * * @author Oliver Drotbohm */ - @AllArgsConstructor private static class ProxyCreatingMethodCallback implements MethodCallback { - private @NonNull final Supplier registry; - private @NonNull final String beanName; - private @NonNull @Getter Object bean; + private final Supplier registry; + private final String beanName; + private Object bean; private boolean methodFound; + /** + * Creates a new {@link ProxyCreatingMethodCallback} for the given {@link EventPublicationRegistry}, bean name, bean + * and whether a completing method has been found. + * + * @param registry must not be {@literal null}. + * @param beanName must not be {@literal null} or empty. + * @param bean must not be {@literal null}. + */ + ProxyCreatingMethodCallback(Supplier registry, String beanName, Object bean) { + + Assert.notNull(registry, "EventPublicationRegistry must not be null!"); + Assert.hasText(beanName, "Bean name must not be null or empty!"); + Assert.notNull(bean, "Bean must not be null!"); + + this.registry = registry; + this.beanName = beanName; + this.bean = bean; + this.methodFound = false; + } + /* * (non-Javadoc) * @see org.springframework.util.ReflectionUtils.MethodCallback#doWith(java.lang.reflect.Method) @@ -127,10 +153,9 @@ public class CompletionRegisteringBeanPostProcessor implements BeanPostProcessor * * @author Oliver Drotbohm */ - @Slf4j - @RequiredArgsConstructor private static class CompletionRegisteringMethodInterceptor implements MethodInterceptor, Ordered { + private static final Logger LOG = LoggerFactory.getLogger(CompletionRegisteringMethodInterceptor.class); private static final ConcurrentLruCache COMPLETING_METHOD = new ConcurrentLruCache<>(100, CompletionRegisteringMethodInterceptor::calculateIsCompletingMethod); private static final ConcurrentLruCache ADAPTERS = new ConcurrentLruCache<>( @@ -139,6 +164,19 @@ public class CompletionRegisteringBeanPostProcessor implements BeanPostProcessor private final @NonNull Supplier registry; private final @NonNull String beanName; + /** + * @param registry + * @param beanName + */ + CompletionRegisteringMethodInterceptor(Supplier registry, String beanName) { + + Assert.notNull(registry, "EventPublicationRegistry must not be null!"); + Assert.hasText(beanName, "Bean name must not be null or empty!"); + + this.registry = registry; + this.beanName = beanName; + } + /* * (non-Javadoc) * @see org.aopalliance.intercept.MethodInterceptor#invoke(org.aopalliance.intercept.MethodInvocation) @@ -172,7 +210,7 @@ public class CompletionRegisteringBeanPostProcessor implements BeanPostProcessor } // Mark publication complete if the method is a transactional event listener. - String adapterId = ADAPTERS.get(CacheKey.of(beanName, method)).getListenerId(); + String adapterId = ADAPTERS.get(new CacheKey(beanName, method)).getListenerId(); PublicationTargetIdentifier identifier = PublicationTargetIdentifier.of(adapterId); registry.get().markCompleted(invocation.getArguments()[0], identifier); @@ -213,12 +251,8 @@ public class CompletionRegisteringBeanPostProcessor implements BeanPostProcessor return new TransactionalApplicationListenerMethodAdapter(key.beanName, key.method.getDeclaringClass(), key.method); } + } - @Value(staticConstructor = "of") - static class CacheKey { - - String beanName; - Method method; - } + static record CacheKey(String beanName, Method method) {} } diff --git a/spring-modulith-events/spring-modulith-events-core/src/main/java/org/springframework/modulith/events/support/PersistentApplicationEventMulticaster.java b/spring-modulith-events/spring-modulith-events-core/src/main/java/org/springframework/modulith/events/support/PersistentApplicationEventMulticaster.java index 40e32fef..a5b0a728 100644 --- a/spring-modulith-events/spring-modulith-events-core/src/main/java/org/springframework/modulith/events/support/PersistentApplicationEventMulticaster.java +++ b/spring-modulith-events/spring-modulith-events-core/src/main/java/org/springframework/modulith/events/support/PersistentApplicationEventMulticaster.java @@ -15,16 +15,14 @@ */ package org.springframework.modulith.events.support; -import lombok.NonNull; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; - import java.util.Collection; import java.util.List; import java.util.function.Consumer; import java.util.function.Supplier; import java.util.stream.Stream; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.beans.factory.SmartInitializingSingleton; import org.springframework.context.ApplicationEvent; import org.springframework.context.ApplicationListener; @@ -33,6 +31,7 @@ import org.springframework.context.event.AbstractApplicationEventMulticaster; import org.springframework.context.event.ApplicationEventMulticaster; import org.springframework.core.ResolvableType; import org.springframework.core.annotation.AnnotationAwareOrderComparator; +import org.springframework.lang.NonNull; import org.springframework.modulith.events.EventPublication; import org.springframework.modulith.events.EventPublicationRegistry; import org.springframework.modulith.events.PublicationTargetIdentifier; @@ -52,13 +51,25 @@ import org.springframework.util.Assert; * @author Oliver Drotbohm * @see CompletionRegisteringBeanPostProcessor */ -@Slf4j -@RequiredArgsConstructor public class PersistentApplicationEventMulticaster extends AbstractApplicationEventMulticaster implements SmartInitializingSingleton { + private static final Logger LOGGER = LoggerFactory.getLogger(PersistentApplicationEventMulticaster.class); + private final @NonNull Supplier registry; + /** + * Creates a new {@link PersistentApplicationEventMulticaster} for the given {@link EventPublicationRegistry}. + * + * @param registry must not be {@literal null}. + */ + public PersistentApplicationEventMulticaster(Supplier registry) { + + Assert.notNull(registry, "EventPublicationRegistry must not be null!"); + + this.registry = registry; + } + /* * (non-Javadoc) * @see org.springframework.context.event.ApplicationEventMulticaster#multicastEvent(org.springframework.context.ApplicationEvent) @@ -76,15 +87,16 @@ public class PersistentApplicationEventMulticaster extends AbstractApplicationEv @SuppressWarnings({ "unchecked", "rawtypes" }) public void multicastEvent(ApplicationEvent event, ResolvableType eventType) { - ResolvableType type = eventType == null ? ResolvableType.forInstance(event) : eventType; - Collection> listeners = getApplicationListeners(event, type); + var type = eventType == null ? ResolvableType.forInstance(event) : eventType; + var listeners = getApplicationListeners(event, type); if (listeners.isEmpty()) { return; } - TransactionalEventListeners txListeners = new TransactionalEventListeners(listeners); - Object eventToPersist = getEventToPersist(event); + var txListeners = new TransactionalEventListeners(listeners); + var eventToPersist = getEventToPersist(event); + registry.get().store(eventToPersist, txListeners.stream() // .map(TransactionalApplicationListener::getListenerId) // .map(PublicationTargetIdentifier::of)); @@ -108,7 +120,7 @@ public class PersistentApplicationEventMulticaster extends AbstractApplicationEv private void invokeTargetListener(EventPublication publication) { - TransactionalEventListeners listeners = new TransactionalEventListeners( + var listeners = new TransactionalEventListeners( getApplicationListeners()); listeners.stream() // @@ -117,7 +129,7 @@ public class PersistentApplicationEventMulticaster extends AbstractApplicationEv .map(it -> executeListenerWithCompletion(publication, it)) // .orElseGet(() -> { - LOG.debug("Listener {} not found!", publication.getTargetIdentifier()); + LOGGER.debug("Listener {} not found!", publication.getTargetIdentifier()); return null; }); } diff --git a/spring-modulith-events/spring-modulith-events-core/src/test/java/org/springframework/modulith/events/CompletableEventPublicationTest.java b/spring-modulith-events/spring-modulith-events-core/src/test/java/org/springframework/modulith/events/CompletableEventPublicationTest.java index 894235a5..115ed3f9 100644 --- a/spring-modulith-events/spring-modulith-events-core/src/test/java/org/springframework/modulith/events/CompletableEventPublicationTest.java +++ b/spring-modulith-events/spring-modulith-events-core/src/test/java/org/springframework/modulith/events/CompletableEventPublicationTest.java @@ -31,7 +31,7 @@ class CompletableEventPublicationUnitTests { assertThatExceptionOfType(IllegalArgumentException.class)// .isThrownBy(() -> CompletableEventPublication.of(null, PublicationTargetIdentifier.of("foo")))// - .withMessageContaining("event"); + .withMessageContaining("Event"); } @Test @@ -39,7 +39,7 @@ class CompletableEventPublicationUnitTests { assertThatExceptionOfType(IllegalArgumentException.class)// .isThrownBy(() -> CompletableEventPublication.of(new Object(), null))// - .withMessageContaining("targetIdentifier"); + .withMessageContaining("TargetIdentifier"); } @Test diff --git a/spring-modulith-events/spring-modulith-events-jackson/src/main/java/org/springframework/modulith/events/jackson/JacksonEventSerializationConfiguration.java b/spring-modulith-events/spring-modulith-events-jackson/src/main/java/org/springframework/modulith/events/jackson/JacksonEventSerializationConfiguration.java index da49398d..966fcb7e 100644 --- a/spring-modulith-events/spring-modulith-events-jackson/src/main/java/org/springframework/modulith/events/jackson/JacksonEventSerializationConfiguration.java +++ b/spring-modulith-events/spring-modulith-events-jackson/src/main/java/org/springframework/modulith/events/jackson/JacksonEventSerializationConfiguration.java @@ -15,14 +15,13 @@ */ package org.springframework.modulith.events.jackson; -import lombok.RequiredArgsConstructor; - import org.springframework.beans.factory.ObjectProvider; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.modulith.events.EventSerializer; import org.springframework.modulith.events.config.EventSerializationConfigurationExtension; +import org.springframework.util.Assert; import com.fasterxml.jackson.databind.Module; import com.fasterxml.jackson.databind.ObjectMapper; @@ -34,12 +33,27 @@ import com.fasterxml.jackson.databind.SerializationFeature; * @author Oliver Drotbohm */ @Configuration(proxyBeanMethods = false) -@RequiredArgsConstructor class JacksonEventSerializationConfiguration implements EventSerializationConfigurationExtension { private final ObjectProvider mapper; private final ApplicationContext context; + /** + * Creates a new {@link JacksonEventSerializationConfiguration} for the given {@link ObjectMapper} and + * {@link ApplicationContext}. + * + * @param mapper must not be {@literal null}. + * @param context must not be {@literal null}. + */ + public JacksonEventSerializationConfiguration(ObjectProvider mapper, ApplicationContext context) { + + Assert.notNull(mapper, "ObjectMapper must not be null!"); + Assert.notNull(context, "ApplicationContext must not be null!"); + + this.mapper = mapper; + this.context = context; + } + @Bean public JacksonEventSerializer jacksonEventSerializer() { return new JacksonEventSerializer(() -> mapper.getIfAvailable(() -> defaultObjectMapper())); @@ -47,7 +61,7 @@ class JacksonEventSerializationConfiguration implements EventSerializationConfig private ObjectMapper defaultObjectMapper() { - ObjectMapper mapper = new ObjectMapper(); + var mapper = new ObjectMapper(); mapper.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS); mapper.registerModules(context.getBeansOfType(Module.class).values()); diff --git a/spring-modulith-events/spring-modulith-events-jackson/src/main/java/org/springframework/modulith/events/jackson/JacksonEventSerializer.java b/spring-modulith-events/spring-modulith-events-jackson/src/main/java/org/springframework/modulith/events/jackson/JacksonEventSerializer.java index 0d640d04..dc08b37d 100644 --- a/spring-modulith-events/spring-modulith-events-jackson/src/main/java/org/springframework/modulith/events/jackson/JacksonEventSerializer.java +++ b/spring-modulith-events/spring-modulith-events-jackson/src/main/java/org/springframework/modulith/events/jackson/JacksonEventSerializer.java @@ -15,24 +15,36 @@ */ package org.springframework.modulith.events.jackson; -import lombok.RequiredArgsConstructor; - import java.io.IOException; import java.util.function.Supplier; import org.springframework.modulith.events.EventSerializer; +import org.springframework.util.Assert; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; /** + * A Jackson-based {@link EventSerializer}. + * * @author Oliver Drotbohm */ -@RequiredArgsConstructor class JacksonEventSerializer implements EventSerializer { private final Supplier mapper; + /** + * Creates a new {@link JacksonEventSerializer} for the given {@link ObjectMapper}. + * + * @param mapper must not be {@literal null}. + */ + public JacksonEventSerializer(Supplier mapper) { + + Assert.notNull(mapper, "ObjectMapper must not be null!"); + + this.mapper = mapper; + } + /* * (non-Javadoc) * @see de.oliverDrotbohm.events.EventSerializer#serialize(java.lang.Object) diff --git a/spring-modulith-events/spring-modulith-events-jdbc/src/main/java/org/springframework/modulith/events/jdbc/DatabaseSchemaInitializer.java b/spring-modulith-events/spring-modulith-events-jdbc/src/main/java/org/springframework/modulith/events/jdbc/DatabaseSchemaInitializer.java index 6b85e2d2..6467b2dd 100644 --- a/spring-modulith-events/spring-modulith-events-jdbc/src/main/java/org/springframework/modulith/events/jdbc/DatabaseSchemaInitializer.java +++ b/spring-modulith-events/spring-modulith-events-jdbc/src/main/java/org/springframework/modulith/events/jdbc/DatabaseSchemaInitializer.java @@ -15,8 +15,6 @@ */ package org.springframework.modulith.events.jdbc; -import lombok.RequiredArgsConstructor; - import java.io.IOException; import java.io.UncheckedIOException; import java.nio.charset.StandardCharsets; @@ -24,7 +22,7 @@ import java.nio.charset.StandardCharsets; import org.springframework.beans.factory.InitializingBean; import org.springframework.core.io.Resource; import org.springframework.core.io.ResourceLoader; -import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.JdbcOperations; import org.springframework.util.StreamUtils; /** @@ -34,13 +32,32 @@ import org.springframework.util.StreamUtils; * @author Björn Kieling * @author Oliver Drotbohm */ -@RequiredArgsConstructor class DatabaseSchemaInitializer implements InitializingBean { - private final JdbcTemplate jdbcTemplate; + private final JdbcOperations jdbcOperations; private final ResourceLoader resourceLoader; private final DatabaseType databaseType; + /** + * Creates a new {@link DatabaseSchemaInitializer} for the given {@link JdbcOperations}, {@link ResourceLoader} and + * {@link DatabaseType}. + * + * @param jdbcOperations must not be {@literal null}. + * @param resourceLoader must not be {@literal null}. + * @param databaseType must not be {@literal null}. + */ + public DatabaseSchemaInitializer(JdbcOperations jdbcOperations, ResourceLoader resourceLoader, + DatabaseType databaseType) { + + this.jdbcOperations = jdbcOperations; + this.resourceLoader = resourceLoader; + this.databaseType = databaseType; + } + + /* + * (non-Javadoc) + * @see org.springframework.beans.factory.InitializingBean#afterPropertiesSet() + */ @Override public void afterPropertiesSet() { @@ -48,10 +65,10 @@ class DatabaseSchemaInitializer implements InitializingBean { var schemaDdlResource = resourceLoader.getResource(schemaResourceFilename); var schemaDdl = asString(schemaDdlResource); - jdbcTemplate.execute(schemaDdl); + jdbcOperations.execute(schemaDdl); } - private String asString(Resource resource) { + private static String asString(Resource resource) { try { return StreamUtils.copyToString(resource.getInputStream(), StandardCharsets.UTF_8); diff --git a/spring-modulith-events/spring-modulith-events-jdbc/src/main/java/org/springframework/modulith/events/jdbc/JdbcEventPublicationRepository.java b/spring-modulith-events/spring-modulith-events-jdbc/src/main/java/org/springframework/modulith/events/jdbc/JdbcEventPublicationRepository.java index 51b8d956..67c634c0 100644 --- a/spring-modulith-events/spring-modulith-events-jdbc/src/main/java/org/springframework/modulith/events/jdbc/JdbcEventPublicationRepository.java +++ b/spring-modulith-events/spring-modulith-events-jdbc/src/main/java/org/springframework/modulith/events/jdbc/JdbcEventPublicationRepository.java @@ -15,20 +15,18 @@ */ package org.springframework.modulith.events.jdbc; -import lombok.Builder; -import lombok.EqualsAndHashCode; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; - import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Timestamp; import java.time.Instant; import java.util.ArrayList; import java.util.List; +import java.util.Objects; import java.util.Optional; import java.util.UUID; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.jdbc.core.JdbcOperations; import org.springframework.jdbc.core.ResultSetExtractor; import org.springframework.jdbc.core.RowMapper; @@ -39,6 +37,7 @@ import org.springframework.modulith.events.EventPublicationRepository; import org.springframework.modulith.events.EventSerializer; import org.springframework.modulith.events.PublicationTargetIdentifier; import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.Assert; /** * JDBC-based repository to store {@link EventPublication}s. @@ -47,10 +46,10 @@ import org.springframework.transaction.annotation.Transactional; * @author Björn Kieling * @author Oliver Drotbohm */ -@Slf4j -@RequiredArgsConstructor class JdbcEventPublicationRepository implements EventPublicationRepository { + private static final Logger LOGGER = LoggerFactory.getLogger(JdbcEventPublicationRepository.class); + private static final String SQL_STATEMENT_INSERT = """ INSERT INTO EVENT_PUBLICATION (ID, EVENT_TYPE, LISTENER_ID, PUBLICATION_DATE, SERIALIZED_EVENT) VALUES (?, ?, ?, ?, ?) @@ -89,6 +88,30 @@ class JdbcEventPublicationRepository implements EventPublicationRepository { private final EventSerializer serializer; private final DatabaseType databaseType; + /** + * Creates a new {@link JdbcEventPublicationRepository} for the given {@link JdbcOperations}, {@link EventSerializer} + * and {@link DatabaseType}. + * + * @param operations must not be {@literal null}. + * @param serializer must not be {@literal null}. + * @param databaseType must not be {@literal null}. + */ + public JdbcEventPublicationRepository(JdbcOperations operations, EventSerializer serializer, + DatabaseType databaseType) { + + Assert.notNull(operations, "JdbcOperations must not be null!"); + Assert.notNull(serializer, "EventSerializer must not be null!"); + Assert.notNull(databaseType, "DatabaseType must not be null!"); + + this.operations = operations; + this.serializer = serializer; + this.databaseType = databaseType; + } + + /* + * (non-Javadoc) + * @see org.springframework.modulith.events.EventPublicationRepository#create(org.springframework.modulith.events.EventPublication) + */ @Override @Transactional public EventPublication create(EventPublication publication) { @@ -218,14 +241,12 @@ class JdbcEventPublicationRepository implements EventPublicationRepository { } var completionDate = rs.getTimestamp("COMPLETION_DATE"); + var publicationDate = rs.getTimestamp("PUBLICATION_DATE").toInstant(); + var listenerId = rs.getString("LISTENER_ID"); + var serializedEvent = rs.getString("SERIALIZED_EVENT"); - return JdbcEventPublication.builder().completionDate(completionDate == null ? null : completionDate.toInstant()) - .eventType(eventClass) // - .listenerId(rs.getString("LISTENER_ID")) // - .publicationDate(rs.getTimestamp("PUBLICATION_DATE").toInstant()) // - .serializedEvent(rs.getString("SERIALIZED_EVENT")) // - .serializer(serializer) // - .build(); + return new JdbcEventPublication(id, publicationDate, listenerId, serializedEvent, eventClass, serializer, + completionDate == null ? null : completionDate.toInstant()); } private Object uuidToDatabase(UUID id) { @@ -242,13 +263,11 @@ class JdbcEventPublicationRepository implements EventPublicationRepository { try { return Class.forName(className); } catch (ClassNotFoundException e) { - LOG.warn("Event '{}' of unknown type '{}' found", id, className); + LOGGER.warn("Event '{}' of unknown type '{}' found", id, className); return null; } } - @EqualsAndHashCode - @Builder private static class JdbcEventPublication implements CompletableEventPublication { private final UUID id; @@ -260,35 +279,122 @@ class JdbcEventPublicationRepository implements EventPublicationRepository { private final EventSerializer serializer; private @Nullable Instant completionDate; + /** + * @param id must not be {@literal null}. + * @param publicationDate must not be {@literal null}. + * @param listenerId must not be {@literal null} or empty. + * @param serializedEvent must not be {@literal null} or empty. + * @param eventType must not be {@literal null}. + * @param serializer must not be {@literal null}. + * @param completionDate can be {@literal null}. + */ + public JdbcEventPublication(UUID id, Instant publicationDate, String listenerId, String serializedEvent, + Class eventType, EventSerializer serializer, @Nullable Instant completionDate) { + + Assert.notNull(id, "Id must not be null!"); + Assert.notNull(publicationDate, "Publication date must not be null!"); + Assert.hasText(listenerId, "Listener id must not be null or empty!"); + Assert.hasText(serializedEvent, "Serialized event must not be null or empty!"); + Assert.notNull(eventType, "Event type must not be null!"); + Assert.notNull(serializer, "EventSerializer must not be null!"); + + this.id = id; + this.publicationDate = publicationDate; + this.listenerId = listenerId; + this.serializedEvent = serializedEvent; + this.eventType = eventType; + this.serializer = serializer; + this.completionDate = completionDate; + } + + /* + * (non-Javadoc) + * @see org.springframework.modulith.events.EventPublication#getEvent() + */ @Override public Object getEvent() { return serializer.deserialize(serializedEvent, eventType); } + /* + * (non-Javadoc) + * @see org.springframework.modulith.events.EventPublication#getTargetIdentifier() + */ @Override public PublicationTargetIdentifier getTargetIdentifier() { return PublicationTargetIdentifier.of(listenerId); } + /* + * (non-Javadoc) + * @see org.springframework.modulith.events.EventPublication#getPublicationDate() + */ @Override public Instant getPublicationDate() { return publicationDate; } + /* + * (non-Javadoc) + * @see org.springframework.modulith.events.CompletableEventPublication#getCompletionDate() + */ @Override public Optional getCompletionDate() { return Optional.ofNullable(completionDate); } + /* + * (non-Javadoc) + * @see org.springframework.modulith.events.CompletableEventPublication#isPublicationCompleted() + */ @Override public boolean isPublicationCompleted() { return completionDate != null; } + /* + * (non-Javadoc) + * @see org.springframework.modulith.events.CompletableEventPublication#markCompleted() + */ @Override public CompletableEventPublication markCompleted() { + this.completionDate = Instant.now(); + return this; } + + /* + * (non-Javadoc) + * @see java.lang.Object#equals(java.lang.Object) + */ + @Override + public boolean equals(@Nullable Object obj) { + + if (this == obj) { + return true; + } + + if (!(obj instanceof JdbcEventPublication that)) { + return false; + } + + return Objects.equals(completionDate, that.completionDate) // + && Objects.equals(eventType, that.eventType) // + && Objects.equals(id, that.id) // + && Objects.equals(listenerId, that.listenerId) // + && Objects.equals(publicationDate, that.publicationDate) // + && Objects.equals(serializedEvent, that.serializedEvent) // + && Objects.equals(serializer, that.serializer); + } + + /* + * (non-Javadoc) + * @see java.lang.Object#hashCode() + */ + @Override + public int hashCode() { + return Objects.hash(completionDate, eventType, id, listenerId, publicationDate, serializedEvent, serializer); + } } } diff --git a/spring-modulith-events/spring-modulith-events-jpa/src/main/java/org/springframework/modulith/events/jpa/JpaEventPublication.java b/spring-modulith-events/spring-modulith-events-jpa/src/main/java/org/springframework/modulith/events/jpa/JpaEventPublication.java index 99b1a89d..dc196b1c 100644 --- a/spring-modulith-events/spring-modulith-events-jpa/src/main/java/org/springframework/modulith/events/jpa/JpaEventPublication.java +++ b/spring-modulith-events/spring-modulith-events-jpa/src/main/java/org/springframework/modulith/events/jpa/JpaEventPublication.java @@ -18,15 +18,12 @@ package org.springframework.modulith.events.jpa; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.Id; -import lombok.AccessLevel; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; -import lombok.RequiredArgsConstructor; import java.time.Instant; import java.util.UUID; +import org.springframework.util.Assert; + /** * JPA entity to represent event publications. * @@ -34,25 +31,47 @@ import java.util.UUID; * @author Dmitry Belyaev * @author Björn Kieling */ -@Data @Entity -@NoArgsConstructor(force = true) -@RequiredArgsConstructor(access = AccessLevel.PRIVATE) class JpaEventPublication { - private final @Id @Column(length = 16) UUID id; - private final Instant publicationDate; - private final String listenerId; - private final String serializedEvent; - private final Class eventType; + final @Id @Column(length = 16) UUID id; + final Instant publicationDate; + final String listenerId; + final String serializedEvent; + final Class eventType; - private Instant completionDate; + Instant completionDate; - @Builder - static JpaEventPublication of(Instant publicationDate, String listenerId, Object serializedEvent, - Class eventType) { - return new JpaEventPublication(UUID.randomUUID(), publicationDate, listenerId, serializedEvent.toString(), - eventType); + /** + * Creates a new {@link JpaEventPublication} for the given publication date, listener id, serialized event and event + * type. + * + * @param publicationDate must not be {@literal null}. + * @param listenerId must not be {@literal null} or empty. + * @param serializedEvent must not be {@literal null} or empty. + * @param eventType must not be {@literal null}. + */ + JpaEventPublication(Instant publicationDate, String listenerId, String serializedEvent, Class eventType) { + + Assert.notNull(publicationDate, "Publication date must not be null!"); + Assert.notNull(listenerId, "Listener id must not be null or empty!"); + Assert.notNull(serializedEvent, "Serialized event must not be null or empty!"); + Assert.notNull(eventType, "Event type must not be null!"); + + this.id = UUID.randomUUID(); + this.publicationDate = publicationDate; + this.listenerId = listenerId; + this.serializedEvent = serializedEvent; + this.eventType = eventType; + } + + JpaEventPublication() { + + this.id = null; + this.publicationDate = null; + this.listenerId = null; + this.serializedEvent = null; + this.eventType = null; } JpaEventPublication markCompleted() { diff --git a/spring-modulith-events/spring-modulith-events-jpa/src/main/java/org/springframework/modulith/events/jpa/JpaEventPublicationConfiguration.java b/spring-modulith-events/spring-modulith-events-jpa/src/main/java/org/springframework/modulith/events/jpa/JpaEventPublicationConfiguration.java index f93a16d1..ee09ecb9 100644 --- a/spring-modulith-events/spring-modulith-events-jpa/src/main/java/org/springframework/modulith/events/jpa/JpaEventPublicationConfiguration.java +++ b/spring-modulith-events/spring-modulith-events-jpa/src/main/java/org/springframework/modulith/events/jpa/JpaEventPublicationConfiguration.java @@ -16,7 +16,6 @@ package org.springframework.modulith.events.jpa; import jakarta.persistence.EntityManager; -import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -29,7 +28,6 @@ import org.springframework.modulith.events.config.EventPublicationConfigurationE * @author Björn Kieling */ @Configuration(proxyBeanMethods = false) -@RequiredArgsConstructor class JpaEventPublicationConfiguration implements EventPublicationConfigurationExtension { @Bean diff --git a/spring-modulith-events/spring-modulith-events-jpa/src/main/java/org/springframework/modulith/events/jpa/JpaEventPublicationRepository.java b/spring-modulith-events/spring-modulith-events-jpa/src/main/java/org/springframework/modulith/events/jpa/JpaEventPublicationRepository.java index 7f3c494d..cfbb9dc6 100644 --- a/spring-modulith-events/spring-modulith-events-jpa/src/main/java/org/springframework/modulith/events/jpa/JpaEventPublicationRepository.java +++ b/spring-modulith-events/spring-modulith-events-jpa/src/main/java/org/springframework/modulith/events/jpa/JpaEventPublicationRepository.java @@ -16,11 +16,10 @@ package org.springframework.modulith.events.jpa; import jakarta.persistence.EntityManager; -import lombok.EqualsAndHashCode; -import lombok.RequiredArgsConstructor; import java.time.Instant; import java.util.List; +import java.util.Objects; import java.util.Optional; import org.springframework.modulith.events.CompletableEventPublication; @@ -29,6 +28,7 @@ import org.springframework.modulith.events.EventPublicationRepository; import org.springframework.modulith.events.EventSerializer; import org.springframework.modulith.events.PublicationTargetIdentifier; import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.Assert; /** * Repository to store {@link EventPublication}s. @@ -37,7 +37,6 @@ import org.springframework.transaction.annotation.Transactional; * @author Dmitry Belyaev * @author Björn Kieling */ -@RequiredArgsConstructor class JpaEventPublicationRepository implements EventPublicationRepository { private static String BY_EVENT_AND_LISTENER_ID = """ @@ -66,6 +65,26 @@ class JpaEventPublicationRepository implements EventPublicationRepository { private final EntityManager entityManager; private final EventSerializer serializer; + /** + * Creates a new {@link JpaEventPublicationRepository} for the given {@link EntityManager} and + * {@link EventSerializer}. + * + * @param entityManager must not be {@literal null}. + * @param serializer must not be {@literal null}. + */ + public JpaEventPublicationRepository(EntityManager entityManager, EventSerializer serializer) { + + Assert.notNull(entityManager, "EntityManager must not be null!"); + Assert.notNull(serializer, "EventSerializer must not be null!"); + + this.entityManager = entityManager; + this.serializer = serializer; + } + + /* + * (non-Javadoc) + * @see org.springframework.modulith.events.EventPublicationRepository#create(org.springframework.modulith.events.EventPublication) + */ @Override @Transactional public EventPublication create(EventPublication publication) { @@ -75,6 +94,10 @@ class JpaEventPublicationRepository implements EventPublicationRepository { return publication; } + /* + * (non-Javadoc) + * @see org.springframework.modulith.events.EventPublicationRepository#update(org.springframework.modulith.events.CompletableEventPublication) + */ @Override @Transactional public EventPublication update(CompletableEventPublication publication) { @@ -83,11 +106,15 @@ class JpaEventPublicationRepository implements EventPublicationRepository { var event = publication.getEvent(); findEntityBySerializedEventAndListenerIdAndCompletionDateNull(event, id) // - .ifPresent(entity -> entity.setCompletionDate(publication.getCompletionDate().orElse(null))); + .ifPresent(entity -> entity.completionDate = publication.getCompletionDate().orElse(null)); return publication; } + /* + * (non-Javadoc) + * @see org.springframework.modulith.events.EventPublicationRepository#findIncompletePublications() + */ @Override @Transactional(readOnly = true) public List findIncompletePublications() { @@ -98,6 +125,10 @@ class JpaEventPublicationRepository implements EventPublicationRepository { .toList(); } + /* + * (non-Javadoc) + * @see org.springframework.modulith.events.EventPublicationRepository#findIncompletePublicationsByEventAndTargetIdentifier(java.lang.Object, org.springframework.modulith.events.PublicationTargetIdentifier) + */ @Override @Transactional(readOnly = true) public Optional findIncompletePublicationsByEventAndTargetIdentifier( // @@ -107,6 +138,10 @@ class JpaEventPublicationRepository implements EventPublicationRepository { .map(this::entityToDomain); } + /* + * (non-Javadoc) + * @see org.springframework.modulith.events.EventPublicationRepository#deleteCompletedPublications() + */ @Override @Transactional public void deleteCompletedPublications() { @@ -130,55 +165,116 @@ class JpaEventPublicationRepository implements EventPublicationRepository { } private JpaEventPublication domainToEntity(EventPublication domain) { - - return JpaEventPublication.builder() // - .publicationDate(domain.getPublicationDate()) // - .listenerId(domain.getTargetIdentifier().getValue()) // - .serializedEvent(serializeEvent(domain.getEvent())) // - .eventType(domain.getEvent().getClass()) // - .build(); + return new JpaEventPublication(domain.getPublicationDate(), domain.getTargetIdentifier().getValue(), + serializeEvent(domain.getEvent()), domain.getEvent().getClass()); } private EventPublication entityToDomain(JpaEventPublication entity) { - return JpaEventPublicationAdapter.of(entity, serializer); + return new JpaEventPublicationAdapter(entity, serializer); } - @EqualsAndHashCode - @RequiredArgsConstructor(staticName = "of") private static class JpaEventPublicationAdapter implements CompletableEventPublication { private final JpaEventPublication publication; private final EventSerializer serializer; + /** + * Creates a new {@link JpaEventPublicationAdapter} for the given {@link JpaEventPublication} and + * {@link EventSerializer}. + * + * @param publication must not be {@literal null}. + * @param serializer must not be {@literal null}. + */ + public JpaEventPublicationAdapter(JpaEventPublication publication, EventSerializer serializer) { + + Assert.notNull(publication, "JpaEventPublication must not be null!"); + Assert.notNull(serializer, "EventSerializer must not be null!"); + + this.publication = publication; + this.serializer = serializer; + } + + /* + * (non-Javadoc) + * @see org.springframework.modulith.events.EventPublication#getEvent() + */ @Override public Object getEvent() { - return serializer.deserialize(publication.getSerializedEvent(), publication.getEventType()); + return serializer.deserialize(publication.serializedEvent, publication.eventType); } + /* + * (non-Javadoc) + * @see org.springframework.modulith.events.EventPublication#getTargetIdentifier() + */ @Override public PublicationTargetIdentifier getTargetIdentifier() { - return PublicationTargetIdentifier.of(publication.getListenerId()); + return PublicationTargetIdentifier.of(publication.listenerId); } + /* + * (non-Javadoc) + * @see org.springframework.modulith.events.EventPublication#getPublicationDate() + */ @Override public Instant getPublicationDate() { - return publication.getPublicationDate(); + return publication.publicationDate; } + /* + * (non-Javadoc) + * @see org.springframework.modulith.events.CompletableEventPublication#getCompletionDate() + */ @Override public Optional getCompletionDate() { - return Optional.ofNullable(publication.getCompletionDate()); + return Optional.ofNullable(publication.completionDate); } + /* + * (non-Javadoc) + * @see org.springframework.modulith.events.CompletableEventPublication#isPublicationCompleted() + */ @Override public boolean isPublicationCompleted() { - return publication.getCompletionDate() != null; + return publication.completionDate != null; } + /* + * (non-Javadoc) + * @see org.springframework.modulith.events.CompletableEventPublication#markCompleted() + */ @Override public CompletableEventPublication markCompleted() { publication.markCompleted(); return this; } + + /* + * (non-Javadoc) + * @see java.lang.Object#equals(java.lang.Object) + */ + @Override + public boolean equals(Object obj) { + + if (this == obj) { + return true; + } + + if (!(obj instanceof JpaEventPublicationAdapter that)) { + return false; + } + + return Objects.equals(publication, that.publication) + && Objects.equals(serializer, that.serializer); + } + + /* + * (non-Javadoc) + * @see java.lang.Object#hashCode() + */ + @Override + public int hashCode() { + return Objects.hash(publication, serializer); + } } } diff --git a/spring-modulith-events/spring-modulith-events-jpa/src/test/java/org/springframework/modulith/events/jpa/JpaEventPublicationRepositoryIntegrationTests.java b/spring-modulith-events/spring-modulith-events-jpa/src/test/java/org/springframework/modulith/events/jpa/JpaEventPublicationRepositoryIntegrationTests.java index 26653531..5ce070d8 100644 --- a/spring-modulith-events/spring-modulith-events-jpa/src/test/java/org/springframework/modulith/events/jpa/JpaEventPublicationRepositoryIntegrationTests.java +++ b/spring-modulith-events/spring-modulith-events-jpa/src/test/java/org/springframework/modulith/events/jpa/JpaEventPublicationRepositoryIntegrationTests.java @@ -193,7 +193,7 @@ class JpaEventPublicationRepositoryIntegrationTests { assertThat(em.createQuery("select p from JpaEventPublication p", JpaEventPublication.class).getResultList()) .hasSize(1) // - .element(0).extracting(JpaEventPublication::getSerializedEvent).isEqualTo(serializedEvent2); + .element(0).extracting(it -> it.serializedEvent).isEqualTo(serializedEvent2); } @Value diff --git a/spring-modulith-events/spring-modulith-events-mongodb/src/main/java/org/springframework/modulith/events/mongodb/MongoDbEventPublication.java b/spring-modulith-events/spring-modulith-events-mongodb/src/main/java/org/springframework/modulith/events/mongodb/MongoDbEventPublication.java index 3dbb7b9f..56d99383 100644 --- a/spring-modulith-events/spring-modulith-events-mongodb/src/main/java/org/springframework/modulith/events/mongodb/MongoDbEventPublication.java +++ b/spring-modulith-events/spring-modulith-events-mongodb/src/main/java/org/springframework/modulith/events/mongodb/MongoDbEventPublication.java @@ -15,16 +15,13 @@ */ package org.springframework.modulith.events.mongodb; -import lombok.AccessLevel; -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.RequiredArgsConstructor; -import lombok.Setter; -import org.springframework.data.annotation.Id; +import java.time.Instant; + +import org.bson.types.ObjectId; import org.springframework.data.annotation.PersistenceCreator; import org.springframework.data.mongodb.core.mapping.Document; - -import java.time.Instant; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; /** * A MongoDB Document to represent event publications. @@ -33,18 +30,63 @@ import java.time.Instant; * @author Björn Kieling */ @Document(collection = "org_springframework_modulith_events") -@Getter -@RequiredArgsConstructor -@AllArgsConstructor(access = AccessLevel.PACKAGE, onConstructor = @__(@PersistenceCreator)) class MongoDbEventPublication { - @Id - private String id; + final ObjectId id; + final Instant publicationDate; + final String listenerId; + final Object event; - private final Instant publicationDate; - private final String listenerId; - private final Object event; + @Nullable Instant completionDate; - @Setter(AccessLevel.PACKAGE) - private Instant completionDate; + /** + * Creates a new {@link MongoDbEventPublication} for the given id, publication date, listener id, event and completion + * date. + * + * @param id must not be {@literal null}. + * @param publicationDate must not be {@literal null}. + * @param listenerId must not be {@literal null} or empty. + * @param event must not be {@literal null}. + * @param completionDate can be {@literal null}. + */ + @PersistenceCreator + MongoDbEventPublication(ObjectId id, Instant publicationDate, String listenerId, Object event, + @Nullable Instant completionDate) { + + Assert.notNull(id, "Id must not be null!"); + Assert.notNull(publicationDate, "Publication date must not be null!"); + Assert.notNull(listenerId, "Listener id must not be null!"); + Assert.notNull(event, "Event must not be null!"); + + this.id = id; + this.publicationDate = publicationDate; + this.listenerId = listenerId; + this.event = event; + this.completionDate = completionDate; + } + + /** + * Creates a new {@link MongoDbEventPublication} for the given publication date, listener id and event. + * + * @param publicationDate must not be {@literal null}. + * @param listenerId must not be {@literal null}. + * @param event must not be {@literal null}. + */ + MongoDbEventPublication(Instant publicationDate, String listenerId, Object event) { + this(new ObjectId(), publicationDate, listenerId, event, null); + } + + /** + * Marks the publication as completed at the given {@link Instant}. + * + * @param instant must not be {@literal null}. + * @return will never be {@literal null}. + */ + MongoDbEventPublication markCompleted(Instant instant) { + + Assert.notNull(instant, "Instant must not be null!"); + + this.completionDate = instant; + return this; + } } diff --git a/spring-modulith-events/spring-modulith-events-mongodb/src/main/java/org/springframework/modulith/events/mongodb/MongoDbEventPublicationRepository.java b/spring-modulith-events/spring-modulith-events-mongodb/src/main/java/org/springframework/modulith/events/mongodb/MongoDbEventPublicationRepository.java index 10ad5716..9fa869ce 100644 --- a/spring-modulith-events/spring-modulith-events-mongodb/src/main/java/org/springframework/modulith/events/mongodb/MongoDbEventPublicationRepository.java +++ b/spring-modulith-events/spring-modulith-events-mongodb/src/main/java/org/springframework/modulith/events/mongodb/MongoDbEventPublicationRepository.java @@ -18,11 +18,9 @@ package org.springframework.modulith.events.mongodb; import static org.springframework.data.mongodb.core.query.Criteria.*; import static org.springframework.data.mongodb.core.query.Query.*; -import lombok.EqualsAndHashCode; -import lombok.RequiredArgsConstructor; - import java.time.Instant; import java.util.List; +import java.util.Objects; import java.util.Optional; import org.springframework.data.domain.Sort; @@ -72,7 +70,7 @@ class MongoDbEventPublicationRepository implements EventPublicationRepository { publication.getTargetIdentifier()) // .stream() // .findFirst() // - .map(document -> document.setCompletionDate(publication.getCompletionDate().orElse(null))) // + .map(document -> document.markCompleted(publication.getCompletionDate().orElse(null))) // .map(mongoTemplate::save) // .map(this::documentToDomain) // .orElse(publication); @@ -131,44 +129,73 @@ class MongoDbEventPublicationRepository implements EventPublicationRepository { } private CompletableEventPublication documentToDomain(MongoDbEventPublication document) { - return MongoDbEventPublicationAdapter.of(document); + return new MongoDbEventPublicationAdapter(document); } - @EqualsAndHashCode - @RequiredArgsConstructor(staticName = "of") private static class MongoDbEventPublicationAdapter implements CompletableEventPublication { private final MongoDbEventPublication publication; + MongoDbEventPublicationAdapter(MongoDbEventPublication publication) { + this.publication = publication; + } + @Override public Object getEvent() { - return publication.getEvent(); + return publication.event; } @Override public PublicationTargetIdentifier getTargetIdentifier() { - return PublicationTargetIdentifier.of(publication.getListenerId()); + return PublicationTargetIdentifier.of(publication.listenerId); } @Override public Instant getPublicationDate() { - return publication.getPublicationDate(); + return publication.publicationDate; } @Override public Optional getCompletionDate() { - return Optional.ofNullable(publication.getCompletionDate()); + return Optional.ofNullable(publication.completionDate); } @Override public boolean isPublicationCompleted() { - return publication.getCompletionDate() != null; + return publication.completionDate != null; } @Override public CompletableEventPublication markCompleted() { - publication.setCompletionDate(Instant.now()); + publication.completionDate = Instant.now(); return this; } + + /* + * (non-Javadoc) + * @see java.lang.Object#equals(java.lang.Object) + */ + @Override + public boolean equals(Object obj) { + + if (this == obj) { + return true; + } + + if (!(obj instanceof MongoDbEventPublicationAdapter other)) { + return false; + } + + return Objects.equals(publication, other.publication); + } + + /* + * (non-Javadoc) + * @see java.lang.Object#hashCode() + */ + @Override + public int hashCode() { + return Objects.hash(publication); + } } } diff --git a/spring-modulith-events/spring-modulith-events-mongodb/src/test/java/org/springframework/modulith/events/mongodb/MongoDbEventPublicationRepositoryTest.java b/spring-modulith-events/spring-modulith-events-mongodb/src/test/java/org/springframework/modulith/events/mongodb/MongoDbEventPublicationRepositoryTest.java index af10c61a..cc521fae 100644 --- a/spring-modulith-events/spring-modulith-events-mongodb/src/test/java/org/springframework/modulith/events/mongodb/MongoDbEventPublicationRepositoryTest.java +++ b/spring-modulith-events/spring-modulith-events-mongodb/src/test/java/org/springframework/modulith/events/mongodb/MongoDbEventPublicationRepositoryTest.java @@ -186,7 +186,7 @@ class MongoDbEventPublicationRepositoryTest { assertThat(mongoTemplate.findAll(MongoDbEventPublication.class)) // .hasSize(1) // - .element(0).extracting(MongoDbEventPublication::getEvent).isEqualTo(testEvent2); + .element(0).extracting(it -> it.event).isEqualTo(testEvent2); } } diff --git a/spring-modulith-example/pom.xml b/spring-modulith-example/pom.xml index dddb603b..13415489 100644 --- a/spring-modulith-example/pom.xml +++ b/spring-modulith-example/pom.xml @@ -89,12 +89,6 @@ - - org.projectlombok - lombok - true - - org.springframework.boot spring-boot-configuration-processor diff --git a/spring-modulith-example/src/main/java/example/inventory/InventoryManagement.java b/spring-modulith-example/src/main/java/example/inventory/InventoryManagement.java index bfbc0e6f..8ed3b72e 100644 --- a/spring-modulith-example/src/main/java/example/inventory/InventoryManagement.java +++ b/spring-modulith-example/src/main/java/example/inventory/InventoryManagement.java @@ -16,9 +16,9 @@ package example.inventory; import example.order.OrderCompleted; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.modulith.ApplicationModuleListener; import org.springframework.stereotype.Service; @@ -27,17 +27,21 @@ import org.springframework.stereotype.Service; * * @author Oliver Drotbohm */ -@Slf4j @Service -@RequiredArgsConstructor public class InventoryManagement { - private final InventoryInternal dependency; + private static final Logger LOG = LoggerFactory.getLogger(InventoryManagement.class); + + final InventoryInternal dependency; + + InventoryManagement(InventoryInternal dependency) { + this.dependency = dependency; + } @ApplicationModuleListener void on(OrderCompleted event) throws InterruptedException { - var orderId = event.getOrderId(); + var orderId = event.orderId(); LOG.info("Received order completion for {}.", orderId); diff --git a/spring-modulith-example/src/main/java/example/inventory/InventorySettings.java b/spring-modulith-example/src/main/java/example/inventory/InventorySettings.java index 6b1f6d4f..031d3a0e 100644 --- a/spring-modulith-example/src/main/java/example/inventory/InventorySettings.java +++ b/spring-modulith-example/src/main/java/example/inventory/InventorySettings.java @@ -15,25 +15,22 @@ */ package example.inventory; -import lombok.AccessLevel; -import lombok.RequiredArgsConstructor; -import lombok.Value; - import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.boot.context.properties.ConstructorBinding; /** * Some Spring Boot configuration properties exposed by the inventory. * * @author Oliver Drotbohm */ -@Value -@RequiredArgsConstructor(access = AccessLevel.PACKAGE, onConstructor = @__(@ConstructorBinding)) @ConfigurationProperties("example.inventory") -class InventorySettings { +record InventorySettings(int stockThreshold) { /** * Some Javadoc. + * + * @return */ - int stockThreshold; + public int stockThreshold() { + return stockThreshold; + } } diff --git a/spring-modulith-example/src/main/java/example/order/Order.java b/spring-modulith-example/src/main/java/example/order/Order.java index 1a5a5d68..c310caa8 100644 --- a/spring-modulith-example/src/main/java/example/order/Order.java +++ b/spring-modulith-example/src/main/java/example/order/Order.java @@ -15,20 +15,18 @@ */ package example.order; -import lombok.Value; - import java.util.UUID; /** * @author Oliver Drotbohm */ -@Value public class Order { private OrderIdentifier id = new OrderIdentifier(UUID.randomUUID()); - @Value - public static class OrderIdentifier { - UUID id; + public OrderIdentifier getId() { + return id; } + + public static record OrderIdentifier(UUID id) {} } diff --git a/spring-modulith-example/src/main/java/example/order/OrderCompleted.java b/spring-modulith-example/src/main/java/example/order/OrderCompleted.java index 1b9999b0..91f14e1d 100644 --- a/spring-modulith-example/src/main/java/example/order/OrderCompleted.java +++ b/spring-modulith-example/src/main/java/example/order/OrderCompleted.java @@ -16,14 +16,10 @@ package example.order; import example.order.Order.OrderIdentifier; -import lombok.Value; import org.jmolecules.event.types.DomainEvent; /** * @author Oliver Drotbohm */ -@Value -public class OrderCompleted implements DomainEvent { - OrderIdentifier orderId; -} +public record OrderCompleted(OrderIdentifier orderId) implements DomainEvent {} diff --git a/spring-modulith-example/src/main/java/example/order/OrderManagement.java b/spring-modulith-example/src/main/java/example/order/OrderManagement.java index aed3e720..5bbe1693 100644 --- a/spring-modulith-example/src/main/java/example/order/OrderManagement.java +++ b/spring-modulith-example/src/main/java/example/order/OrderManagement.java @@ -16,7 +16,6 @@ package example.order; import example.order.internal.OrderInternal; -import lombok.RequiredArgsConstructor; import org.springframework.context.ApplicationEventPublisher; import org.springframework.stereotype.Service; @@ -26,11 +25,16 @@ import org.springframework.transaction.annotation.Transactional; * @author Oliver Drotbohm */ @Service -@RequiredArgsConstructor public class OrderManagement { - private final ApplicationEventPublisher events; - private final OrderInternal dependency; + final ApplicationEventPublisher events; + final OrderInternal dependency; + + OrderManagement(ApplicationEventPublisher events, OrderInternal dependency) { + + this.events = events; + this.dependency = dependency; + } @Transactional public void complete(Order order) { diff --git a/spring-modulith-example/src/test/java/example/order/EventPublicationRegistryTests.java b/spring-modulith-example/src/test/java/example/order/EventPublicationRegistryTests.java index 6e4f86c1..55d74752 100644 --- a/spring-modulith-example/src/test/java/example/order/EventPublicationRegistryTests.java +++ b/spring-modulith-example/src/test/java/example/order/EventPublicationRegistryTests.java @@ -18,7 +18,6 @@ package example.order; import static org.assertj.core.api.Assertions.*; import example.order.EventPublicationRegistryTests.FailingAsyncTransactionalEventListener; -import lombok.RequiredArgsConstructor; import org.junit.jupiter.api.Test; import org.springframework.context.annotation.Import; @@ -34,7 +33,6 @@ import org.springframework.test.annotation.DirtiesContext; * @author Oliver Drotbohm */ @ApplicationModuleTest -@RequiredArgsConstructor @Import(FailingAsyncTransactionalEventListener.class) @DirtiesContext class EventPublicationRegistryTests { @@ -42,6 +40,15 @@ class EventPublicationRegistryTests { private final OrderManagement orders; private final EventPublicationRegistry registry; + /** + * @param orders + * @param registry + */ + EventPublicationRegistryTests(OrderManagement orders, EventPublicationRegistry registry) { + this.orders = orders; + this.registry = registry; + } + @Test void leavesPublicationIncompleteForFailingListener() throws Exception { diff --git a/spring-modulith-example/src/test/java/example/order/OrderIntegrationTests.java b/spring-modulith-example/src/test/java/example/order/OrderIntegrationTests.java index 04e8deb6..d8884dc0 100644 --- a/spring-modulith-example/src/test/java/example/order/OrderIntegrationTests.java +++ b/spring-modulith-example/src/test/java/example/order/OrderIntegrationTests.java @@ -17,8 +17,6 @@ package example.order; import static org.assertj.core.api.Assertions.*; -import lombok.RequiredArgsConstructor; - import org.junit.jupiter.api.Test; import org.springframework.modulith.test.ApplicationModuleTest; import org.springframework.modulith.test.AssertablePublishedEvents; @@ -27,11 +25,14 @@ import org.springframework.modulith.test.AssertablePublishedEvents; * @author Oliver Drotbohm */ @ApplicationModuleTest -@RequiredArgsConstructor class OrderIntegrationTests { private final OrderManagement orders; + OrderIntegrationTests(OrderManagement orders) { + this.orders = orders; + } + @Test void publishesOrderCompletion(AssertablePublishedEvents events) { @@ -42,7 +43,7 @@ class OrderIntegrationTests { // Verification var matchingMapped = events.ofType(OrderCompleted.class) - .matching(OrderCompleted::getOrderId, reference.getId()); + .matching(OrderCompleted::orderId, reference.getId()); assertThat(matchingMapped).hasSize(1); @@ -50,6 +51,6 @@ class OrderIntegrationTests { assertThat(events) .contains(OrderCompleted.class) - .matching(OrderCompleted::getOrderId, reference.getId()); + .matching(OrderCompleted::orderId, reference.getId()); } } diff --git a/spring-modulith-integration-test/src/main/java/com/acme/myproject/moduleA/ServiceComponentA.java b/spring-modulith-integration-test/src/main/java/com/acme/myproject/moduleA/ServiceComponentA.java index 03f55873..1750a940 100644 --- a/spring-modulith-integration-test/src/main/java/com/acme/myproject/moduleA/ServiceComponentA.java +++ b/spring-modulith-integration-test/src/main/java/com/acme/myproject/moduleA/ServiceComponentA.java @@ -15,8 +15,6 @@ */ package com.acme.myproject.moduleA; -import lombok.RequiredArgsConstructor; - import org.springframework.context.ApplicationEventPublisher; import org.springframework.stereotype.Component; @@ -24,11 +22,14 @@ import org.springframework.stereotype.Component; * @author Oliver Drotbohm */ @Component -@RequiredArgsConstructor public class ServiceComponentA { private final ApplicationEventPublisher publisher; + ServiceComponentA(ApplicationEventPublisher publisher) { + this.publisher = publisher; + } + public void fireEvent() { publisher.publishEvent(new SomeEventA("Message")); } diff --git a/spring-modulith-integration-test/src/main/java/com/acme/myproject/moduleA/SomeEventA.java b/spring-modulith-integration-test/src/main/java/com/acme/myproject/moduleA/SomeEventA.java index c6275fce..264a2d99 100644 --- a/spring-modulith-integration-test/src/main/java/com/acme/myproject/moduleA/SomeEventA.java +++ b/spring-modulith-integration-test/src/main/java/com/acme/myproject/moduleA/SomeEventA.java @@ -15,15 +15,10 @@ */ package com.acme.myproject.moduleA; -import lombok.Value; - import org.jmolecules.event.annotation.DomainEvent; /** * @author Oliver Drotbohm */ -@Value @DomainEvent -public class SomeEventA { - String message; -} +public record SomeEventA(String message) {} diff --git a/spring-modulith-integration-test/src/main/java/com/acme/myproject/moduleB/ServiceComponentB.java b/spring-modulith-integration-test/src/main/java/com/acme/myproject/moduleB/ServiceComponentB.java index dc0aefc6..e1a7e949 100644 --- a/spring-modulith-integration-test/src/main/java/com/acme/myproject/moduleB/ServiceComponentB.java +++ b/spring-modulith-integration-test/src/main/java/com/acme/myproject/moduleB/ServiceComponentB.java @@ -15,8 +15,6 @@ */ package com.acme.myproject.moduleB; -import lombok.RequiredArgsConstructor; - import org.springframework.stereotype.Component; import com.acme.myproject.moduleA.ServiceComponentA; @@ -26,9 +24,13 @@ import com.acme.myproject.moduleB.internal.InternalComponentB; * @author Oliver Drotbohm */ @Component -@RequiredArgsConstructor public class ServiceComponentB { - private final ServiceComponentA serviceComponentA; - private final InternalComponentB internalComponentB; + final ServiceComponentA serviceComponentA; + final InternalComponentB internalComponentB; + + ServiceComponentB(ServiceComponentA serviceComponentA, InternalComponentB internalComponentB) { + this.serviceComponentA = serviceComponentA; + this.internalComponentB = internalComponentB; + } } diff --git a/spring-modulith-integration-test/src/main/java/com/acme/myproject/moduleC/ServiceComponentC.java b/spring-modulith-integration-test/src/main/java/com/acme/myproject/moduleC/ServiceComponentC.java index 65a2ca43..ddda9089 100644 --- a/spring-modulith-integration-test/src/main/java/com/acme/myproject/moduleC/ServiceComponentC.java +++ b/spring-modulith-integration-test/src/main/java/com/acme/myproject/moduleC/ServiceComponentC.java @@ -15,8 +15,6 @@ */ package com.acme.myproject.moduleC; -import lombok.RequiredArgsConstructor; - import org.springframework.stereotype.Component; import com.acme.myproject.moduleB.ServiceComponentB; @@ -25,7 +23,11 @@ import com.acme.myproject.moduleB.ServiceComponentB; * @author Oliver Drotbohm */ @Component -@RequiredArgsConstructor class ServiceComponentC { - private final ServiceComponentB serviceComponentB; + + final ServiceComponentB serviceComponentB; + + ServiceComponentC(ServiceComponentB serviceComponentB) { + this.serviceComponentB = serviceComponentB; + } } diff --git a/spring-modulith-integration-test/src/main/java/com/acme/myproject/moduleD/ConfigurationPropertiesD.java b/spring-modulith-integration-test/src/main/java/com/acme/myproject/moduleD/ConfigurationPropertiesD.java index a7073955..62931d6f 100644 --- a/spring-modulith-integration-test/src/main/java/com/acme/myproject/moduleD/ConfigurationPropertiesD.java +++ b/spring-modulith-integration-test/src/main/java/com/acme/myproject/moduleD/ConfigurationPropertiesD.java @@ -15,8 +15,6 @@ */ package com.acme.myproject.moduleD; -import lombok.RequiredArgsConstructor; - import org.springframework.boot.context.properties.ConfigurationProperties; import com.acme.myproject.moduleC.SomeValueC; @@ -25,8 +23,11 @@ import com.acme.myproject.moduleC.SomeValueC; * @author Oliver Drotbohm */ @ConfigurationProperties -@RequiredArgsConstructor class ConfigurationPropertiesD { - private final SomeValueC value; + final SomeValueC value; + + ConfigurationPropertiesD(SomeValueC value) { + this.value = value; + } } diff --git a/spring-modulith-integration-test/src/main/java/com/acme/myproject/stereotypes/Stereotypes.java b/spring-modulith-integration-test/src/main/java/com/acme/myproject/stereotypes/Stereotypes.java index 81d44992..56984162 100644 --- a/spring-modulith-integration-test/src/main/java/com/acme/myproject/stereotypes/Stereotypes.java +++ b/spring-modulith-integration-test/src/main/java/com/acme/myproject/stereotypes/Stereotypes.java @@ -15,8 +15,6 @@ */ package com.acme.myproject.stereotypes; -import lombok.Value; - import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.event.EventListener; import org.springframework.stereotype.Component; @@ -65,17 +63,6 @@ public class Stereotypes { @Component static class SomeAppInterfaceImplementation implements SomeAppInterface {} - @Value @ConfigurationProperties("org.springframework.modulith.sample") - static class SomeConfigurationProperties { - - /** - * Some test property. - */ - String test; - - public SomeConfigurationProperties(String test) { - this.test = test; - } - } + static record SomeConfigurationProperties(String test) {} } diff --git a/spring-modulith-integration-test/src/test/java/com/acme/myproject/moduleA/ModuleATest.java b/spring-modulith-integration-test/src/test/java/com/acme/myproject/moduleA/ModuleATest.java index c5354750..f9088551 100644 --- a/spring-modulith-integration-test/src/test/java/com/acme/myproject/moduleA/ModuleATest.java +++ b/spring-modulith-integration-test/src/test/java/com/acme/myproject/moduleA/ModuleATest.java @@ -51,7 +51,7 @@ class ModuleATest { context.getBean(ServiceComponentA.class).fireEvent(); TypedPublishedEvents matching = events.ofType(SomeEventA.class) // - .matching(it -> it.getMessage().equals("Message")); + .matching(it -> it.message().equals("Message")); assertThat(matching).hasSize(1); } diff --git a/spring-modulith-integration-test/src/test/java/org/springframework/modulith/docs/DocumenterUnitTests.java b/spring-modulith-integration-test/src/test/java/org/springframework/modulith/docs/DocumenterUnitTests.java index 7d8cbd73..15ae5cba 100644 --- a/spring-modulith-integration-test/src/test/java/org/springframework/modulith/docs/DocumenterUnitTests.java +++ b/spring-modulith-integration-test/src/test/java/org/springframework/modulith/docs/DocumenterUnitTests.java @@ -48,7 +48,7 @@ class DocumenterUnitTests { .groupBeans(modules.getModuleByName("stereotypes").orElseThrow(RuntimeException::new)); assertThat(result.keySet()) - .extracting(CanvasOptions.Grouping::getName) + .extracting(it -> it.getName()) .containsExactlyInAnyOrder("Controllers", "Services", "Repositories", "Event listeners", "Configuration properties", "Representations", "Interface implementations", "Others"); diff --git a/spring-modulith-moments/src/main/java/org/springframework/modulith/moments/DayHasPassed.java b/spring-modulith-moments/src/main/java/org/springframework/modulith/moments/DayHasPassed.java index 0e82bdd5..937a9d65 100644 --- a/spring-modulith-moments/src/main/java/org/springframework/modulith/moments/DayHasPassed.java +++ b/spring-modulith-moments/src/main/java/org/springframework/modulith/moments/DayHasPassed.java @@ -15,22 +15,78 @@ */ package org.springframework.modulith.moments; -import lombok.Value; - import java.time.LocalDate; +import java.util.Objects; import org.jmolecules.event.types.DomainEvent; +import org.springframework.util.Assert; /** * A {@link DomainEvent} published on each day. * * @author Oliver Drotbohm */ -@Value(staticConstructor = "of") public class DayHasPassed implements DomainEvent { /** * The day that has just passed. */ private final LocalDate date; + + /** + * Creates a new {@link DayHasPassed} for the given {@link LocalDate}. + * + * @param month must not be {@literal null}. + */ + private DayHasPassed(LocalDate date) { + + Assert.notNull(date, "LocalDate must not be null!"); + + this.date = date; + } + + /** + * Creates a new {@link DayHasPassed} for the given {@link LocalDate}. + * + * @param month must not be {@literal null}. + */ + public static DayHasPassed of(LocalDate date) { + return new DayHasPassed(date); + } + + /** + * The day that has just passed. + * + * @return will never be {@literal null}. + */ + public LocalDate getDate() { + return date; + } + + /* + * (non-Javadoc) + * @see java.lang.Object#equals(java.lang.Object) + */ + @Override + public boolean equals(Object obj) { + + if (this == obj) { + return true; + } + + if (!(obj instanceof DayHasPassed that)) { + return false; + } + + return Objects.equals(date, that.date); + } + + /* + * (non-Javadoc) + * @see java.lang.Object#hashCode() + */ + @Override + public int hashCode() { + return Objects.hash(date); + } } diff --git a/spring-modulith-moments/src/main/java/org/springframework/modulith/moments/HourHasPassed.java b/spring-modulith-moments/src/main/java/org/springframework/modulith/moments/HourHasPassed.java index f71e7d1c..99220f7b 100644 --- a/spring-modulith-moments/src/main/java/org/springframework/modulith/moments/HourHasPassed.java +++ b/spring-modulith-moments/src/main/java/org/springframework/modulith/moments/HourHasPassed.java @@ -15,22 +15,78 @@ */ package org.springframework.modulith.moments; -import lombok.Value; - import java.time.LocalDateTime; +import java.util.Objects; import org.jmolecules.event.types.DomainEvent; +import org.springframework.util.Assert; /** * A {@link DomainEvent} published on each day. * * @author Oliver Drotbohm */ -@Value(staticConstructor = "of") public class HourHasPassed implements DomainEvent { /** * The hour that has just passed. */ private final LocalDateTime time; + + /** + * Creates a new {@link HourHasPassed} for the given {@link LocalDateTime}. + * + * @param time must not be {@literal null}. + */ + private HourHasPassed(LocalDateTime time) { + + Assert.notNull(time, "YearMonth must not be null!"); + + this.time = time; + } + + /** + * Creates a new {@link HourHasPassed} for the given {@link LocalDateTime}. + * + * @param time must not be {@literal null}. + */ + public static HourHasPassed of(LocalDateTime time) { + return new HourHasPassed(time); + } + + /** + * The hour that has just passed. + * + * @return will never be {@literal null}. + */ + public LocalDateTime getTime() { + return time; + } + + /* + * (non-Javadoc) + * @see java.lang.Object#equals(java.lang.Object) + */ + @Override + public boolean equals(Object obj) { + + if (this == obj) { + return true; + } + + if (!(obj instanceof HourHasPassed that)) { + return false; + } + + return Objects.equals(time, that.time); + } + + /* + * (non-Javadoc) + * @see java.lang.Object#hashCode() + */ + @Override + public int hashCode() { + return Objects.hash(time); + } } diff --git a/spring-modulith-moments/src/main/java/org/springframework/modulith/moments/MonthHasPassed.java b/spring-modulith-moments/src/main/java/org/springframework/modulith/moments/MonthHasPassed.java index 994eb90b..47be733b 100644 --- a/spring-modulith-moments/src/main/java/org/springframework/modulith/moments/MonthHasPassed.java +++ b/spring-modulith-moments/src/main/java/org/springframework/modulith/moments/MonthHasPassed.java @@ -15,23 +15,75 @@ */ package org.springframework.modulith.moments; -import lombok.Value; - import java.time.YearMonth; +import java.util.Objects; import org.jmolecules.event.types.DomainEvent; +import org.springframework.util.Assert; /** * A {@link DomainEvent} published on the last day of the month. * * @author Oliver Drotbohm - * @since 1.3 */ -@Value(staticConstructor = "of") public class MonthHasPassed implements DomainEvent { + private final YearMonth month; + + /** + * Creates a new {@link MonthHasPassed} for the given {@link YearMonth}. + * + * @param month must not be {@literal null}. + */ + private MonthHasPassed(YearMonth month) { + + Assert.notNull(month, "YearMonth must not be null!"); + + this.month = month; + } + + /** + * Creates a new {@link MonthHasPassed} for the given {@link YearMonth}. + * + * @param month must not be {@literal null}. + */ + public static MonthHasPassed of(YearMonth month) { + return new MonthHasPassed(month); + } + /** * The month that has just passed. + * + * @return will never be {@literal null}. */ - private final YearMonth month; + public YearMonth getMonth() { + return month; + } + + /* + * (non-Javadoc) + * @see java.lang.Object#equals(java.lang.Object) + */ + @Override + public boolean equals(Object obj) { + + if (this == obj) { + return true; + } + + if (!(obj instanceof MonthHasPassed that)) { + return false; + } + + return Objects.equals(month, that.month); + } + + /* + * (non-Javadoc) + * @see java.lang.Object#hashCode() + */ + @Override + public int hashCode() { + return Objects.hash(month); + } } diff --git a/spring-modulith-moments/src/main/java/org/springframework/modulith/moments/Quarter.java b/spring-modulith-moments/src/main/java/org/springframework/modulith/moments/Quarter.java index 7536dde5..5a5ccd66 100644 --- a/spring-modulith-moments/src/main/java/org/springframework/modulith/moments/Quarter.java +++ b/spring-modulith-moments/src/main/java/org/springframework/modulith/moments/Quarter.java @@ -17,19 +17,16 @@ package org.springframework.modulith.moments; import static java.time.MonthDay.*; -import lombok.AccessLevel; -import lombok.Getter; -import lombok.RequiredArgsConstructor; - import java.time.Month; import java.time.MonthDay; +import org.springframework.util.Assert; + /** * A logical {@link Quarter} of the year. * * @author Oliver Drotbohm */ -@RequiredArgsConstructor(access = AccessLevel.PRIVATE) public enum Quarter { Q1(of(Month.JANUARY, 1), of(Month.MARCH, 31)), // @@ -37,7 +34,40 @@ public enum Quarter { Q3(of(Month.JULY, 1), of(Month.SEPTEMBER, 30)), // Q4(of(Month.OCTOBER, 1), of(Month.DECEMBER, 31)); - private final @Getter MonthDay start, end; + private final MonthDay start, end; + + /** + * Creates a new {@link Quarter} for the given start and end {@link MonthDay}. + * + * @param start must not be {@literal null}. + * @param end must not be {@literal null}. + */ + private Quarter(MonthDay start, MonthDay end) { + + Assert.notNull(start, "Start MonthDay must not be null!"); + Assert.notNull(end, "End MonthDay must not be null!"); + + this.start = start; + this.end = end; + } + + /** + * Returns the start {@link MonthDay}. + * + * @return will never be {@literal null}. + */ + public MonthDay getStart() { + return start; + } + + /** + * Returns the end {@link MonthDay}. + * + * @return will never be {@literal null}. + */ + public MonthDay getEnd() { + return end; + } /** * Returns the next logical {@link Quarter}. diff --git a/spring-modulith-moments/src/main/java/org/springframework/modulith/moments/QuarterHasPassed.java b/spring-modulith-moments/src/main/java/org/springframework/modulith/moments/QuarterHasPassed.java index b395b0a9..9a810897 100644 --- a/spring-modulith-moments/src/main/java/org/springframework/modulith/moments/QuarterHasPassed.java +++ b/spring-modulith-moments/src/main/java/org/springframework/modulith/moments/QuarterHasPassed.java @@ -15,25 +15,49 @@ */ package org.springframework.modulith.moments; -import lombok.NonNull; -import lombok.Value; - import java.time.LocalDate; import java.time.Month; import java.time.Year; +import java.util.Objects; import org.jmolecules.event.types.DomainEvent; +import org.springframework.util.Assert; /** * A {@link DomainEvent} published once a quarter has passed. * * @author Oliver Drotbohm */ -@Value(staticConstructor = "of") public class QuarterHasPassed implements DomainEvent { - private final @NonNull Year year; - private final @NonNull ShiftedQuarter quarter; + private final Year year; + private final ShiftedQuarter quarter; + + /** + * Creates a new {@link QuarterHasPassed} for the given {@link Year} and {@link ShiftedQuarter}. + * + * @param year must not be {@literal null}. + * @param quarter must not be {@literal null}. + */ + private QuarterHasPassed(Year year, ShiftedQuarter quarter) { + + Assert.notNull(year, "Year must not be null!"); + Assert.notNull(quarter, "ShiftedQuarter must not be null!"); + + this.year = year; + this.quarter = quarter; + } + + /** + * Returns a {@link QuarterHasPassed} for the given {@link Year} and {@link ShiftedQuarter}. + * + * @param year must not be {@literal null}. + * @param quarter must not be {@literal null}. + * @return will never be {@literal null}. + */ + public static QuarterHasPassed of(Year year, ShiftedQuarter quarter) { + return new QuarterHasPassed(year, quarter); + } /** * Returns a {@link QuarterHasPassed} for the given {@link Year} and logical {@link Quarter}. @@ -75,4 +99,32 @@ public class QuarterHasPassed implements DomainEvent { public LocalDate getEndDate() { return quarter.getEndDate(year); } + + /* + * (non-Javadoc) + * @see java.lang.Object#equals(java.lang.Object) + */ + @Override + public boolean equals(Object obj) { + + if (this == obj) { + return true; + } + + if (!(obj instanceof QuarterHasPassed that)) { + return false; + } + + return Objects.equals(quarter, that.quarter) // + && Objects.equals(year, that.year); + } + + /* + * (non-Javadoc) + * @see java.lang.Object#hashCode() + */ + @Override + public int hashCode() { + return Objects.hash(quarter, year); + } } diff --git a/spring-modulith-moments/src/main/java/org/springframework/modulith/moments/ShiftedQuarter.java b/spring-modulith-moments/src/main/java/org/springframework/modulith/moments/ShiftedQuarter.java index 2ffaedef..18b26c3d 100644 --- a/spring-modulith-moments/src/main/java/org/springframework/modulith/moments/ShiftedQuarter.java +++ b/spring-modulith-moments/src/main/java/org/springframework/modulith/moments/ShiftedQuarter.java @@ -15,15 +15,11 @@ */ package org.springframework.modulith.moments; -import lombok.AccessLevel; -import lombok.Getter; -import lombok.NonNull; -import lombok.Value; - import java.time.LocalDate; import java.time.Month; import java.time.MonthDay; import java.time.Year; +import java.util.Objects; import java.util.stream.Stream; import org.springframework.util.Assert; @@ -33,14 +29,38 @@ import org.springframework.util.Assert; * * @author Oliver Drotbohm */ -@Value(staticConstructor = "of") public class ShiftedQuarter { private static final MonthDay FIRST_DAY = MonthDay.of(Month.JANUARY, 1); private static final MonthDay LAST_DAY = MonthDay.of(Month.DECEMBER, 31); - private final @NonNull Quarter quarter; - private final @NonNull @Getter(AccessLevel.NONE) Month startMonth; + private final Quarter quarter; + private final Month startMonth; + + /** + * Creates a new {@link ShiftedQuarter} for the given {@link Quarter} and start {@link Month}. + * + * @param quarter must not be {@literal null}. + * @param startMonth must not be {@literal null}. + */ + private ShiftedQuarter(Quarter quarter, Month startMonth) { + + Assert.notNull(quarter, "Quarter must not be null!"); + Assert.notNull(startMonth, "Start Month must not be null!"); + + this.quarter = quarter; + this.startMonth = startMonth; + } + + /** + * Creates a new {@link ShiftedQuarter} for the given {@link Quarter} and start {@link Month}. + * + * @param quarter must not be {@literal null}. + * @param startMonth must not be {@literal null}. + */ + public static ShiftedQuarter of(Quarter quarter, Month startMonth) { + return new ShiftedQuarter(quarter, startMonth); + } /*+ * Creates a new ShiftedQuarter for the given logical {@link Quarter}. @@ -52,6 +72,15 @@ public class ShiftedQuarter { return new ShiftedQuarter(quarter, Month.JANUARY); } + /** + * Returns the logical {@link Quarter}. + * + * @return will never be {@literal null}. + */ + public Quarter getQuarter() { + return quarter; + } + /** * Returns the next {@link ShiftedQuarter}. * @@ -71,25 +100,36 @@ public class ShiftedQuarter { Assert.notNull(date, "Reference date must not be null!"); - MonthDay shiftedStart = getStart(); - MonthDay shiftedEnd = getEnd(); - MonthDay reference = MonthDay.from(date); + var shiftedStart = getStart(); + var shiftedEnd = getEnd(); + var reference = MonthDay.from(date); - Stream ranges = shiftedEnd.isAfter(shiftedStart) - ? Stream.of(Range.of(shiftedStart, shiftedEnd)) - : Stream.of(Range.of(shiftedStart, LAST_DAY), Range.of(FIRST_DAY, shiftedEnd)); + var ranges = shiftedEnd.isAfter(shiftedStart) + ? Stream.of(new Range(shiftedStart, shiftedEnd)) + : Stream.of(new Range(shiftedStart, LAST_DAY), new Range(FIRST_DAY, shiftedEnd)); return ranges.anyMatch(it -> it.contains(reference)); } + /** + * @return will never be {@literal null}. + */ public MonthDay getStart() { return getShifted(quarter.getStart()); } + /** + * @return will never be {@literal null}. + */ public MonthDay getEnd() { return getShifted(quarter.getEnd()); } + /** + * Returns whether the given {@link LocalDate} is the last day of the {@link ShiftedQuarter}. + * + * @param date must not be {@literal null}. + */ public boolean isLastDay(LocalDate date) { return MonthDay.from(date).equals(getEnd()); } @@ -122,14 +162,39 @@ public class ShiftedQuarter { return getStartDate(year).plusMonths(3).minusDays(1); } + /* + * (non-Javadoc) + * @see java.lang.Object#equals(java.lang.Object) + */ + @Override + public boolean equals(Object obj) { + + if (this == obj) { + return true; + } + + if (!(obj instanceof ShiftedQuarter that)) { + return false; + } + + return quarter == that.quarter // + && startMonth == that.startMonth; + } + + /* + * (non-Javadoc) + * @see java.lang.Object#hashCode() + */ + @Override + public int hashCode() { + return Objects.hash(quarter, startMonth); + } + private MonthDay getShifted(MonthDay source) { return source.with(source.getMonth().plus(startMonth.getValue() - 1)); } - @Value(staticConstructor = "of") - private static class Range { - - MonthDay start, end; + private static record Range(MonthDay start, MonthDay end) { public boolean contains(MonthDay day) { diff --git a/spring-modulith-moments/src/main/java/org/springframework/modulith/moments/WeekHasPassed.java b/spring-modulith-moments/src/main/java/org/springframework/modulith/moments/WeekHasPassed.java index 40013cbf..4d2e224e 100644 --- a/spring-modulith-moments/src/main/java/org/springframework/modulith/moments/WeekHasPassed.java +++ b/spring-modulith-moments/src/main/java/org/springframework/modulith/moments/WeekHasPassed.java @@ -15,18 +15,15 @@ */ package org.springframework.modulith.moments; -import lombok.AccessLevel; -import lombok.Getter; -import lombok.NonNull; -import lombok.Value; - import java.time.LocalDate; import java.time.Year; import java.time.temporal.ChronoField; import java.time.temporal.WeekFields; import java.util.Locale; +import java.util.Objects; import org.jmolecules.event.types.DomainEvent; +import org.springframework.util.Assert; /** * A {@link DomainEvent} published if a week has passed. The semantics of what constitutes are depended on the @@ -34,23 +31,41 @@ import org.jmolecules.event.types.DomainEvent; * * @author Oliver Drotbohm */ -@Value(staticConstructor = "of") public class WeekHasPassed implements DomainEvent { - /** - * The year of the week that has just passed. - */ - private final @NonNull Year year; - - /** - * The week of the {@link Year} that has just passed. - */ + private final Year year; private final int week; + private final Locale locale; /** - * The {@link Locale} to be used to calculate the start date of the week. + * Creates a new {@link WeekHasPassed} for the given {@link Year}, week and {@link Locale}. + * + * @param year must not be {@literal null}. + * @param week must be between 0 and 53 (inclusive). + * @param locale must not be {@literal null}. */ - private final @NonNull @Getter(AccessLevel.NONE) Locale locale; + WeekHasPassed(Year year, int week, Locale locale) { + + Assert.notNull(year, "Year must not be null!"); + Assert.isTrue(week >= 0 && week <= 53, "Week must be between 0 and 53!"); + Assert.notNull(locale, "Locale must not be null!"); + + this.year = year; + this.week = week; + this.locale = locale; + } + + /** + * Creates a new {@link WeekHasPassed} for the given {@link Year}, week and {@link Locale}. + * + * @param year must not be {@literal null}. + * @param week must be between 0 and 53 (inclusive). + * @param locale must not be {@literal null}. + * @return will never be {@literal null}. + */ + public static WeekHasPassed of(Year year, int week, Locale locale) { + return new WeekHasPassed(year, week, locale); + } /** * Creates a new {@link WeekHasPassed} for the given {@link Year} and week of the year. @@ -63,6 +78,24 @@ public class WeekHasPassed implements DomainEvent { return WeekHasPassed.of(year, week, Locale.getDefault()); } + /** + * The year of the week that has just passed. + * + * @return will never be {@literal null}. + */ + public Year getYear() { + return year; + } + + /** + * The {@link Locale} to be used to calculate the start date of the week. + * + * @return will never be {@literal null}. + */ + public int getWeek() { + return week; + } + /** * Returns the start date of the week that has passed. * @@ -83,4 +116,40 @@ public class WeekHasPassed implements DomainEvent { public LocalDate getEndDate() { return getStartDate().plusDays(6); } + + /** + * @return will never be {@literal null}. + */ + Locale getLocale() { + return locale; + } + + /* + * (non-Javadoc) + * @see java.lang.Object#equals(java.lang.Object) + */ + @Override + public boolean equals(Object obj) { + + if (this == obj) { + return true; + } + + if (!(obj instanceof WeekHasPassed that)) { + return false; + } + + return week == that.week // + && Objects.equals(year, that.year) // + && Objects.equals(locale, that.locale); + } + + /* + * (non-Javadoc) + * @see java.lang.Object#hashCode() + */ + @Override + public int hashCode() { + return Objects.hash(locale, week, year); + } } diff --git a/spring-modulith-moments/src/main/java/org/springframework/modulith/moments/YearHasPassed.java b/spring-modulith-moments/src/main/java/org/springframework/modulith/moments/YearHasPassed.java index 2bc5e3da..a5e841d7 100644 --- a/spring-modulith-moments/src/main/java/org/springframework/modulith/moments/YearHasPassed.java +++ b/spring-modulith-moments/src/main/java/org/springframework/modulith/moments/YearHasPassed.java @@ -15,27 +15,44 @@ */ package org.springframework.modulith.moments; -import lombok.Value; - import java.time.LocalDate; import java.time.Month; import java.time.Year; +import java.util.Objects; import org.jmolecules.event.types.DomainEvent; +import org.springframework.util.Assert; /** * A {@link DomainEvent} published on the last day of the year. * * @author Oliver Drotbohm */ -@Value(staticConstructor = "of") public class YearHasPassed implements DomainEvent { - /** - * The month that has just passed. - */ private final Year year; + /** + * Creates a new {@link YearHasPassed} for the given {@link Year}. + * + * @param year must not be {@literal null}. + */ + private YearHasPassed(Year year) { + + Assert.notNull(year, "Year must not be null!"); + + this.year = year; + } + + /** + * Creates a new {@link YearHasPassed} for the given {@link Year}. + * + * @param year must not be {@literal null}. + */ + public static YearHasPassed of(Year year) { + return new YearHasPassed(year); + } + /** * Creates a new {@link YearHasPassed} event for the given year. * @@ -46,6 +63,15 @@ public class YearHasPassed implements DomainEvent { return of(Year.of(year)); } + /** + * The {@link Year} that has just passed. + * + * @return will never be {@literal null}. + */ + public Year getYear() { + return year; + } + /** * Returns the start date of the year passed. * @@ -63,4 +89,31 @@ public class YearHasPassed implements DomainEvent { LocalDate getEndDate() { return LocalDate.of(year.getValue(), Month.DECEMBER, 31); } + + /* + * (non-Javadoc) + * @see java.lang.Object#equals(java.lang.Object) + */ + @Override + public boolean equals(Object obj) { + + if (this == obj) { + return true; + } + + if (!(obj instanceof YearHasPassed that)) { + return false; + } + + return Objects.equals(year, that.year); + } + + /* + * (non-Javadoc) + * @see java.lang.Object#hashCode() + */ + @Override + public int hashCode() { + return Objects.hash(year); + } } diff --git a/spring-modulith-moments/src/main/java/org/springframework/modulith/moments/support/Moments.java b/spring-modulith-moments/src/main/java/org/springframework/modulith/moments/support/Moments.java index 7a6fd2ad..d2cf0a8f 100644 --- a/spring-modulith-moments/src/main/java/org/springframework/modulith/moments/support/Moments.java +++ b/spring-modulith-moments/src/main/java/org/springframework/modulith/moments/support/Moments.java @@ -15,9 +15,6 @@ */ package org.springframework.modulith.moments.support; -import lombok.NonNull; -import lombok.RequiredArgsConstructor; - import java.time.Clock; import java.time.Duration; import java.time.Instant; @@ -39,21 +36,40 @@ import org.springframework.modulith.moments.ShiftedQuarter; import org.springframework.modulith.moments.WeekHasPassed; import org.springframework.modulith.moments.YearHasPassed; import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.util.Assert; /** * @author Oliver Drotbohm */ -@RequiredArgsConstructor public class Moments { private static final MonthDay DEC_31ST = MonthDay.of(Month.DECEMBER, 31); - private final @NonNull Clock clock; - private final @NonNull ApplicationEventPublisher events; - private final @NonNull MomentsProperties properties; + private final Clock clock; + private final ApplicationEventPublisher events; + private final MomentsProperties properties; private Duration shift = Duration.ZERO; + /** + * Creates a new {@link Moments} for the given {@link Clock}, {@link ApplicationEventPublisher} and + * {@link MomentsProperties}. + * + * @param clock must not be {@literal null}. + * @param events must not be {@literal null}. + * @param properties must not be {@literal null}. + */ + public Moments(Clock clock, ApplicationEventPublisher events, MomentsProperties properties) { + + Assert.notNull(clock, "Clock must not be null!"); + Assert.notNull(events, "ApplicationEventPublisher must not be null!"); + Assert.notNull(properties, "MomentsProperties must not be null!"); + + this.clock = clock; + this.events = events; + this.properties = properties; + } + /** * Triggers event publication every hour. */ @@ -73,48 +89,6 @@ public class Moments { emitEventsFor(now().toLocalDate().minusDays(1)); } - void emitEventsFor(LocalDateTime time) { - events.publishEvent(HourHasPassed.of(time.truncatedTo(ChronoUnit.HOURS))); - } - - void emitEventsFor(LocalDate date) { - - // Day has passed - events.publishEvent(DayHasPassed.of(date)); - - var year = Year.from(date); - - // Week has passed - var weekFields = WeekFields.of(properties.getLocale()); - var field = weekFields.weekOfWeekBasedYear(); - var currentWeek = date.get(field); - var tomorrowsWeek = date.plusDays(1).get(field); - - if (tomorrowsWeek != currentWeek) { - - var eventYear = Year.of(date.get(weekFields.weekBasedYear())); - - events.publishEvent(WeekHasPassed.of(eventYear, currentWeek, properties.getLocale())); - } - - // Month has passed - if (date.getDayOfMonth() == date.lengthOfMonth()) { - events.publishEvent(MonthHasPassed.of(YearMonth.from(date))); - } - - // Quarter has passed - ShiftedQuarter quarter = properties.getShiftedQuarter(date); - - if (quarter.isLastDay(date)) { - events.publishEvent(QuarterHasPassed.of(year, quarter)); - } - - // Year has passed - if (MonthDay.from(date).equals(DEC_31ST)) { - events.publishEvent(YearHasPassed.of(year)); - } - } - Moments shiftBy(Duration duration) { LocalDateTime before = now(); @@ -153,4 +127,46 @@ public class Moments { return LocalDateTime.ofInstant(instant, properties.getZoneId()); } + + private void emitEventsFor(LocalDateTime time) { + events.publishEvent(HourHasPassed.of(time.truncatedTo(ChronoUnit.HOURS))); + } + + private void emitEventsFor(LocalDate date) { + + // Day has passed + events.publishEvent(DayHasPassed.of(date)); + + var year = Year.from(date); + + // Week has passed + var weekFields = WeekFields.of(properties.getLocale()); + var field = weekFields.weekOfWeekBasedYear(); + var currentWeek = date.get(field); + var tomorrowsWeek = date.plusDays(1).get(field); + + if (tomorrowsWeek != currentWeek) { + + var eventYear = Year.of(date.get(weekFields.weekBasedYear())); + + events.publishEvent(WeekHasPassed.of(eventYear, currentWeek, properties.getLocale())); + } + + // Month has passed + if (date.getDayOfMonth() == date.lengthOfMonth()) { + events.publishEvent(MonthHasPassed.of(YearMonth.from(date))); + } + + // Quarter has passed + ShiftedQuarter quarter = properties.getShiftedQuarter(date); + + if (quarter.isLastDay(date)) { + events.publishEvent(QuarterHasPassed.of(year, quarter)); + } + + // Year has passed + if (MonthDay.from(date).equals(DEC_31ST)) { + events.publishEvent(YearHasPassed.of(year)); + } + } } diff --git a/spring-modulith-moments/src/main/java/org/springframework/modulith/moments/support/MomentsProperties.java b/spring-modulith-moments/src/main/java/org/springframework/modulith/moments/support/MomentsProperties.java index 7d97865d..fb586808 100644 --- a/spring-modulith-moments/src/main/java/org/springframework/modulith/moments/support/MomentsProperties.java +++ b/spring-modulith-moments/src/main/java/org/springframework/modulith/moments/support/MomentsProperties.java @@ -15,11 +15,6 @@ */ package org.springframework.modulith.moments.support; -import lombok.AccessLevel; -import lombok.Getter; -import lombok.RequiredArgsConstructor; -import lombok.With; - import java.time.LocalDate; import java.time.Month; import java.time.ZoneId; @@ -27,10 +22,9 @@ import java.time.ZoneOffset; import java.util.Arrays; import java.util.List; import java.util.Locale; -import java.util.stream.Collectors; import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.boot.context.properties.ConstructorBinding; +import org.springframework.boot.context.properties.bind.ConstructorBinding; import org.springframework.boot.context.properties.bind.DefaultValue; import org.springframework.lang.Nullable; import org.springframework.modulith.moments.Quarter; @@ -43,25 +37,15 @@ import org.springframework.util.Assert; * @author Oliver Drotbohm */ @ConfigurationProperties(prefix = "spring.modulith.moments") -@RequiredArgsConstructor(access = AccessLevel.PRIVATE) public class MomentsProperties { public static final MomentsProperties DEFAULTS = new MomentsProperties(null, null, null, (Month) null, false); - private final @With Granularity granularity; + private final Granularity granularity; + private final ZoneId zoneId; + private final Locale locale; - /** - * The {@link ZoneId} to determine times which are attached to the events published. Defaults to - * {@value ZoneOffset#UTC}. - */ - private final @With @Getter ZoneId zoneId; - - /** - * The {@link Locale} to use when determining week boundaries. Defaults to {@value Locale#getDefault()}. - */ - private final @With @Getter Locale locale; - - private final @Getter boolean enableTimeMachine; + private final boolean enableTimeMachine; private final ShiftedQuarters quarters; @@ -88,9 +72,58 @@ public class MomentsProperties { } /** - * Returns whether to create hourly events. + * Creates a new {@link MomentsProperties} for the given {@link Granularity}, {@link ZoneId}, {@link Locale}, whether + * to enable the {@link TimeMachine} and {@link ShiftedQuarters}. * - * @return + * @param granularity must not be {@literal null}. + * @param zoneId must not be {@literal null}. + * @param locale must not be {@literal null}. + * @param enableTimeMachine + * @param quarters must not be {@literal null}. + */ + private MomentsProperties(Granularity granularity, ZoneId zoneId, Locale locale, boolean enableTimeMachine, + ShiftedQuarters quarters) { + + Assert.notNull(granularity, "Granilarity must not be null!"); + Assert.notNull(zoneId, "ZoneId must not be null!"); + Assert.notNull(locale, "Locale must not be null!"); + Assert.notNull(quarters, "ShiftedQuarters must not be null!"); + + this.granularity = granularity; + this.zoneId = zoneId; + this.locale = locale; + this.enableTimeMachine = enableTimeMachine; + this.quarters = quarters; + } + + /** + * The {@link ZoneId} to determine times which are attached to the events published. Defaults to + * {@value ZoneOffset#UTC}. + * + * @return will never be {@literal null}. + */ + public ZoneId getZoneId() { + return zoneId; + } + + /** + * The {@link Locale} to use when determining week boundaries. Defaults to {@value Locale#getDefault()}. + * + * @return will never be {@literal null}. + */ + public Locale getLocale() { + return locale; + } + + /** + * Whether to enable the {@link TimeMachine}. + */ + public boolean isEnableTimeMachine() { + return enableTimeMachine; + } + + /** + * Returns whether to create hourly events. */ boolean isHourly() { return Granularity.HOURS.equals(granularity); @@ -100,7 +133,7 @@ public class MomentsProperties { * Returns the {@link ShiftedQuarter} for the given reference date. * * @param reference must not be {@literal null}. - * @return + * @return will never be {@literal null}. */ public ShiftedQuarter getShiftedQuarter(LocalDate reference) { @@ -109,15 +142,30 @@ public class MomentsProperties { return quarters.getCurrent(reference); } + MomentsProperties withGranularity(Granularity granularity) { + return new MomentsProperties(granularity, zoneId, locale, enableTimeMachine, quarters); + } + + MomentsProperties withZoneId(ZoneId zoneId) { + return new MomentsProperties(granularity, zoneId, locale, enableTimeMachine, quarters); + } + + MomentsProperties withLocale(Locale locale) { + return new MomentsProperties(granularity, zoneId, locale, enableTimeMachine, quarters); + } + static enum Granularity { HOURS, DAYS; } - @RequiredArgsConstructor private static class ShiftedQuarters { private final List quarters; + private ShiftedQuarters(List quarters) { + this.quarters = quarters; + } + public ShiftedQuarter getCurrent(LocalDate reference) { return quarters.stream() diff --git a/spring-modulith-observability/src/main/java/org/springframework/modulith/observability/DefaultObservedModule.java b/spring-modulith-observability/src/main/java/org/springframework/modulith/observability/DefaultObservedModule.java index 28035bd0..4bedc5ae 100644 --- a/spring-modulith-observability/src/main/java/org/springframework/modulith/observability/DefaultObservedModule.java +++ b/spring-modulith-observability/src/main/java/org/springframework/modulith/observability/DefaultObservedModule.java @@ -15,8 +15,6 @@ */ package org.springframework.modulith.observability; -import lombok.RequiredArgsConstructor; - import java.lang.reflect.Method; import java.util.Arrays; @@ -31,11 +29,22 @@ import org.springframework.util.Assert; import com.tngtech.archunit.core.domain.JavaClass; -@RequiredArgsConstructor class DefaultObservedModule implements ObservedModule { private final ApplicationModule module; + /** + * Creates a new {@link DefaultObservedModule} for the given {@link ApplicationModule}. + * + * @param module must not be {@literal null}. + */ + DefaultObservedModule(ApplicationModule module) { + + Assert.notNull(module, "ApplicationModule must not be null!"); + + this.module = module; + } + /* * (non-Javadoc) * @see org.springframework.modulith.observability.ObservedModule#getName() @@ -73,8 +82,8 @@ class DefaultObservedModule implements ObservedModule { // For class-based proxies, use the target class - Advised advised = (Advised) ((ProxyMethodInvocation) invocation).getProxy(); - Class targetClass = advised.getTargetClass(); + var advised = (Advised) ((ProxyMethodInvocation) invocation).getProxy(); + var targetClass = advised.getTargetClass(); if (module.contains(targetClass)) { return toString(targetClass, method, module); @@ -141,11 +150,11 @@ class DefaultObservedModule implements ObservedModule { private static String toString(Class type, Method method, ApplicationModule module) { - String typeName = module.getType(type.getName()) + var typeName = module.getType(type.getName()) .map(FormatableType::of) .map(FormatableType::getAbbreviatedFullName) .orElseGet(() -> type.getName()); - return String.format("%s.%s(…)", typeName, method.getName()); + return typeName + "." + method.getName() + "(…)"; } } diff --git a/spring-modulith-observability/src/main/java/org/springframework/modulith/observability/ModuleEntryInterceptor.java b/spring-modulith-observability/src/main/java/org/springframework/modulith/observability/ModuleEntryInterceptor.java index b8b53025..305d5d07 100644 --- a/spring-modulith-observability/src/main/java/org/springframework/modulith/observability/ModuleEntryInterceptor.java +++ b/spring-modulith-observability/src/main/java/org/springframework/modulith/observability/ModuleEntryInterceptor.java @@ -15,34 +15,44 @@ */ package org.springframework.modulith.observability; -import io.micrometer.tracing.Baggage; -import io.micrometer.tracing.Span; import io.micrometer.tracing.Tracer; import io.micrometer.tracing.Tracer.SpanInScope; -import lombok.AccessLevel; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; import java.util.HashMap; import java.util.Map; import org.aopalliance.intercept.MethodInterceptor; import org.aopalliance.intercept.MethodInvocation; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.util.Assert; -@Slf4j -@RequiredArgsConstructor(access = AccessLevel.PRIVATE) class ModuleEntryInterceptor implements MethodInterceptor { + private static Logger LOGGER = LoggerFactory.getLogger(ModuleEntryInterceptor.class); private static Map CACHE = new HashMap<>(); private final ObservedModule module; private final Tracer tracer; + /** + * Creates a new {@link ModuleEntryInterceptor} for the given {@link ObservedModule} and {@link Tracer}. + * + * @param module must not be {@literal null}. + * @param tracer must not be {@literal null}. + */ + private ModuleEntryInterceptor(ObservedModule module, Tracer tracer) { + + Assert.notNull(module, "ObservedModule must not be null!"); + Assert.notNull(tracer, "Tracer must not be null!"); + + this.module = module; + this.tracer = tracer; + } + public static ModuleEntryInterceptor of(ObservedModule module, Tracer tracer) { - String name = module.getName(); - - return CACHE.computeIfAbsent(name, __ -> { + return CACHE.computeIfAbsent(module.getName(), __ -> { return new ModuleEntryInterceptor(module, tracer); }); } @@ -54,23 +64,23 @@ class ModuleEntryInterceptor implements MethodInterceptor { @Override public Object invoke(MethodInvocation invocation) throws Throwable { - String moduleName = module.getName(); - Span currentSpan = tracer.currentSpan(); + var moduleName = module.getName(); + var currentSpan = tracer.currentSpan(); if (currentSpan != null) { - Baggage currentBaggage = tracer.getBaggage(ModuleTracingBeanPostProcessor.MODULE_BAGGAGE_KEY); + var currentBaggage = tracer.getBaggage(ModuleTracingBeanPostProcessor.MODULE_BAGGAGE_KEY); if (currentBaggage != null && moduleName.equals(currentBaggage.get())) { return invocation.proceed(); } } - String invokedMethod = module.getInvokedMethod(invocation); + var invokedMethod = module.getInvokedMethod(invocation); - LOG.trace("Entering {} via {}.", module.getDisplayName(), invokedMethod); + LOGGER.trace("Entering {} via {}.", module.getDisplayName(), invokedMethod); - Span span = tracer.spanBuilder() + var span = tracer.spanBuilder() .name(moduleName) .tag("module.method", invokedMethod) .tag(ModuleTracingBeanPostProcessor.MODULE_BAGGAGE_KEY, moduleName) @@ -84,7 +94,7 @@ class ModuleEntryInterceptor implements MethodInterceptor { } finally { - LOG.trace("Leaving {}", module.getDisplayName()); + LOGGER.trace("Leaving {}", module.getDisplayName()); span.end(); } diff --git a/spring-modulith-observability/src/main/java/org/springframework/modulith/observability/ModuleEventListener.java b/spring-modulith-observability/src/main/java/org/springframework/modulith/observability/ModuleEventListener.java index 79b5b94b..c0a48bea 100644 --- a/spring-modulith-observability/src/main/java/org/springframework/modulith/observability/ModuleEventListener.java +++ b/spring-modulith-observability/src/main/java/org/springframework/modulith/observability/ModuleEventListener.java @@ -15,27 +15,39 @@ */ package org.springframework.modulith.observability; -import io.micrometer.tracing.Span; import io.micrometer.tracing.Tracer; -import lombok.RequiredArgsConstructor; import java.util.function.Supplier; import org.springframework.context.ApplicationEvent; import org.springframework.context.ApplicationListener; import org.springframework.context.PayloadApplicationEvent; -import org.springframework.modulith.model.ApplicationModule; import org.springframework.modulith.runtime.ApplicationModulesRuntime; +import org.springframework.util.Assert; /** * @author Oliver Drotbohm */ -@RequiredArgsConstructor public class ModuleEventListener implements ApplicationListener { private final ApplicationModulesRuntime runtime; private final Supplier tracer; + /** + * Creates a new {@link ModuleEventListener} for the given {@link ApplicationModulesRuntime} and {@link Tracer}. + * + * @param runtime must not be {@literal null}. + * @param tracer must not be {@literal null}. + */ + public ModuleEventListener(ApplicationModulesRuntime runtime, Supplier tracer) { + + Assert.notNull(runtime, "ApplicationModulesRuntime must not be null!"); + Assert.notNull(tracer, "Tracer must not be null!"); + + this.runtime = runtime; + this.tracer = tracer; + } + /* * (non-Javadoc) * @see org.springframework.context.ApplicationListener#onApplicationEvent(org.springframework.context.ApplicationEvent) @@ -43,19 +55,18 @@ public class ModuleEventListener implements ApplicationListener payloadEvent)) { return; } - PayloadApplicationEvent foo = (PayloadApplicationEvent) event; - Object object = foo.getPayload(); - Class payloadType = object.getClass(); + var object = payloadEvent.getPayload(); + var payloadType = object.getClass(); if (!runtime.isApplicationClass(payloadType)) { return; } - ApplicationModule moduleByType = runtime.get() + var moduleByType = runtime.get() .getModuleByType(payloadType.getSimpleName()) .orElse(null); @@ -63,7 +74,7 @@ public class ModuleEventListener implements ApplicationListener tracer; - private final Map advisors = new HashMap<>(); + private final Map advisors; + + /** + * Creates a new {@link ModuleTracingBeanPostProcessor} for the given {@link ApplicationModulesRuntime} and + * {@link Tracer}. + * + * @param runtime must not be {@literal null}. + * @param tracer must not be {@literal null}. + */ + public ModuleTracingBeanPostProcessor(ApplicationModulesRuntime runtime, Supplier tracer) { + + Assert.notNull(runtime, "ApplicationModulesRuntime must not be null!"); + Assert.notNull(tracer, "Tracer must not be null!"); + + this.runtime = runtime; + this.tracer = tracer; + this.advisors = new HashMap<>(); + } /* * (non-Javadoc) @@ -69,7 +82,7 @@ public class ModuleTracingBeanPostProcessor extends ModuleTracingSupport impleme .map(DefaultObservedModule::new) .map(it -> { - ObservedModuleType moduleType = it.getObservedModuleType(type, modules); + var moduleType = it.getObservedModuleType(type, modules); return moduleType != null // ? addAdvisor(bean, getOrBuildAdvisor(it, moduleType)) // @@ -82,19 +95,30 @@ public class ModuleTracingBeanPostProcessor extends ModuleTracingSupport impleme return advisors.computeIfAbsent(module.getName(), __ -> { - Advice interceptor = ModuleEntryInterceptor.of(module, tracer.get()); - MethodMatcher matcher = new ObservableTypeMethodMatcher(type); - Pointcut pointcut = new ComposablePointcut(matcher); + var interceptor = ModuleEntryInterceptor.of(module, tracer.get()); + var matcher = new ObservableTypeMethodMatcher(type); + var pointcut = new ComposablePointcut(matcher); return new DefaultPointcutAdvisor(pointcut, interceptor); }); } - @RequiredArgsConstructor private static class ObservableTypeMethodMatcher extends StaticMethodMatcher { private final ObservedModuleType type; + /** + * Creates a new {@link ObservableTypeMethodMatcher} for the given {@link ObservedModuleType}. + * + * @param type must not be {@literal null}. + */ + private ObservableTypeMethodMatcher(ObservedModuleType type) { + + Assert.notNull(type, "ObservableModuleType must not be null!"); + + this.type = type; + } + /* * (non-Javadoc) * @see org.springframework.aop.MethodMatcher#matches(java.lang.reflect.Method, java.lang.Class) diff --git a/spring-modulith-observability/src/main/java/org/springframework/modulith/observability/ObservedModuleType.java b/spring-modulith-observability/src/main/java/org/springframework/modulith/observability/ObservedModuleType.java index 01a8c934..8d7888bf 100644 --- a/spring-modulith-observability/src/main/java/org/springframework/modulith/observability/ObservedModuleType.java +++ b/spring-modulith-observability/src/main/java/org/springframework/modulith/observability/ObservedModuleType.java @@ -15,8 +15,6 @@ */ package org.springframework.modulith.observability; -import lombok.RequiredArgsConstructor; - import java.lang.reflect.Method; import java.util.Collection; import java.util.List; @@ -28,6 +26,7 @@ import org.springframework.aop.framework.Advised; import org.springframework.modulith.model.ApplicationModules; import org.springframework.modulith.model.ArchitecturallyEvidentType; import org.springframework.modulith.model.ArchitecturallyEvidentType.ReferenceMethod; +import org.springframework.util.Assert; import org.springframework.util.ReflectionUtils; /** @@ -35,7 +34,6 @@ import org.springframework.util.ReflectionUtils; * * @author Oliver Drotbohm */ -@RequiredArgsConstructor public class ObservedModuleType { private static Collection> IGNORED_TYPES = List.of(Advised.class, TargetClassAware.class); @@ -44,6 +42,25 @@ public class ObservedModuleType { private final ObservedModule module; private final ArchitecturallyEvidentType type; + /** + * Creates a new {@link ObservedModuleType} for the given {@link ApplicationModules}, {@link ObservedModule} and + * {@link ArchitecturallyEvidentType}. + * + * @param modules must not be {@literal null}. + * @param module must not be {@literal null}. + * @param type must not be {@literal null}. + */ + ObservedModuleType(ApplicationModules modules, ObservedModule module, ArchitecturallyEvidentType type) { + + Assert.notNull(modules, "ApplicationModules must not be null!"); + Assert.notNull(module, "ObservedModule must not be null!"); + Assert.notNull(type, "ArchitecturallyEvidentType must not be null!"); + + this.modules = modules; + this.module = module; + this.type = type; + } + /** * Returns whether the type should be traced at all. Can be skipped for types not exposed by the module unless they * listen to events of other modules. diff --git a/spring-modulith-observability/src/main/java/org/springframework/modulith/observability/SpringDataRestModuleTracingBeanPostProcessor.java b/spring-modulith-observability/src/main/java/org/springframework/modulith/observability/SpringDataRestModuleTracingBeanPostProcessor.java index 6ab9cb66..356d5fd1 100644 --- a/spring-modulith-observability/src/main/java/org/springframework/modulith/observability/SpringDataRestModuleTracingBeanPostProcessor.java +++ b/spring-modulith-observability/src/main/java/org/springframework/modulith/observability/SpringDataRestModuleTracingBeanPostProcessor.java @@ -16,7 +16,6 @@ package org.springframework.modulith.observability; import io.micrometer.tracing.Tracer; -import lombok.RequiredArgsConstructor; import java.util.function.Supplier; @@ -33,16 +32,32 @@ import org.springframework.data.rest.webmvc.RootResourceInformation; import org.springframework.modulith.model.ApplicationModule; import org.springframework.modulith.model.ApplicationModules; import org.springframework.modulith.runtime.ApplicationModulesRuntime; +import org.springframework.util.Assert; /** * @author Oliver Drotbohm */ -@RequiredArgsConstructor public class SpringDataRestModuleTracingBeanPostProcessor extends ModuleTracingSupport implements BeanPostProcessor { private final ApplicationModulesRuntime runtime; private final Supplier tracer; + /** + * Creates a new {@link SpringDataRestModuleTracingBeanPostProcessor} for the given {@link ApplicationModulesRuntime} + * and {@link Tracer}. + * + * @param runtime must not be {@literal null}. + * @param tracer must not be {@literal null}. + */ + public SpringDataRestModuleTracingBeanPostProcessor(ApplicationModulesRuntime runtime, Supplier tracer) { + + Assert.notNull(runtime, "ApplicationModulesRuntime must not be null!"); + Assert.notNull(tracer, "Tracer must not be null!"); + + this.runtime = runtime; + this.tracer = tracer; + } + /* * (non-Javadoc) * @see org.springframework.beans.factory.config.BeanPostProcessor#postProcessAfterInitialization(java.lang.Object, java.lang.String) @@ -62,12 +77,26 @@ public class SpringDataRestModuleTracingBeanPostProcessor extends ModuleTracingS return addAdvisor(bean, advisor, it -> it.setProxyTargetClass(true)); } - @RequiredArgsConstructor private static class DataRestControllerInterceptor implements MethodInterceptor { private final Supplier modules; private final Supplier tracer; + /** + * Creates a new {@link DataRestControllerInterceptor} for the given {@link ApplicationModules} and {@link Tracer}. + * + * @param modules must not be {@literal null}. + * @param tracer must not be {@literal null}. + */ + private DataRestControllerInterceptor(Supplier modules, Supplier tracer) { + + Assert.notNull(modules, "ApplicationModules must not be null!"); + Assert.notNull(tracer, "Tracer must not be null!"); + + this.modules = modules; + this.tracer = tracer; + } + /* * (non-Javadoc) * @see org.aopalliance.intercept.MethodInterceptor#invoke(org.aopalliance.intercept.MethodInvocation) @@ -75,13 +104,13 @@ public class SpringDataRestModuleTracingBeanPostProcessor extends ModuleTracingS @Override public Object invoke(MethodInvocation invocation) throws Throwable { - ApplicationModule module = getModuleFrom(invocation.getArguments()); + var module = getModuleFrom(invocation.getArguments()); if (module == null) { return invocation.proceed(); } - ObservedModule observed = new DefaultObservedModule(module); + var observed = new DefaultObservedModule(module); return ModuleEntryInterceptor.of(observed, tracer.get()).invoke(invocation); } @@ -90,17 +119,14 @@ public class SpringDataRestModuleTracingBeanPostProcessor extends ModuleTracingS for (Object argument : arguments) { - if (!RootResourceInformation.class.isInstance(arguments)) { + if (!(argument instanceof RootResourceInformation info)) { continue; } - RootResourceInformation info = (RootResourceInformation) argument; - return modules.get().getModuleByType(info.getDomainType().getName()).orElse(null); } return null; } } - } diff --git a/spring-modulith-runtime/src/main/java/org/springframework/modulith/runtime/ApplicationModulesRuntime.java b/spring-modulith-runtime/src/main/java/org/springframework/modulith/runtime/ApplicationModulesRuntime.java index e2dafc04..cf2bc215 100644 --- a/spring-modulith-runtime/src/main/java/org/springframework/modulith/runtime/ApplicationModulesRuntime.java +++ b/spring-modulith-runtime/src/main/java/org/springframework/modulith/runtime/ApplicationModulesRuntime.java @@ -15,12 +15,10 @@ */ package org.springframework.modulith.runtime; -import lombok.NonNull; -import lombok.RequiredArgsConstructor; - import java.util.function.Supplier; import org.springframework.modulith.model.ApplicationModules; +import org.springframework.util.Assert; /** * Bootstrap type to make sure we only bootstrap the initialization of a {@link ApplicationModules} instance once per @@ -28,11 +26,26 @@ import org.springframework.modulith.model.ApplicationModules; * * @author Oliver Drotbohm */ -@RequiredArgsConstructor public class ApplicationModulesRuntime implements Supplier { - private final @NonNull Supplier modules; - private final @NonNull ApplicationRuntime runtime; + private final Supplier modules; + private final ApplicationRuntime runtime; + + /** + * Creates a new {@link ApplicationModulesRuntime} for the given {@link ApplicationModules} and + * {@link ApplicationRuntime}. + * + * @param modules must not be {@literal null}. + * @param runtime must not be {@literal null}. + */ + public ApplicationModulesRuntime(Supplier modules, ApplicationRuntime runtime) { + + Assert.notNull(modules, "ApplicationModules must not be null!"); + Assert.notNull(runtime, "ApplicationRuntime must not be null!"); + + this.modules = modules; + this.runtime = runtime; + } /* * (non-Javadoc) diff --git a/spring-modulith-runtime/src/main/java/org/springframework/modulith/runtime/autoconfigure/SpringBootApplicationRuntime.java b/spring-modulith-runtime/src/main/java/org/springframework/modulith/runtime/autoconfigure/SpringBootApplicationRuntime.java index 14e8b02a..e05741af 100644 --- a/spring-modulith-runtime/src/main/java/org/springframework/modulith/runtime/autoconfigure/SpringBootApplicationRuntime.java +++ b/spring-modulith-runtime/src/main/java/org/springframework/modulith/runtime/autoconfigure/SpringBootApplicationRuntime.java @@ -15,8 +15,6 @@ */ package org.springframework.modulith.runtime.autoconfigure; -import lombok.RequiredArgsConstructor; - import java.util.Arrays; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; @@ -25,6 +23,7 @@ import org.springframework.boot.autoconfigure.AutoConfigurationPackages; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.ApplicationContext; import org.springframework.modulith.runtime.ApplicationRuntime; +import org.springframework.util.Assert; import org.springframework.util.ClassUtils; /** @@ -33,7 +32,6 @@ import org.springframework.util.ClassUtils; * * @author Oliver Drotbohm */ -@RequiredArgsConstructor class SpringBootApplicationRuntime implements ApplicationRuntime { private static final Map APPLICATION_CLASSES = new ConcurrentHashMap<>(); @@ -41,6 +39,18 @@ class SpringBootApplicationRuntime implements ApplicationRuntime { private final ApplicationContext context; private Class mainApplicationClass; + /** + * Creates a new {@link SpringBootApplicationRuntime} for the given {@link ApplicationContext}. + * + * @param context must not be {@literal null}. + */ + SpringBootApplicationRuntime(ApplicationContext context) { + + Assert.notNull(context, "ApplicationContext must not be null!"); + + this.context = context; + } + /* * (non-Javadoc) * @see org.springframework.modulith.observability.ApplicationRuntime#getId() diff --git a/spring-modulith-runtime/src/main/java/org/springframework/modulith/runtime/autoconfigure/SpringModulithRuntimeAutoConfiguration.java b/spring-modulith-runtime/src/main/java/org/springframework/modulith/runtime/autoconfigure/SpringModulithRuntimeAutoConfiguration.java index 83866ee7..207ee91f 100644 --- a/spring-modulith-runtime/src/main/java/org/springframework/modulith/runtime/autoconfigure/SpringModulithRuntimeAutoConfiguration.java +++ b/spring-modulith-runtime/src/main/java/org/springframework/modulith/runtime/autoconfigure/SpringModulithRuntimeAutoConfiguration.java @@ -15,14 +15,13 @@ */ package org.springframework.modulith.runtime.autoconfigure; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; - import java.util.List; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.function.Supplier; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.aop.support.AopUtils; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.boot.autoconfigure.AutoConfiguration; @@ -38,6 +37,7 @@ import org.springframework.modulith.model.ApplicationModules; import org.springframework.modulith.model.FormatableType; import org.springframework.modulith.runtime.ApplicationModulesRuntime; import org.springframework.modulith.runtime.ApplicationRuntime; +import org.springframework.util.Assert; /** * Auto-configuration to register a {@link SpringBootApplicationRuntime} and {@link ApplicationModulesRuntime} as Spring @@ -45,10 +45,11 @@ import org.springframework.modulith.runtime.ApplicationRuntime; * * @author Oliver Drotbohm */ -@Slf4j @AutoConfiguration class SpringModulithRuntimeAutoConfiguration { + private static final Logger LOGGER = LoggerFactory.getLogger(SpringModulithRuntimeAutoConfiguration.class); + @Bean @Role(BeanDefinition.ROLE_INFRASTRUCTURE) @ConditionalOnMissingBean(ApplicationRuntime.class) @@ -78,18 +79,35 @@ class SpringModulithRuntimeAutoConfiguration { initializers.stream() // .sorted(modules.getComparator()) // - .map(it -> LOG.isDebugEnabled() ? new LoggingApplicationModuleInitializerAdapter(it, modules) : it) + .map(it -> LOGGER.isDebugEnabled() ? new LoggingApplicationModuleInitializerAdapter(it, modules) : it) .forEach(ApplicationModuleInitializer::initialize); }; } - @Slf4j - @RequiredArgsConstructor private static class LoggingApplicationModuleInitializerAdapter implements ApplicationModuleInitializer { + private static final Logger LOGGER = LoggerFactory.getLogger(LoggingApplicationModuleInitializerAdapter.class); + private final ApplicationModuleInitializer delegate; private final ApplicationModules modules; + /** + * Creates a new {@link LoggingApplicationModuleInitializerAdapter} for the given + * {@link ApplicationModuleInitializer} and {@link ApplicationModule}. + * + * @param delegate must not be {@literal null}. + * @param modules must not be {@literal null}. + */ + public LoggingApplicationModuleInitializerAdapter(ApplicationModuleInitializer delegate, + ApplicationModules modules) { + + Assert.notNull(delegate, "ApplicationModuleInitializer must not be null!"); + Assert.notNull(modules, "ApplicationModules must not be null!"); + + this.delegate = delegate; + this.modules = modules; + } + /* * (non-Javadoc) * @see org.springframework.modulith.ApplicationModuleInitializer#initialize() @@ -104,31 +122,32 @@ class SpringModulithRuntimeAutoConfiguration { .map(formattable::getAbbreviatedFullName) .orElseGet(formattable::getAbbreviatedFullName); - LOG.debug("Initializing {}.", formattedListenerType); + LOGGER.debug("Initializing {}.", formattedListenerType); delegate.initialize(); - LOG.debug("Initializing {} done.", formattedListenerType); + LOGGER.debug("Initializing {} done.", formattedListenerType); } } - @Slf4j private static class ApplicationModulesBootstrap { + private static final Logger LOGGER = LoggerFactory.getLogger(ApplicationModulesBootstrap.class); + static ApplicationModules initializeApplicationModules(Class applicationMainClass) { - LOG.debug("Obtaining Spring Modulith application modules…"); + LOGGER.debug("Obtaining Spring Modulith application modules…"); var result = ApplicationModules.of(applicationMainClass); var numberOfModules = result.stream().count(); if (numberOfModules == 0) { - LOG.warn("No application modules detected!"); + LOGGER.warn("No application modules detected!"); } else { - LOG.debug("Detected {} application modules: {}", // + LOGGER.debug("Detected {} application modules: {}", // result.stream().count(), // result.stream().map(ApplicationModule::getName).toList()); } @@ -144,7 +163,6 @@ class SpringModulithRuntimeAutoConfiguration { return modules.get(); } catch (Exception o_O) { throw new RuntimeException(o_O); - // TODO: handle exception } }; } diff --git a/spring-modulith-test/src/main/java/org/springframework/modulith/test/ApplicationModuleTest.java b/spring-modulith-test/src/main/java/org/springframework/modulith/test/ApplicationModuleTest.java index 243422b9..9b42bd9c 100644 --- a/spring-modulith-test/src/main/java/org/springframework/modulith/test/ApplicationModuleTest.java +++ b/spring-modulith-test/src/main/java/org/springframework/modulith/test/ApplicationModuleTest.java @@ -15,9 +15,6 @@ */ package org.springframework.modulith.test; -import lombok.Getter; -import lombok.RequiredArgsConstructor; - import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -35,8 +32,8 @@ import org.springframework.test.context.TestConstructor.AutowireMode; import org.springframework.test.context.junit.jupiter.SpringExtension; /** - * Bootstraps the module containing the package of the test class annotated with {@link ApplicationModuleTest}. Will apply the - * following modifications to the Spring Boot configuration: + * Bootstraps the module containing the package of the test class annotated with {@link ApplicationModuleTest}. Will + * apply the following modifications to the Spring Boot configuration: *
    *
  • Restricts the component scanning to the module's package. *
  • @@ -76,7 +73,6 @@ public @interface ApplicationModuleTest { */ String[] extraIncludes() default {}; - @RequiredArgsConstructor public enum BootstrapMode { /** @@ -94,6 +90,19 @@ public @interface ApplicationModuleTest { */ ALL_DEPENDENCIES(DependencyDepth.ALL); - private final @Getter DependencyDepth depth; + private final DependencyDepth depth; + + private BootstrapMode(DependencyDepth depth) { + this.depth = depth; + } + + /** + * Returns the {@link DependencyDepth} associated with the {@link BootstrapMode}. + * + * @return will never be {@literal null}. + */ + public DependencyDepth getDepth() { + return depth; + } } } diff --git a/spring-modulith-test/src/main/java/org/springframework/modulith/test/DefaultAssertablePublishedEvents.java b/spring-modulith-test/src/main/java/org/springframework/modulith/test/DefaultAssertablePublishedEvents.java index a7692fa3..b7afd051 100644 --- a/spring-modulith-test/src/main/java/org/springframework/modulith/test/DefaultAssertablePublishedEvents.java +++ b/spring-modulith-test/src/main/java/org/springframework/modulith/test/DefaultAssertablePublishedEvents.java @@ -15,21 +15,31 @@ */ package org.springframework.modulith.test; -import lombok.RequiredArgsConstructor; - import org.springframework.context.ApplicationEvent; import org.springframework.context.ApplicationListener; +import org.springframework.util.Assert; /** * Default implementation of {@link AssertablePublishedEvents}. * * @author Oliver Drotbohm */ -@RequiredArgsConstructor class DefaultAssertablePublishedEvents implements AssertablePublishedEvents, ApplicationListener { private final DefaultPublishedEvents delegate; + /** + * Creates a new {@link DefaultAssertablePublishedEvents} with the given {@link DefaultPublishedEvents} delegate. + * + * @param delegate must not be {@literal null}. + */ + DefaultAssertablePublishedEvents(DefaultPublishedEvents delegate) { + + Assert.notNull(delegate, "DefaultPublishedEvents must not be null!"); + + this.delegate = delegate; + } + /** * Creates a new {@link DefaultAssertablePublishedEvents}. */ diff --git a/spring-modulith-test/src/main/java/org/springframework/modulith/test/DefaultPublishedEvents.java b/spring-modulith-test/src/main/java/org/springframework/modulith/test/DefaultPublishedEvents.java index 8f182414..8c4a1854 100644 --- a/spring-modulith-test/src/main/java/org/springframework/modulith/test/DefaultPublishedEvents.java +++ b/spring-modulith-test/src/main/java/org/springframework/modulith/test/DefaultPublishedEvents.java @@ -15,8 +15,6 @@ */ package org.springframework.modulith.test; -import lombok.RequiredArgsConstructor; - import java.util.ArrayList; import java.util.Collection; import java.util.Collections; @@ -88,11 +86,14 @@ class DefaultPublishedEvents implements PublishedEvents, ApplicationListener implements TypedPublishedEvents { private final List events; + private SimpleTypedPublishedEvents(List events) { + this.events = events; + } + private static SimpleTypedPublishedEvents of(Stream stream) { return new SimpleTypedPublishedEvents<>(stream.toList()); } diff --git a/spring-modulith-test/src/main/java/org/springframework/modulith/test/ModuleContextCustomizerFactory.java b/spring-modulith-test/src/main/java/org/springframework/modulith/test/ModuleContextCustomizerFactory.java index 52c4f7e2..a5d6c5f1 100644 --- a/spring-modulith-test/src/main/java/org/springframework/modulith/test/ModuleContextCustomizerFactory.java +++ b/spring-modulith-test/src/main/java/org/springframework/modulith/test/ModuleContextCustomizerFactory.java @@ -15,17 +15,15 @@ */ package org.springframework.modulith.test; -import lombok.EqualsAndHashCode; -import lombok.extern.slf4j.Slf4j; - import java.util.Arrays; import java.util.List; -import java.util.Set; +import java.util.Objects; import java.util.function.Supplier; import java.util.stream.Collectors; import java.util.stream.Stream; -import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.modulith.model.ApplicationModule; @@ -47,16 +45,14 @@ class ModuleContextCustomizerFactory implements ContextCustomizerFactory { public ContextCustomizer createContextCustomizer(Class testClass, List configAttributes) { - ApplicationModuleTest moduleTest = AnnotatedElementUtils.getMergedAnnotation(testClass, - ApplicationModuleTest.class); + var moduleTest = AnnotatedElementUtils.getMergedAnnotation(testClass, ApplicationModuleTest.class); return moduleTest == null ? null : new ModuleContextCustomizer(testClass); } - @Slf4j - @EqualsAndHashCode static class ModuleContextCustomizer implements ContextCustomizer { + private static final Logger LOGGER = LoggerFactory.getLogger(ModuleContextCustomizer.class); private static final String BEAN_NAME = ModuleTestExecution.class.getName(); private final Supplier execution; @@ -72,14 +68,14 @@ class ModuleContextCustomizerFactory implements ContextCustomizerFactory { @Override public void customizeContext(ConfigurableApplicationContext context, MergedContextConfiguration mergedConfig) { - ModuleTestExecution testExecution = execution.get(); + var testExecution = execution.get(); logModules(testExecution); - ConfigurableListableBeanFactory beanFactory = context.getBeanFactory(); + var beanFactory = context.getBeanFactory(); beanFactory.registerSingleton(BEAN_NAME, testExecution); - DefaultPublishedEvents events = new DefaultPublishedEvents(); + var events = new DefaultPublishedEvents(); beanFactory.registerSingleton(events.getClass().getName(), events); context.addApplicationListener(events); } @@ -94,48 +90,47 @@ class ModuleContextCustomizerFactory implements ContextCustomizerFactory { var message = "Bootstrapping @%s for %s in mode %s (%s)…" .formatted(ApplicationModuleTest.class.getName(), moduleName, bootstrapMode, modules.getModulithSource()); - LOG.info(message); - LOG.info(""); + LOGGER.info(message); + LOGGER.info(""); - Arrays.stream(module.toString(modules).split("\n")).forEach(LOG::info); + Arrays.stream(module.toString(modules).split("\n")).forEach(LOGGER::info); - List extraIncludes = execution.getExtraIncludes(); + var extraIncludes = execution.getExtraIncludes(); if (!extraIncludes.isEmpty()) { logHeadline("Extra includes:"); - LOG.info("> " + extraIncludes.stream().map(ApplicationModule::getName).collect(Collectors.joining(", "))); + LOGGER.info("> " + extraIncludes.stream().map(ApplicationModule::getName).collect(Collectors.joining(", "))); } - Set sharedModules = modules.getSharedModules(); + var sharedModules = modules.getSharedModules(); if (!sharedModules.isEmpty()) { logHeadline("Shared modules:"); - LOG.info("> " + sharedModules.stream().map(ApplicationModule::getName).collect(Collectors.joining(", "))); + LOGGER.info("> " + sharedModules.stream().map(ApplicationModule::getName).collect(Collectors.joining(", "))); } - List dependencies = execution.getDependencies(); + var dependencies = execution.getDependencies(); if (!dependencies.isEmpty() || !sharedModules.isEmpty()) { logHeadline("Included dependencies:"); - Stream dependenciesPlusMissingSharedOnes = // - Stream.concat(dependencies.stream(), sharedModules.stream() // - .filter(it -> !dependencies.contains(it))); + var dependenciesPlusMissingSharedOnes = Stream.concat(dependencies.stream(), sharedModules.stream() // + .filter(it -> !dependencies.contains(it))); dependenciesPlusMissingSharedOnes // .map(it -> it.toString(modules)) // .forEach(it -> { - LOG.info(""); - Arrays.stream(it.split("\n")).forEach(LOG::info); + LOGGER.info(""); + Arrays.stream(it.split("\n")).forEach(LOGGER::info); }); } - LOG.info(""); + LOGGER.info(""); } private static void logHeadline(String headline) { @@ -144,9 +139,36 @@ class ModuleContextCustomizerFactory implements ContextCustomizerFactory { private static void logHeadline(String headline, Runnable additional) { - LOG.info(""); - LOG.info(headline); + LOGGER.info(""); + LOGGER.info(headline); additional.run(); } + + /* + * (non-Javadoc) + * @see java.lang.Object#equals(java.lang.Object) + */ + @Override + public boolean equals(Object obj) { + + if (this == obj) { + return true; + } + + if (!(obj instanceof ModuleContextCustomizer that)) { + return false; + } + + return Objects.equals(execution, that.execution); + } + + /* + * (non-Javadoc) + * @see java.lang.Object#hashCode() + */ + @Override + public int hashCode() { + return Objects.hash(execution); + } } } diff --git a/spring-modulith-test/src/main/java/org/springframework/modulith/test/ModuleTestAutoConfiguration.java b/spring-modulith-test/src/main/java/org/springframework/modulith/test/ModuleTestAutoConfiguration.java index 17e017c6..9ecceec9 100644 --- a/spring-modulith-test/src/main/java/org/springframework/modulith/test/ModuleTestAutoConfiguration.java +++ b/spring-modulith-test/src/main/java/org/springframework/modulith/test/ModuleTestAutoConfiguration.java @@ -15,16 +15,14 @@ */ package org.springframework.modulith.test; -import lombok.extern.slf4j.Slf4j; - -import java.lang.reflect.Field; import java.util.Arrays; import java.util.HashSet; import java.util.List; import java.util.Set; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.beans.factory.BeanFactory; -import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.config.ConstructorArgumentValues.ValueHolder; import org.springframework.beans.factory.support.BeanDefinitionRegistry; import org.springframework.boot.autoconfigure.AutoConfigureOrder; @@ -43,7 +41,7 @@ import org.springframework.util.StringUtils; * * @author Oliver Drotbohm */ -@Configuration +@Configuration(proxyBeanMethods = false) @Import(ModuleTestAutoConfiguration.AutoConfigurationAndEntityScanPackageCustomizer.class) @AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE) class ModuleTestAutoConfiguration { @@ -51,9 +49,10 @@ class ModuleTestAutoConfiguration { private static final String AUTOCONFIG_PACKAGES = "org.springframework.boot.autoconfigure.AutoConfigurationPackages"; private static final String ENTITY_SCAN_PACKAGE = "org.springframework.boot.autoconfigure.domain.EntityScanPackages"; - @Slf4j static class AutoConfigurationAndEntityScanPackageCustomizer implements ImportBeanDefinitionRegistrar { + private static final Logger LOGGER = LoggerFactory.getLogger(AutoConfigurationAndEntityScanPackageCustomizer.class); + /* * (non-Javadoc) * @see org.springframework.context.annotation.ImportBeanDefinitionRegistrar#registerBeanDefinitions(org.springframework.core.type.AnnotationMetadata, org.springframework.beans.factory.support.BeanDefinitionRegistry) @@ -61,10 +60,10 @@ class ModuleTestAutoConfiguration { @Override public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) { - ModuleTestExecution execution = ((BeanFactory) registry).getBean(ModuleTestExecution.class); - List basePackages = execution.getBasePackages().toList(); + var execution = ((BeanFactory) registry).getBean(ModuleTestExecution.class); + var basePackages = execution.getBasePackages().toList(); - LOG.info("Re-configuring auto-configuration and entity scan packages to: {}.", + LOGGER.info("Re-configuring auto-configuration and entity scan packages to: {}.", StringUtils.collectionToDelimitedString(basePackages, ", ")); setBasePackagesOn(registry, AUTOCONFIG_PACKAGES, "BasePackagesBeanDefinition", "basePackages", basePackages); @@ -80,10 +79,10 @@ class ModuleTestAutoConfiguration { return; } - BeanDefinition definition = registry.getBeanDefinition(beanName); + var definition = registry.getBeanDefinition(beanName); // For Boot 2.4, we deal with a BasePackagesBeanDefinition - Field field = Arrays.stream(definition.getClass().getDeclaredFields()) + var field = Arrays.stream(definition.getClass().getDeclaredFields()) .filter(__ -> definition.getClass().getSimpleName().equals(definitionType)) .filter(it -> it.getName().equals(fieldName)) .findFirst() diff --git a/spring-modulith-test/src/main/java/org/springframework/modulith/test/ModuleTestExecution.java b/spring-modulith-test/src/main/java/org/springframework/modulith/test/ModuleTestExecution.java index b83840e8..3a9bb97f 100644 --- a/spring-modulith-test/src/main/java/org/springframework/modulith/test/ModuleTestExecution.java +++ b/spring-modulith-test/src/main/java/org/springframework/modulith/test/ModuleTestExecution.java @@ -15,20 +15,16 @@ */ package org.springframework.modulith.test; -import lombok.AccessLevel; -import lombok.EqualsAndHashCode; -import lombok.Getter; -import lombok.RequiredArgsConstructor; -import lombok.Value; -import lombok.extern.slf4j.Slf4j; - import java.util.Arrays; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.stream.Stream; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.test.context.AnnotatedClassFinder; import org.springframework.core.annotation.AnnotatedElementUtils; @@ -43,26 +39,26 @@ import com.tngtech.archunit.thirdparty.com.google.common.base.Suppliers; /** * @author Oliver Drotbohm */ -@Slf4j -@EqualsAndHashCode(of = "key") public class ModuleTestExecution implements Iterable { + private static final Logger LOGGER = LoggerFactory.getLogger(ModuleTestExecution.class); + private static Map, Class> MODULITH_TYPES = new HashMap<>(); private static Map EXECUTIONS = new HashMap<>(); private final Key key; - private final @Getter BootstrapMode bootstrapMode; - private final @Getter ApplicationModule module; - private final @Getter ApplicationModules modules; - private final @Getter List extraIncludes; + private final BootstrapMode bootstrapMode; + private final ApplicationModule module; + private final ApplicationModules modules; + private final List extraIncludes; private final Supplier> basePackages; private final Supplier> dependencies; private ModuleTestExecution(ApplicationModuleTest annotation, ApplicationModules modules, ApplicationModule module) { - this.key = Key.of(module.getBasePackage().getName(), annotation); + this.key = new Key(module.getBasePackage().getName(), annotation); this.modules = modules; this.bootstrapMode = annotation.mode(); this.module = module; @@ -71,18 +67,18 @@ public class ModuleTestExecution implements Iterable { this.basePackages = Suppliers.memoize(() -> { - Stream moduleBasePackages = module.getBootstrapBasePackages(modules, bootstrapMode.getDepth()); - Stream sharedBasePackages = modules.getSharedModules().stream().map(it -> it.getBasePackage()); - Stream extraPackages = extraIncludes.stream().map(ApplicationModule::getBasePackage); + var moduleBasePackages = module.getBootstrapBasePackages(modules, bootstrapMode.getDepth()); + var sharedBasePackages = modules.getSharedModules().stream().map(it -> it.getBasePackage()); + var extraPackages = extraIncludes.stream().map(ApplicationModule::getBasePackage); - Stream intermediate = Stream.concat(moduleBasePackages, extraPackages); + var intermediate = Stream.concat(moduleBasePackages, extraPackages); return Stream.concat(intermediate, sharedBasePackages).distinct().toList(); }); this.dependencies = Suppliers.memoize(() -> { - Stream bootstrapDependencies = module.getBootstrapDependencies(modules, + var bootstrapDependencies = module.getBootstrapDependencies(modules, bootstrapMode.getDepth()); return Stream.concat(bootstrapDependencies, extraIncludes.stream()).toList(); }); @@ -96,17 +92,16 @@ public class ModuleTestExecution implements Iterable { return () -> { - ApplicationModuleTest annotation = AnnotatedElementUtils.findMergedAnnotation(type, ApplicationModuleTest.class); - String packageName = type.getPackage().getName(); + var annotation = AnnotatedElementUtils.findMergedAnnotation(type, ApplicationModuleTest.class); + var packageName = type.getPackage().getName(); - Class modulithType = MODULITH_TYPES.computeIfAbsent(type, + var modulithType = MODULITH_TYPES.computeIfAbsent(type, it -> new AnnotatedClassFinder(SpringBootApplication.class).findFromPackage(packageName)); - ApplicationModules modules = ApplicationModules.of(modulithType); - ApplicationModule module = modules.getModuleForPackage(packageName) // - .orElseThrow( - () -> new IllegalStateException(String.format("Package %s is not part of any module!", packageName))); + var modules = ApplicationModules.of(modulithType); + var module = modules.getModuleForPackage(packageName).orElseThrow( // + () -> new IllegalStateException(String.format("Package %s is not part of any module!", packageName))); - return EXECUTIONS.computeIfAbsent(Key.of(module.getBasePackage().getName(), annotation), + return EXECUTIONS.computeIfAbsent(new Key(module.getBasePackage().getName(), annotation), it -> new ModuleTestExecution(annotation, modules, module)); }; } @@ -122,11 +117,11 @@ public class ModuleTestExecution implements Iterable { public boolean includes(String className) { - boolean result = modules.withinRootPackages(className) // + var result = modules.withinRootPackages(className) // || basePackages.get().stream().anyMatch(it -> it.contains(className)); if (result) { - LOG.trace("Including class {}.", className); + LOGGER.trace("Including class {}.", className); } return !result; @@ -155,6 +150,42 @@ public class ModuleTestExecution implements Iterable { module.verifyDependencies(modules); } + /** + * Returns the {@link BootstrapMode} to be used for the executions. + * + * @return will never be {@literal null}. + */ + public BootstrapMode getBootstrapMode() { + return bootstrapMode; + } + + /** + * Returns the primary {@link ApplicationModule} to bootstrap. + * + * @return the module will never be {@literal null}. + */ + public ApplicationModule getModule() { + return module; + } + + /** + * Returns all {@link ApplicationModules} of the application. + * + * @return the modules will never be {@literal null}. + */ + public ApplicationModules getModules() { + return modules; + } + + /** + * Returns all {@link ApplicationModule}s registered as extra includes for the execution. + * + * @return the extraIncludes will never be {@literal null}. + */ + public List getExtraIncludes() { + return extraIncludes; + } + /* * (non-Javadoc) * @see java.lang.Iterable#iterator() @@ -164,6 +195,33 @@ public class ModuleTestExecution implements Iterable { return modules.iterator(); } + /* + * (non-Javadoc) + * @see java.lang.Object#equals(java.lang.Object) + */ + @Override + public boolean equals(Object obj) { + + if (this == obj) { + return true; + } + + if (!(obj instanceof ModuleTestExecution that)) { + return false; + } + + return Objects.equals(key, that.key); + } + + /* + * (non-Javadoc) + * @see java.lang.Object#hashCode() + */ + @Override + public int hashCode() { + return Objects.hash(key); + } + private static Stream getExtraModules(ApplicationModuleTest annotation, ApplicationModules modules) { @@ -172,11 +230,5 @@ public class ModuleTestExecution implements Iterable { .flatMap(it -> it.map(Stream::of).orElseGet(Stream::empty)); } - @Value - @RequiredArgsConstructor(staticName = "of", access = AccessLevel.PRIVATE) - private static class Key { - - String moduleBasePackage; - ApplicationModuleTest annotation; - } + private static record Key(String moduleBasePackage, ApplicationModuleTest annotation) {} } diff --git a/spring-modulith-test/src/main/java/org/springframework/modulith/test/ModuleTypeExcludeFilter.java b/spring-modulith-test/src/main/java/org/springframework/modulith/test/ModuleTypeExcludeFilter.java index e7d2e45a..2c4a3e5d 100644 --- a/spring-modulith-test/src/main/java/org/springframework/modulith/test/ModuleTypeExcludeFilter.java +++ b/spring-modulith-test/src/main/java/org/springframework/modulith/test/ModuleTypeExcludeFilter.java @@ -15,9 +15,8 @@ */ package org.springframework.modulith.test; -import lombok.EqualsAndHashCode; - import java.io.IOException; +import java.util.Objects; import java.util.function.Supplier; import org.springframework.boot.context.TypeExcludeFilter; @@ -27,7 +26,6 @@ import org.springframework.core.type.classreading.MetadataReaderFactory; /** * @author Oliver Drotbohm */ -@EqualsAndHashCode(callSuper = false) class ModuleTypeExcludeFilter extends TypeExcludeFilter { private final Supplier execution; @@ -36,7 +34,7 @@ class ModuleTypeExcludeFilter extends TypeExcludeFilter { this.execution = ModuleTestExecution.of(testClass); } - /* + /* * (non-Javadoc) * @see org.springframework.boot.context.TypeExcludeFilter#match(org.springframework.core.type.classreading.MetadataReader, org.springframework.core.type.classreading.MetadataReaderFactory) */ @@ -44,4 +42,31 @@ class ModuleTypeExcludeFilter extends TypeExcludeFilter { public boolean match(MetadataReader metadataReader, MetadataReaderFactory metadataReaderFactory) throws IOException { return execution.get().includes(metadataReader.getClassMetadata().getClassName()); } + + /* + * (non-Javadoc) + * @see java.lang.Object#equals(java.lang.Object) + */ + @Override + public boolean equals(Object obj) { + + if (this == obj) { + return true; + } + + if (!(obj instanceof ModuleTypeExcludeFilter that)) { + return false; + } + + return Objects.equals(execution, that.execution); + } + + /* + * (non-Javadoc) + * @see java.lang.Object#hashCode() + */ + @Override + public int hashCode() { + return Objects.hash(execution); + } } diff --git a/spring-modulith-test/src/main/java/org/springframework/modulith/test/PublishedEventsAssert.java b/spring-modulith-test/src/main/java/org/springframework/modulith/test/PublishedEventsAssert.java index 7682e3ca..325c8d1b 100644 --- a/spring-modulith-test/src/main/java/org/springframework/modulith/test/PublishedEventsAssert.java +++ b/spring-modulith-test/src/main/java/org/springframework/modulith/test/PublishedEventsAssert.java @@ -17,9 +17,6 @@ package org.springframework.modulith.test; import static org.assertj.core.api.Assertions.*; -import lombok.AccessLevel; -import lombok.RequiredArgsConstructor; - import java.util.function.Function; import java.util.function.Predicate; @@ -69,11 +66,22 @@ public class PublishedEventsAssert extends AbstractAssert { private final TypedPublishedEvents events; + /** + * Creates a new {@link PublishedEventAssert} for the given {@link TypedPublishedEvents}. + * + * @param events must not be {@literal null}. + */ + private PublishedEventAssert(TypedPublishedEvents events) { + + Assert.notNull(events, "TypedPublishedEvents must not be null!"); + + this.events = events; + } + /** * Asserts that at least one event matches the given predicate. *