GH-14 - Remove Lombok from production sources.
Polished a lot of Javadoc.
This commit is contained in:
21
pom.xml
21
pom.xml
@@ -120,24 +120,6 @@ limitations under the License.
|
||||
<build>
|
||||
<plugins>
|
||||
|
||||
<plugin>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok-maven-plugin</artifactId>
|
||||
<version>1.18.20.0</version>
|
||||
<configuration>
|
||||
<addOutputDirectory>false</addOutputDirectory>
|
||||
<sourceDirectory>${project.basedir}/src/main/java</sourceDirectory>
|
||||
</configuration>
|
||||
<executions>
|
||||
<execution>
|
||||
<phase>generate-sources</phase>
|
||||
<goals>
|
||||
<goal>delombok</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-javadoc-plugin</artifactId>
|
||||
@@ -154,7 +136,6 @@ limitations under the License.
|
||||
<doclint>none</doclint>
|
||||
<quiet>true</quiet>
|
||||
<show>package</show>
|
||||
<sourcepath>target/generated-sources/delombok</sourcepath>
|
||||
</configuration>
|
||||
</plugin>
|
||||
|
||||
@@ -433,7 +414,7 @@ limitations under the License.
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<scope>provided</scope>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
@@ -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<DependencyType>, Set<DependencyType>> 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;
|
||||
}
|
||||
|
||||
@@ -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 {};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -23,18 +23,11 @@ import static org.springframework.modulith.model.Types.JavaXTypes.*;
|
||||
import static org.springframework.modulith.model.Types.SpringDataTypes.*;
|
||||
import static org.springframework.modulith.model.Types.SpringTypes.*;
|
||||
|
||||
import lombok.AccessLevel;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Getter;
|
||||
import lombok.NonNull;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.ToString;
|
||||
import lombok.Value;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.stream.Collectors;
|
||||
@@ -63,20 +56,19 @@ import com.tngtech.archunit.thirdparty.com.google.common.base.Suppliers;
|
||||
*
|
||||
* @author Oliver Drotbohm
|
||||
*/
|
||||
@EqualsAndHashCode(doNotUseGetters = true)
|
||||
public class ApplicationModule {
|
||||
|
||||
/**
|
||||
* The base package of the {@link ApplicationModule}.
|
||||
*/
|
||||
private final @Getter JavaPackage basePackage;
|
||||
private final JavaPackage basePackage;
|
||||
private final ApplicationModuleInformation information;
|
||||
|
||||
/**
|
||||
* All {@link NamedInterfaces} of the {@link ApplicationModule} either declared explicitly via {@link NamedInterface}
|
||||
* or implicitly.
|
||||
*/
|
||||
private final @Getter NamedInterfaces namedInterfaces;
|
||||
private final NamedInterfaces namedInterfaces;
|
||||
private final boolean useFullyQualifiedModuleNames;
|
||||
|
||||
private final Supplier<Classes> springBeans;
|
||||
@@ -104,6 +96,24 @@ public class ApplicationModule {
|
||||
this.publishedEvents = Suppliers.memoize(() -> findPublishedEvents());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the module's base package.
|
||||
*
|
||||
* @return the basePackage
|
||||
*/
|
||||
public JavaPackage getBasePackage() {
|
||||
return basePackage;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all {@link NamedInterfaces} exposed by the module.
|
||||
*
|
||||
* @return the namedInterfaces will never be {@literal null}.
|
||||
*/
|
||||
public NamedInterfaces getNamedInterfaces() {
|
||||
return namedInterfaces;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the logical name of the module.
|
||||
*
|
||||
@@ -407,6 +417,41 @@ public class ApplicationModule {
|
||||
return getType(candidate).isPresent();
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see java.lang.Object#equals(java.lang.Object)
|
||||
*/
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
|
||||
if (this == obj) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!(obj instanceof ApplicationModule that)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return Objects.equals(this.basePackage, that.basePackage) //
|
||||
&& Objects.equals(this.entities, that.entities) //
|
||||
&& Objects.equals(this.information, that.information) //
|
||||
&& Objects.equals(this.namedInterfaces, that.namedInterfaces) //
|
||||
&& Objects.equals(this.publishedEvents, that.publishedEvents) //
|
||||
&& Objects.equals(this.springBeans, that.springBeans) //
|
||||
&& Objects.equals(this.useFullyQualifiedModuleNames, that.useFullyQualifiedModuleNames) //
|
||||
&& Objects.equals(this.valueTypes, that.valueTypes);
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see java.lang.Object#hashCode()
|
||||
*/
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(basePackage, entities, information, namedInterfaces, publishedEvents, springBeans,
|
||||
useFullyQualifiedModuleNames, valueTypes);
|
||||
}
|
||||
|
||||
private List<EventType> findPublishedEvents() {
|
||||
|
||||
DescribedPredicate<JavaClass> isEvent = implement(JMoleculesTypes.DOMAIN_EVENT) //
|
||||
@@ -529,15 +574,28 @@ public class ApplicationModule {
|
||||
.toList();
|
||||
}
|
||||
|
||||
@Value
|
||||
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
static class DeclaredDependency {
|
||||
|
||||
private static final String INVALID_EXPLICIT_MODULE_DEPENDENCY = "Invalid explicit module dependency in %s! No module found with name '%s'.";
|
||||
private static final String INVALID_NAMED_INTERFACE_DECLARATION = "No named interface named '%s' found! Original dependency declaration: %s -> %s.";
|
||||
|
||||
@NonNull ApplicationModule target;
|
||||
@NonNull NamedInterface namedInterface;
|
||||
private final ApplicationModule target;
|
||||
private final NamedInterface namedInterface;
|
||||
|
||||
/**
|
||||
* Creates a new {@link DeclaredDependency} for the given {@link ApplicationModule} and {@link NamedInterface}.
|
||||
*
|
||||
* @param target must not be {@literal null}.
|
||||
* @param namedInterface must not be {@literal null}.
|
||||
*/
|
||||
private DeclaredDependency(ApplicationModule target, NamedInterface namedInterface) {
|
||||
|
||||
Assert.notNull(target, "Target ApplicationModule must not be null!");
|
||||
Assert.notNull(namedInterface, "NamedInterface must not be null!");
|
||||
|
||||
this.target = target;
|
||||
this.namedInterface = namedInterface;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an {@link DeclaredDependency} to the module and optionally named interface defined by the given
|
||||
@@ -580,10 +638,22 @@ public class ApplicationModule {
|
||||
* @return
|
||||
*/
|
||||
public static DeclaredDependency to(ApplicationModule module) {
|
||||
|
||||
Assert.notNull(module, "ApplicationModule must not be null!");
|
||||
|
||||
return new DeclaredDependency(module, module.getNamedInterfaces().getUnnamedInterface());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the {@link DeclaredDependency} contains the given {@link JavaClass}.
|
||||
*
|
||||
* @param type must not be {@literal null}.
|
||||
* @return
|
||||
*/
|
||||
public boolean contains(JavaClass type) {
|
||||
|
||||
Assert.notNull(type, "Type must not be null!");
|
||||
|
||||
return namedInterface.contains(type);
|
||||
}
|
||||
|
||||
@@ -595,6 +665,35 @@ public class ApplicationModule {
|
||||
public String toString() {
|
||||
return namedInterface.isUnnamed() ? target.getName() : target.getName() + "::" + namedInterface.getName();
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see java.lang.Object#equals(java.lang.Object)
|
||||
*/
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
|
||||
if (this == obj) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!(obj instanceof DeclaredDependency that)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return Objects.equals(this.target, that.target) //
|
||||
&& Objects.equals(this.namedInterface, that.namedInterface);
|
||||
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see java.lang.Object#hashCode()
|
||||
*/
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(target, namedInterface);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -602,16 +701,26 @@ public class ApplicationModule {
|
||||
*
|
||||
* @author Oliver Drotbohm
|
||||
*/
|
||||
@Value
|
||||
static class DeclaredDependencies {
|
||||
|
||||
List<DeclaredDependency> dependencies;
|
||||
private final List<DeclaredDependency> dependencies;
|
||||
|
||||
/**
|
||||
* Creates a new {@link DeclaredDependencies} for the given {@link List} of {@link DeclaredDependency}.
|
||||
*
|
||||
* @param dependencies must not be {@literal null}.
|
||||
*/
|
||||
public DeclaredDependencies(List<DeclaredDependency> dependencies) {
|
||||
|
||||
Assert.notNull(dependencies, "Dependencies must not be null!");
|
||||
|
||||
this.dependencies = dependencies;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether any of the dependencies contains the given {@link JavaClass}.
|
||||
*
|
||||
* @param type must not be {@literal null}.
|
||||
* @return
|
||||
*/
|
||||
public boolean contains(JavaClass type) {
|
||||
|
||||
@@ -621,10 +730,17 @@ public class ApplicationModule {
|
||||
.anyMatch(it -> it.contains(type));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the {@link DeclaredDependencies} are empty.
|
||||
*/
|
||||
public boolean isEmpty() {
|
||||
return dependencies.isEmpty();
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see java.lang.Object#toString()
|
||||
*/
|
||||
@Override
|
||||
public String toString() {
|
||||
|
||||
@@ -632,18 +748,64 @@ public class ApplicationModule {
|
||||
.map(DeclaredDependency::toString)
|
||||
.collect(Collectors.joining(", "));
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see java.lang.Object#equals(java.lang.Object)
|
||||
*/
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
|
||||
if (this == obj) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!(obj instanceof DeclaredDependencies that)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return Objects.equals(this.dependencies, that.dependencies);
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see java.lang.Object#hashCode()
|
||||
*/
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(dependencies);
|
||||
}
|
||||
}
|
||||
|
||||
@EqualsAndHashCode
|
||||
@RequiredArgsConstructor
|
||||
static class QualifiedDependency {
|
||||
|
||||
private static final List<String> INJECTION_TYPES = Arrays.asList(//
|
||||
AT_AUTOWIRED, AT_RESOURCE, AT_INJECT);
|
||||
private static final List<String> INJECTION_TYPES = Arrays.asList(AT_AUTOWIRED, AT_RESOURCE, AT_INJECT);
|
||||
|
||||
private final @NonNull @Getter JavaClass source, target;
|
||||
private final @NonNull String description;
|
||||
private final @NonNull DependencyType type;
|
||||
private final JavaClass source, target;
|
||||
private final String description;
|
||||
private final DependencyType type;
|
||||
|
||||
/**
|
||||
* Creates a new {@link QualifiedDependency} from the given source and target {@link JavaClass}, description and
|
||||
* {@link DependencyType}.
|
||||
*
|
||||
* @param source must not be {@literal null}.
|
||||
* @param target must not be {@literal null}.
|
||||
* @param description must not be {@literal null}.
|
||||
* @param type must not be {@literal null}.
|
||||
*/
|
||||
public QualifiedDependency(JavaClass source, JavaClass target, String description, DependencyType type) {
|
||||
|
||||
Assert.notNull(source, "Source JavaClass must not be null!");
|
||||
Assert.notNull(target, "Target JavaClass must not be null!");
|
||||
Assert.notNull(description, "Description must not be null!");
|
||||
Assert.notNull(type, "DependencyType must not be null!");
|
||||
|
||||
this.source = source;
|
||||
this.target = target;
|
||||
this.description = description;
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
QualifiedDependency(Dependency dependency) {
|
||||
this(dependency.getOriginClass(), //
|
||||
@@ -652,6 +814,65 @@ public class ApplicationModule {
|
||||
DependencyType.forDependency(dependency));
|
||||
}
|
||||
|
||||
static QualifiedDependency fromCodeUnitParameter(JavaCodeUnit codeUnit, JavaClass parameter) {
|
||||
|
||||
var description = createDescription(codeUnit, parameter, "parameter");
|
||||
var type = DependencyType.forCodeUnit(codeUnit) //
|
||||
.defaultOr(() -> DependencyType.forParameter(parameter));
|
||||
|
||||
return new QualifiedDependency(codeUnit.getOwner(), parameter, description, type);
|
||||
}
|
||||
|
||||
static QualifiedDependency fromCodeUnitReturnType(JavaCodeUnit codeUnit) {
|
||||
|
||||
var description = createDescription(codeUnit, codeUnit.getRawReturnType(), "return type");
|
||||
|
||||
return new QualifiedDependency(codeUnit.getOwner(), codeUnit.getRawReturnType(), description,
|
||||
DependencyType.DEFAULT);
|
||||
}
|
||||
|
||||
static Stream<QualifiedDependency> fromType(ArchitecturallyEvidentType type) {
|
||||
|
||||
var source = type.getType();
|
||||
|
||||
return Stream.concat(Stream.concat(fromConstructorOf(type), fromMethodsOf(source)), fromFieldsOf(source));
|
||||
}
|
||||
|
||||
static Stream<QualifiedDependency> allFrom(JavaCodeUnit codeUnit) {
|
||||
|
||||
var parameterDependencies = codeUnit.getRawParameterTypes()//
|
||||
.stream() //
|
||||
.map(it -> fromCodeUnitParameter(codeUnit, it));
|
||||
|
||||
var returnType = Stream.of(fromCodeUnitReturnType(codeUnit));
|
||||
|
||||
return Stream.concat(parameterDependencies, returnType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the source {@link JavaClass}.
|
||||
*
|
||||
* @return the source will never be {@literal null}.
|
||||
*/
|
||||
public JavaClass getSource() {
|
||||
return source;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the target {@link JavaClass}.
|
||||
*
|
||||
* @return the target must not be {@literal null}.
|
||||
*/
|
||||
public JavaClass getTarget() {
|
||||
return target;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the {@link QualifiedDependency} has the given {@link DependencyType}.
|
||||
*
|
||||
* @param type must not be {@literal null}.
|
||||
* @return
|
||||
*/
|
||||
boolean hasType(DependencyType type) {
|
||||
return this.type.equals(type);
|
||||
}
|
||||
@@ -704,39 +925,34 @@ public class ApplicationModule {
|
||||
return type.format(FormatableType.of(source), FormatableType.of(target));
|
||||
}
|
||||
|
||||
static QualifiedDependency fromCodeUnitParameter(JavaCodeUnit codeUnit, JavaClass parameter) {
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see java.lang.Object#equals(java.lang.Object)
|
||||
*/
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
|
||||
var description = createDescription(codeUnit, parameter, "parameter");
|
||||
var type = DependencyType.forCodeUnit(codeUnit) //
|
||||
.defaultOr(() -> DependencyType.forParameter(parameter));
|
||||
if (this == obj) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return new QualifiedDependency(codeUnit.getOwner(), parameter, description, type);
|
||||
if (!(obj instanceof QualifiedDependency other)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return Objects.equals(this.source, other.source) //
|
||||
&& Objects.equals(this.target, other.target) //
|
||||
&& Objects.equals(this.description, other.description) //
|
||||
&& Objects.equals(this.type, other.type); //
|
||||
}
|
||||
|
||||
static QualifiedDependency fromCodeUnitReturnType(JavaCodeUnit codeUnit) {
|
||||
|
||||
var description = createDescription(codeUnit, codeUnit.getRawReturnType(), "return type");
|
||||
|
||||
return new QualifiedDependency(codeUnit.getOwner(), codeUnit.getRawReturnType(), description,
|
||||
DependencyType.DEFAULT);
|
||||
}
|
||||
|
||||
static Stream<QualifiedDependency> fromType(ArchitecturallyEvidentType type) {
|
||||
|
||||
var source = type.getType();
|
||||
|
||||
return Stream.concat(Stream.concat(fromConstructorOf(type), fromMethodsOf(source)), fromFieldsOf(source));
|
||||
}
|
||||
|
||||
static Stream<QualifiedDependency> allFrom(JavaCodeUnit codeUnit) {
|
||||
|
||||
var parameterDependencies = codeUnit.getRawParameterTypes()//
|
||||
.stream() //
|
||||
.map(it -> fromCodeUnitParameter(codeUnit, it));
|
||||
|
||||
var returnType = Stream.of(fromCodeUnitReturnType(codeUnit));
|
||||
|
||||
return Stream.concat(parameterDependencies, returnType);
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see java.lang.Object#hashCode()
|
||||
*/
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(source, target, description, type);
|
||||
}
|
||||
|
||||
private static Stream<QualifiedDependency> fromConstructorOf(ArchitecturallyEvidentType source) {
|
||||
@@ -885,15 +1101,35 @@ public class ApplicationModule {
|
||||
}
|
||||
}
|
||||
|
||||
@ToString
|
||||
@EqualsAndHashCode
|
||||
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
private static class DefaultApplicationModuleDependency implements ApplicationModuleDependency {
|
||||
|
||||
private final QualifiedDependency dependency;
|
||||
private final ApplicationModule target;
|
||||
|
||||
static Stream<DefaultApplicationModuleDependency> of(QualifiedDependency dependency, ApplicationModules modules) {
|
||||
/**
|
||||
* Creates a new {@link ApplicationModuleDependency} for the given {@link QualifiedDependency} and
|
||||
* {@link ApplicationModules}.
|
||||
*
|
||||
* @param dependency must not be {@literal null}.
|
||||
* @param target must not be {@literal null}.
|
||||
*/
|
||||
private DefaultApplicationModuleDependency(QualifiedDependency dependency, ApplicationModule target) {
|
||||
|
||||
Assert.notNull(dependency, "QualifiedDependency must not be null!");
|
||||
Assert.notNull(target, "Target ApplicationModule must not be null!");
|
||||
|
||||
this.dependency = dependency;
|
||||
this.target = target;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new {@link Stream} of {@link ApplicationModuleDependency} for the given {@link QualifiedDependency} and
|
||||
* {@link ApplicationModules}.
|
||||
*
|
||||
* @param dependency must not be {@literal null}.
|
||||
* @param modules must not be {@literal null}.
|
||||
*/
|
||||
static Stream<ApplicationModuleDependency> of(QualifiedDependency dependency, ApplicationModules modules) {
|
||||
|
||||
return modules.getModuleByType(dependency.getTarget()).stream()
|
||||
.map(it -> new DefaultApplicationModuleDependency(dependency, it));
|
||||
@@ -934,5 +1170,42 @@ public class ApplicationModule {
|
||||
public ApplicationModule getTargetModule() {
|
||||
return target;
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see java.lang.Object#toString()
|
||||
*/
|
||||
@Override
|
||||
public String toString() {
|
||||
return "DefaultApplicationModuleDependency [dependency=" + dependency + ", target=" + target + "]";
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see java.lang.Object#equals(java.lang.Object)
|
||||
*/
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
|
||||
if (this == obj) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!(obj instanceof DefaultApplicationModuleDependency other)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return Objects.equals(this.target, other.target) //
|
||||
&& Objects.equals(this.dependency, other.dependency);
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see java.lang.Object#hashCode()
|
||||
*/
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(target, dependency);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,8 +15,6 @@
|
||||
*/
|
||||
package org.springframework.modulith.model;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.function.Function;
|
||||
@@ -25,16 +23,45 @@ import java.util.stream.Stream;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* The materialized, in other words actually present dependencies of the current module towards other modules.
|
||||
* The materialized, in other words actually present, dependencies of the current module towards other modules.
|
||||
*
|
||||
* @author Oliver Drotbohm
|
||||
*/
|
||||
@RequiredArgsConstructor(staticName = "of")
|
||||
public class ApplicationModuleDependencies {
|
||||
|
||||
private final List<ApplicationModuleDependency> dependencies;
|
||||
private final ApplicationModules modules;
|
||||
|
||||
/**
|
||||
* Creates a new {@link ApplicationModuleDependencies} for the given {@link List} of
|
||||
* {@link ApplicationModuleDependency} and {@link ApplicationModules}.
|
||||
*
|
||||
* @param dependencies must not be {@literal null}.
|
||||
* @param modules must not be {@literal null}.
|
||||
*/
|
||||
private ApplicationModuleDependencies(List<ApplicationModuleDependency> dependencies, ApplicationModules modules) {
|
||||
|
||||
Assert.notNull(dependencies, "ApplicationModuleDependency list must not be null!");
|
||||
Assert.notNull(modules, "ApplicationModules must not be null!");
|
||||
|
||||
this.dependencies = dependencies;
|
||||
this.modules = modules;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new {@link ApplicationModuleDependencies} for the given {@link List} of
|
||||
* {@link ApplicationModuleDependency} and {@link ApplicationModules}.
|
||||
*
|
||||
* @param dependencies must not be {@literal null}.
|
||||
* @param modules must not be {@literal null}.
|
||||
* @return will never be {@literal null}.
|
||||
*/
|
||||
static ApplicationModuleDependencies of(List<ApplicationModuleDependency> dependencies,
|
||||
ApplicationModules modules) {
|
||||
|
||||
return new ApplicationModuleDependencies(dependencies, modules);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the dependencies contain the given {@link ApplicationModule}.
|
||||
*
|
||||
|
||||
@@ -19,12 +19,6 @@ import static com.tngtech.archunit.base.DescribedPredicate.*;
|
||||
import static com.tngtech.archunit.core.domain.JavaClass.Predicates.*;
|
||||
import static java.util.stream.Collectors.*;
|
||||
|
||||
import lombok.AccessLevel;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import lombok.Value;
|
||||
import lombok.With;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
@@ -60,7 +54,6 @@ import com.tngtech.archunit.library.dependencies.SlicesRuleDefinition;
|
||||
* @author Oliver Drotbohm
|
||||
* @author Peter Gafert
|
||||
*/
|
||||
@AllArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
public class ApplicationModules implements Iterable<ApplicationModule> {
|
||||
|
||||
private static final Map<CacheKey, ApplicationModules> CACHE = new HashMap<>();
|
||||
@@ -90,7 +83,7 @@ public class ApplicationModules implements Iterable<ApplicationModule> {
|
||||
private final Map<String, ApplicationModule> modules;
|
||||
private final JavaClasses allClasses;
|
||||
private final List<JavaPackage> rootPackages;
|
||||
private final @With(AccessLevel.PRIVATE) @Getter Set<ApplicationModule> sharedModules;
|
||||
private final Set<ApplicationModule> sharedModules;
|
||||
private final List<String> orderedNames;
|
||||
|
||||
private boolean verified;
|
||||
@@ -123,6 +116,39 @@ public class ApplicationModules implements Iterable<ApplicationModule> {
|
||||
: modules.values().stream().map(ApplicationModule::getName).toList();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new {@link ApplicationModules} for the given {@link ModulithMetadata}, {@link ApplicationModule}s,
|
||||
* {@link JavaClasses}, {@link JavaPackage}s, shared {@link ApplicationModule}s, ordered module names and verified
|
||||
* flag.
|
||||
*
|
||||
* @param metadata must not be {@literal null}.
|
||||
* @param modules must not be {@literal null}.
|
||||
* @param allClasses must not be {@literal null}.
|
||||
* @param rootPackages must not be {@literal null}.
|
||||
* @param sharedModules must not be {@literal null}.
|
||||
* @param orderedNames must not be {@literal null}.
|
||||
* @param verified
|
||||
*/
|
||||
private ApplicationModules(ModulithMetadata metadata, Map<String, ApplicationModule> modules, JavaClasses classes,
|
||||
List<JavaPackage> rootPackages, Set<ApplicationModule> sharedModules, List<String> orderedNames,
|
||||
boolean verified) {
|
||||
|
||||
Assert.notNull(metadata, "ModulithMetadata must not be null!");
|
||||
Assert.notNull(modules, "Application modules must not be null!");
|
||||
Assert.notNull(classes, "JavaClasses must not be null!");
|
||||
Assert.notNull(rootPackages, "Root JavaPackages must not be null!");
|
||||
Assert.notNull(sharedModules, "Shared ApplicationModules must not be null!");
|
||||
Assert.notNull(orderedNames, "Ordered application module names must not be null!");
|
||||
|
||||
this.metadata = metadata;
|
||||
this.modules = modules;
|
||||
this.allClasses = classes;
|
||||
this.rootPackages = rootPackages;
|
||||
this.sharedModules = sharedModules;
|
||||
this.orderedNames = orderedNames;
|
||||
this.verified = verified;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new {@link ApplicationModules} relative to the given modulith type. Will inspect the {@link Modulith}
|
||||
* annotation on the class given for advanced customizations of the module setup.
|
||||
@@ -147,7 +173,7 @@ public class ApplicationModules implements Iterable<ApplicationModule> {
|
||||
*/
|
||||
public static ApplicationModules of(Class<?> modulithType, DescribedPredicate<JavaClass> ignored) {
|
||||
|
||||
CacheKey key = TypeKey.of(modulithType, ignored);
|
||||
CacheKey key = new TypeKey(modulithType, ignored);
|
||||
|
||||
return CACHE.computeIfAbsent(key, it -> {
|
||||
|
||||
@@ -177,7 +203,7 @@ public class ApplicationModules implements Iterable<ApplicationModule> {
|
||||
*/
|
||||
public static ApplicationModules of(String javaPackage, DescribedPredicate<JavaClass> ignored) {
|
||||
|
||||
CacheKey key = PackageKey.of(javaPackage, ignored);
|
||||
CacheKey key = new PackageKey(javaPackage, ignored);
|
||||
|
||||
return CACHE.computeIfAbsent(key, it -> {
|
||||
|
||||
@@ -189,33 +215,32 @@ public class ApplicationModules implements Iterable<ApplicationModule> {
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new {@link ApplicationModules} instance for the given {@link CacheKey}.
|
||||
* Returns the source of the {@link ApplicationModules}. Either a main application class or a package name.
|
||||
*
|
||||
* @param key must not be {@literal null}.
|
||||
* @return will never be {@literal null}.
|
||||
* @deprecated use {@link #getSource()} instead
|
||||
*/
|
||||
private static ApplicationModules of(CacheKey key) {
|
||||
|
||||
Assert.notNull(key, "Cache key must not be null!");
|
||||
|
||||
ModulithMetadata metadata = key.getMetadata();
|
||||
|
||||
Set<String> basePackages = new HashSet<>();
|
||||
basePackages.add(key.getBasePackage());
|
||||
basePackages.addAll(metadata.getAdditionalPackages());
|
||||
|
||||
ApplicationModules modules = new ApplicationModules(metadata, basePackages, key.getIgnored(),
|
||||
metadata.useFullyQualifiedModuleNames(), IMPORT_OPTION);
|
||||
|
||||
Set<ApplicationModule> sharedModules = metadata.getSharedModuleNames() //
|
||||
.map(modules::getRequiredModule) //
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
return modules.withSharedModules(sharedModules);
|
||||
@Deprecated(forRemoval = true)
|
||||
public Object getModulithSource() {
|
||||
return metadata.getSource();
|
||||
}
|
||||
|
||||
public Object getModulithSource() {
|
||||
return metadata.getModulithSource();
|
||||
/**
|
||||
* Returns the source of the {@link ApplicationModules}. Either a main application class or a package name.
|
||||
*
|
||||
* @return will never be {@literal null}.
|
||||
*/
|
||||
public Object getSource() {
|
||||
return metadata.getSource();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all {@link ApplicationModule}s registered as shared ones.
|
||||
*
|
||||
* @return will never be {@literal null}.
|
||||
*/
|
||||
public Set<ApplicationModule> getSharedModules() {
|
||||
return sharedModules;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -421,9 +446,13 @@ public class ApplicationModules implements Iterable<ApplicationModule> {
|
||||
return this.stream().map(ApplicationModule::toString).collect(Collectors.joining("\n"));
|
||||
}
|
||||
|
||||
private ApplicationModules withSharedModules(Set<ApplicationModule> sharedModules) {
|
||||
return new ApplicationModules(metadata, modules, allClasses, rootPackages, sharedModules, orderedNames, verified);
|
||||
}
|
||||
|
||||
private FailureReport assertNoCyclesFor(JavaPackage rootPackage) {
|
||||
|
||||
EvaluationResult result = SlicesRuleDefinition.slices() //
|
||||
var result = SlicesRuleDefinition.slices() //
|
||||
.matching(rootPackage.getName().concat(".(*)..")) //
|
||||
.should().beFreeOfCycles() //
|
||||
.evaluate(allClasses.that(resideInAPackage(rootPackage.getName().concat(".."))));
|
||||
@@ -457,7 +486,7 @@ public class ApplicationModules implements Iterable<ApplicationModule> {
|
||||
*/
|
||||
private ApplicationModule getRequiredModule(String moduleName) {
|
||||
|
||||
ApplicationModule module = modules.get(moduleName);
|
||||
var module = modules.get(moduleName);
|
||||
|
||||
if (module == null) {
|
||||
throw new IllegalArgumentException(String.format("Module %s does not exist!", moduleName));
|
||||
@@ -466,6 +495,32 @@ public class ApplicationModules implements Iterable<ApplicationModule> {
|
||||
return module;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new {@link ApplicationModules} instance for the given {@link CacheKey}.
|
||||
*
|
||||
* @param key must not be {@literal null}.
|
||||
* @return will never be {@literal null}.
|
||||
*/
|
||||
private static ApplicationModules of(CacheKey key) {
|
||||
|
||||
Assert.notNull(key, "Cache key must not be null!");
|
||||
|
||||
var metadata = key.getMetadata();
|
||||
|
||||
var basePackages = new HashSet<String>();
|
||||
basePackages.add(key.getBasePackage());
|
||||
basePackages.addAll(metadata.getAdditionalPackages());
|
||||
|
||||
var modules = new ApplicationModules(metadata, basePackages, key.getIgnored(),
|
||||
metadata.useFullyQualifiedModuleNames(), IMPORT_OPTION);
|
||||
|
||||
var sharedModules = metadata.getSharedModuleNames() //
|
||||
.map(modules::getRequiredModule) //
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
return modules.withSharedModules(sharedModules);
|
||||
}
|
||||
|
||||
public static class Filters {
|
||||
|
||||
public static DescribedPredicate<JavaClass> withoutModules(String... names) {
|
||||
@@ -489,11 +544,22 @@ public class ApplicationModules implements Iterable<ApplicationModule> {
|
||||
ModulithMetadata getMetadata();
|
||||
}
|
||||
|
||||
@Value(staticConstructor = "of")
|
||||
private static final class TypeKey implements CacheKey {
|
||||
|
||||
Class<?> type;
|
||||
DescribedPredicate<JavaClass> ignored;
|
||||
private final Class<?> type;
|
||||
private final DescribedPredicate<JavaClass> ignored;
|
||||
|
||||
/**
|
||||
* Creates a new {@link TypeKey} for the given type and {@link DescribedPredicate} of ignored {@link JavaClass}es.
|
||||
*
|
||||
* @param type must not be {@literal null}.
|
||||
* @param ignored must not be {@literal null}.
|
||||
*/
|
||||
TypeKey(Class<?> type, DescribedPredicate<JavaClass> ignored) {
|
||||
|
||||
this.type = type;
|
||||
this.ignored = ignored;
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
@@ -512,13 +578,79 @@ public class ApplicationModules implements Iterable<ApplicationModule> {
|
||||
public ModulithMetadata getMetadata() {
|
||||
return ModulithMetadata.of(type);
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.modulith.model.ApplicationModules.CacheKey#getIgnored()
|
||||
*/
|
||||
@Override
|
||||
public DescribedPredicate<JavaClass> getIgnored() {
|
||||
return ignored;
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see java.lang.Object#equals(java.lang.Object)
|
||||
*/
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
|
||||
if (this == obj) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!(obj instanceof TypeKey other)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return Objects.equals(this.type, other.type) //
|
||||
&& Objects.equals(this.ignored, other.ignored);
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see java.lang.Object#hashCode()
|
||||
*/
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(type, ignored);
|
||||
}
|
||||
}
|
||||
|
||||
@Value(staticConstructor = "of")
|
||||
private static final class PackageKey implements CacheKey {
|
||||
|
||||
String basePackage;
|
||||
DescribedPredicate<JavaClass> ignored;
|
||||
private final String basePackage;
|
||||
private final DescribedPredicate<JavaClass> ignored;
|
||||
|
||||
/**
|
||||
* Creates a new {@link PackageKey} for the given base package and {@link DescribedPredicate} of ignored
|
||||
* {@link JavaClass}es.
|
||||
*
|
||||
* @param basePackage must not be {@literal null}.
|
||||
* @param ignored must not be {@literal null}.
|
||||
*/
|
||||
PackageKey(String basePackage, DescribedPredicate<JavaClass> ignored) {
|
||||
|
||||
this.basePackage = basePackage;
|
||||
this.ignored = ignored;
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.modulith.model.ApplicationModules.CacheKey#getBasePackage()
|
||||
*/
|
||||
@Override
|
||||
public String getBasePackage() {
|
||||
return basePackage;
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.modulith.model.ApplicationModules.CacheKey#getIgnored()
|
||||
*/
|
||||
public DescribedPredicate<JavaClass> getIgnored() {
|
||||
return ignored;
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
@@ -528,6 +660,34 @@ public class ApplicationModules implements Iterable<ApplicationModule> {
|
||||
public ModulithMetadata getMetadata() {
|
||||
return ModulithMetadata.of(basePackage);
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see java.lang.Object#equals(java.lang.Object)
|
||||
*/
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
|
||||
if (this == obj) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!(obj instanceof PackageKey that)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return Objects.equals(this.basePackage, that.basePackage) //
|
||||
&& Objects.equals(this.ignored, that.ignored);
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see java.lang.Object#hashCode()
|
||||
*/
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(basePackage, ignored);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -17,11 +17,6 @@ package org.springframework.modulith.model;
|
||||
|
||||
import static org.springframework.modulith.model.Types.JavaXTypes.*;
|
||||
|
||||
import lombok.AccessLevel;
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.Value;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Comparator;
|
||||
@@ -51,12 +46,15 @@ import com.tngtech.archunit.thirdparty.com.google.common.base.Suppliers;
|
||||
*
|
||||
* @author Oliver Drotbohm
|
||||
*/
|
||||
@RequiredArgsConstructor(access = AccessLevel.PROTECTED)
|
||||
public abstract class ArchitecturallyEvidentType {
|
||||
|
||||
private static Map<Key, ArchitecturallyEvidentType> CACHE = new HashMap<>();
|
||||
|
||||
private final @Getter JavaClass type;
|
||||
private final JavaClass type;
|
||||
|
||||
protected ArchitecturallyEvidentType(JavaClass type) {
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new {@link ArchitecturallyEvidentType} for the given {@link JavaType} and {@link Classes} of Spring
|
||||
@@ -68,7 +66,7 @@ public abstract class ArchitecturallyEvidentType {
|
||||
*/
|
||||
public static ArchitecturallyEvidentType of(JavaClass type, Classes beanTypes) {
|
||||
|
||||
return CACHE.computeIfAbsent(Key.of(type, beanTypes), it -> {
|
||||
return CACHE.computeIfAbsent(new Key(type, beanTypes), it -> {
|
||||
|
||||
List<ArchitecturallyEvidentType> delegates = new ArrayList<>();
|
||||
|
||||
@@ -86,6 +84,15 @@ public abstract class ArchitecturallyEvidentType {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@link JavaClass} backing the {@link ArchitecturallyEvidentType}.
|
||||
*
|
||||
* @return the type wnn
|
||||
*/
|
||||
public JavaClass getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the abbreviated (i.e. every package fragment reduced to its first character) full name.
|
||||
*
|
||||
@@ -620,18 +627,20 @@ public abstract class ArchitecturallyEvidentType {
|
||||
}
|
||||
}
|
||||
|
||||
@Value(staticConstructor = "of")
|
||||
private static class Key {
|
||||
private static record Key(JavaClass type, Classes beanTypes) {}
|
||||
|
||||
JavaClass type;
|
||||
Classes beanTypes;
|
||||
}
|
||||
|
||||
@Value
|
||||
public final class ReferenceMethod {
|
||||
public static class ReferenceMethod {
|
||||
|
||||
private final JavaMethod method;
|
||||
|
||||
public ReferenceMethod(JavaMethod method) {
|
||||
this.method = method;
|
||||
}
|
||||
|
||||
public JavaMethod getMethod() {
|
||||
return method;
|
||||
}
|
||||
|
||||
public boolean isAsync() {
|
||||
return method.isAnnotatedWith(SpringTypes.AT_ASYNC) || method.isMetaAnnotatedWith(SpringTypes.AT_ASYNC);
|
||||
}
|
||||
|
||||
@@ -15,15 +15,13 @@
|
||||
*/
|
||||
package org.springframework.modulith.model;
|
||||
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.ToString;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.stream.Collector;
|
||||
@@ -46,8 +44,6 @@ import com.tngtech.archunit.core.domain.properties.HasName;
|
||||
/**
|
||||
* @author Oliver Drotbohm
|
||||
*/
|
||||
@ToString
|
||||
@EqualsAndHashCode
|
||||
class Classes implements DescribedIterable<JavaClass> {
|
||||
|
||||
public static Classes NONE = Classes.of(Collections.emptyList());
|
||||
@@ -65,7 +61,7 @@ class Classes implements DescribedIterable<JavaClass> {
|
||||
|
||||
this.classes = classes.stream() //
|
||||
.sorted(Comparator.comparing(JavaClass::getName)) //
|
||||
.collect(Collectors.collectingAndThen(Collectors.toList(), Collections::unmodifiableList));
|
||||
.toList();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -190,35 +186,73 @@ class Classes implements DescribedIterable<JavaClass> {
|
||||
return classes.iterator();
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see java.lang.Object#toString()
|
||||
*/
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Classes [classes=" + classes + "]";
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see java.lang.Object#equals(java.lang.Object)
|
||||
*/
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
|
||||
if (this == obj) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!(obj instanceof Classes that)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return Objects.equals(classes, that.classes);
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see java.lang.Object#hashCode()
|
||||
*/
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(classes);
|
||||
}
|
||||
|
||||
String format() {
|
||||
|
||||
return classes.stream() //
|
||||
.map(Classes::format) //
|
||||
.collect(Collectors.joining("\n"));
|
||||
}
|
||||
|
||||
String format(String basePackage) {
|
||||
|
||||
return classes.stream() //
|
||||
.map(it -> Classes.format(it, basePackage)) //
|
||||
.collect(Collectors.joining("\n"));
|
||||
}
|
||||
|
||||
private static String format(JavaClass type) {
|
||||
return format(type, "");
|
||||
}
|
||||
|
||||
static String format(JavaClass type, String basePackage) {
|
||||
|
||||
Assert.notNull(type, "Type must not be null!");
|
||||
Assert.notNull(basePackage, "Base package must not be null!");
|
||||
|
||||
String prefix = type.getModifiers().contains(JavaModifier.PUBLIC) ? "+" : "o";
|
||||
String name = StringUtils.hasText(basePackage) //
|
||||
var prefix = type.getModifiers().contains(JavaModifier.PUBLIC) ? "+" : "o";
|
||||
var name = StringUtils.hasText(basePackage) //
|
||||
? type.getName().replace(basePackage, "…") //
|
||||
: type.getName();
|
||||
|
||||
return String.format("%s %s", prefix, name);
|
||||
}
|
||||
|
||||
private static String format(JavaClass type) {
|
||||
return format(type, "");
|
||||
}
|
||||
|
||||
private static class SameClass extends DescribedPredicate<JavaClass> {
|
||||
|
||||
private final JavaClass reference;
|
||||
|
||||
@@ -15,10 +15,6 @@
|
||||
*/
|
||||
package org.springframework.modulith.model;
|
||||
|
||||
import lombok.AccessLevel;
|
||||
import lombok.NonNull;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
@@ -26,6 +22,7 @@ import java.util.Optional;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.springframework.core.annotation.AnnotatedElementUtils;
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.modulith.Modulith;
|
||||
import org.springframework.modulith.Modulithic;
|
||||
import org.springframework.modulith.model.Types.SpringTypes;
|
||||
@@ -37,13 +34,24 @@ import org.springframework.util.Assert;
|
||||
*
|
||||
* @author Oliver Drotbohm
|
||||
*/
|
||||
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
class DefaultModulithMetadata implements ModulithMetadata {
|
||||
|
||||
private static final Class<? extends Annotation> AT_SPRING_BOOT_APPLICATION = Types
|
||||
.loadIfPresent(SpringTypes.AT_SPRING_BOOT_APPLICATION);
|
||||
|
||||
private final @NonNull Object modulithSource;
|
||||
private final @NonNull Object source;
|
||||
|
||||
/**
|
||||
* Creates a new {@link DefaultModulithMetadata} for the given source.
|
||||
*
|
||||
* @param source must not be {@literal null}.
|
||||
*/
|
||||
private DefaultModulithMetadata(Object source) {
|
||||
|
||||
Assert.notNull(source, "Source must not be null!");
|
||||
|
||||
this.source = source;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new {@link ModulithMetadata} representing the defaults of a class annotated but not customized with
|
||||
@@ -80,7 +88,16 @@ class DefaultModulithMetadata implements ModulithMetadata {
|
||||
*/
|
||||
@Override
|
||||
public Object getModulithSource() {
|
||||
return modulithSource;
|
||||
return getSource();
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.modulith.model.ModulithMetadata#getModulithSource()
|
||||
*/
|
||||
@Override
|
||||
public Object getSource() {
|
||||
return source;
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
@@ -15,9 +15,8 @@
|
||||
*/
|
||||
package org.springframework.modulith.model;
|
||||
|
||||
import lombok.Value;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.springframework.util.Assert;
|
||||
@@ -30,15 +29,9 @@ import com.tngtech.archunit.core.domain.JavaModifier;
|
||||
*
|
||||
* @author Oliver Drotbohm
|
||||
*/
|
||||
@Value
|
||||
public class EventType {
|
||||
|
||||
private final JavaClass type;
|
||||
|
||||
/**
|
||||
* The sources that create that event. Includes static factory methods that return an instance of the event type
|
||||
* itself as well as constructor invocations, except ones from the factory methods.
|
||||
*/
|
||||
private final List<Source> sources;
|
||||
|
||||
/**
|
||||
@@ -66,7 +59,68 @@ public class EventType {
|
||||
.toList();
|
||||
}
|
||||
|
||||
/**
|
||||
* The {@link JavaClass} of the {@link EventType}.
|
||||
*
|
||||
* @return will never be {@literal null}.
|
||||
*/
|
||||
public JavaClass getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
/**
|
||||
* The sources that create that event. Includes static factory methods that return an instance of the event type
|
||||
* itself as well as constructor invocations, except ones from the factory methods.
|
||||
*
|
||||
* @return will never be {@literal null}.
|
||||
*/
|
||||
public List<Source> getSources() {
|
||||
return sources;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether any sources exist at all.
|
||||
*
|
||||
* @see #getSources()
|
||||
*/
|
||||
public boolean hasSources() {
|
||||
return !this.sources.isEmpty();
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see java.lang.Object#toString()
|
||||
*/
|
||||
@Override
|
||||
public String toString() {
|
||||
return "EventType [type=" + type + ", sources=" + sources + "]";
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see java.lang.Object#equals(java.lang.Object)
|
||||
*/
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
|
||||
if (this == obj) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!(obj instanceof EventType thaType)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return Objects.equals(sources, thaType.sources) //
|
||||
&& Objects.equals(type, thaType.type);
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see java.lang.Object#hashCode()
|
||||
*/
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(sources, type);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,9 +15,6 @@
|
||||
*/
|
||||
package org.springframework.modulith.model;
|
||||
|
||||
import lombok.AccessLevel;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.stream.Collectors;
|
||||
@@ -37,7 +34,6 @@ import com.tngtech.archunit.thirdparty.com.google.common.base.Suppliers;
|
||||
*
|
||||
* @author Oliver Drotbohm
|
||||
*/
|
||||
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
public class FormatableType {
|
||||
|
||||
private static final Map<String, FormatableType> CACHE = new ConcurrentHashMap<>();
|
||||
@@ -45,6 +41,21 @@ public class FormatableType {
|
||||
private final String type;
|
||||
private final Supplier<String> abbreviatedName;
|
||||
|
||||
/**
|
||||
* Creates a new {@link FormatableType} for the given source {@link String} and lazily computed abbreviated name.
|
||||
*
|
||||
* @param type must not be {@literal null} or empty.
|
||||
* @param abbreviatedName must not be {@literal null}.
|
||||
*/
|
||||
private FormatableType(String type, Supplier<String> abbreviatedName) {
|
||||
|
||||
Assert.hasText(type, "Type string must not be null or empty!");
|
||||
Assert.notNull(abbreviatedName, "Computed abbreviated name must not be null!");
|
||||
|
||||
this.type = type;
|
||||
this.abbreviatedName = abbreviatedName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new {@link FormatableType} for the given {@link JavaClass}.
|
||||
*
|
||||
|
||||
@@ -17,20 +17,17 @@ package org.springframework.modulith.model;
|
||||
|
||||
import static com.tngtech.archunit.core.domain.JavaClass.Predicates.*;
|
||||
|
||||
import lombok.AccessLevel;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.util.Collection;
|
||||
import java.util.Iterator;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.springframework.core.annotation.AnnotatedElementUtils;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
import com.tngtech.archunit.base.DescribedIterable;
|
||||
import com.tngtech.archunit.base.DescribedPredicate;
|
||||
@@ -42,19 +39,27 @@ import com.tngtech.archunit.thirdparty.com.google.common.base.Suppliers;
|
||||
/**
|
||||
* @author Oliver Drotbohm
|
||||
*/
|
||||
@EqualsAndHashCode
|
||||
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
public class JavaPackage implements DescribedIterable<JavaClass> {
|
||||
|
||||
private static final String PACKAGE_INFO_NAME = "package-info";
|
||||
|
||||
private final @Getter String name;
|
||||
private final String name;
|
||||
private final Classes classes;
|
||||
private final Classes packageClasses;
|
||||
private final Supplier<Set<JavaPackage>> directSubPackages;
|
||||
|
||||
/**
|
||||
* Creates a new {@link JavaPackage} for the given {@link Classes}, name and whether to include all sub-packages.
|
||||
*
|
||||
* @param classes must not be {@literal null}.
|
||||
* @param name must not be {@literal null} or empty.
|
||||
* @param includeSubPackages
|
||||
*/
|
||||
private JavaPackage(Classes classes, String name, boolean includeSubPackages) {
|
||||
|
||||
Assert.notNull(classes, "Classes must not be null!");
|
||||
Assert.hasText(name, "Name must not be null or empty!");
|
||||
|
||||
this.classes = classes;
|
||||
this.packageClasses = classes.that(resideInAPackage(includeSubPackages ? name.concat("..") : name));
|
||||
this.name = name;
|
||||
@@ -67,22 +72,61 @@ public class JavaPackage implements DescribedIterable<JavaClass> {
|
||||
.collect(Collectors.toSet()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new {@link JavaPackage} for the given classes and name.
|
||||
*
|
||||
* @param classes must not be {@literal null}.
|
||||
* @param name must not be {@literal null} or empty.
|
||||
* @return
|
||||
*/
|
||||
public static JavaPackage of(Classes classes, String name) {
|
||||
return new JavaPackage(classes, name, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the given type is the {@code package-info.java} one.
|
||||
*
|
||||
* @param type must not be {@literal null}.
|
||||
*/
|
||||
public static boolean isPackageInfoType(JavaClass type) {
|
||||
|
||||
Assert.notNull(type, "Type must not be null!");
|
||||
|
||||
return type.getSimpleName().equals(PACKAGE_INFO_NAME);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the name of the package.
|
||||
*
|
||||
* @return will never be {@literal null}.
|
||||
*/
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reduces the {@link JavaPackage} to only its base package.
|
||||
*
|
||||
* @return will never be {@literal null}.
|
||||
*/
|
||||
public JavaPackage toSingle() {
|
||||
return new JavaPackage(classes, name, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the local name of the package, i.e. the last segment of the qualified package name.
|
||||
*
|
||||
* @return will never be {@literal null}.
|
||||
*/
|
||||
public String getLocalName() {
|
||||
return name.substring(name.lastIndexOf(".") + 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all direct sub-packages of the current one.
|
||||
*
|
||||
* @return will never be {@literal null}.
|
||||
*/
|
||||
public Collection<JavaPackage> getDirectSubPackages() {
|
||||
return directSubPackages.get();
|
||||
}
|
||||
@@ -91,32 +135,22 @@ public class JavaPackage implements DescribedIterable<JavaClass> {
|
||||
* Returns all classes residing in the current package and potentially in sub-packages if the current package was
|
||||
* created to include them.
|
||||
*
|
||||
* @return
|
||||
* @return will never be {@literal null}.
|
||||
*/
|
||||
public Classes getClasses() {
|
||||
return packageClasses;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the direct sub-package name of the given candidate.
|
||||
* Returns all sub-packages that carry the given annotation type.
|
||||
*
|
||||
* @param candidate
|
||||
* @return
|
||||
* @param annotation must not be {@literal null}.
|
||||
* @return will never be {@literal null}.
|
||||
*/
|
||||
private String extractDirectSubPackage(String candidate) {
|
||||
|
||||
if (candidate.length() <= name.length()) {
|
||||
return candidate;
|
||||
}
|
||||
|
||||
int subSubPackageIndex = candidate.indexOf('.', name.length() + 1);
|
||||
int endIndex = subSubPackageIndex == -1 ? candidate.length() : subSubPackageIndex;
|
||||
|
||||
return candidate.substring(0, endIndex);
|
||||
}
|
||||
|
||||
public Stream<JavaPackage> getSubPackagesAnnotatedWith(Class<? extends Annotation> annotation) {
|
||||
|
||||
Assert.notNull(annotation, "Annotation must not be null!");
|
||||
|
||||
return packageClasses.that(JavaClass.Predicates.simpleName(PACKAGE_INFO_NAME) //
|
||||
.and(CanBeAnnotated.Predicates.annotatedWith(annotation))).stream() //
|
||||
.map(JavaClass::getPackageName) //
|
||||
@@ -124,22 +158,59 @@ public class JavaPackage implements DescribedIterable<JavaClass> {
|
||||
.map(it -> of(classes, it));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all {@link Classes} that match the given {@link DescribedPredicate}.
|
||||
*
|
||||
* @param predicate must not be {@literal null}.
|
||||
* @return
|
||||
*/
|
||||
public Classes that(DescribedPredicate<? super JavaClass> predicate) {
|
||||
|
||||
Assert.notNull(predicate, "Predicate must not be null!");
|
||||
|
||||
return packageClasses.that(predicate);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return whether the {@link JavaPackage} contains the given type.
|
||||
*
|
||||
* @param type must not be {@literal null}.
|
||||
*/
|
||||
public boolean contains(JavaClass type) {
|
||||
|
||||
Assert.notNull(type, "Type must not be null!");
|
||||
|
||||
return packageClasses.contains(type);
|
||||
}
|
||||
|
||||
public boolean contains(String className) {
|
||||
return packageClasses.contains(className);
|
||||
/**
|
||||
* Returns whether the {@link JavaPackage} contains the type with the given name.
|
||||
*
|
||||
* @param typeName must not be {@literal null} or empty.
|
||||
*/
|
||||
public boolean contains(String typeName) {
|
||||
|
||||
Assert.hasText(typeName, "Type name must not be null or empty!");
|
||||
|
||||
return packageClasses.contains(typeName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a {@link Stream} of all {@link JavaClass}es contained in the {@link JavaPackage}.
|
||||
*
|
||||
* @return will never be {@literal null}.
|
||||
*/
|
||||
public Stream<JavaClass> stream() {
|
||||
return packageClasses.stream();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the annotation of the given type declared on the package.
|
||||
*
|
||||
* @param <A> the annotation type.
|
||||
* @param annotationType the annotation type to be found.
|
||||
* @return will never be {@literal null}.
|
||||
*/
|
||||
public <A extends Annotation> Optional<A> getAnnotation(Class<A> annotationType) {
|
||||
|
||||
return packageClasses.that(JavaClass.Predicates.simpleName(PACKAGE_INFO_NAME) //
|
||||
@@ -180,4 +251,52 @@ public class JavaPackage implements DescribedIterable<JavaClass> {
|
||||
.append('\n') //
|
||||
.toString();
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see java.lang.Object#equals(java.lang.Object)
|
||||
*/
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
|
||||
if (this == obj) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!(obj instanceof JavaPackage that)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return Objects.equals(this.classes, that.classes) //
|
||||
&& Objects.equals(this.getDirectSubPackages(), that.getDirectSubPackages()) //
|
||||
&& Objects.equals(this.name, that.name) //
|
||||
&& Objects.equals(this.packageClasses, that.packageClasses);
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see java.lang.Object#hashCode()
|
||||
*/
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(classes, directSubPackages, name, packageClasses);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the direct sub-package name of the given candidate.
|
||||
*
|
||||
* @param candidate
|
||||
* @return will never be {@literal null}.
|
||||
*/
|
||||
private String extractDirectSubPackage(String candidate) {
|
||||
|
||||
if (candidate.length() <= name.length()) {
|
||||
return candidate;
|
||||
}
|
||||
|
||||
int subSubPackageIndex = candidate.indexOf('.', name.length() + 1);
|
||||
int endIndex = subSubPackageIndex == -1 ? candidate.length() : subSubPackageIndex;
|
||||
|
||||
return candidate.substring(0, endIndex);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -15,10 +15,6 @@
|
||||
*/
|
||||
package org.springframework.modulith.model;
|
||||
|
||||
import lombok.AccessLevel;
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
@@ -32,21 +28,40 @@ import com.tngtech.archunit.core.domain.JavaModifier;
|
||||
import com.tngtech.archunit.core.domain.properties.HasModifiers;
|
||||
|
||||
/**
|
||||
* A named interface into an {@link ApplicationModule}. This can either be a package, explicitly annotated with
|
||||
* {@link org.springframework.modulith.NamedInterface} or a set of types annotated with the same annotation. Other
|
||||
* {@link ApplicationModules} can define allowed dependencies to particular named interfaces via the
|
||||
* {@code $moduleName::$namedInterfaceName} syntax.
|
||||
*
|
||||
* @author Oliver Drotbohm
|
||||
* @see org.springframework.modulith.ApplicationModule#allowedDependencies()
|
||||
*/
|
||||
@RequiredArgsConstructor(access = AccessLevel.PROTECTED)
|
||||
public abstract class NamedInterface implements Iterable<JavaClass> {
|
||||
|
||||
private static final String UNNAMED_NAME = "<<UNNAMED>>";
|
||||
private static final String PACKAGE_INFO_NAME = "package-info";
|
||||
|
||||
protected final @Getter String name;
|
||||
protected final String name;
|
||||
|
||||
static NamedInterface unnamed(JavaPackage javaPackage) {
|
||||
return new PackageBasedNamedInterface(UNNAMED_NAME, javaPackage);
|
||||
/**
|
||||
* Creates a new {@link NamedInterface} with the given name.
|
||||
*
|
||||
* @param name must not be {@literal null} or empty.
|
||||
*/
|
||||
protected NamedInterface(String name) {
|
||||
|
||||
Assert.hasText(name, "Name must not be null or empty!");
|
||||
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public static List<PackageBasedNamedInterface> of(JavaPackage javaPackage) {
|
||||
/**
|
||||
* Returns all {@link PackageBasedNamedInterface}s for the given {@link JavaPackage}.
|
||||
*
|
||||
* @param javaPackage must not be {@literal null}.
|
||||
* @return will never be {@literal null}.
|
||||
*/
|
||||
public static List<NamedInterface> of(JavaPackage javaPackage) {
|
||||
|
||||
String[] name = javaPackage.getAnnotation(org.springframework.modulith.NamedInterface.class) //
|
||||
.map(it -> it.name()) //
|
||||
@@ -54,33 +69,81 @@ public abstract class NamedInterface implements Iterable<JavaClass> {
|
||||
String.format("Couldn't find NamedInterface annotation on package %s!", javaPackage)));
|
||||
|
||||
return Arrays.stream(name) //
|
||||
.map(it -> new PackageBasedNamedInterface(it, javaPackage)) //
|
||||
.<NamedInterface> map(it -> new PackageBasedNamedInterface(it, javaPackage)) //
|
||||
.toList();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a {@link TypeBasedNamedInterface} with the given name, {@link Classes} and base {@link JavaPackage}.
|
||||
*
|
||||
* @param name must not be {@literal null} or empty.
|
||||
* @param classes must not be {@literal null}.
|
||||
* @param basePackage must not be {@literal null}.
|
||||
* @return will never be {@literal null}.
|
||||
*/
|
||||
public static TypeBasedNamedInterface of(String name, Classes classes, JavaPackage basePackage) {
|
||||
return new TypeBasedNamedInterface(name, classes, basePackage);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an unnamed {@link NamedInterface} for the given {@link JavaPackage}.
|
||||
*
|
||||
* @param javaPackage must not be {@literal null}.
|
||||
* @return will never be {@literal null}.
|
||||
*/
|
||||
static NamedInterface unnamed(JavaPackage javaPackage) {
|
||||
return new PackageBasedNamedInterface(UNNAMED_NAME, javaPackage);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@link NamedInterface}'s name.
|
||||
*
|
||||
* @return will never be {@literal null} or empty.
|
||||
*/
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether this is the unnamed (implicit) {@link NamedInterface}.
|
||||
*/
|
||||
public boolean isUnnamed() {
|
||||
return name.equals(UNNAMED_NAME);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the {@link NamedInterface} contains the given {@link JavaClass}.
|
||||
*
|
||||
* @param type must not be {@literal null}.
|
||||
*/
|
||||
public boolean contains(JavaClass type) {
|
||||
|
||||
Assert.notNull(type, "JavaClass must not be null!");
|
||||
|
||||
return getClasses().contains(type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the {@link NamedInterface} contains the given type.
|
||||
*
|
||||
* @param type must not be {@literal null}.
|
||||
*/
|
||||
public boolean contains(Class<?> type) {
|
||||
|
||||
Assert.notNull(type, "Type must not be null!");
|
||||
|
||||
return !getClasses().that(Predicates.equivalentTo(type)).isEmpty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the given {@link NamedInterface} has the same name as the current one.
|
||||
*
|
||||
* @param other
|
||||
* @return
|
||||
* @param other must not be {@literal null}.
|
||||
*/
|
||||
boolean hasSameNameAs(NamedInterface other) {
|
||||
|
||||
Assert.notNull(other, "NamedInterface must not be null!");
|
||||
|
||||
return this.name.equals(other.name);
|
||||
}
|
||||
|
||||
@@ -93,13 +156,24 @@ public abstract class NamedInterface implements Iterable<JavaClass> {
|
||||
return getClasses().iterator();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all {@link Classes} making up this {@link NamedInterface}.
|
||||
*
|
||||
* @return will never be {@literal null}.
|
||||
*/
|
||||
protected abstract Classes getClasses();
|
||||
|
||||
/**
|
||||
* Merges the current {@link NamedInterface} with the given {@link TypeBasedNamedInterface}.
|
||||
*
|
||||
* @param other must not be {@literal null}.
|
||||
* @return will never be {@literal null}.
|
||||
*/
|
||||
public abstract NamedInterface merge(TypeBasedNamedInterface other);
|
||||
|
||||
static class PackageBasedNamedInterface extends NamedInterface {
|
||||
private static class PackageBasedNamedInterface extends NamedInterface {
|
||||
|
||||
private final @Getter Classes classes;
|
||||
private final Classes classes;
|
||||
private final JavaPackage javaPackage;
|
||||
|
||||
public PackageBasedNamedInterface(String name, JavaPackage pkg) {
|
||||
@@ -123,6 +197,15 @@ public abstract class NamedInterface implements Iterable<JavaClass> {
|
||||
this.javaPackage = pkg;
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.modulith.model.NamedInterface#getClasses()
|
||||
*/
|
||||
@Override
|
||||
public Classes getClasses() {
|
||||
return classes;
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.modulith.model.NamedInterface#merge(org.springframework.modulith.model.NamedInterface.TypeBasedNamedInterface)
|
||||
@@ -143,18 +226,38 @@ public abstract class NamedInterface implements Iterable<JavaClass> {
|
||||
}
|
||||
}
|
||||
|
||||
static class TypeBasedNamedInterface extends NamedInterface {
|
||||
public static class TypeBasedNamedInterface extends NamedInterface {
|
||||
|
||||
private final @Getter Classes classes;
|
||||
private final Classes classes;
|
||||
private final JavaPackage pkg;
|
||||
|
||||
/**
|
||||
* Creates a new {@link TypeBasedNamedInterface} with the given name, {@link Classes} and {@link JavaPackage}.
|
||||
*
|
||||
* @param name must not be {@literal null} or empty.
|
||||
* @param types must not be {@literal null}.
|
||||
* @param pkg must not be {@literal null}.
|
||||
*/
|
||||
public TypeBasedNamedInterface(String name, Classes types, JavaPackage pkg) {
|
||||
|
||||
super(name);
|
||||
|
||||
Assert.notNull(types, "Classes must not be null!");
|
||||
Assert.notNull(pkg, "JavaPackage must not be null!");
|
||||
|
||||
this.classes = types;
|
||||
this.pkg = pkg;
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.modulith.model.NamedInterface#getClasses()
|
||||
*/
|
||||
@Override
|
||||
public Classes getClasses() {
|
||||
return classes;
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.modulith.model.NamedInterface#merge(org.springframework.modulith.model.NamedInterface.TypeBasedNamedInterface)
|
||||
|
||||
@@ -15,9 +15,6 @@
|
||||
*/
|
||||
package org.springframework.modulith.model;
|
||||
|
||||
import lombok.AccessLevel;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Iterator;
|
||||
@@ -28,112 +25,101 @@ import java.util.stream.Stream;
|
||||
|
||||
import org.springframework.core.annotation.AnnotatedElementUtils;
|
||||
import org.springframework.modulith.model.NamedInterface.TypeBasedNamedInterface;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.LinkedMultiValueMap;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
|
||||
import com.tngtech.archunit.core.domain.JavaClass;
|
||||
|
||||
/**
|
||||
* A collection of {@link NamedInterface}s.
|
||||
*
|
||||
* @author Oliver Drotbohm
|
||||
*/
|
||||
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
public class NamedInterfaces implements Iterable<NamedInterface> {
|
||||
|
||||
public static final NamedInterfaces NONE = new NamedInterfaces(Collections.emptyList());
|
||||
|
||||
private final List<NamedInterface> namedInterfaces;
|
||||
|
||||
public static NamedInterfaces discoverNamedInterfaces(JavaPackage basePackage) {
|
||||
/**
|
||||
* Creates a new {@link NamedInterfaces} for all {@link NamedInterface}s.
|
||||
*
|
||||
* @param namedInterfaces must not be {@literal null}.
|
||||
*/
|
||||
private NamedInterfaces(List<NamedInterface> namedInterfaces) {
|
||||
|
||||
Assert.notNull(namedInterfaces, "Named interfaces must not be null!");
|
||||
|
||||
this.namedInterfaces = namedInterfaces;
|
||||
}
|
||||
|
||||
/**
|
||||
* Discovers all {@link NamedInterfaces} declared for the given {@link JavaPackage}.
|
||||
*
|
||||
* @param basePackage must not be {@literal null}.
|
||||
* @return will never be {@literal null}.
|
||||
*/
|
||||
static NamedInterfaces discoverNamedInterfaces(JavaPackage basePackage) {
|
||||
|
||||
return NamedInterfaces.ofAnnotatedPackages(basePackage) //
|
||||
.and(NamedInterfaces.ofAnnotatedTypes(basePackage)) //
|
||||
.and(NamedInterface.unnamed(basePackage));
|
||||
}
|
||||
|
||||
public static NamedInterfaces of(List<NamedInterface> interfaces) {
|
||||
/**
|
||||
* Creates a new {@link NamedInterfaces} for the given {@link NamedInterface}s.
|
||||
*
|
||||
* @param interfaces must not be {@literal null}.
|
||||
* @return will never be {@literal null}.
|
||||
*/
|
||||
static NamedInterfaces of(List<NamedInterface> interfaces) {
|
||||
return interfaces.isEmpty() ? NONE : new NamedInterfaces(interfaces);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new {@link NamedInterfaces} for the given base {@link JavaPackage}.
|
||||
*
|
||||
* @param basePackage must not be {@literal null}.
|
||||
* @return will never be {@literal null}.
|
||||
*/
|
||||
static NamedInterfaces ofAnnotatedPackages(JavaPackage basePackage) {
|
||||
|
||||
Assert.notNull(basePackage, "Base package must not be null!");
|
||||
|
||||
return basePackage //
|
||||
.getSubPackagesAnnotatedWith(org.springframework.modulith.NamedInterface.class) //
|
||||
.flatMap(it -> NamedInterface.of(it).stream()) //
|
||||
.collect(Collectors.collectingAndThen(Collectors.toList(), NamedInterfaces::of));
|
||||
}
|
||||
|
||||
private static List<TypeBasedNamedInterface> ofAnnotatedTypes(JavaPackage basePackage) {
|
||||
|
||||
MultiValueMap<String, JavaClass> mappings = new LinkedMultiValueMap<>();
|
||||
|
||||
basePackage.stream() //
|
||||
.filter(it -> !JavaPackage.isPackageInfoType(it)) //
|
||||
.forEach(it -> {
|
||||
|
||||
if (!it.isAnnotatedWith(org.springframework.modulith.NamedInterface.class)) {
|
||||
return;
|
||||
}
|
||||
|
||||
org.springframework.modulith.NamedInterface annotation = AnnotatedElementUtils
|
||||
.getMergedAnnotation(it.reflect(), org.springframework.modulith.NamedInterface.class);
|
||||
|
||||
for (String name : annotation.name()) {
|
||||
mappings.add(name, it);
|
||||
}
|
||||
});
|
||||
|
||||
return mappings.entrySet().stream() //
|
||||
.map(entry -> NamedInterface.of(entry.getKey(), Classes.of(entry.getValue()), basePackage)) //
|
||||
.toList();
|
||||
}
|
||||
|
||||
private NamedInterfaces and(NamedInterface namedInterface) {
|
||||
|
||||
List<NamedInterface> result = new ArrayList<>(namedInterfaces.size() + 1);
|
||||
result.addAll(namedInterfaces);
|
||||
result.add(namedInterface);
|
||||
|
||||
return new NamedInterfaces(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether at least one explicit {@link NamedInterface} is declared.
|
||||
*
|
||||
* @return will never be {@literal null}.
|
||||
*/
|
||||
public boolean hasExplicitInterfaces() {
|
||||
return namedInterfaces.size() > 1 || !namedInterfaces.get(0).isUnnamed();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a {@link Stream} of {@link NamedInterface}s.
|
||||
*
|
||||
* @return will never be {@literal null}.
|
||||
*/
|
||||
public Stream<NamedInterface> stream() {
|
||||
return namedInterfaces.stream();
|
||||
}
|
||||
|
||||
public NamedInterfaces and(List<TypeBasedNamedInterface> others) {
|
||||
|
||||
List<NamedInterface> namedInterfaces = new ArrayList<>();
|
||||
List<NamedInterface> unmergedInterface = this.namedInterfaces;
|
||||
|
||||
for (TypeBasedNamedInterface candidate : others) {
|
||||
|
||||
Optional<NamedInterface> existing = namedInterfaces.stream() //
|
||||
.filter(it -> it.hasSameNameAs(candidate)) //
|
||||
.findFirst();
|
||||
|
||||
// Merge existing with new and add to result
|
||||
existing.ifPresent(it -> {
|
||||
namedInterfaces.add(it.merge(candidate));
|
||||
namedInterfaces.add(it);
|
||||
unmergedInterface.remove(it);
|
||||
});
|
||||
|
||||
// Simply add candidate
|
||||
if (!existing.isPresent()) {
|
||||
namedInterfaces.add(candidate);
|
||||
}
|
||||
}
|
||||
|
||||
namedInterfaces.addAll(unmergedInterface);
|
||||
|
||||
return new NamedInterfaces(namedInterfaces);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@link NamedInterface} with the given name if present.
|
||||
*
|
||||
* @param name must not be {@literal null} or empty.
|
||||
* @return will never be {@literal null}.
|
||||
*/
|
||||
public Optional<NamedInterface> getByName(String name) {
|
||||
|
||||
Assert.hasText(name, "Named interface name must not be null or empty!");
|
||||
|
||||
return namedInterfaces.stream().filter(it -> it.getName().equals(name)).findFirst();
|
||||
}
|
||||
|
||||
@@ -158,4 +144,79 @@ public class NamedInterfaces implements Iterable<NamedInterface> {
|
||||
public Iterator<NamedInterface> iterator() {
|
||||
return namedInterfaces.iterator();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new {@link NamedInterfaces} instance with the given {@link TypeBasedNamedInterface}s added.
|
||||
*
|
||||
* @param others must not be {@literal null}.
|
||||
* @return will never be {@literal null}.
|
||||
*/
|
||||
NamedInterfaces and(List<TypeBasedNamedInterface> others) {
|
||||
|
||||
Assert.notNull(others, "Other TypeBasedNamedInterfaces must not be null!");
|
||||
|
||||
var namedInterfaces = new ArrayList<NamedInterface>();
|
||||
var unmergedInterface = this.namedInterfaces;
|
||||
|
||||
if (others.isEmpty()) {
|
||||
return this;
|
||||
}
|
||||
|
||||
for (TypeBasedNamedInterface candidate : others) {
|
||||
|
||||
var existing = namedInterfaces.stream() //
|
||||
.filter(it -> it.hasSameNameAs(candidate)) //
|
||||
.findFirst();
|
||||
|
||||
// Merge existing with new and add to result
|
||||
existing.ifPresent(it -> {
|
||||
namedInterfaces.add(it.merge(candidate));
|
||||
namedInterfaces.add(it);
|
||||
unmergedInterface.remove(it);
|
||||
});
|
||||
|
||||
// Simply add candidate
|
||||
if (!existing.isPresent()) {
|
||||
namedInterfaces.add(candidate);
|
||||
}
|
||||
}
|
||||
|
||||
namedInterfaces.addAll(unmergedInterface);
|
||||
|
||||
return new NamedInterfaces(namedInterfaces);
|
||||
}
|
||||
|
||||
private NamedInterfaces and(NamedInterface namedInterface) {
|
||||
|
||||
var result = new ArrayList<NamedInterface>(namedInterfaces.size() + 1);
|
||||
result.addAll(namedInterfaces);
|
||||
result.add(namedInterface);
|
||||
|
||||
return new NamedInterfaces(result);
|
||||
}
|
||||
|
||||
private static List<TypeBasedNamedInterface> ofAnnotatedTypes(JavaPackage basePackage) {
|
||||
|
||||
var mappings = new LinkedMultiValueMap<String, JavaClass>();
|
||||
|
||||
basePackage.stream() //
|
||||
.filter(it -> !JavaPackage.isPackageInfoType(it)) //
|
||||
.forEach(it -> {
|
||||
|
||||
if (!it.isAnnotatedWith(org.springframework.modulith.NamedInterface.class)) {
|
||||
return;
|
||||
}
|
||||
|
||||
var annotation = AnnotatedElementUtils.getMergedAnnotation(it.reflect(),
|
||||
org.springframework.modulith.NamedInterface.class);
|
||||
|
||||
for (String name : annotation.name()) {
|
||||
mappings.add(name, it);
|
||||
}
|
||||
});
|
||||
|
||||
return mappings.entrySet().stream() //
|
||||
.map(entry -> NamedInterface.of(entry.getKey(), Classes.of(entry.getValue()), basePackage)) //
|
||||
.toList();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,12 +15,10 @@
|
||||
*/
|
||||
package org.springframework.modulith.model;
|
||||
|
||||
import lombok.AccessLevel;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
import com.tngtech.archunit.core.domain.JavaClass;
|
||||
|
||||
@@ -29,17 +27,49 @@ import com.tngtech.archunit.core.domain.JavaClass;
|
||||
*
|
||||
* @author Oliver Drotbohm
|
||||
*/
|
||||
@EqualsAndHashCode
|
||||
@RequiredArgsConstructor(staticName = "of", access = AccessLevel.PACKAGE)
|
||||
public class SpringBean {
|
||||
|
||||
private final @Getter JavaClass type;
|
||||
private final JavaClass type;
|
||||
private final ApplicationModule module;
|
||||
|
||||
/**
|
||||
* Creates a new {@link SpringBean} for the given {@link JavaClass} and {@link ApplicationModule}.
|
||||
*
|
||||
* @param type must not be {@literal null}.
|
||||
* @param module must not be {@literal null}.
|
||||
*/
|
||||
private SpringBean(JavaClass type, ApplicationModule module) {
|
||||
|
||||
Assert.notNull(type, "JavaClass must not be null!");
|
||||
Assert.notNull(module, "ApplicationModule must not be null!");
|
||||
|
||||
this.type = type;
|
||||
this.module = module;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new {@link SpringBean} for the given {@link JavaClass} and {@link ApplicationModule}.
|
||||
*
|
||||
* @param type must not be {@literal null}.
|
||||
* @param module must not be {@literal null}.
|
||||
*/
|
||||
static SpringBean of(JavaClass type, ApplicationModule module) {
|
||||
return new SpringBean(type, module);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@link JavaClass} of the {@link SpringBean}.
|
||||
*
|
||||
* @return will never be {@literal null}.
|
||||
*/
|
||||
public JavaClass getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the fully-qualified name of the Spring bean type.
|
||||
*
|
||||
* @return
|
||||
* @return will never be {@literal null} or empty.
|
||||
*/
|
||||
public String getFullyQualifiedTypeName() {
|
||||
return type.getFullName();
|
||||
@@ -56,9 +86,9 @@ public class SpringBean {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all interfaces implemented by the bean that are part of the same module.
|
||||
* Returns all interfaces implemented by the bean that are part of the same application module.
|
||||
*
|
||||
* @return
|
||||
* @return will never be {@literal null}.
|
||||
*/
|
||||
public List<JavaClass> getInterfacesWithinModule() {
|
||||
|
||||
@@ -67,11 +97,40 @@ public class SpringBean {
|
||||
.toList();
|
||||
}
|
||||
|
||||
public boolean isAnnotatedWith(Class<?> type) {
|
||||
return Types.isAnnotatedWith(type).test(this.type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new {@link ArchitecturallyEvidentType} from the current {@link SpringBean}.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public ArchitecturallyEvidentType toArchitecturallyEvidentType() {
|
||||
return ArchitecturallyEvidentType.of(type, module.getSpringBeansInternal());
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see java.lang.Object#equals(java.lang.Object)
|
||||
*/
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
|
||||
if (this == obj) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!(obj instanceof SpringBean that)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return Objects.equals(this.module, that.module) //
|
||||
&& Objects.equals(this.type, that.type);
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see java.lang.Object#hashCode()
|
||||
*/
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(type, module);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,8 +17,6 @@ package org.springframework.modulith.model;
|
||||
|
||||
import static com.tngtech.archunit.core.domain.JavaClass.Predicates.*;
|
||||
|
||||
import lombok.experimental.UtilityClass;
|
||||
|
||||
import java.lang.annotation.Annotation;
|
||||
|
||||
import org.springframework.lang.Nullable;
|
||||
@@ -33,12 +31,11 @@ import com.tngtech.archunit.core.domain.properties.CanBeAnnotated.Predicates;
|
||||
/**
|
||||
* @author Oliver Drotbohm
|
||||
*/
|
||||
@UtilityClass
|
||||
class Types {
|
||||
|
||||
@Nullable
|
||||
@SuppressWarnings("unchecked")
|
||||
<T> Class<T> loadIfPresent(String name) {
|
||||
static <T> Class<T> loadIfPresent(String name) {
|
||||
|
||||
ClassLoader loader = Types.class.getClassLoader();
|
||||
|
||||
@@ -79,7 +76,6 @@ class Types {
|
||||
}
|
||||
}
|
||||
|
||||
@UtilityClass
|
||||
static class JavaXTypes {
|
||||
|
||||
private static final String BASE_PACKAGE = "jakarta";
|
||||
@@ -93,7 +89,6 @@ class Types {
|
||||
}
|
||||
}
|
||||
|
||||
@UtilityClass
|
||||
static class SpringTypes {
|
||||
|
||||
private static final String BASE_PACKAGE = "org.springframework";
|
||||
@@ -129,7 +124,6 @@ class Types {
|
||||
}
|
||||
}
|
||||
|
||||
@UtilityClass
|
||||
static class SpringDataTypes {
|
||||
|
||||
private static final String BASE_PACKAGE = SpringTypes.BASE_PACKAGE + ".data";
|
||||
@@ -147,11 +141,11 @@ class Types {
|
||||
}
|
||||
}
|
||||
|
||||
DescribedPredicate<CanBeAnnotated> isAnnotatedWith(Class<?> type) {
|
||||
static DescribedPredicate<CanBeAnnotated> isAnnotatedWith(Class<?> type) {
|
||||
return isAnnotatedWith(type.getName());
|
||||
}
|
||||
|
||||
DescribedPredicate<CanBeAnnotated> isAnnotatedWith(String type) {
|
||||
static DescribedPredicate<CanBeAnnotated> isAnnotatedWith(String type) {
|
||||
return Predicates.annotatedWith(type) //
|
||||
.or(Predicates.metaAnnotatedWith(type));
|
||||
}
|
||||
|
||||
@@ -15,9 +15,6 @@
|
||||
*/
|
||||
package org.springframework.modulith.model;
|
||||
|
||||
import lombok.AccessLevel;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
@@ -32,7 +29,6 @@ import org.springframework.util.Assert;
|
||||
*
|
||||
* @author Oliver Drotbohm
|
||||
*/
|
||||
@RequiredArgsConstructor(staticName = "of", access = AccessLevel.PRIVATE)
|
||||
public class Violations extends RuntimeException {
|
||||
|
||||
private static final long serialVersionUID = 6863781504675034691L;
|
||||
@@ -41,13 +37,25 @@ public class Violations extends RuntimeException {
|
||||
|
||||
private final List<RuntimeException> exceptions;
|
||||
|
||||
/**
|
||||
* Creates a new {@link Violations} from the given {@link RuntimeException}s.
|
||||
*
|
||||
* @param exceptions must not be {@literal null}.
|
||||
*/
|
||||
private Violations(List<RuntimeException> exceptions) {
|
||||
|
||||
Assert.notNull(exceptions, "Exceptions must not be null!");
|
||||
|
||||
this.exceptions = exceptions;
|
||||
}
|
||||
|
||||
/**
|
||||
* A {@link Collector} to turn a {@link Stream} of {@link RuntimeException}s into a {@link Violations} instance.
|
||||
*
|
||||
* @return will never be {@literal null}.
|
||||
*/
|
||||
static Collector<RuntimeException, ?, Violations> toViolations() {
|
||||
return Collectors.collectingAndThen(Collectors.toList(), Violations::of);
|
||||
return Collectors.collectingAndThen(Collectors.toList(), Violations::new);
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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<String> 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.<String> 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<JavaClass> 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<EventType> 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<String> 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<String> 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<JavaClass> 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));
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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<ConfigurationProperty> {
|
||||
*/
|
||||
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<ConfigurationProperty> {
|
||||
private Stream<ModuleProperty> 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<ConfigurationProperty> {
|
||||
|
||||
try (InputStream stream = source.getInputStream()) {
|
||||
|
||||
DocumentContext context = JsonPath.parse(stream);
|
||||
var context = JsonPath.parse(stream);
|
||||
List<Object> read = context.read(PATH, List.class);
|
||||
|
||||
return read.stream()
|
||||
@@ -124,13 +121,8 @@ class ConfigurationProperties implements Iterable<ConfigurationProperty> {
|
||||
}
|
||||
}
|
||||
|
||||
@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<ConfigurationProperty> of(Map<String, Object> source) {
|
||||
@@ -162,12 +154,6 @@ class ConfigurationProperties implements Iterable<ConfigurationProperty> {
|
||||
}
|
||||
}
|
||||
|
||||
@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) {}
|
||||
}
|
||||
|
||||
@@ -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<DependencyType, String> 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<ApplicationModule, Component> 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 <T> String addTableRow(List<T> types, String header, Function<List<T>, String> mapper,
|
||||
CanvasOptions options) {
|
||||
return options.hideEmptyLines && types.isEmpty() ? "" : writeTableRow(header, mapper.apply(types));
|
||||
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<Stream<ApplicationModule>> bootstrapDependencies = () -> module.getBootstrapDependencies(modules,
|
||||
options.getDependencyDepth());
|
||||
options.dependencyDepth);
|
||||
Supplier<Stream<ApplicationModule>> otherDependencies = () -> options.getDependencyTypes()
|
||||
.flatMap(it -> module.getDependencies(modules, it).stream()
|
||||
.map(ApplicationModuleDependency::getTargetModule));
|
||||
@@ -413,14 +405,14 @@ public class Documenter {
|
||||
DiagramOptions options,
|
||||
Consumer<ComponentView> afterCleanup) {
|
||||
|
||||
Styles styles = view.getViewSet().getConfiguration().getStyles();
|
||||
Map<ApplicationModule, Component> 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<ApplicationModule, Component> components,
|
||||
DiagramOptions options,
|
||||
Styles styles) {
|
||||
|
||||
Component component = components.get(module);
|
||||
Function<ApplicationModule, Optional<String>> 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<ApplicationModule, Component> 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 <T> String addTableRow(List<T> types, String header, Function<List<T>, String> mapper,
|
||||
CanvasOptions options) {
|
||||
return options.hideEmptyLines && types.isEmpty() ? "" : writeTableRow(header, mapper.apply(types));
|
||||
}
|
||||
|
||||
/**
|
||||
* 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<DependencyType> ALL_TYPES = Arrays.stream(DependencyType.values())
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
private final Set<DependencyType> dependencyTypes;
|
||||
private final DependencyDepth dependencyDepth;
|
||||
private final Predicate<ApplicationModule> exclusions;
|
||||
private final Predicate<Component> componentFilter;
|
||||
private final Predicate<ApplicationModule> targetOnly;
|
||||
private final @Nullable String targetFileName;
|
||||
private final Function<ApplicationModule, Optional<String>> colorSelector;
|
||||
private final Function<ApplicationModule, String> 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<DependencyType> dependencyTypes, DependencyDepth dependencyDepth,
|
||||
Predicate<ApplicationModule> exclusions, Predicate<Component> componentFilter,
|
||||
Predicate<ApplicationModule> targetOnly, @Nullable String targetFileName,
|
||||
Function<ApplicationModule, Optional<String>> colorSelector,
|
||||
Function<ApplicationModule, String> 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<ApplicationModule> exclusions;
|
||||
public DiagramOptions withExcusions(Predicate<ApplicationModule> 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<Component> componentFilter;
|
||||
public DiagramOptions withComponentFilter(Predicate<Component> 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<ApplicationModule> targetOnly;
|
||||
public DiagramOptions withTargetOnly(Predicate<ApplicationModule> 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<ApplicationModule, Optional<String>> colorSelector;
|
||||
public DiagramOptions withColorSelector(Function<ApplicationModule, Optional<String>> 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<ApplicationModule, String> defaultDisplayName;
|
||||
public DiagramOptions withDefaultDisplayName(Function<ApplicationModule, String> 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<Grouping> 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<Grouping> 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<Grouping> 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<SpringBean> 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<SpringBean> 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<Grouping> sources = new ArrayList<>(groupers);
|
||||
var sources = new ArrayList<Grouping>(groupers);
|
||||
sources.add(FALLBACK_GROUP);
|
||||
|
||||
MultiValueMap<Grouping, SpringBean> result = new LinkedMultiValueMap<>();
|
||||
List<SpringBean> alreadyMapped = new ArrayList<>();
|
||||
var result = new LinkedMultiValueMap<Grouping, SpringBean>();
|
||||
var alreadyMapped = new ArrayList<SpringBean>();
|
||||
|
||||
sources.forEach(it -> {
|
||||
|
||||
List<SpringBean> 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<JavaClass> hideInternalFilter(ApplicationModule module) {
|
||||
return hideInternals ? module::isExposed : __ -> true;
|
||||
}
|
||||
|
||||
private Optional<String> getTargetFileName() {
|
||||
return Optional.ofNullable(targetFileName);
|
||||
private String getTargetFileName(String moduleName) {
|
||||
return (targetFileName == null ? "module-%s.adoc" : targetFileName).formatted(moduleName);
|
||||
}
|
||||
|
||||
private List<SpringBean> 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<SpringBean> predicate;
|
||||
private final String name;
|
||||
private final Predicate<SpringBean> 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<SpringBean> 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<SpringBean> 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<SpringBean> 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<SpringBean> 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<SpringBean> 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<SpringBean> 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<Grouping, SpringBean> groupings;
|
||||
|
||||
Groupings(MultiValueMap<Grouping, SpringBean> groupings) {
|
||||
|
||||
Assert.notNull(groupings, "Groupings must not be null!");
|
||||
|
||||
this.groupings = groupings;
|
||||
}
|
||||
|
||||
Set<Grouping> keySet() {
|
||||
return groupings.keySet();
|
||||
}
|
||||
@@ -873,7 +1116,7 @@ public class Documenter {
|
||||
}
|
||||
|
||||
List<SpringBean> byGroupName(String name) {
|
||||
return byFilter(it -> it.getName().equals(name));
|
||||
return byFilter(it -> it.name.equals(name));
|
||||
}
|
||||
|
||||
void forEach(BiConsumer<Grouping, List<SpringBean>> consumer) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<Instant> completionDate = Optional.empty();
|
||||
private Optional<Instant> 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<Instant> 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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<PublicationTargetIdentifier> 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<EventPublication> 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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -79,6 +79,10 @@ public interface EventPublication extends Comparable<EventPublication> {
|
||||
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());
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
*/
|
||||
|
||||
@@ -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<EventPublicationRegistry> registry;
|
||||
|
||||
/**
|
||||
* Creates a new {@link CompletionRegisteringBeanPostProcessor} for the given {@link EventPublicationRegistry}.
|
||||
*
|
||||
* @param registry must not be {@literal null}.
|
||||
*/
|
||||
public CompletionRegisteringBeanPostProcessor(Supplier<EventPublicationRegistry> 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<EventPublicationRegistry> registry;
|
||||
private @NonNull final String beanName;
|
||||
private @NonNull @Getter Object bean;
|
||||
private final Supplier<EventPublicationRegistry> 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<EventPublicationRegistry> 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<Method, Boolean> COMPLETING_METHOD = new ConcurrentLruCache<>(100,
|
||||
CompletionRegisteringMethodInterceptor::calculateIsCompletingMethod);
|
||||
private static final ConcurrentLruCache<CacheKey, TransactionalApplicationListenerMethodAdapter> ADAPTERS = new ConcurrentLruCache<>(
|
||||
@@ -139,6 +164,19 @@ public class CompletionRegisteringBeanPostProcessor implements BeanPostProcessor
|
||||
private final @NonNull Supplier<EventPublicationRegistry> registry;
|
||||
private final @NonNull String beanName;
|
||||
|
||||
/**
|
||||
* @param registry
|
||||
* @param beanName
|
||||
*/
|
||||
CompletionRegisteringMethodInterceptor(Supplier<EventPublicationRegistry> 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) {}
|
||||
}
|
||||
|
||||
@@ -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<EventPublicationRegistry> registry;
|
||||
|
||||
/**
|
||||
* Creates a new {@link PersistentApplicationEventMulticaster} for the given {@link EventPublicationRegistry}.
|
||||
*
|
||||
* @param registry must not be {@literal null}.
|
||||
*/
|
||||
public PersistentApplicationEventMulticaster(Supplier<EventPublicationRegistry> 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<ApplicationListener<?>> 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;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<ObjectMapper> 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<ObjectMapper> 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());
|
||||
|
||||
@@ -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<ObjectMapper> mapper;
|
||||
|
||||
/**
|
||||
* Creates a new {@link JacksonEventSerializer} for the given {@link ObjectMapper}.
|
||||
*
|
||||
* @param mapper must not be {@literal null}.
|
||||
*/
|
||||
public JacksonEventSerializer(Supplier<ObjectMapper> mapper) {
|
||||
|
||||
Assert.notNull(mapper, "ObjectMapper must not be null!");
|
||||
|
||||
this.mapper = mapper;
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see de.oliverDrotbohm.events.EventSerializer#serialize(java.lang.Object)
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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<Instant> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<EventPublication> 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<EventPublication> 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<Instant> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<Instant> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -89,12 +89,6 @@
|
||||
|
||||
<!-- Infrastructure -->
|
||||
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-configuration-processor</artifactId>
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {}
|
||||
}
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"));
|
||||
}
|
||||
|
||||
@@ -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) {}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {}
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@ class ModuleATest {
|
||||
context.getBean(ServiceComponentA.class).fireEvent();
|
||||
|
||||
TypedPublishedEvents<SomeEventA> matching = events.ofType(SomeEventA.class) //
|
||||
.matching(it -> it.getMessage().equals("Message"));
|
||||
.matching(it -> it.message().equals("Message"));
|
||||
|
||||
assertThat(matching).hasSize(1);
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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}.
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<Range> 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) {
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<ShiftedQuarter> quarters;
|
||||
|
||||
private ShiftedQuarters(List<ShiftedQuarter> quarters) {
|
||||
this.quarters = quarters;
|
||||
}
|
||||
|
||||
public ShiftedQuarter getCurrent(LocalDate reference) {
|
||||
|
||||
return quarters.stream()
|
||||
|
||||
@@ -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() + "(…)";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<String, ModuleEntryInterceptor> 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();
|
||||
}
|
||||
|
||||
@@ -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<ApplicationEvent> {
|
||||
|
||||
private final ApplicationModulesRuntime runtime;
|
||||
private final Supplier<Tracer> 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> 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<ApplicationEvent
|
||||
@Override
|
||||
public void onApplicationEvent(ApplicationEvent event) {
|
||||
|
||||
if (!PayloadApplicationEvent.class.isInstance(event)) {
|
||||
if (!(event instanceof PayloadApplicationEvent<?> payloadEvent)) {
|
||||
return;
|
||||
}
|
||||
|
||||
PayloadApplicationEvent<?> foo = (PayloadApplicationEvent<?>) event;
|
||||
Object object = foo.getPayload();
|
||||
Class<? extends Object> 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<ApplicationEvent
|
||||
return;
|
||||
}
|
||||
|
||||
Span span = tracer.get().currentSpan();
|
||||
var span = tracer.get().currentSpan();
|
||||
|
||||
if (span == null) {
|
||||
return;
|
||||
|
||||
@@ -16,17 +16,13 @@
|
||||
package org.springframework.modulith.observability;
|
||||
|
||||
import io.micrometer.tracing.Tracer;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import org.aopalliance.aop.Advice;
|
||||
import org.springframework.aop.Advisor;
|
||||
import org.springframework.aop.MethodMatcher;
|
||||
import org.springframework.aop.Pointcut;
|
||||
import org.springframework.aop.support.ComposablePointcut;
|
||||
import org.springframework.aop.support.DefaultPointcutAdvisor;
|
||||
import org.springframework.aop.support.StaticMethodMatcher;
|
||||
@@ -34,6 +30,7 @@ import org.springframework.beans.BeansException;
|
||||
import org.springframework.beans.factory.config.BeanPostProcessor;
|
||||
import org.springframework.modulith.model.ApplicationModules;
|
||||
import org.springframework.modulith.runtime.ApplicationModulesRuntime;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* A {@link BeanPostProcessor} that decorates beans exposed by application modules with an interceptor that registers
|
||||
@@ -41,14 +38,30 @@ import org.springframework.modulith.runtime.ApplicationModulesRuntime;
|
||||
*
|
||||
* @author Oliver Drotbohm
|
||||
*/
|
||||
@RequiredArgsConstructor
|
||||
public class ModuleTracingBeanPostProcessor extends ModuleTracingSupport implements BeanPostProcessor {
|
||||
|
||||
public static final String MODULE_BAGGAGE_KEY = "org.springframework.modulith.module";
|
||||
|
||||
private final ApplicationModulesRuntime runtime;
|
||||
private final Supplier<Tracer> tracer;
|
||||
private final Map<String, Advisor> advisors = new HashMap<>();
|
||||
private final Map<String, Advisor> 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> 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)
|
||||
|
||||
@@ -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<Class<?>> 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.
|
||||
|
||||
@@ -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> 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> 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<ApplicationModules> modules;
|
||||
private final Supplier<Tracer> 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<ApplicationModules> modules, Supplier<Tracer> 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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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<ApplicationModules> {
|
||||
|
||||
private final @NonNull Supplier<ApplicationModules> modules;
|
||||
private final @NonNull ApplicationRuntime runtime;
|
||||
private final Supplier<ApplicationModules> 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<ApplicationModules> 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)
|
||||
|
||||
@@ -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<String, Boolean> 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()
|
||||
|
||||
@@ -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
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
* <ul>
|
||||
* <li>Restricts the component scanning to the module's package.
|
||||
* <li>
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<ApplicationEvent> {
|
||||
|
||||
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}.
|
||||
*/
|
||||
|
||||
@@ -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<App
|
||||
: source;
|
||||
}
|
||||
|
||||
@RequiredArgsConstructor(staticName = "of")
|
||||
private static class SimpleTypedPublishedEvents<T> implements TypedPublishedEvents<T> {
|
||||
|
||||
private final List<T> events;
|
||||
|
||||
private SimpleTypedPublishedEvents(List<T> events) {
|
||||
this.events = events;
|
||||
}
|
||||
|
||||
private static <T> SimpleTypedPublishedEvents<T> of(Stream<T> stream) {
|
||||
return new SimpleTypedPublishedEvents<>(stream.toList());
|
||||
}
|
||||
|
||||
@@ -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<ContextConfigurationAttributes> 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<ModuleTestExecution> 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<ApplicationModule> 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<ApplicationModule> 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<ApplicationModule> dependencies = execution.getDependencies();
|
||||
var dependencies = execution.getDependencies();
|
||||
|
||||
if (!dependencies.isEmpty() || !sharedModules.isEmpty()) {
|
||||
|
||||
logHeadline("Included dependencies:");
|
||||
|
||||
Stream<ApplicationModule> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<String> 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()
|
||||
|
||||
@@ -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<ApplicationModule> {
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(ModuleTestExecution.class);
|
||||
|
||||
private static Map<Class<?>, Class<?>> MODULITH_TYPES = new HashMap<>();
|
||||
private static Map<Key, ModuleTestExecution> 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<ApplicationModule> extraIncludes;
|
||||
private final BootstrapMode bootstrapMode;
|
||||
private final ApplicationModule module;
|
||||
private final ApplicationModules modules;
|
||||
private final List<ApplicationModule> extraIncludes;
|
||||
|
||||
private final Supplier<List<JavaPackage>> basePackages;
|
||||
private final Supplier<List<ApplicationModule>> 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<ApplicationModule> {
|
||||
|
||||
this.basePackages = Suppliers.memoize(() -> {
|
||||
|
||||
Stream<JavaPackage> moduleBasePackages = module.getBootstrapBasePackages(modules, bootstrapMode.getDepth());
|
||||
Stream<JavaPackage> sharedBasePackages = modules.getSharedModules().stream().map(it -> it.getBasePackage());
|
||||
Stream<JavaPackage> 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<JavaPackage> intermediate = Stream.concat(moduleBasePackages, extraPackages);
|
||||
var intermediate = Stream.concat(moduleBasePackages, extraPackages);
|
||||
|
||||
return Stream.concat(intermediate, sharedBasePackages).distinct().toList();
|
||||
});
|
||||
|
||||
this.dependencies = Suppliers.memoize(() -> {
|
||||
|
||||
Stream<ApplicationModule> 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<ApplicationModule> {
|
||||
|
||||
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<ApplicationModule> {
|
||||
|
||||
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<ApplicationModule> {
|
||||
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<ApplicationModule> getExtraIncludes() {
|
||||
return extraIncludes;
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see java.lang.Iterable#iterator()
|
||||
@@ -164,6 +195,33 @@ public class ModuleTestExecution implements Iterable<ApplicationModule> {
|
||||
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<ApplicationModule> getExtraModules(ApplicationModuleTest annotation,
|
||||
ApplicationModules modules) {
|
||||
|
||||
@@ -172,11 +230,5 @@ public class ModuleTestExecution implements Iterable<ApplicationModule> {
|
||||
.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) {}
|
||||
}
|
||||
|
||||
@@ -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<ModuleTestExecution> 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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<PublishedEventsAssert,
|
||||
*
|
||||
* @author Oliver Drotbohm
|
||||
*/
|
||||
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
public class PublishedEventAssert<T> {
|
||||
|
||||
private final TypedPublishedEvents<T> events;
|
||||
|
||||
/**
|
||||
* Creates a new {@link PublishedEventAssert} for the given {@link TypedPublishedEvents}.
|
||||
*
|
||||
* @param events must not be {@literal null}.
|
||||
*/
|
||||
private PublishedEventAssert(TypedPublishedEvents<T> events) {
|
||||
|
||||
Assert.notNull(events, "TypedPublishedEvents must not be null!");
|
||||
|
||||
this.events = events;
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that at least one event matches the given predicate.
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user