diff --git a/spring-modulith-events/spring-modulith-events-core/src/main/java/org/springframework/modulith/events/CompletableEventPublication.java b/spring-modulith-events/spring-modulith-events-core/src/main/java/org/springframework/modulith/events/CompletableEventPublication.java index 3ee69225..63a7eac0 100644 --- a/spring-modulith-events/spring-modulith-events-core/src/main/java/org/springframework/modulith/events/CompletableEventPublication.java +++ b/spring-modulith-events/spring-modulith-events-core/src/main/java/org/springframework/modulith/events/CompletableEventPublication.java @@ -15,6 +15,7 @@ */ package org.springframework.modulith.events; +import java.time.Clock; import java.time.Instant; import java.util.Optional; @@ -49,13 +50,28 @@ public interface CompletableEventPublication extends EventPublication { CompletableEventPublication markCompleted(); /** - * Creates a {@link CompletableEventPublication} for the given event an listener identifier. + * Creates a {@link CompletableEventPublication} for the given event an listener identifier using a default + * {@link Instant}. Prefer using {@link #of(Object, PublicationTargetIdentifier, Instant)} with a dedicated + * {@link Instant} obtained from a {@link Clock}. * * @param event must not be {@literal null}. * @param id must not be {@literal null}. - * @return + * @return will never be {@literal null}. + * @see #of(Object, PublicationTargetIdentifier, Instant) */ static CompletableEventPublication of(Object event, PublicationTargetIdentifier id) { - return new DefaultEventPublication(event, id); + return new DefaultEventPublication(event, id, Instant.now()); + } + + /** + * Creates a {@link CompletableEventPublication} for the given event an listener identifier and publication date. + * + * @param event must not be {@literal null}. + * @param id must not be {@literal null}. + * @param publicationDate must not be {@literal null}. + * @return will never be {@literal null}. + */ + static CompletableEventPublication of(Object event, PublicationTargetIdentifier id, Instant publicationDate) { + return new DefaultEventPublication(event, id, publicationDate); } } diff --git a/spring-modulith-events/spring-modulith-events-core/src/main/java/org/springframework/modulith/events/DefaultEventPublication.java b/spring-modulith-events/spring-modulith-events-core/src/main/java/org/springframework/modulith/events/DefaultEventPublication.java index fbd706b9..ccd11350 100644 --- a/spring-modulith-events/spring-modulith-events-core/src/main/java/org/springframework/modulith/events/DefaultEventPublication.java +++ b/spring-modulith-events/spring-modulith-events-core/src/main/java/org/springframework/modulith/events/DefaultEventPublication.java @@ -39,15 +39,17 @@ class DefaultEventPublication implements CompletableEventPublication { * * @param event must not be {@literal null}. * @param targetIdentifier must not be {@literal null}. + * @param publicationDate must not be {@literal null}. */ - DefaultEventPublication(Object event, PublicationTargetIdentifier targetIdentifier) { + DefaultEventPublication(Object event, PublicationTargetIdentifier targetIdentifier, Instant publicationDate) { Assert.notNull(event, "Event must not be null!"); Assert.notNull(targetIdentifier, "PublicationTargetIdentifier must not be null!"); + Assert.notNull(publicationDate, "Publication date must not be null!"); this.event = event; this.targetIdentifier = targetIdentifier; - this.publicationDate = Instant.now(); + this.publicationDate = publicationDate; this.completionDate = Optional.empty(); } diff --git a/spring-modulith-events/spring-modulith-events-core/src/main/java/org/springframework/modulith/events/DefaultEventPublicationRegistry.java b/spring-modulith-events/spring-modulith-events-core/src/main/java/org/springframework/modulith/events/DefaultEventPublicationRegistry.java index 68ec6198..dc4a11ee 100644 --- a/spring-modulith-events/spring-modulith-events-core/src/main/java/org/springframework/modulith/events/DefaultEventPublicationRegistry.java +++ b/spring-modulith-events/spring-modulith-events-core/src/main/java/org/springframework/modulith/events/DefaultEventPublicationRegistry.java @@ -15,6 +15,7 @@ */ package org.springframework.modulith.events; +import java.time.Clock; import java.util.Collection; import java.util.List; import java.util.stream.Stream; @@ -40,17 +41,21 @@ public class DefaultEventPublicationRegistry implements DisposableBean, EventPub private static final Logger LOGGER = LoggerFactory.getLogger(DefaultEventPublicationRegistry.class); private final EventPublicationRepository events; + private final Clock clock; /** * Creates a new {@link DefaultEventPublicationRegistry} for the given {@link EventPublicationRepository}. * * @param events must not be {@literal null}. + * @param clock must not be {@literal null}. */ - public DefaultEventPublicationRegistry(EventPublicationRepository events) { + public DefaultEventPublicationRegistry(EventPublicationRepository events, Clock clock) { Assert.notNull(events, "EventPublicationRepository must not be null!"); + Assert.notNull(clock, "Clock must not be null!"); this.events = events; + this.clock = clock; } /* @@ -58,10 +63,11 @@ public class DefaultEventPublicationRegistry implements DisposableBean, EventPub * @see org.springframework.modulith.events.EventPublicationRegistry#store(java.lang.Object, java.util.stream.Stream) */ @Override - public void store(Object event, Stream listeners) { + public Collection store(Object event, Stream listeners) { - listeners.map(it -> map(event, it)) - .forEach(events::create); + return listeners.map(it -> map(event, it)) + .map(events::create) + .toList(); } /* @@ -118,7 +124,7 @@ public class DefaultEventPublicationRegistry implements DisposableBean, EventPub private EventPublication map(Object event, PublicationTargetIdentifier targetIdentifier) { - EventPublication result = CompletableEventPublication.of(event, targetIdentifier); + var result = CompletableEventPublication.of(event, targetIdentifier, clock.instant()); LOGGER.debug("Registering publication of {} for {}.", // result.getEvent().getClass().getName(), result.getTargetIdentifier().getValue()); diff --git a/spring-modulith-events/spring-modulith-events-core/src/main/java/org/springframework/modulith/events/EventPublicationRegistry.java b/spring-modulith-events/spring-modulith-events-core/src/main/java/org/springframework/modulith/events/EventPublicationRegistry.java index 0e747abc..d36ae63c 100644 --- a/spring-modulith-events/spring-modulith-events-core/src/main/java/org/springframework/modulith/events/EventPublicationRegistry.java +++ b/spring-modulith-events/spring-modulith-events-core/src/main/java/org/springframework/modulith/events/EventPublicationRegistry.java @@ -36,7 +36,7 @@ public interface EventPublicationRegistry { * @param event must not be {@literal null}. * @param listeners must not be {@literal null}. */ - void store(Object event, Stream listeners); + Collection store(Object event, Stream listeners); /** * Returns all {@link EventPublication}s that have not been completed yet. diff --git a/spring-modulith-events/spring-modulith-events-core/src/main/java/org/springframework/modulith/events/config/EventPublicationConfiguration.java b/spring-modulith-events/spring-modulith-events-core/src/main/java/org/springframework/modulith/events/config/EventPublicationConfiguration.java index a50919b1..80bea6a2 100644 --- a/spring-modulith-events/spring-modulith-events-core/src/main/java/org/springframework/modulith/events/config/EventPublicationConfiguration.java +++ b/spring-modulith-events/spring-modulith-events-core/src/main/java/org/springframework/modulith/events/config/EventPublicationConfiguration.java @@ -15,6 +15,7 @@ */ package org.springframework.modulith.events.config; +import java.time.Clock; import java.time.Duration; import java.util.Arrays; @@ -22,6 +23,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.BeansException; import org.springframework.beans.factory.ObjectFactory; +import org.springframework.beans.factory.ObjectProvider; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.config.BeanPostProcessor; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; @@ -55,8 +57,9 @@ class EventPublicationConfiguration { @Bean @Role(BeanDefinition.ROLE_INFRASTRUCTURE) - EventPublicationRegistry eventPublicationRegistry(EventPublicationRepository repository) { - return new DefaultEventPublicationRegistry(repository); + EventPublicationRegistry eventPublicationRegistry(EventPublicationRepository repository, + ObjectProvider clock) { + return new DefaultEventPublicationRegistry(repository, clock.getIfAvailable(() -> Clock.systemUTC())); } @Bean diff --git a/spring-modulith-events/spring-modulith-events-core/src/test/java/org/springframework/modulith/events/DefaultEventPublicationRegistryUnitTests.java b/spring-modulith-events/spring-modulith-events-core/src/test/java/org/springframework/modulith/events/DefaultEventPublicationRegistryUnitTests.java new file mode 100644 index 00000000..395794bf --- /dev/null +++ b/spring-modulith-events/spring-modulith-events-core/src/test/java/org/springframework/modulith/events/DefaultEventPublicationRegistryUnitTests.java @@ -0,0 +1,62 @@ +/* + * Copyright 2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.modulith.events; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.AdditionalAnswers.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +import java.time.Clock; +import java.time.Instant; +import java.time.ZoneId; +import java.util.stream.Stream; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +/** + * Unit tests for {@link DefaultEventPublicationRegistry}. + * + * @author Oliver Drotbohm + */ +@ExtendWith(MockitoExtension.class) +class DefaultEventPublicationRegistryUnitTests { + + @Mock EventPublicationRepository repository; + @Mock Clock clock; + + @Test // GH-206 + void usesCustomClockIfConfigured() { + + when(repository.create(any())).then(returnsFirstArg()); + + var now = Instant.now(); + var clock = Clock.fixed(now, ZoneId.systemDefault()); + + var registry = new DefaultEventPublicationRegistry(repository, clock); + + var identifier = PublicationTargetIdentifier.of("id"); + var publications = registry.store(new Object(), Stream.of(identifier)); + + assertThat(publications).hasSize(1).element(0).satisfies(it -> { + assertThat(it.getPublicationDate()).isEqualTo(now); + assertThat(it.getTargetIdentifier()).isEqualTo(identifier); + }); + } +} diff --git a/spring-modulith-events/spring-modulith-events-core/src/test/java/org/springframework/modulith/events/config/EventPublicationConfigurationIntegrationTests.java b/spring-modulith-events/spring-modulith-events-core/src/test/java/org/springframework/modulith/events/config/EventPublicationConfigurationIntegrationTests.java index 8bcf001e..336ea38f 100644 --- a/spring-modulith-events/spring-modulith-events-core/src/test/java/org/springframework/modulith/events/config/EventPublicationConfigurationIntegrationTests.java +++ b/spring-modulith-events/spring-modulith-events-core/src/test/java/org/springframework/modulith/events/config/EventPublicationConfigurationIntegrationTests.java @@ -17,7 +17,10 @@ package org.springframework.modulith.events.config; import static org.assertj.core.api.Assertions.*; +import java.time.Clock; import java.time.Duration; +import java.time.Instant; +import java.time.ZoneId; import java.util.function.Function; import org.junit.jupiter.api.Test; @@ -32,11 +35,13 @@ import org.springframework.boot.test.context.assertj.AssertableApplicationContex import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.boot.test.context.runner.ContextConsumer; import org.springframework.context.annotation.AdviceMode; +import org.springframework.modulith.events.EventPublicationRegistry; import org.springframework.modulith.events.EventPublicationRepository; import org.springframework.modulith.events.config.EventPublicationConfiguration.AsyncPropertiesDefaulter; import org.springframework.scheduling.annotation.EnableAsync; import org.springframework.scheduling.annotation.ProxyAsyncConfiguration; import org.springframework.scheduling.aspectj.AspectJAsyncConfiguration; +import org.springframework.test.util.ReflectionTestUtils; /** * Unit tests for {@link EventPublicationConfiguration}. @@ -105,6 +110,21 @@ class EventPublicationConfigurationIntegrationTests { }); } + @Test // GH-206 + void wiresCustomClockIntoEventPublicationRegistryIfConfigured() { + + var clock = Clock.fixed(Instant.now(), ZoneId.systemDefault()); + + basicSetup() + .withBean(Clock.class, () -> clock) + .run(context -> { + + var registry = context.getBean(EventPublicationRegistry.class); + + assertThat(ReflectionTestUtils.getField(registry, "clock")).isSameAs(clock); + }); + } + private ContextConsumer expect(Function extractor, T expected) {