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 +