From 3899d9d59c754023afecbfb17909c14420748556 Mon Sep 17 00:00:00 2001 From: Oliver Drotbohm Date: Fri, 22 Jul 2022 22:19:47 +0200 Subject: [PATCH] GH-3 - Polishing. A couple of renames in the exposed APIs. MapBasedEventPublicationRepository now uses a a TreeMap again instead of a List (closer to what the original EventPublicationRegistry did). Cleanups in the JDBC event registry module. Polish pom.xml to minimize dependencies. Removed custom DatabaseType in favor of Boot's DatabaseDriver. Tweaked the auto-configuration of the JDBC module to not even expose a DatabaseSchemaInitializer in the first place to avoid having to wire the boolean flag into the latter. Cleanups in test cases to avoid code duplication. Move to @JdbcTest to avoid having to depend on Spring Data JDBC. Enable nullability checks, formatting, Javadoc, author tags, missing license headers. --- lombok.config | 1 + .../events/CompletableEventPublication.java | 1 + .../DefaultEventPublicationRegistry.java | 42 +-- .../modulith/events/EventPublication.java | 5 +- .../events/EventPublicationRegistry.java | 4 +- .../events/EventPublicationRepository.java | 61 +++- .../modulith/events/EventSerializer.java | 4 +- .../events/PublicationTargetIdentifier.java | 2 +- .../config/EventPublicationConfiguration.java | 37 +- .../MapBackedEventPublicationRepository.java | 65 ---- .../MapEventPublicationRepository.java | 89 +++++ ...PersistentApplicationEventMulticaster.java | 4 +- .../CompletableEventPublicationTest.java | 9 +- ...egisteringBeanPostProcessorUnitTests.java} | 2 +- .../spring-modulith-events-jdbc/pom.xml | 45 +-- .../jdbc/DatabaseSchemaInitializer.java | 81 ++--- .../modulith/events/jdbc/DatabaseType.java | 112 ------ ...JdbcEventPublicationAutoConfiguration.java | 26 +- .../jdbc/JdbcEventPublicationRepository.java | 133 +++++--- .../modulith/events/jdbc/package-info.java | 2 + .../{schema-hsql.sql => schema-hsqldb.sql} | 0 ...baseSchemaInitializerIntegrationTests.java | 121 +++++++ ...tionAutoConfigurationIntegrationTests.java | 21 +- ...PublicationRepositoryIntegrationTests.java | 193 +++++++++++ .../DatabaseSchemaInitializerTest.java | 143 -------- ...PublicationRepositoryIntegrationTests.java | 319 ------------------ ...operties => application-hsqldb.properties} | 1 - .../events/jpa/JpaEventPublication.java | 25 +- .../jpa/JpaEventPublicationConfiguration.java | 11 +- .../jpa/JpaEventPublicationRepository.java | 76 +++-- ...licationConfigurationIntegrationTests.java | 9 +- ...PublicationRepositoryIntegrationTests.java | 30 +- .../spring-modulith-events-tests/pom.xml | 4 +- 33 files changed, 744 insertions(+), 934 deletions(-) delete mode 100644 spring-modulith-events/spring-modulith-events-core/src/main/java/org/springframework/modulith/events/support/MapBackedEventPublicationRepository.java create mode 100644 spring-modulith-events/spring-modulith-events-core/src/main/java/org/springframework/modulith/events/support/MapEventPublicationRepository.java rename spring-modulith-events/spring-modulith-events-core/src/test/java/org/springframework/modulith/events/support/{CompletionRegisteringBeanPostProcessorUnitTest.java => CompletionRegisteringBeanPostProcessorUnitTests.java} (98%) delete mode 100644 spring-modulith-events/spring-modulith-events-jdbc/src/main/java/org/springframework/modulith/events/jdbc/DatabaseType.java create mode 100644 spring-modulith-events/spring-modulith-events-jdbc/src/main/java/org/springframework/modulith/events/jdbc/package-info.java rename spring-modulith-events/spring-modulith-events-jdbc/src/main/resources/{schema-hsql.sql => schema-hsqldb.sql} (100%) create mode 100644 spring-modulith-events/spring-modulith-events-jdbc/src/test/java/org/springframework/modulith/events/jdbc/DatabaseSchemaInitializerIntegrationTests.java create mode 100644 spring-modulith-events/spring-modulith-events-jdbc/src/test/java/org/springframework/modulith/events/jdbc/JdbcEventPublicationRepositoryIntegrationTests.java delete mode 100644 spring-modulith-events/spring-modulith-events-jdbc/src/test/java/org/springframework/modulith/testapp/DatabaseSchemaInitializerTest.java delete mode 100644 spring-modulith-events/spring-modulith-events-jdbc/src/test/java/org/springframework/modulith/testapp/JdbcEventPublicationRepositoryIntegrationTests.java rename spring-modulith-events/spring-modulith-events-jdbc/src/test/resources/{application-hsql.properties => application-hsqldb.properties} (70%) diff --git a/lombok.config b/lombok.config index 25fc177c..6e8e3e0f 100644 --- a/lombok.config +++ b/lombok.config @@ -1,3 +1,4 @@ lombok.nonNull.exceptionType = IllegalArgumentException lombok.log.fieldName = LOG lombok.addLombokGeneratedAnnotation = true +lombok.accessors.chain=true 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 0a79c584..9d9fd105 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 @@ -24,6 +24,7 @@ import java.util.Optional; * @author Oliver Drotbohm */ public interface CompletableEventPublication extends EventPublication { + /** * Returns the completion date of the publication. * 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 70037326..af40ae20 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,10 @@ */ 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; @@ -24,15 +28,13 @@ import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; import org.springframework.util.Assert; -import lombok.NonNull; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; - /** * A registry to capture event publications to {@link ApplicationListener}s. Allows to register those publications, mark * them as completed and lookup incomplete publications. * - * @author Oliver Drotbohm, Björn Kieling, Dmitry Belyaev + * @author Oliver Drotbohm + * @author Björn Kieling + * @author Dmitry Belyaev */ @Slf4j @RequiredArgsConstructor @@ -40,33 +42,19 @@ public class DefaultEventPublicationRegistry implements DisposableBean, EventPub private final @NonNull EventPublicationRepository events; - /** - * Stores {@link EventPublication}s for the given event and {@link ApplicationListener}s. - * - * @param event must not be {@literal null}. - * @param listeners must not be {@literal null}. - */ + @Override public void store(Object event, Stream listeners) { listeners.map(it -> map(event, it)) .forEach(events::create); } - /** - * Returns all {@link EventPublication}s that have not been completed yet. - * - * @return will never be {@literal null}. - */ + @Override public Iterable findIncompletePublications() { - return events.findByCompletionDateIsNull(); + return events.findIncompletePublications(); } - /** - * Marks the publication for the given event and {@link PublicationTargetIdentifier} as completed. - * - * @param event must not be {@literal null}. - * @param targetIdentifier must not be {@literal null}. - */ + @Override @Transactional(propagation = Propagation.REQUIRES_NEW) public void markCompleted(Object event, PublicationTargetIdentifier targetIdentifier) { @@ -74,15 +62,15 @@ public class DefaultEventPublicationRegistry implements DisposableBean, EventPub Assert.notNull(targetIdentifier, "Listener identifier must not be null!"); events.findByEventAndTargetIdentifier(event, targetIdentifier) // - .map(DefaultEventPublicationRegistry::LOGCompleted) // + .map(DefaultEventPublicationRegistry::logCompleted) // .map(e -> CompletableEventPublication.of(e.getEvent(), e.getTargetIdentifier())) - .ifPresent(it -> events.updateCompletionDate(it.markCompleted())); + .ifPresent(it -> events.update(it.markCompleted())); } @Override public void destroy() { - List publications = events.findByCompletionDateIsNull(); + List publications = events.findIncompletePublications(); if (publications.isEmpty()) { @@ -111,7 +99,7 @@ public class DefaultEventPublicationRegistry implements DisposableBean, EventPub return result; } - private static EventPublication LOGCompleted(EventPublication publication) { + private static EventPublication logCompleted(EventPublication publication) { LOG.debug("Marking publication of event {} to listener {} completed.", // publication.getEvent().getClass().getName(), publication.getTargetIdentifier().getValue()); diff --git a/spring-modulith-events/spring-modulith-events-core/src/main/java/org/springframework/modulith/events/EventPublication.java b/spring-modulith-events/spring-modulith-events-core/src/main/java/org/springframework/modulith/events/EventPublication.java index 5070c8fa..7b6ec381 100644 --- a/spring-modulith-events/spring-modulith-events-core/src/main/java/org/springframework/modulith/events/EventPublication.java +++ b/spring-modulith-events/spring-modulith-events-core/src/main/java/org/springframework/modulith/events/EventPublication.java @@ -24,9 +24,12 @@ import org.springframework.util.Assert; /** * An event publication. * - * @author Oliver Drotbohm, Björn Kieling, Dmitry Belyaev + * @author Oliver Drotbohm + * @author Björn Kieling + * @author Dmitry Belyaev */ public interface EventPublication extends Comparable { + /** * Returns the event that is published. * 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 571b3b7b..c8c0affc 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 @@ -23,7 +23,9 @@ import org.springframework.context.ApplicationListener; * A registry to capture event publications to {@link ApplicationListener}s. Allows to register those publications, mark * them as completed and lookup incomplete publications. * - * @author Oliver Drotbohm, Björn Kieling, Dmitry Belyaev + * @author Oliver Drotbohm + * @author Björn Kieling + * @author Dmitry Belyaev */ public interface EventPublicationRegistry { diff --git a/spring-modulith-events/spring-modulith-events-core/src/main/java/org/springframework/modulith/events/EventPublicationRepository.java b/spring-modulith-events/spring-modulith-events-core/src/main/java/org/springframework/modulith/events/EventPublicationRepository.java index 427eba73..7860b07f 100644 --- a/spring-modulith-events/spring-modulith-events-core/src/main/java/org/springframework/modulith/events/EventPublicationRepository.java +++ b/spring-modulith-events/spring-modulith-events-core/src/main/java/org/springframework/modulith/events/EventPublicationRepository.java @@ -1,3 +1,18 @@ +/* + * Copyright 2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package org.springframework.modulith.events; import java.util.List; @@ -6,25 +21,41 @@ import java.util.Optional; /** * Repository to store {@link EventPublication}s. * - * @author Björn Kieling, Dmitry Belyaev + * @author Björn Kieling + * @author Dmitry Belyaev + * @author Oliver Drotbohm */ public interface EventPublicationRepository { - EventPublication create(EventPublication publication); + /** + * Persists the given {@link EventPublication}. + * + * @param publication must not be {@literal null}. + * @return will never be {@literal null}. + */ + EventPublication create(EventPublication publication); - EventPublication updateCompletionDate(CompletableEventPublication publication); + /** + * Update the data store to mark the backing log entry as completed. + * + * @param publication must not be {@literal null}. + * @return will never be {@literal null}. + */ + EventPublication update(CompletableEventPublication publication); - /** - * Returns all {@link EventPublication} that have not been completed yet. - */ - List findByCompletionDateIsNull(); + /** + * Returns all {@link EventPublication} that have not been completed yet. + * + * @return will never be {@literal null}. + */ + List findIncompletePublications(); - /** - * Return the {@link EventPublication} for the given serialized event and listener identifier. - * - * @param event must not be {@literal null}. - * @param targetIdentifier must not be {@literal null}. - * @return - */ - Optional findByEventAndTargetIdentifier(Object event, PublicationTargetIdentifier targetIdentifier); + /** + * Return the {@link EventPublication} for the given serialized event and listener identifier. + * + * @param event must not be {@literal null}. + * @param targetIdentifier must not be {@literal null}. + * @return will never be {@literal null}. + */ + Optional findByEventAndTargetIdentifier(Object event, PublicationTargetIdentifier targetIdentifier); } diff --git a/spring-modulith-events/spring-modulith-events-core/src/main/java/org/springframework/modulith/events/EventSerializer.java b/spring-modulith-events/spring-modulith-events-core/src/main/java/org/springframework/modulith/events/EventSerializer.java index 91874ba5..e4e30e20 100644 --- a/spring-modulith-events/spring-modulith-events-core/src/main/java/org/springframework/modulith/events/EventSerializer.java +++ b/spring-modulith-events/spring-modulith-events-core/src/main/java/org/springframework/modulith/events/EventSerializer.java @@ -24,7 +24,7 @@ public interface EventSerializer { * Serializes the given event into a storable format. * * @param event must not be {@literal null}. - * @return + * @return will never be {@literal null}. */ Object serialize(Object event); @@ -33,7 +33,7 @@ public interface EventSerializer { * * @param serialized must not be {@literal null}. * @param type must not be {@literal null}. - * @return + * @return will never be {@literal null}. */ T deserialize(Object serialized, Class type); } diff --git a/spring-modulith-events/spring-modulith-events-core/src/main/java/org/springframework/modulith/events/PublicationTargetIdentifier.java b/spring-modulith-events/spring-modulith-events-core/src/main/java/org/springframework/modulith/events/PublicationTargetIdentifier.java index f16b4158..ed278647 100644 --- a/spring-modulith-events/spring-modulith-events-core/src/main/java/org/springframework/modulith/events/PublicationTargetIdentifier.java +++ b/spring-modulith-events/spring-modulith-events-core/src/main/java/org/springframework/modulith/events/PublicationTargetIdentifier.java @@ -27,7 +27,7 @@ import lombok.Value; @RequiredArgsConstructor(staticName = "of") public class PublicationTargetIdentifier { - private String value; + String value; /* * (non-Javadoc) 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 0902721a..23d0665a 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 @@ -23,33 +23,34 @@ import org.springframework.modulith.events.DefaultEventPublicationRegistry; import org.springframework.modulith.events.EventPublicationRegistry; import org.springframework.modulith.events.EventPublicationRepository; import org.springframework.modulith.events.support.CompletionRegisteringBeanPostProcessor; -import org.springframework.modulith.events.support.MapBackedEventPublicationRepository; +import org.springframework.modulith.events.support.MapEventPublicationRepository; import org.springframework.modulith.events.support.PersistentApplicationEventMulticaster; /** - * @author Oliver Drotbohm, Björn Kieling, Dmitry Belyaev + * @author Oliver Drotbohm + * @author Björn Kieling + * @author Dmitry Belyaev */ @Configuration(proxyBeanMethods = false) class EventPublicationConfiguration { - @Bean - EventPublicationRegistry eventPublicationRegistry( - ObjectProvider repositoryProvider) { + @Bean + EventPublicationRegistry eventPublicationRegistry( + ObjectProvider repositoryProvider) { - return new DefaultEventPublicationRegistry( - repositoryProvider.getIfAvailable(MapBackedEventPublicationRepository::new) - ); - } + return new DefaultEventPublicationRegistry( + repositoryProvider.getIfAvailable(MapEventPublicationRepository::new)); + } - @Bean - PersistentApplicationEventMulticaster applicationEventMulticaster( - EventPublicationRegistry eventPublicationRegistry) { + @Bean + PersistentApplicationEventMulticaster applicationEventMulticaster( + EventPublicationRegistry eventPublicationRegistry) { - return new PersistentApplicationEventMulticaster(() -> eventPublicationRegistry); - } + return new PersistentApplicationEventMulticaster(() -> eventPublicationRegistry); + } - @Bean - static CompletionRegisteringBeanPostProcessor bpp(ObjectFactory store) { - return new CompletionRegisteringBeanPostProcessor(store::getObject); - } + @Bean + static CompletionRegisteringBeanPostProcessor bpp(ObjectFactory store) { + return new CompletionRegisteringBeanPostProcessor(store::getObject); + } } diff --git a/spring-modulith-events/spring-modulith-events-core/src/main/java/org/springframework/modulith/events/support/MapBackedEventPublicationRepository.java b/spring-modulith-events/spring-modulith-events-core/src/main/java/org/springframework/modulith/events/support/MapBackedEventPublicationRepository.java deleted file mode 100644 index f94ddd60..00000000 --- a/spring-modulith-events/spring-modulith-events-core/src/main/java/org/springframework/modulith/events/support/MapBackedEventPublicationRepository.java +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright 2017-2022 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://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.support; - -import java.util.ArrayList; -import java.util.List; -import java.util.Optional; -import java.util.stream.Collectors; - -import org.springframework.modulith.events.CompletableEventPublication; -import org.springframework.modulith.events.EventPublication; -import org.springframework.modulith.events.EventPublicationRepository; -import org.springframework.modulith.events.PublicationTargetIdentifier; - -/** - * Map based {@link EventPublicationRepository}, for testing purposes only. - * - * @author Björn Kieling, Dmitry Belyaev - */ -public class MapBackedEventPublicationRepository implements EventPublicationRepository { - - private final List events = new ArrayList<>(); - - @Override - public EventPublication create(EventPublication publication) { - events.add(CompletableEventPublication.of(publication.getEvent(), publication.getTargetIdentifier())); - return publication; - } - - @Override - public EventPublication updateCompletionDate(CompletableEventPublication publication) { - findByEventAndTargetIdentifier(publication.getEvent(), publication.getTargetIdentifier()) - .ifPresent(eventPublication -> ((CompletableEventPublication) eventPublication).markCompleted()); - return publication; - } - - @Override - public List findByCompletionDateIsNull() { - return events.stream() - .filter(publication -> !publication.isPublicationCompleted()) - .collect(Collectors.toList()); - } - - @Override - public Optional findByEventAndTargetIdentifier(Object event, PublicationTargetIdentifier targetIdentifier) { - return events.stream() - .filter(publication -> - publication.equals(publication.getEvent()) && publication.getTargetIdentifier().equals(targetIdentifier)) - .map(EventPublication.class::cast) - .findAny(); - } -} diff --git a/spring-modulith-events/spring-modulith-events-core/src/main/java/org/springframework/modulith/events/support/MapEventPublicationRepository.java b/spring-modulith-events/spring-modulith-events-core/src/main/java/org/springframework/modulith/events/support/MapEventPublicationRepository.java new file mode 100644 index 00000000..90321fb9 --- /dev/null +++ b/spring-modulith-events/spring-modulith-events-core/src/main/java/org/springframework/modulith/events/support/MapEventPublicationRepository.java @@ -0,0 +1,89 @@ +/* + * Copyright 2017-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://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.support; + +import lombok.AccessLevel; +import lombok.RequiredArgsConstructor; +import lombok.Value; + +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.TreeMap; + +import org.springframework.modulith.events.CompletableEventPublication; +import org.springframework.modulith.events.EventPublication; +import org.springframework.modulith.events.EventPublicationRepository; +import org.springframework.modulith.events.PublicationTargetIdentifier; + +/** + * Map based {@link EventPublicationRepository}, for testing purposes only. + * + * @author Oliver Drotbohm + * @author Björn Kieling + * @author Dmitry Belyaev + */ +public class MapEventPublicationRepository implements EventPublicationRepository { + + private final Map events = new TreeMap<>(); + + @Override + public EventPublication create(EventPublication publication) { + + return events.computeIfAbsent(Key.of(publication), + it -> CompletableEventPublication.of(it.getEvent(), it.getIdentifier())); + } + + @Override + public EventPublication update(CompletableEventPublication publication) { + + var result = events.computeIfPresent(Key.of(publication), (__, it) -> it.markCompleted()); + + if (result == null) { + throw new IllegalArgumentException("Couldn't find publication %s!".formatted(publication)); + } + + return result; + } + + @Override + public List findIncompletePublications() { + + return events.values().stream() // + .filter(it -> !it.isPublicationCompleted()) + .map(EventPublication.class::cast) + .toList(); + } + + @Override + public Optional findByEventAndTargetIdentifier(Object event, + PublicationTargetIdentifier targetIdentifier) { + + return Optional.ofNullable(events.get(new Key(event, targetIdentifier))); + } + + @Value + @RequiredArgsConstructor(access = AccessLevel.PRIVATE) + private static class Key { + + Object event; + PublicationTargetIdentifier identifier; + + static Key of(EventPublication publication) { + return new Key(publication.getEvent(), publication.getTargetIdentifier()); + } + } +} diff --git a/spring-modulith-events/spring-modulith-events-core/src/main/java/org/springframework/modulith/events/support/PersistentApplicationEventMulticaster.java b/spring-modulith-events/spring-modulith-events-core/src/main/java/org/springframework/modulith/events/support/PersistentApplicationEventMulticaster.java index e1d086f6..07e7e5a0 100644 --- a/spring-modulith-events/spring-modulith-events-core/src/main/java/org/springframework/modulith/events/support/PersistentApplicationEventMulticaster.java +++ b/spring-modulith-events/spring-modulith-events-core/src/main/java/org/springframework/modulith/events/support/PersistentApplicationEventMulticaster.java @@ -123,8 +123,8 @@ public class PersistentApplicationEventMulticaster extends AbstractApplicationEv }); } - private ApplicationListener executeListenerWithCompletion( - EventPublication publication, TransactionalApplicationListener listener) { + private ApplicationListener executeListenerWithCompletion(EventPublication publication, + TransactionalApplicationListener listener) { listener.processEvent(publication.getApplicationEvent()); diff --git a/spring-modulith-events/spring-modulith-events-core/src/test/java/org/springframework/modulith/events/CompletableEventPublicationTest.java b/spring-modulith-events/spring-modulith-events-core/src/test/java/org/springframework/modulith/events/CompletableEventPublicationTest.java index 14dbbfcf..d5d88df1 100644 --- a/spring-modulith-events/spring-modulith-events-core/src/test/java/org/springframework/modulith/events/CompletableEventPublicationTest.java +++ b/spring-modulith-events/spring-modulith-events-core/src/test/java/org/springframework/modulith/events/CompletableEventPublicationTest.java @@ -15,15 +15,16 @@ */ package org.springframework.modulith.events; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.*; import org.junit.jupiter.api.Test; /** - * @author Oliver Drotbohm, Björn Kieling, Dmitry Belyaev + * @author Oliver Drotbohm + * @author Björn Kieling + * @author Dmitry Belyaev */ -class CompletableEventPublicationTest { +class CompletableEventPublicationUnitTests { @Test void rejectsNullEvent() { diff --git a/spring-modulith-events/spring-modulith-events-core/src/test/java/org/springframework/modulith/events/support/CompletionRegisteringBeanPostProcessorUnitTest.java b/spring-modulith-events/spring-modulith-events-core/src/test/java/org/springframework/modulith/events/support/CompletionRegisteringBeanPostProcessorUnitTests.java similarity index 98% rename from spring-modulith-events/spring-modulith-events-core/src/test/java/org/springframework/modulith/events/support/CompletionRegisteringBeanPostProcessorUnitTest.java rename to spring-modulith-events/spring-modulith-events-core/src/test/java/org/springframework/modulith/events/support/CompletionRegisteringBeanPostProcessorUnitTests.java index d6b1c6b6..e72b34c0 100644 --- a/spring-modulith-events/spring-modulith-events-core/src/test/java/org/springframework/modulith/events/support/CompletionRegisteringBeanPostProcessorUnitTest.java +++ b/spring-modulith-events/spring-modulith-events-core/src/test/java/org/springframework/modulith/events/support/CompletionRegisteringBeanPostProcessorUnitTests.java @@ -34,7 +34,7 @@ import org.springframework.transaction.event.TransactionalEventListener; * * @author Oliver Drotbohm */ -class CompletionRegisteringBeanPostProcessorUnitTest { +class CompletionRegisteringBeanPostProcessorUnitTests { EventPublicationRegistry registry = mock(EventPublicationRegistry.class); BeanPostProcessor processor = new CompletionRegisteringBeanPostProcessor(() -> registry); diff --git a/spring-modulith-events/spring-modulith-events-jdbc/pom.xml b/spring-modulith-events/spring-modulith-events-jdbc/pom.xml index cae93718..e5205475 100644 --- a/spring-modulith-events/spring-modulith-events-jdbc/pom.xml +++ b/spring-modulith-events/spring-modulith-events-jdbc/pom.xml @@ -12,7 +12,6 @@ spring-modulith-events-jdbc - 17 org.springframework.modulith.events.jdbc @@ -23,71 +22,43 @@ spring-modulith-events-core ${project.version} + - org.springframework.data - spring-data-jdbc + org.springframework + spring-jdbc - - - - - - org.springframework.boot spring-boot-starter-test test - - org.springframework.boot - spring-boot-starter-data-jdbc - test - org.hsqldb hsqldb test + com.h2database h2 test + org.postgresql postgresql test + - - - spring-milestone - https://repo.spring.io/milestone - - false - - - - - \ No newline at end of file + diff --git a/spring-modulith-events/spring-modulith-events-jdbc/src/main/java/org/springframework/modulith/events/jdbc/DatabaseSchemaInitializer.java b/spring-modulith-events/spring-modulith-events-jdbc/src/main/java/org/springframework/modulith/events/jdbc/DatabaseSchemaInitializer.java index f067e9cf..92cd3583 100644 --- a/spring-modulith-events/spring-modulith-events-jdbc/src/main/java/org/springframework/modulith/events/jdbc/DatabaseSchemaInitializer.java +++ b/spring-modulith-events/spring-modulith-events-jdbc/src/main/java/org/springframework/modulith/events/jdbc/DatabaseSchemaInitializer.java @@ -15,62 +15,65 @@ */ package org.springframework.modulith.events.jdbc; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.charset.StandardCharsets; + import org.springframework.beans.factory.InitializingBean; -import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.jdbc.DatabaseDriver; import org.springframework.context.ResourceLoaderAware; import org.springframework.core.io.Resource; import org.springframework.core.io.ResourceLoader; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.support.MetaDataAccessException; -import org.springframework.util.FileCopyUtils; - -import java.io.IOException; -import java.io.InputStreamReader; -import java.io.Reader; -import java.io.UncheckedIOException; -import java.nio.charset.StandardCharsets; +import org.springframework.util.StreamUtils; /** * Initializes the DB schema used to store events * - * @author Dmitry Belyaev, Björn Kieling + * @author Dmitry Belyaev + * @author Björn Kieling + * @author Oliver Drotbohm */ -public class DatabaseSchemaInitializer implements ResourceLoaderAware, InitializingBean { +class DatabaseSchemaInitializer implements ResourceLoaderAware, InitializingBean { - private final JdbcTemplate jdbcTemplate; + private final JdbcTemplate jdbcTemplate; - private ResourceLoader resourceLoader; + private ResourceLoader resourceLoader; - @Value("${spring.modulith.events.schema-initialization.enabled:false}") - private boolean initEnabled; + /** + * Creates a new {@link DatabaseSchemaInitializer} for the given {@link JdbcTemplate} and ini + * + * @param jdbcTemplate + * @param initEnabled + */ + public DatabaseSchemaInitializer(JdbcTemplate jdbcTemplate) { - public DatabaseSchemaInitializer(JdbcTemplate jdbcTemplate) { - this.jdbcTemplate = jdbcTemplate; - } + this.jdbcTemplate = jdbcTemplate; + } - @Override - public void setResourceLoader(ResourceLoader resourceLoader) { - this.resourceLoader = resourceLoader; - } + @Override + public void setResourceLoader(ResourceLoader resourceLoader) { + this.resourceLoader = resourceLoader; + } - @Override - public void afterPropertiesSet() throws MetaDataAccessException { - if (!initEnabled) { - return; - } + @Override + public void afterPropertiesSet() throws MetaDataAccessException { - DatabaseType databaseType = DatabaseType.fromMetaData(jdbcTemplate.getDataSource()); - String databaseName = databaseType.name().toLowerCase(); - var schemaDdlResource = resourceLoader.getResource("/schema-" + databaseName + ".sql"); - var schemaDdl = asString(schemaDdlResource); - jdbcTemplate.execute(schemaDdl); - } + var fromDataSource = DatabaseDriver.fromDataSource(jdbcTemplate.getDataSource()); + var databaseName = fromDataSource.name().toLowerCase(); + var schemaDdlResource = resourceLoader.getResource("/schema-" + databaseName + ".sql"); + var schemaDdl = asString(schemaDdlResource); - private String asString(Resource resource) { - try (Reader reader = new InputStreamReader(resource.getInputStream(), StandardCharsets.UTF_8)) { - return FileCopyUtils.copyToString(reader); - } catch (IOException e) { - throw new UncheckedIOException(e); - } - } + jdbcTemplate.execute(schemaDdl); + } + + private String asString(Resource resource) { + + try { + return StreamUtils.copyToString(resource.getInputStream(), StandardCharsets.UTF_8); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } } diff --git a/spring-modulith-events/spring-modulith-events-jdbc/src/main/java/org/springframework/modulith/events/jdbc/DatabaseType.java b/spring-modulith-events/spring-modulith-events-jdbc/src/main/java/org/springframework/modulith/events/jdbc/DatabaseType.java deleted file mode 100644 index 9d17ed4d..00000000 --- a/spring-modulith-events/spring-modulith-events-jdbc/src/main/java/org/springframework/modulith/events/jdbc/DatabaseType.java +++ /dev/null @@ -1,112 +0,0 @@ -/* - * Copyright 2006-2022 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.modulith.events.jdbc; - -import org.springframework.jdbc.support.JdbcUtils; -import org.springframework.jdbc.support.MetaDataAccessException; -import org.springframework.util.StringUtils; - -import javax.sql.DataSource; -import java.sql.DatabaseMetaData; -import java.util.HashMap; -import java.util.Map; - -/** - * Enum representing a database type, such as DB2 or oracle. The type also contains a - * product name, which is expected to be the same as the product name provided by the - * database driver's metadata. - * - * @author Lucas Ward - */ -public enum DatabaseType { - - DERBY("Apache Derby"), DB2("DB2"), DB2VSE("DB2VSE"), DB2ZOS("DB2ZOS"), DB2AS400("DB2AS400"), - HSQL("HSQL Database Engine"), SQLSERVER("Microsoft SQL Server"), MYSQL("MySQL"), ORACLE("Oracle"), - POSTGRES("PostgreSQL"), SYBASE("Sybase"), H2("H2"), SQLITE("SQLite"), HANA("HDB"); - - private static final Map nameMap; - - static { - nameMap = new HashMap<>(); - for (DatabaseType type : values()) { - nameMap.put(type.getProductName(), type); - } - } - // A description is necessary due to the nature of database descriptions - // in metadata. - private final String productName; - - private DatabaseType(String productName) { - this.productName = productName; - } - - public String getProductName() { - return productName; - } - - /** - * Static method to obtain a DatabaseType from the provided product name. - * @param productName {@link String} containing the product name. - * @return the {@link DatabaseType} for given product name. - * @throws IllegalArgumentException if none is found. - */ - public static DatabaseType fromProductName(String productName) { - if (productName.equals("MariaDB")) - productName = "MySQL"; - if (!nameMap.containsKey(productName)) { - throw new IllegalArgumentException("DatabaseType not found for product name: [" + productName + "]"); - } - else { - return nameMap.get(productName); - } - } - - /** - * Convenience method that pulls a database product name from the DataSource's - * metadata. - * @param dataSource {@link DataSource} to the database to be used. - * @return {@link DatabaseType} for the {@link DataSource} specified. - * @throws MetaDataAccessException thrown if error occured during Metadata lookup. - */ - public static DatabaseType fromMetaData(DataSource dataSource) throws MetaDataAccessException { - String databaseProductName = JdbcUtils.extractDatabaseMetaData(dataSource, - DatabaseMetaData::getDatabaseProductName); - if (StringUtils.hasText(databaseProductName) && databaseProductName.startsWith("DB2")) { - String databaseProductVersion = JdbcUtils.extractDatabaseMetaData(dataSource, - DatabaseMetaData::getDatabaseProductVersion); - if (databaseProductVersion.startsWith("ARI")) { - databaseProductName = "DB2VSE"; - } - else if (databaseProductVersion.startsWith("DSN")) { - databaseProductName = "DB2ZOS"; - } - else if (databaseProductName.contains("AS") - && (databaseProductVersion.startsWith("QSQ") || databaseProductVersion - .substring(databaseProductVersion.indexOf('V')).matches("V\\dR\\d[mM]\\d"))) { - databaseProductName = "DB2AS400"; - } - else { - databaseProductName = JdbcUtils.commonDatabaseName(databaseProductName); - } - } - else { - databaseProductName = JdbcUtils.commonDatabaseName(databaseProductName); - } - return fromProductName(databaseProductName); - } - -} diff --git a/spring-modulith-events/spring-modulith-events-jdbc/src/main/java/org/springframework/modulith/events/jdbc/JdbcEventPublicationAutoConfiguration.java b/spring-modulith-events/spring-modulith-events-jdbc/src/main/java/org/springframework/modulith/events/jdbc/JdbcEventPublicationAutoConfiguration.java index d8ef802b..e9bd4435 100644 --- a/spring-modulith-events/spring-modulith-events-jdbc/src/main/java/org/springframework/modulith/events/jdbc/JdbcEventPublicationAutoConfiguration.java +++ b/spring-modulith-events/spring-modulith-events-jdbc/src/main/java/org/springframework/modulith/events/jdbc/JdbcEventPublicationAutoConfiguration.java @@ -15,6 +15,7 @@ */ package org.springframework.modulith.events.jdbc; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.jdbc.core.JdbcTemplate; @@ -22,22 +23,21 @@ import org.springframework.modulith.events.EventSerializer; import org.springframework.modulith.events.config.EventPublicationConfigurationExtension; /** - * @author Dmitry Belyaev, Björn Kieling + * @author Dmitry Belyaev + * @author Björn Kieling + * @author Oliver Drotbohm */ @Configuration(proxyBeanMethods = false) class JdbcEventPublicationAutoConfiguration implements EventPublicationConfigurationExtension { - @Bean - public JdbcEventPublicationRepository jpaEventPublicationRepository( - JdbcTemplate jdbcTemplate, EventSerializer serializer) { - // TODO Why do we want to instantiate the serializer here and what - // happens if no serializer is available or is not compatible to - // JDBC serialization? - return new JdbcEventPublicationRepository(jdbcTemplate, serializer); - } + @Bean + JdbcEventPublicationRepository jpaEventPublicationRepository(JdbcTemplate jdbcTemplate, EventSerializer serializer) { + return new JdbcEventPublicationRepository(jdbcTemplate, serializer); + } - @Bean - public DatabaseSchemaInitializer databaseSchemaInitializer(JdbcTemplate jdbcTemplate) { - return new DatabaseSchemaInitializer(jdbcTemplate); - } + @Bean + @ConditionalOnProperty(name = "spring.modulith.events.schema-initialization.enabled", havingValue = "true") + DatabaseSchemaInitializer databaseSchemaInitializer(JdbcTemplate jdbcTemplate) { + return new DatabaseSchemaInitializer(jdbcTemplate); + } } diff --git a/spring-modulith-events/spring-modulith-events-jdbc/src/main/java/org/springframework/modulith/events/jdbc/JdbcEventPublicationRepository.java b/spring-modulith-events/spring-modulith-events-jdbc/src/main/java/org/springframework/modulith/events/jdbc/JdbcEventPublicationRepository.java index 7f2fcb26..fd5d2323 100644 --- a/spring-modulith-events/spring-modulith-events-jdbc/src/main/java/org/springframework/modulith/events/jdbc/JdbcEventPublicationRepository.java +++ b/spring-modulith-events/spring-modulith-events-jdbc/src/main/java/org/springframework/modulith/events/jdbc/JdbcEventPublicationRepository.java @@ -15,16 +15,23 @@ */ 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.Optional; import java.util.UUID; -import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.JdbcOperations; +import org.springframework.jdbc.core.ResultSetExtractor; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.lang.Nullable; import org.springframework.modulith.events.CompletableEventPublication; import org.springframework.modulith.events.EventPublication; import org.springframework.modulith.events.EventPublicationRepository; @@ -32,19 +39,16 @@ import org.springframework.modulith.events.EventSerializer; import org.springframework.modulith.events.PublicationTargetIdentifier; import org.springframework.transaction.annotation.Transactional; -import lombok.Builder; -import lombok.EqualsAndHashCode; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; - /** - * Repository to store {@link EventPublication}s. + * JDBC-based repository to store {@link EventPublication}s. * - * @author Dmitry Belyaev, Björn Kieling + * @author Dmitry Belyaev + * @author Björn Kieling + * @author Oliver Drotbohm */ @Slf4j @RequiredArgsConstructor -public class JdbcEventPublicationRepository implements EventPublicationRepository { +class JdbcEventPublicationRepository implements EventPublicationRepository { private static final String SQL_STATEMENT_INSERT = """ INSERT INTO EVENT_PUBLICATION (ID, EVENT_TYPE, LISTENER_ID, PUBLICATION_DATE, SERIALIZED_EVENT) @@ -66,89 +70,120 @@ public class JdbcEventPublicationRepository implements EventPublicationRepositor ORDER BY PUBLICATION_DATE """; - private final JdbcTemplate jdbcTemplate; + private final JdbcOperations operations; private final EventSerializer serializer; @Override @Transactional public EventPublication create(EventPublication publication) { - String serializedEvent = serializeEvent(publication.getEvent()); - jdbcTemplate.update(SQL_STATEMENT_INSERT, // + + operations.update(SQL_STATEMENT_INSERT, // UUID.randomUUID(), // publication.getEvent().getClass().getName(), // publication.getTargetIdentifier().getValue(), // publication.getPublicationDate(), // - serializedEvent); + serializeEvent(publication.getEvent())); return publication; } @Override @Transactional - public EventPublication updateCompletionDate(CompletableEventPublication publication) { - String serializedEvent = serializeEvent(publication.getEvent()); - List results = jdbcTemplate.query(SQL_STATEMENT_FIND_BY_EVENT_AND_LISTENER_ID, + public EventPublication update(CompletableEventPublication publication) { + + var serializedEvent = serializeEvent(publication.getEvent()); + var results = operations.query(SQL_STATEMENT_FIND_BY_EVENT_AND_LISTENER_ID, (rs, rowNum) -> rs.getObject("ID", UUID.class), serializedEvent, publication.getTargetIdentifier().getValue()); + if (!results.isEmpty()) { - jdbcTemplate.update(SQL_STATEMENT_UPDATE, publication.getCompletionDate().orElse(null), results.get(0)); + operations.update(SQL_STATEMENT_UPDATE, publication.getCompletionDate().orElse(null), results.get(0)); } return publication; } - @Override - @Transactional(readOnly = true) - public List findByCompletionDateIsNull() { - return jdbcTemplate.query(SQL_STATEMENT_FIND_UNCOMPLETED, this::mapResultSetToEventPublications); - } - @Override @Transactional(readOnly = true) public Optional findByEventAndTargetIdentifier(Object event, PublicationTargetIdentifier targetIdentifier) { - String serializedEvent = serializeEvent(event); - List results = jdbcTemplate.query(SQL_STATEMENT_FIND_BY_EVENT_AND_LISTENER_ID, - this::mapResultSetToEventPublications, serializedEvent, targetIdentifier.getValue()); - if (results.isEmpty()) { - return Optional.empty(); - } else { - // if there are several events with exactly the same payload we return the oldest one first - return Optional.of(results.get(0)); - } + var results = operations.query(SQL_STATEMENT_FIND_BY_EVENT_AND_LISTENER_ID, this::resultSetToPublications, + serializeEvent(event), targetIdentifier.getValue()); + + return Optional.ofNullable((results == null) || results.isEmpty() ? null : results.get(0)); + } + + @Override + @Transactional(readOnly = true) + public List findIncompletePublications() { + return operations.query(SQL_STATEMENT_FIND_UNCOMPLETED, this::resultSetToPublications); } private String serializeEvent(Object event) { return serializer.serialize(event).toString(); } - private List mapResultSetToEventPublications(ResultSet rs) throws SQLException { - var result = new ArrayList(); - while (rs.next()) { - entityToDomain(rs).ifPresent(result::add); + /** + * Effectively a {@link ResultSetExtractor} to drop {@link EventPublication}s that cannot be deserialized. + * + * @param resultSet must not be {@literal null}. + * @return will never be {@literal null}. + * @throws SQLException + */ + private List resultSetToPublications(ResultSet resultSet) throws SQLException { + + List result = new ArrayList<>(); + + while (resultSet.next()) { + + EventPublication publication = resultSetToPublication(resultSet); + + if (publication != null) { + result.add(publication); + } } + return result; } - private Optional entityToDomain(ResultSet rs) throws SQLException { + /** + * Effectively a {@link RowMapper} to turn a single row into an {@link EventPublication}. + * + * @param rs must not be {@literal null}. + * @return can be {@literal null}. + * @throws SQLException + */ + @Nullable + private EventPublication resultSetToPublication(ResultSet rs) throws SQLException { + var id = rs.getObject("ID", UUID.class); - var eventClassName = rs.getString("EVENT_TYPE"); - Class eventClass; - try { - eventClass = Class.forName(eventClassName); - } catch (ClassNotFoundException e) { - LOG.warn("Event '{}' of unknown type '{}' found", id, eventClassName); - return Optional.empty(); + var eventClass = loadClass(id, rs.getString("EVENT_TYPE")); + + if (eventClass == null) { + return null; } - return Optional.of(JdbcEventPublication.builder() - .completionDate(Optional.ofNullable(rs.getTimestamp("COMPLETION_DATE")).map(Timestamp::toInstant).orElse(null)) + var completionDate = rs.getTimestamp("COMPLETION_DATE"); + + 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()); + .build(); + } + + @Nullable + private Class loadClass(UUID id, String className) { + + try { + return Class.forName(className); + } catch (ClassNotFoundException e) { + LOG.warn("Event '{}' of unknown type '{}' found", id, className); + return null; + } } @EqualsAndHashCode @@ -156,7 +191,7 @@ public class JdbcEventPublicationRepository implements EventPublicationRepositor private static class JdbcEventPublication implements CompletableEventPublication { private final UUID id; - private final Instant publicationDate; + private final @Nullable Instant publicationDate; private final String listenerId; private final String serializedEvent; private final Class eventType; diff --git a/spring-modulith-events/spring-modulith-events-jdbc/src/main/java/org/springframework/modulith/events/jdbc/package-info.java b/spring-modulith-events/spring-modulith-events-jdbc/src/main/java/org/springframework/modulith/events/jdbc/package-info.java new file mode 100644 index 00000000..148e6b1a --- /dev/null +++ b/spring-modulith-events/spring-modulith-events-jdbc/src/main/java/org/springframework/modulith/events/jdbc/package-info.java @@ -0,0 +1,2 @@ +@org.springframework.lang.NonNullApi +package org.springframework.modulith.events.jdbc; diff --git a/spring-modulith-events/spring-modulith-events-jdbc/src/main/resources/schema-hsql.sql b/spring-modulith-events/spring-modulith-events-jdbc/src/main/resources/schema-hsqldb.sql similarity index 100% rename from spring-modulith-events/spring-modulith-events-jdbc/src/main/resources/schema-hsql.sql rename to spring-modulith-events/spring-modulith-events-jdbc/src/main/resources/schema-hsqldb.sql diff --git a/spring-modulith-events/spring-modulith-events-jdbc/src/test/java/org/springframework/modulith/events/jdbc/DatabaseSchemaInitializerIntegrationTests.java b/spring-modulith-events/spring-modulith-events-jdbc/src/test/java/org/springframework/modulith/events/jdbc/DatabaseSchemaInitializerIntegrationTests.java new file mode 100644 index 00000000..4bbb189d --- /dev/null +++ b/spring-modulith-events/spring-modulith-events-jdbc/src/test/java/org/springframework/modulith/events/jdbc/DatabaseSchemaInitializerIntegrationTests.java @@ -0,0 +1,121 @@ +/* + * Copyright 2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.modulith.events.jdbc; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +import java.util.Optional; + +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.ImportAutoConfiguration; +import org.springframework.boot.test.autoconfigure.jdbc.JdbcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.boot.test.mock.mockito.SpyBean; +import org.springframework.jdbc.core.JdbcOperations; +import org.springframework.modulith.events.EventSerializer; +import org.springframework.modulith.testapp.TestApplication; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.ContextConfiguration; + +/** + * @author Dmitry Belyaev + * @author Björn Kieling + * @author Oliver Drotbohm + */ +class DatabaseSchemaInitializerIntegrationTests { + + private static final String COUNT_PUBLICATIONS = "SELECT COUNT(*) FROM EVENT_PUBLICATION"; + + @JdbcTest + @ImportAutoConfiguration(JdbcEventPublicationAutoConfiguration.class) + @ContextConfiguration(classes = TestApplication.class) + static class TestBase { + @MockBean EventSerializer serializer; + } + + @Nested + @JdbcTest(properties = "spring.modulith.events.schema-initialization.enabled=true") + static class WithInitEnabled extends TestBase { + + @Autowired JdbcOperations operations; + @Autowired Optional initializer; + + @Test // GH-3 + void doesNotRegisterAnInitializerBean() { + assertThat(initializer).isPresent(); + } + + @Test // GH-3 + void shouldCreateDatabaseSchemaOnStartUp() { + assertThat(operations.queryForObject(COUNT_PUBLICATIONS, Long.class)).isEqualTo(0); + } + } + + @Nested + @JdbcTest(properties = "spring.modulith.events.schema-initialization.enabled=false") + static class WithInitDisabled extends TestBase { + + @SpyBean JdbcOperations operations; + @Autowired Optional initializer; + + @Test // GH-3 + void doesNotRegisterAnInitializerBean() { + assertThat(initializer).isEmpty(); + } + + @Test // GH-3 + void shouldNotCreateDatabaseSchemaOnStartUp() { + verify(operations, never()).execute(anyString()); + } + } + + @Nested + class InitializationDisabledByDefault extends TestBase { + + @SpyBean JdbcOperations operations; + @Autowired Optional initializer; + + @Test // GH-3 + void doesNotRegisterAnInitializerBean() { + assertThat(initializer).isEmpty(); + } + + @Test // GH-3 + void shouldNotCreateDatabaseSchemaOnStartUp() { + verify(operations, never()).execute(anyString()); + } + } + + @Nested + @ActiveProfiles("hsqldb") + class HSQLDB extends WithInitEnabled { + + } + + @Nested + @ActiveProfiles("h2") + class H2 extends WithInitEnabled {} + + @Nested + @Disabled + @ActiveProfiles("postgres") + class Postgres extends WithInitEnabled {} +} diff --git a/spring-modulith-events/spring-modulith-events-jdbc/src/test/java/org/springframework/modulith/events/jdbc/JdbcEventPublicationAutoConfigurationIntegrationTests.java b/spring-modulith-events/spring-modulith-events-jdbc/src/test/java/org/springframework/modulith/events/jdbc/JdbcEventPublicationAutoConfigurationIntegrationTests.java index b736217f..aefff60d 100644 --- a/spring-modulith-events/spring-modulith-events-jdbc/src/test/java/org/springframework/modulith/events/jdbc/JdbcEventPublicationAutoConfigurationIntegrationTests.java +++ b/spring-modulith-events/spring-modulith-events-jdbc/src/test/java/org/springframework/modulith/events/jdbc/JdbcEventPublicationAutoConfigurationIntegrationTests.java @@ -15,6 +15,8 @@ */ package org.springframework.modulith.events.jdbc; +import static org.assertj.core.api.Assertions.*; + import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; @@ -24,24 +26,21 @@ import org.springframework.modulith.events.EventPublicationRegistry; import org.springframework.modulith.events.EventSerializer; import org.springframework.modulith.testapp.TestApplication; -import static org.assertj.core.api.Assertions.assertThat; - /** - * @author Dmitry Belyaev, Björn Kieling + * @author Dmitry Belyaev + * @author Björn Kieling + * @author Oliver Drotbohm */ @SpringBootTest( classes = TestApplication.class, - properties = "spring.modulith.events.schema-initialization.enabled=true" -) -public class JdbcEventPublicationAutoConfigurationIntegrationTests { + properties = "spring.modulith.events.schema-initialization.enabled=true") +class JdbcEventPublicationAutoConfigurationIntegrationTests { - @Autowired - private ApplicationContext context; + @Autowired ApplicationContext context; - @MockBean - private EventSerializer serializer; + @MockBean EventSerializer serializer; - @Test + @Test // GH-3 void bootstrapsApplicationComponents() { assertThat(context.getBean(EventPublicationRegistry.class)).isNotNull(); diff --git a/spring-modulith-events/spring-modulith-events-jdbc/src/test/java/org/springframework/modulith/events/jdbc/JdbcEventPublicationRepositoryIntegrationTests.java b/spring-modulith-events/spring-modulith-events-jdbc/src/test/java/org/springframework/modulith/events/jdbc/JdbcEventPublicationRepositoryIntegrationTests.java new file mode 100644 index 00000000..e153d8c9 --- /dev/null +++ b/spring-modulith-events/spring-modulith-events-jdbc/src/test/java/org/springframework/modulith/events/jdbc/JdbcEventPublicationRepositoryIntegrationTests.java @@ -0,0 +1,193 @@ +/* + * Copyright 2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.modulith.events.jdbc; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; + +import lombok.Value; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.jdbc.JdbcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Import; +import org.springframework.jdbc.core.JdbcOperations; +import org.springframework.modulith.events.CompletableEventPublication; +import org.springframework.modulith.events.EventPublication; +import org.springframework.modulith.events.EventSerializer; +import org.springframework.modulith.events.PublicationTargetIdentifier; +import org.springframework.modulith.testapp.TestApplication; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.ContextConfiguration; + +/** + * Integration tests for {@link JdbcEventPublicationRepository}. + * + * @author Dmitry Belyaev + * @author Björn Kieling + * @author Oliver Drotbohm + */ +class JdbcEventPublicationRepositoryIntegrationTests { + + static final PublicationTargetIdentifier TARGET_IDENTIFIER = PublicationTargetIdentifier.of("listener"); + + @JdbcTest + @Import(TestApplication.class) + @ContextConfiguration(classes = JdbcEventPublicationAutoConfiguration.class) + abstract class TestBase { + + @Autowired JdbcOperations operations; + @Autowired JdbcEventPublicationRepository repository; + + @MockBean EventSerializer serializer; + + @BeforeEach + void cleanUp() { + operations.execute("TRUNCATE TABLE EVENT_PUBLICATION"); + } + + @Test // GH-3 + void shouldPersistAndUpdateEventPublication() { + + var testEvent = new TestEvent("id"); + var serializedEvent = "{\"eventId\":\"id\"}"; + + when(serializer.serialize(testEvent)).thenReturn(serializedEvent); + when(serializer.deserialize(serializedEvent, TestEvent.class)).thenReturn(testEvent); + + var publication = CompletableEventPublication.of(testEvent, TARGET_IDENTIFIER); + + // Store publication + repository.create(publication); + + var eventPublications = repository.findIncompletePublications(); + + assertThat(eventPublications).hasSize(1); + assertThat(eventPublications).element(0).satisfies(it -> { + assertThat(it.getEvent()).isEqualTo(publication.getEvent()); + assertThat(it.getTargetIdentifier()).isEqualTo(publication.getTargetIdentifier()); + }); + + assertThat(repository.findByEventAndTargetIdentifier(testEvent, TARGET_IDENTIFIER)) + .isPresent(); + + // Complete publication + repository.update(publication.markCompleted()); + + assertThat(repository.findIncompletePublications()).isEmpty(); + } + + @Test // GH-3 + void shouldUpdateSingleEventPublication() { + + var testEvent1 = new TestEvent("id1"); + var testEvent2 = new TestEvent("id2"); + var serializedEvent1 = "{\"eventId\":\"id1\"}"; + var serializedEvent2 = "{\"eventId\":\"id2\"}"; + + when(serializer.serialize(testEvent1)).thenReturn(serializedEvent1); + when(serializer.deserialize(serializedEvent1, TestEvent.class)).thenReturn(testEvent1); + when(serializer.serialize(testEvent2)).thenReturn(serializedEvent2); + when(serializer.deserialize(serializedEvent2, TestEvent.class)).thenReturn(testEvent2); + + var publication1 = CompletableEventPublication.of(testEvent1, TARGET_IDENTIFIER); + var publication2 = CompletableEventPublication.of(testEvent2, TARGET_IDENTIFIER); + + // Store publication + repository.create(publication1); + repository.create(publication2); + + // Complete publication + repository.update(publication2.markCompleted()); + + assertThat(repository.findIncompletePublications()).hasSize(1) + .element(0).extracting(EventPublication::getEvent).isEqualTo(testEvent1); + } + + @Test // GH-3 + void shouldTolerateEmptyResult() { + + var testEvent = new TestEvent("id"); + var serializedEvent = "{\"eventId\":\"id\"}"; + + when(serializer.serialize(testEvent)).thenReturn(serializedEvent); + + assertThat(repository.findByEventAndTargetIdentifier(testEvent, TARGET_IDENTIFIER)).isEmpty(); + } + + @Test // GH-3 + void shouldReturnTheOldestEvent() throws Exception { + + var testEvent = new TestEvent("id"); + var serializedEvent = "{\"eventId\":\"id\"}"; + + when(serializer.serialize(testEvent)).thenReturn(serializedEvent); + when(serializer.deserialize(serializedEvent, TestEvent.class)).thenReturn(testEvent); + + var publicationOld = CompletableEventPublication.of(testEvent, TARGET_IDENTIFIER); + Thread.sleep(10); + var publicationNew = CompletableEventPublication.of(testEvent, TARGET_IDENTIFIER); + + repository.create(publicationNew); + repository.create(publicationOld); + + var actual = repository.findByEventAndTargetIdentifier(testEvent, TARGET_IDENTIFIER); + + assertThat(actual).hasValueSatisfying(it -> { + assertThat(it.getPublicationDate()).isEqualTo(publicationOld.getPublicationDate()); + }); + } + + @Test // GH-3 + void shouldSilentlyIgnoreNotSerializableEvents() { + + var testEvent = new TestEvent("id"); + var serializedEvent = "{\"eventId\":\"id\"}"; + + when(serializer.serialize(testEvent)).thenReturn(serializedEvent); + when(serializer.deserialize(serializedEvent, TestEvent.class)).thenReturn(testEvent); + + // Store publication + repository.create(CompletableEventPublication.of(testEvent, TARGET_IDENTIFIER)); + + operations.update("UPDATE EVENT_PUBLICATION SET EVENT_TYPE='abc'"); + + assertThat(repository.findByEventAndTargetIdentifier(testEvent, TARGET_IDENTIFIER)).isEmpty(); + } + } + + @Nested + @ActiveProfiles("hsqldb") + class HSQL extends TestBase {} + + @Nested + @ActiveProfiles("h2") + class H2 extends TestBase {} + + @Nested + @Disabled + @ActiveProfiles("postgres") + class Postgres extends TestBase {} + + @Value + private static final class TestEvent { + String eventId; + } +} diff --git a/spring-modulith-events/spring-modulith-events-jdbc/src/test/java/org/springframework/modulith/testapp/DatabaseSchemaInitializerTest.java b/spring-modulith-events/spring-modulith-events-jdbc/src/test/java/org/springframework/modulith/testapp/DatabaseSchemaInitializerTest.java deleted file mode 100644 index 90261003..00000000 --- a/spring-modulith-events/spring-modulith-events-jdbc/src/test/java/org/springframework/modulith/testapp/DatabaseSchemaInitializerTest.java +++ /dev/null @@ -1,143 +0,0 @@ -/* - * Copyright 2022 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.springframework.modulith.testapp; - -import org.junit.jupiter.api.Disabled; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.mockito.Mockito; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.data.jdbc.DataJdbcTest; -import org.springframework.boot.test.mock.mockito.SpyBean; -import org.springframework.context.annotation.Import; -import org.springframework.jdbc.core.JdbcTemplate; -import org.springframework.modulith.events.jdbc.DatabaseSchemaInitializer; -import org.springframework.test.context.ActiveProfiles; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.ArgumentMatchers.anyString; - -/** - * @author Dmitry Belyaev, Björn Kieling - */ -public class DatabaseSchemaInitializerTest { - - @Nested - @DataJdbcTest(properties = { - "spring.modulith.events.schema-initialization.enabled=true" - }) - @Import(DatabaseSchemaInitializer.class) - class InitializationEnabled { - - @Autowired - private JdbcTemplate jdbcTemplate; - - @Test - public void shouldCreateDatabaseSchemaOnStartUp() { - Long count = jdbcTemplate.queryForObject("SELECT COUNT(*) FROM EVENT_PUBLICATION", Long.class); - - assertThat(count).isEqualTo(0); - } - } - - @Nested - @DataJdbcTest(properties = { - "spring.modulith.events.schema-initialization.enabled=false" - }) - @Import(DatabaseSchemaInitializer.class) - class InitializationDisabled { - - @SpyBean - private JdbcTemplate jdbcTemplate; - - @Test - public void shouldNotCreateDatabaseSchemaOnStartUp() { - Mockito.verify(jdbcTemplate, Mockito.never()).execute(anyString()); - } - } - - @Nested - @DataJdbcTest - @Import(DatabaseSchemaInitializer.class) - class InitializationDisabledByDefault { - - @SpyBean - private JdbcTemplate jdbcTemplate; - - @Test - public void shouldNotCreateDatabaseSchemaOnStartUp() { - Mockito.verify(jdbcTemplate, Mockito.never()).execute(anyString()); - } - } - - @Nested - @DataJdbcTest(properties = { - "spring.modulith.events.schema-initialization.enabled=true" - }) - @ActiveProfiles("hsql") - @Import(DatabaseSchemaInitializer.class) - class InitializationUseHsql { - - @Autowired - private JdbcTemplate jdbcTemplate; - - @Test - public void shouldCreateDatabaseSchemaOnStartUp() { - Long count = jdbcTemplate.queryForObject("SELECT COUNT(*) FROM EVENT_PUBLICATION", Long.class); - - assertThat(count).isEqualTo(0); - } - } - - @Nested - @DataJdbcTest(properties = { - "spring.modulith.events.schema-initialization.enabled=true" - }) - @ActiveProfiles("h2") - @Import(DatabaseSchemaInitializer.class) - class InitializationUseH2 { - - @Autowired - private JdbcTemplate jdbcTemplate; - - @Test - public void shouldCreateDatabaseSchemaOnStartUp() { - Long count = jdbcTemplate.queryForObject("SELECT COUNT(*) FROM EVENT_PUBLICATION", Long.class); - - assertThat(count).isEqualTo(0); - } - } - - @Nested - @Disabled - @DataJdbcTest(properties = { - "spring.modulith.events.schema-initialization.enabled=true" - }) - @ActiveProfiles("postgres") - @Import(DatabaseSchemaInitializer.class) - class InitializationUsePostgres { - - @Autowired - private JdbcTemplate jdbcTemplate; - - @Test - public void shouldCreateDatabaseSchemaOnStartUp() { - Long count = jdbcTemplate.queryForObject("SELECT COUNT(*) FROM EVENT_PUBLICATION", Long.class); - - assertThat(count).isEqualTo(0); - } - } -} diff --git a/spring-modulith-events/spring-modulith-events-jdbc/src/test/java/org/springframework/modulith/testapp/JdbcEventPublicationRepositoryIntegrationTests.java b/spring-modulith-events/spring-modulith-events-jdbc/src/test/java/org/springframework/modulith/testapp/JdbcEventPublicationRepositoryIntegrationTests.java deleted file mode 100644 index 8a5e2c49..00000000 --- a/spring-modulith-events/spring-modulith-events-jdbc/src/test/java/org/springframework/modulith/testapp/JdbcEventPublicationRepositoryIntegrationTests.java +++ /dev/null @@ -1,319 +0,0 @@ -/* - * Copyright 2022 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.springframework.modulith.testapp; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -import java.util.List; -import java.util.Optional; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Disabled; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.data.jdbc.DataJdbcTest; -import org.springframework.context.annotation.Import; -import org.springframework.jdbc.core.JdbcTemplate; -import org.springframework.modulith.events.CompletableEventPublication; -import org.springframework.modulith.events.EventPublication; -import org.springframework.modulith.events.EventSerializer; -import org.springframework.modulith.events.PublicationTargetIdentifier; -import org.springframework.modulith.events.jdbc.DatabaseSchemaInitializer; -import org.springframework.modulith.events.jdbc.JdbcEventPublicationRepository; -import org.springframework.test.context.ActiveProfiles; - -/** - * @author Dmitry Belyaev, Björn Kieling - */ -class JdbcEventPublicationRepositoryIntegrationTests { - - private static final PublicationTargetIdentifier TARGET_IDENTIFIER = PublicationTargetIdentifier.of("listener"); - - private JdbcEventPublicationRepository repository; - - private final EventSerializer eventSerializer = mock(EventSerializer.class); - - private abstract class TestBase { - - @Autowired - protected JdbcTemplate jdbcTemplate; - - @BeforeEach - public void setUp() { - repository = new JdbcEventPublicationRepository(jdbcTemplate, eventSerializer); - } - } - - @Nested - @DataJdbcTest - @ActiveProfiles("hsql") - @Import(DatabaseSchemaInitializer.class) - class HSQL extends TestBase { - - @Nested - class CreateAndUpdate { - - @Test - void shouldPersistAndUpdateEventPublication() { - shouldPersistAndUpdateEventPublicationTest(); - } - - @Test - void shouldUpdateSingleEventPublication() { - shouldUpdateSingleEventPublicationTest(); - } - } - - @Nested - class FindByCompletionDateIsNull { - - @Test - void shouldSilentlyIgnoreNotSerializableEvents() { - shouldSilentlyIgnoreNotSerializableEventsTest(jdbcTemplate); - } - } - - @Nested - class FindBySerializedEventAndListenerId { - - @Test - void shouldTolerateEmptyResult() { - shouldTolerateEmptyResultTest(); - } - - @Test - void shouldReturnTheOldestEvent() throws InterruptedException { - shouldReturnTheOldestEventTest(); - } - - @Test - void shouldSilentlyIgnoreNotSerializableEvents() { - shouldSilentlyIgnoreNotSerializableEventsTest(jdbcTemplate); - } - } - } - - @Nested - @DataJdbcTest - @ActiveProfiles("h2") - @Import(DatabaseSchemaInitializer.class) - class H2 extends TestBase { - - @Nested - class CreateAndUpdate { - - @Test - void shouldPersistAndUpdateEventPublication() { - shouldPersistAndUpdateEventPublicationTest(); - } - - @Test - void shouldUpdateSingleEventPublication() { - shouldUpdateSingleEventPublicationTest(); - } - } - - @Nested - class FindByCompletionDateIsNull { - - @Test - void shouldSilentlyIgnoreNotSerializableEvents() { - shouldSilentlyIgnoreNotSerializableEventsTest(jdbcTemplate); - } - } - - @Nested - class FindBySerializedEventAndListenerId { - - @Test - void shouldTolerateEmptyResult() { - shouldTolerateEmptyResultTest(); - } - - @Test - void shouldReturnTheOldestEvent() throws InterruptedException { - shouldReturnTheOldestEventTest(); - } - - @Test - void shouldSilentlyIgnoreNotSerializableEvents() { - shouldSilentlyIgnoreNotSerializableEventsTest(jdbcTemplate); - } - } - } - - @Nested - @Disabled - @DataJdbcTest - @ActiveProfiles("postgres") - @Import(DatabaseSchemaInitializer.class) - class Postgres extends TestBase { - - @Nested - class CreateAndUpdate { - - @Test - void shouldPersistAndUpdateEventPublication() { - shouldPersistAndUpdateEventPublicationTest(); - } - - @Test - void shouldUpdateSingleEventPublication() { - shouldUpdateSingleEventPublicationTest(); - } - } - - @Nested - class FindByCompletionDateIsNull { - - @Test - void shouldSilentlyIgnoreNotSerializableEvents() { - shouldSilentlyIgnoreNotSerializableEventsTest(jdbcTemplate); - } - } - - @Nested - class FindBySerializedEventAndListenerId { - - @Test - void shouldTolerateEmptyResult() { - shouldTolerateEmptyResultTest(); - } - - @Test - void shouldReturnTheOldestEvent() throws InterruptedException { - shouldReturnTheOldestEventTest(); - } - - @Test - void shouldSilentlyIgnoreNotSerializableEvents() { - shouldSilentlyIgnoreNotSerializableEventsTest(jdbcTemplate); - } - } - } - - private void shouldPersistAndUpdateEventPublicationTest() { - TestEvent testEvent = new TestEvent("id"); - String serializedEvent = "{\"eventId\":\"id\"}"; - - when(eventSerializer.serialize(testEvent)).thenReturn(serializedEvent); - when(eventSerializer.deserialize(serializedEvent, TestEvent.class)).thenReturn(testEvent); - - CompletableEventPublication publication = CompletableEventPublication.of(testEvent, TARGET_IDENTIFIER); - - // Store publication - repository.create(publication); - - List eventPublications = repository.findByCompletionDateIsNull(); - assertThat(eventPublications).hasSize(1); - assertThat(eventPublications.get(0).getEvent()).isEqualTo(publication.getEvent()); - assertThat(eventPublications.get(0).getTargetIdentifier()).isEqualTo(publication.getTargetIdentifier()); - assertThat(repository.findByEventAndTargetIdentifier(testEvent, TARGET_IDENTIFIER)) - .isPresent(); - - // Complete publication - repository.updateCompletionDate(publication.markCompleted()); - - assertThat(repository.findByCompletionDateIsNull()).isEmpty(); - } - - private void shouldUpdateSingleEventPublicationTest() { - TestEvent testEvent1 = new TestEvent("id1"); - TestEvent testEvent2 = new TestEvent("id2"); - String serializedEvent1 = "{\"eventId\":\"id1\"}"; - String serializedEvent2 = "{\"eventId\":\"id2\"}"; - - when(eventSerializer.serialize(testEvent1)).thenReturn(serializedEvent1); - when(eventSerializer.deserialize(serializedEvent1, TestEvent.class)).thenReturn(testEvent1); - when(eventSerializer.serialize(testEvent2)).thenReturn(serializedEvent2); - when(eventSerializer.deserialize(serializedEvent2, TestEvent.class)).thenReturn(testEvent2); - - CompletableEventPublication publication1 = CompletableEventPublication.of(testEvent1, TARGET_IDENTIFIER); - CompletableEventPublication publication2 = CompletableEventPublication.of(testEvent2, TARGET_IDENTIFIER); - - // Store publication - repository.create(publication1); - repository.create(publication2); - - // Complete publication - repository.updateCompletionDate(publication2.markCompleted()); - - List withCompletionDateNull = repository.findByCompletionDateIsNull(); - assertThat(withCompletionDateNull).hasSize(1); - assertThat(withCompletionDateNull.get(0).getEvent()).isEqualTo(testEvent1); - } - - private void shouldTolerateEmptyResultTest() { - TestEvent testEvent = new TestEvent("id"); - String serializedEvent = "{\"eventId\":\"id\"}"; - when(eventSerializer.serialize(testEvent)).thenReturn(serializedEvent); - - Optional actual = - repository.findByEventAndTargetIdentifier(testEvent, TARGET_IDENTIFIER); - - assertThat(actual).isEmpty(); - } - - private void shouldReturnTheOldestEventTest() throws InterruptedException { - TestEvent testEvent = new TestEvent("id"); - String serializedEvent = "{\"eventId\":\"id\"}"; - when(eventSerializer.serialize(testEvent)).thenReturn(serializedEvent); - when(eventSerializer.deserialize(serializedEvent, TestEvent.class)).thenReturn(testEvent); - - CompletableEventPublication publicationOld = CompletableEventPublication.of(testEvent, TARGET_IDENTIFIER); - Thread.sleep(10); - CompletableEventPublication publicationNew = CompletableEventPublication.of(testEvent, TARGET_IDENTIFIER); - - repository.create(publicationNew); - repository.create(publicationOld); - - - Optional actual = - repository.findByEventAndTargetIdentifier(testEvent, TARGET_IDENTIFIER); - - assertThat(actual).isNotEmpty(); - assertThat(actual.get().getPublicationDate()).isEqualTo(publicationOld.getPublicationDate()); - } - - private void shouldSilentlyIgnoreNotSerializableEventsTest(JdbcTemplate jdbcTemplate) { - TestEvent testEvent = new TestEvent("id"); - String serializedEvent = "{\"eventId\":\"id\"}"; - when(eventSerializer.serialize(testEvent)).thenReturn(serializedEvent); - when(eventSerializer.deserialize(serializedEvent, TestEvent.class)).thenReturn(testEvent); - - CompletableEventPublication publication = CompletableEventPublication.of(testEvent, TARGET_IDENTIFIER); - - // Store publication - repository.create(publication); - jdbcTemplate.update("UPDATE EVENT_PUBLICATION SET EVENT_TYPE='abc'"); - - Optional actual = - repository.findByEventAndTargetIdentifier(testEvent, TARGET_IDENTIFIER); - - assertThat(actual).isEmpty(); - } - - private static final class TestEvent { - private final String eventId; - - private TestEvent(String eventId) { - this.eventId = eventId; - } - } -} diff --git a/spring-modulith-events/spring-modulith-events-jdbc/src/test/resources/application-hsql.properties b/spring-modulith-events/spring-modulith-events-jdbc/src/test/resources/application-hsqldb.properties similarity index 70% rename from spring-modulith-events/spring-modulith-events-jdbc/src/test/resources/application-hsql.properties rename to spring-modulith-events/spring-modulith-events-jdbc/src/test/resources/application-hsqldb.properties index f7bdc38c..ee56e637 100644 --- a/spring-modulith-events/spring-modulith-events-jdbc/src/test/resources/application-hsql.properties +++ b/spring-modulith-events/spring-modulith-events-jdbc/src/test/resources/application-hsqldb.properties @@ -1,5 +1,4 @@ spring.datasource.driverClassName=org.hsqldb.jdbc.JDBCDriver -spring.datasource.url=jdbc:hsqldb:mem:testdb;DB_CLOSE_DELAY=-1 spring.test.database.replace=NONE spring.modulith.events.schema-initialization.enabled=true diff --git a/spring-modulith-events/spring-modulith-events-jpa/src/main/java/org/springframework/modulith/events/jpa/JpaEventPublication.java b/spring-modulith-events/spring-modulith-events-jpa/src/main/java/org/springframework/modulith/events/jpa/JpaEventPublication.java index 11d5f8df..62bc888a 100644 --- a/spring-modulith-events/spring-modulith-events-jpa/src/main/java/org/springframework/modulith/events/jpa/JpaEventPublication.java +++ b/spring-modulith-events/spring-modulith-events-jpa/src/main/java/org/springframework/modulith/events/jpa/JpaEventPublication.java @@ -15,26 +15,29 @@ */ package org.springframework.modulith.events.jpa; -import java.time.Instant; -import java.util.UUID; - import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.Id; - import lombok.AccessLevel; -import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; +import lombok.RequiredArgsConstructor; + +import java.time.Instant; +import java.util.UUID; /** - * @author Oliver Drotbohm, Dmitry Belyaev, Björn Kieling + * JPA entity to represent event publications. + * + * @author Oliver Drotbohm + * @author Dmitry Belyaev + * @author Björn Kieling */ @Data @Entity @NoArgsConstructor(force = true) -@AllArgsConstructor(access = AccessLevel.PRIVATE) +@RequiredArgsConstructor(access = AccessLevel.PRIVATE) class JpaEventPublication { private final @Id @Column(length = 16) UUID id; @@ -46,10 +49,10 @@ class JpaEventPublication { private Instant completionDate; @Builder - static JpaEventPublication of(UUID id, Instant publicationDate, String listenerId, Object serializedEvent, - Class eventType, Instant completionDate) { - return new JpaEventPublication(id, publicationDate, listenerId, serializedEvent.toString(), eventType, - completionDate); + static JpaEventPublication of(Instant publicationDate, String listenerId, Object serializedEvent, + Class eventType) { + return new JpaEventPublication(UUID.randomUUID(), publicationDate, listenerId, serializedEvent.toString(), + eventType); } JpaEventPublication markCompleted() { diff --git a/spring-modulith-events/spring-modulith-events-jpa/src/main/java/org/springframework/modulith/events/jpa/JpaEventPublicationConfiguration.java b/spring-modulith-events/spring-modulith-events-jpa/src/main/java/org/springframework/modulith/events/jpa/JpaEventPublicationConfiguration.java index 363448b0..bc449e88 100644 --- a/spring-modulith-events/spring-modulith-events-jpa/src/main/java/org/springframework/modulith/events/jpa/JpaEventPublicationConfiguration.java +++ b/spring-modulith-events/spring-modulith-events-jpa/src/main/java/org/springframework/modulith/events/jpa/JpaEventPublicationConfiguration.java @@ -16,25 +16,24 @@ 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; import org.springframework.modulith.events.EventSerializer; import org.springframework.modulith.events.config.EventPublicationConfigurationExtension; -import lombok.RequiredArgsConstructor; - /** - * @author Oliver Drotbohm, Dmitry Belyaev, Björn Kieling + * @author Oliver Drotbohm + * @author Dmitry Belyaev + * @author Björn Kieling */ @Configuration(proxyBeanMethods = false) @RequiredArgsConstructor class JpaEventPublicationConfiguration implements EventPublicationConfigurationExtension { @Bean - public JpaEventPublicationRepository jpaEventPublicationRepository(EntityManager em, EventSerializer serializer) { - // TODO Why do we want to instantiate the serializer here and what - // happens if no serializer is available? + JpaEventPublicationRepository jpaEventPublicationRepository(EntityManager em, EventSerializer serializer) { return new JpaEventPublicationRepository(em, serializer); } } diff --git a/spring-modulith-events/spring-modulith-events-jpa/src/main/java/org/springframework/modulith/events/jpa/JpaEventPublicationRepository.java b/spring-modulith-events/spring-modulith-events-jpa/src/main/java/org/springframework/modulith/events/jpa/JpaEventPublicationRepository.java index 5a8b2f68..0b7602d4 100644 --- a/spring-modulith-events/spring-modulith-events-jpa/src/main/java/org/springframework/modulith/events/jpa/JpaEventPublicationRepository.java +++ b/spring-modulith-events/spring-modulith-events-jpa/src/main/java/org/springframework/modulith/events/jpa/JpaEventPublicationRepository.java @@ -15,14 +15,13 @@ */ 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.Optional; -import java.util.UUID; -import java.util.stream.Collectors; - -import jakarta.persistence.EntityManager; -import jakarta.persistence.TypedQuery; import org.springframework.modulith.events.CompletableEventPublication; import org.springframework.modulith.events.EventPublication; @@ -31,17 +30,19 @@ import org.springframework.modulith.events.EventSerializer; import org.springframework.modulith.events.PublicationTargetIdentifier; import org.springframework.transaction.annotation.Transactional; -import lombok.EqualsAndHashCode; -import lombok.RequiredArgsConstructor; - /** * Repository to store {@link EventPublication}s. * - * @author Oliver Drotbohm, Dmitry Belyaev, Björn Kieling + * @author Oliver Drotbohm + * @author Dmitry Belyaev + * @author Björn Kieling */ @RequiredArgsConstructor public class JpaEventPublicationRepository implements EventPublicationRepository { + private static String BY_EVENT_AND_LISTENER_ID = "select p from JpaEventPublication p where p.serializedEvent = ?1 and p.listenerId = ?2"; + private static String INCOMPLETE = "select p from JpaEventPublication p where p.completionDate is null"; + private final EntityManager entityManager; private final EventSerializer serializer; @@ -50,29 +51,31 @@ public class JpaEventPublicationRepository implements EventPublicationRepository public EventPublication create(EventPublication publication) { entityManager.persist(domainToEntity(publication)); + return publication; } @Override @Transactional - public EventPublication updateCompletionDate(CompletableEventPublication publication) { + public EventPublication update(CompletableEventPublication publication) { + + var id = publication.getTargetIdentifier().getValue(); + var event = publication.getEvent(); + + findEntityBySerializedEventAndListenerId(event, id) // + .setCompletionDate(publication.getCompletionDate().orElse(null)); - findEntityBySerializedEventAndListenerId(publication.getEvent(), - publication.getTargetIdentifier().getValue()).ifPresent(entity -> { - entity.setCompletionDate(publication.getCompletionDate().orElse(null)); - entityManager.flush(); - }); return publication; } @Override @Transactional(readOnly = true) - public List findByCompletionDateIsNull() { + public List findIncompletePublications() { - String query = "select p from JpaEventPublication p where p.completionDate is null"; - - return entityManager.createQuery(query, JpaEventPublication.class).getResultList().stream() - .map(this::entityToDomain).collect(Collectors.toList()); + return entityManager.createQuery(INCOMPLETE, JpaEventPublication.class) + .getResultStream() + .map(this::entityToDomain) + .toList(); } @Override @@ -80,17 +83,19 @@ public class JpaEventPublicationRepository implements EventPublicationRepository public Optional findByEventAndTargetIdentifier(Object event, PublicationTargetIdentifier targetIdentifier) { - Optional result = findEntityBySerializedEventAndListenerId(event, targetIdentifier.getValue()); - return result.map(this::entityToDomain); + return Optional.ofNullable(findEntityBySerializedEventAndListenerId(event, targetIdentifier.getValue())) + .map(this::entityToDomain); } - private Optional findEntityBySerializedEventAndListenerId(Object event, String listenerId) { - String query = "select p from JpaEventPublication p where p.serializedEvent = ?1 and p.listenerId = ?2"; - String serializedEvent = serializeEvent(event); - TypedQuery typedQuery = entityManager.createQuery(query, JpaEventPublication.class) - .setParameter(1, serializedEvent).setParameter(2, listenerId); - JpaEventPublication resultEntity = typedQuery.getSingleResult(); - return Optional.ofNullable(resultEntity); + private JpaEventPublication findEntityBySerializedEventAndListenerId(Object event, String listenerId) { + + var serializedEvent = serializeEvent(event); + + var query = entityManager.createQuery(BY_EVENT_AND_LISTENER_ID, JpaEventPublication.class) + .setParameter(1, serializedEvent) + .setParameter(2, listenerId); + + return query.getSingleResult(); } private String serializeEvent(Object event) { @@ -98,13 +103,16 @@ public class JpaEventPublicationRepository implements EventPublicationRepository } private JpaEventPublication domainToEntity(EventPublication domain) { - String serializedEvent = serializeEvent(domain.getEvent()); - return JpaEventPublication.builder().id(UUID.randomUUID()).publicationDate(domain.getPublicationDate()) - .listenerId(domain.getTargetIdentifier().getValue()).serializedEvent(serializedEvent) - .eventType(domain.getEvent().getClass()).build(); + + return JpaEventPublication.builder() // + .publicationDate(domain.getPublicationDate()) // + .listenerId(domain.getTargetIdentifier().getValue()) // + .serializedEvent(serializeEvent(domain.getEvent())) // + .eventType(domain.getEvent().getClass()) // + .build(); } - private CompletableEventPublication entityToDomain(JpaEventPublication entity) { + private EventPublication entityToDomain(JpaEventPublication entity) { return JpaEventPublicationAdapter.of(entity, serializer); } diff --git a/spring-modulith-events/spring-modulith-events-jpa/src/test/java/org/springframework/modulith/events/jpa/JpaEventPublicationConfigurationIntegrationTests.java b/spring-modulith-events/spring-modulith-events-jpa/src/test/java/org/springframework/modulith/events/jpa/JpaEventPublicationConfigurationIntegrationTests.java index 9aa98e93..e29490d5 100644 --- a/spring-modulith-events/spring-modulith-events-jpa/src/test/java/org/springframework/modulith/events/jpa/JpaEventPublicationConfigurationIntegrationTests.java +++ b/spring-modulith-events/spring-modulith-events-jpa/src/test/java/org/springframework/modulith/events/jpa/JpaEventPublicationConfigurationIntegrationTests.java @@ -24,13 +24,15 @@ import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.context.ApplicationContext; -import org.springframework.modulith.events.EventSerializer; import org.springframework.modulith.events.EventPublicationRegistry; +import org.springframework.modulith.events.EventSerializer; import org.springframework.test.context.TestConstructor; import org.springframework.test.context.TestConstructor.AutowireMode; /** - * @author Oliver Drotbohm, Dmitry Belyaev, Björn Kieling + * @author Oliver Drotbohm + * @author Dmitry Belyaev + * @author Björn Kieling */ @SpringBootTest(classes = ExampleApplication.class) @TestConstructor(autowireMode = AutowireMode.ALL) @@ -39,8 +41,7 @@ class JpaEventPublicationConfigurationIntegrationTests { private final ApplicationContext context; - @MockBean - private EventSerializer serializer; + @MockBean EventSerializer serializer; @Test void bootstrapsApplicationComponents() { diff --git a/spring-modulith-events/spring-modulith-events-jpa/src/test/java/org/springframework/modulith/events/jpa/JpaEventPublicationRepositoryIntegrationTests.java b/spring-modulith-events/spring-modulith-events-jpa/src/test/java/org/springframework/modulith/events/jpa/JpaEventPublicationRepositoryIntegrationTests.java index f56d33fb..8be138d5 100644 --- a/spring-modulith-events/spring-modulith-events-jpa/src/test/java/org/springframework/modulith/events/jpa/JpaEventPublicationRepositoryIntegrationTests.java +++ b/spring-modulith-events/spring-modulith-events-jpa/src/test/java/org/springframework/modulith/events/jpa/JpaEventPublicationRepositoryIntegrationTests.java @@ -15,14 +15,15 @@ */ package org.springframework.modulith.events.jpa; -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -import java.util.List; +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; import jakarta.persistence.EntityManager; import jakarta.persistence.EntityManagerFactory; +import lombok.RequiredArgsConstructor; +import lombok.Value; + +import java.util.List; import javax.sql.DataSource; @@ -48,10 +49,10 @@ import org.springframework.test.context.TestConstructor.AutowireMode; import org.springframework.test.context.junit.jupiter.SpringExtension; import org.springframework.transaction.annotation.Transactional; -import lombok.RequiredArgsConstructor; - /** - * @author Oliver Drotbohm, Dmitry Belyaev, Björn Kieling + * @author Oliver Drotbohm + * @author Dmitry Belyaev + * @author Björn Kieling */ @ExtendWith(SpringExtension.class) @TestConstructor(autowireMode = AutowireMode.ALL) @@ -122,23 +123,20 @@ class JpaEventPublicationRepositoryIntegrationTests { // Store publication repository.create(publication); - List eventPublications = repository.findByCompletionDateIsNull(); + List eventPublications = repository.findIncompletePublications(); assertThat(eventPublications).hasSize(1); assertThat(eventPublications.get(0).getEvent()).isEqualTo(publication.getEvent()); assertThat(eventPublications.get(0).getTargetIdentifier()).isEqualTo(publication.getTargetIdentifier()); assertThat(repository.findByEventAndTargetIdentifier(testEvent, TARGET_IDENTIFIER)).isPresent(); // Complete publication - repository.updateCompletionDate(publication.markCompleted()); + repository.update(publication.markCompleted()); - assertThat(repository.findByCompletionDateIsNull()).isEmpty(); + assertThat(repository.findIncompletePublications()).isEmpty(); } + @Value private static final class TestEvent { - private final String eventId; - - private TestEvent(String eventId) { - this.eventId = eventId; - } + String eventId; } } diff --git a/spring-modulith-events/spring-modulith-events-tests/pom.xml b/spring-modulith-events/spring-modulith-events-tests/pom.xml index 3b39f393..07541650 100644 --- a/spring-modulith-events/spring-modulith-events-tests/pom.xml +++ b/spring-modulith-events/spring-modulith-events-tests/pom.xml @@ -36,7 +36,6 @@ org.hibernate.orm hibernate-core - 6.1.1.Final @@ -49,6 +48,7 @@ hsqldb test + - \ No newline at end of file +