diff --git a/src/main/java/org/springframework/data/gemfire/CacheFactoryBean.java b/src/main/java/org/springframework/data/gemfire/CacheFactoryBean.java index 6e9f1542..065a02c9 100644 --- a/src/main/java/org/springframework/data/gemfire/CacheFactoryBean.java +++ b/src/main/java/org/springframework/data/gemfire/CacheFactoryBean.java @@ -885,7 +885,7 @@ public class CacheFactoryBean extends AbstractFactoryBeanSupport * @return the copyOnRead */ public Boolean getCopyOnRead() { - return copyOnRead; + return this.copyOnRead; } /** @@ -901,7 +901,7 @@ public class CacheFactoryBean extends AbstractFactoryBeanSupport * @return the criticalHeapPercentage */ public Float getCriticalHeapPercentage() { - return criticalHeapPercentage; + return this.criticalHeapPercentage; } /** @@ -956,7 +956,7 @@ public class CacheFactoryBean extends AbstractFactoryBeanSupport * @return the evictionHeapPercentage */ public Float getEvictionHeapPercentage() { - return evictionHeapPercentage; + return this.evictionHeapPercentage; } /** @@ -989,7 +989,7 @@ public class CacheFactoryBean extends AbstractFactoryBeanSupport * @return the gatewayConflictResolver */ public GatewayConflictResolver getGatewayConflictResolver() { - return gatewayConflictResolver; + return this.gatewayConflictResolver; } /** @@ -1003,7 +1003,7 @@ public class CacheFactoryBean extends AbstractFactoryBeanSupport * @return the list of configured JndiDataSources. */ public List getJndiDataSources() { - return jndiDataSources; + return this.jndiDataSources; } /** @@ -1019,7 +1019,7 @@ public class CacheFactoryBean extends AbstractFactoryBeanSupport * @return the lockLease */ public Integer getLockLease() { - return lockLease; + return this.lockLease; } /** @@ -1035,7 +1035,7 @@ public class CacheFactoryBean extends AbstractFactoryBeanSupport * @return the lockTimeout */ public Integer getLockTimeout() { - return lockTimeout; + return this.lockTimeout; } /** @@ -1054,7 +1054,7 @@ public class CacheFactoryBean extends AbstractFactoryBeanSupport * @return the messageSyncInterval */ public Integer getMessageSyncInterval() { - return messageSyncInterval; + return this.messageSyncInterval; } /** @@ -1094,7 +1094,7 @@ public class CacheFactoryBean extends AbstractFactoryBeanSupport * @return the pdxDiskStoreName */ public String getPdxDiskStoreName() { - return pdxDiskStoreName; + return this.pdxDiskStoreName; } /** @@ -1111,7 +1111,7 @@ public class CacheFactoryBean extends AbstractFactoryBeanSupport * @return the pdxIgnoreUnreadFields */ public Boolean getPdxIgnoreUnreadFields() { - return pdxIgnoreUnreadFields; + return this.pdxIgnoreUnreadFields; } /** @@ -1127,7 +1127,7 @@ public class CacheFactoryBean extends AbstractFactoryBeanSupport * @return the pdxPersistent */ public Boolean getPdxPersistent() { - return pdxPersistent; + return this.pdxPersistent; } /** @@ -1144,7 +1144,7 @@ public class CacheFactoryBean extends AbstractFactoryBeanSupport * @return the pdxReadSerialized */ public Boolean getPdxReadSerialized() { - return pdxReadSerialized; + return this.pdxReadSerialized; } /** @@ -1162,7 +1162,7 @@ public class CacheFactoryBean extends AbstractFactoryBeanSupport * @return the pdxSerializer */ public PdxSerializer getPdxSerializer() { - return pdxSerializer; + return this.pdxSerializer; } /** @@ -1203,7 +1203,7 @@ public class CacheFactoryBean extends AbstractFactoryBeanSupport * @return the searchTimeout */ public Integer getSearchTimeout() { - return searchTimeout; + return this.searchTimeout; } /** @@ -1223,7 +1223,7 @@ public class CacheFactoryBean extends AbstractFactoryBeanSupport * @see org.apache.geode.security.SecurityManager */ public SecurityManager getSecurityManager() { - return securityManager; + return this.securityManager; } /** @@ -1241,14 +1241,14 @@ public class CacheFactoryBean extends AbstractFactoryBeanSupport * @return the transactionListeners */ public List getTransactionListeners() { - return transactionListeners; + return this.transactionListeners; } /** - * Sets the TransactionWriter used to configure the Cache for handling transaction events, such as to veto + * Sets the {@link TransactionWriter} used to configure the cache for handling transaction events, such as to veto * the transaction or update an external DB before the commit. * - * @param transactionWriter the Pivotal GemFire TransactionWriter callback receiving transaction events. + * @param transactionWriter configured {@link TransactionWriter} callback receiving transaction events. * @see org.apache.geode.cache.TransactionWriter */ public void setTransactionWriter(TransactionWriter transactionWriter) { @@ -1256,10 +1256,13 @@ public class CacheFactoryBean extends AbstractFactoryBeanSupport } /** - * @return the transactionWriter + * Return the configured {@link TransactionWriter} used to process and handle transaction events. + * + * @return the configured {@link TransactionWriter}. + * @see org.apache.geode.cache.TransactionWriter */ public TransactionWriter getTransactionWriter() { - return transactionWriter; + return this.transactionWriter; } /** diff --git a/src/main/java/org/springframework/data/gemfire/transaction/GemfireTransactionManager.java b/src/main/java/org/springframework/data/gemfire/transaction/GemfireTransactionManager.java index 1fdbf172..033d722e 100644 --- a/src/main/java/org/springframework/data/gemfire/transaction/GemfireTransactionManager.java +++ b/src/main/java/org/springframework/data/gemfire/transaction/GemfireTransactionManager.java @@ -187,7 +187,8 @@ public class GemfireTransactionManager extends AbstractPlatformTransactionManage } catch (Exception cause) { throw new NoTransactionException( - "No transaction is associated with the current thread; are multiple transaction managers present?", cause); + "No transaction is associated with the current thread. Are multiple transaction managers present?", + cause); } } @@ -274,7 +275,8 @@ public class GemfireTransactionManager extends AbstractPlatformTransactionManage /** * Sets a reference to the {@link GemFireCache} for which this transaction manager - * will manage local cache transactions. + * manages local cache transactions. + * * @param cache reference to the {@link GemFireCache}. * @see org.apache.geode.cache.GemFireCache */ @@ -284,7 +286,7 @@ public class GemfireTransactionManager extends AbstractPlatformTransactionManage /** * Returns a reference to the {@link GemFireCache} for which this transaction manager - * will manage local cache transactions. + * manages local cache transactions. * * @return a reference to the {@link GemFireCache}. * @see org.apache.geode.cache.GemFireCache @@ -293,6 +295,14 @@ public class GemfireTransactionManager extends AbstractPlatformTransactionManage return this.cache; } + /** + * Returns a reference to the {@link CacheTransactionManager} used by Apache Geode to manage local, + * cache transactions. + * + * @return a reference to the {@link CacheTransactionManager}. + * @see org.apache.geode.cache.CacheTransactionManager + * @see #getCache() + */ protected CacheTransactionManager getCacheTransactionManager() { return getCache().getCacheTransactionManager(); } diff --git a/src/main/java/org/springframework/data/gemfire/transaction/config/EnableGemfireCacheTransactions.java b/src/main/java/org/springframework/data/gemfire/transaction/config/EnableGemfireCacheTransactions.java index c157f98f..a4cd4447 100644 --- a/src/main/java/org/springframework/data/gemfire/transaction/config/EnableGemfireCacheTransactions.java +++ b/src/main/java/org/springframework/data/gemfire/transaction/config/EnableGemfireCacheTransactions.java @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package org.springframework.data.gemfire.transaction.config; import java.lang.annotation.Documented; @@ -24,6 +23,7 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import org.springframework.context.annotation.Import; +import org.springframework.data.gemfire.transaction.event.TransactionApplicationEvent; /** * The {@link EnableGemfireCacheTransactions} annotation enables Pivotal GemFire or Apache Geode Cache Transactions @@ -49,4 +49,12 @@ import org.springframework.context.annotation.Import; @SuppressWarnings("unused") public @interface EnableGemfireCacheTransactions { + /** + * Configures whether {@link TransactionApplicationEvent} objects are automatically fired by the framework. + * + * @return a boolean value indicating whether transactional events are automatically fired by the framework + * without the need to manually publish transaction events. Defaults to {@literal false}. + */ + boolean enableAutoTransactionEventPublishing() default false; + } diff --git a/src/main/java/org/springframework/data/gemfire/transaction/config/GemfireCacheTransactionsConfiguration.java b/src/main/java/org/springframework/data/gemfire/transaction/config/GemfireCacheTransactionsConfiguration.java index 784c585f..b9805db1 100644 --- a/src/main/java/org/springframework/data/gemfire/transaction/config/GemfireCacheTransactionsConfiguration.java +++ b/src/main/java/org/springframework/data/gemfire/transaction/config/GemfireCacheTransactionsConfiguration.java @@ -13,14 +13,31 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package org.springframework.data.gemfire.transaction.config; -import org.apache.geode.cache.GemFireCache; +import java.lang.annotation.Annotation; +import java.util.ArrayList; +import java.util.List; +import org.apache.geode.cache.GemFireCache; +import org.apache.geode.cache.TransactionListener; +import org.apache.geode.cache.TransactionWriter; + +import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.ImportAware; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.AnnotationAttributes; +import org.springframework.core.annotation.Order; +import org.springframework.core.type.AnnotationMetadata; +import org.springframework.data.gemfire.CacheFactoryBean; +import org.springframework.data.gemfire.config.annotation.ClientCacheConfigurer; +import org.springframework.data.gemfire.config.annotation.PeerCacheConfigurer; +import org.springframework.data.gemfire.config.annotation.support.AbstractAnnotationConfigSupport; import org.springframework.data.gemfire.transaction.GemfireTransactionManager; +import org.springframework.data.gemfire.transaction.event.ComposableTransactionWriter; +import org.springframework.data.gemfire.transaction.event.TransactionListenerAdapter; import org.springframework.transaction.annotation.EnableTransactionManagement; /** @@ -30,16 +47,47 @@ import org.springframework.transaction.annotation.EnableTransactionManagement; * * @author John Blum * @see org.apache.geode.cache.GemFireCache + * @see org.apache.geode.cache.TransactionListener + * @see org.apache.geode.cache.TransactionWriter + * @see org.springframework.context.ApplicationEventPublisher * @see org.springframework.context.annotation.Bean * @see org.springframework.context.annotation.Configuration + * @see org.springframework.context.annotation.ImportAware + * @see org.springframework.core.annotation.AnnotationAttributes + * @see org.springframework.core.type.AnnotatedTypeMetadata + * @see org.springframework.data.gemfire.CacheFactoryBean + * @see org.springframework.data.gemfire.config.annotation.ClientCacheConfigurer + * @see org.springframework.data.gemfire.config.annotation.PeerCacheConfigurer + * @see org.springframework.data.gemfire.config.annotation.support.AbstractAnnotationConfigSupport * @see org.springframework.data.gemfire.transaction.GemfireTransactionManager + * @see org.springframework.data.gemfire.transaction.event.ComposableTransactionWriter + * @see org.springframework.data.gemfire.transaction.event.TransactionListenerAdapter * @see org.springframework.transaction.annotation.EnableTransactionManagement * @since 2.0.0 */ @Configuration @EnableTransactionManagement @SuppressWarnings("unused") -public class GemfireCacheTransactionsConfiguration { +public class GemfireCacheTransactionsConfiguration extends AbstractAnnotationConfigSupport implements ImportAware { + + private volatile boolean enableAutoTransactionEventPublishing; + + @Override + protected Class getAnnotationType() { + return EnableGemfireCacheTransactions.class; + } + + @Override + public void setImportMetadata(AnnotationMetadata annotationMetadata) { + + if (isAnnotationPresent(annotationMetadata)) { + + AnnotationAttributes enableGemfireCacheTransactionsAttributes = getAnnotationAttributes(annotationMetadata); + + this.enableAutoTransactionEventPublishing = + enableGemfireCacheTransactionsAttributes.getBoolean("enableAutoTransactionEventPublishing"); + } + } /** * Declares and registers SDG's {@link GemfireTransactionManager} as the {@literal transactionManager} @@ -54,4 +102,67 @@ public class GemfireCacheTransactionsConfiguration { public GemfireTransactionManager transactionManager(GemFireCache gemfireCache) { return new GemfireTransactionManager(gemfireCache); } + + @Bean + @Order(Ordered.LOWEST_PRECEDENCE) + public ClientCacheConfigurer registerTransactionListenerAdapterClientCacheConfigurer( + ApplicationEventPublisher applicationEventPublisher) { + + return (beanName, bean) -> { + + TransactionListenerAdapter transactionListener = newTransactionListenerAdapter(applicationEventPublisher); + + registerGemFireCacheTransactionEventHandlers(bean, transactionListener); + }; + } + + @Bean + @Order(Ordered.LOWEST_PRECEDENCE) + public PeerCacheConfigurer registerTransactionListenerAdapterPeerCacheConfigurer( + ApplicationEventPublisher applicationEventPublisher) { + + return (beanName, bean) -> { + + TransactionListenerAdapter transactionListener = newTransactionListenerAdapter(applicationEventPublisher); + + registerGemFireCacheTransactionEventHandlers(bean, transactionListener); + }; + } + + private TransactionListenerAdapter newTransactionListenerAdapter( + ApplicationEventPublisher applicationEventPublisher) { + + return new TransactionListenerAdapter(applicationEventPublisher); + } + + protected void registerGemFireCacheTransactionEventHandlers(CacheFactoryBean cacheFactoryBean, + TransactionListenerAdapter transactionListener) { + + if (this.enableAutoTransactionEventPublishing) { + registerGemFireCacheTransactionListener(cacheFactoryBean, transactionListener); + registerGemFireCacheTransactionWriter(cacheFactoryBean, transactionListener); + } + } + + private void registerGemFireCacheTransactionListener(CacheFactoryBean bean, + TransactionListener transactionListener) { + + List transactionListeners = new ArrayList<>(bean.getTransactionListeners()); + + transactionListeners.add(transactionListener); + + bean.setTransactionListeners(transactionListeners); + } + + private void registerGemFireCacheTransactionWriter(CacheFactoryBean bean, + TransactionWriter transactionWriter) { + + TransactionWriter existingTransactionWriter = bean.getTransactionWriter(); + + TransactionWriter compositeTransactionWriter = + ComposableTransactionWriter.compose(existingTransactionWriter, transactionWriter); + + bean.setTransactionWriter(compositeTransactionWriter); + } + } diff --git a/src/main/java/org/springframework/data/gemfire/transaction/event/ComposableTransactionWriter.java b/src/main/java/org/springframework/data/gemfire/transaction/event/ComposableTransactionWriter.java new file mode 100644 index 00000000..a4bd30dd --- /dev/null +++ b/src/main/java/org/springframework/data/gemfire/transaction/event/ComposableTransactionWriter.java @@ -0,0 +1,107 @@ +/* + * Copyright 2019 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.data.gemfire.transaction.event; + +import java.util.Properties; + +import org.apache.geode.cache.Cache; +import org.apache.geode.cache.TransactionEvent; +import org.apache.geode.cache.TransactionWriter; +import org.apache.geode.cache.TransactionWriterException; + +import org.springframework.util.Assert; + +/** + * An implementation of Apache Geode's {@link TransactionWriter} interface that uses the {@literal Composite Software Design} + * Pattern to compose multiple {@link TransactionWriter} objects into a single instance. + * + * @author John Blum + * @see org.apache.geode.cache.TransactionWriter + * @since 2.3.0 + */ +public class ComposableTransactionWriter implements TransactionWriter { + + /** + * Factory method used to construct and compose 2 {@link TransactionWriter} objects into a composite instance of + * {@link TransactionWriter} functioning as a single instance. + * + * @param transactionWriterOne first {@link TransactionWriter} in the composition. + * @param transactionWriterTwo second {@link TransactionWriter} in the composition. + * @return the first {@link TransactionWriter} if the second {@link TransactionWriter} is {@literal null}, or return + * the second {@link TransactionWriter} if the first {@link TransactionWriter} is {@literal null}, or return + * the composition of both {@link TransactionWriter} one and {@link TransactionWriter} two. + * @see org.apache.geode.cache.TransactionWriter + */ + public static TransactionWriter compose(TransactionWriter transactionWriterOne, + TransactionWriter transactionWriterTwo) { + + return transactionWriterOne == null ? transactionWriterTwo + : transactionWriterTwo == null ? transactionWriterOne + : new ComposableTransactionWriter(transactionWriterOne, transactionWriterTwo); + } + + private final TransactionWriter transactionWriterOne; + private final TransactionWriter transactionWriterTwo; + + private ComposableTransactionWriter(TransactionWriter transactionWriterOne, TransactionWriter transactionWriterTwo) { + + Assert.notNull(transactionWriterOne, "TransactionWriter one must not be null"); + Assert.notNull(transactionWriterTwo, "TransactionWriter two must not be null"); + + this.transactionWriterOne = transactionWriterOne; + this.transactionWriterTwo = transactionWriterTwo; + } + + /** + * @inheritDoc + */ + @Override + public void beforeCommit(TransactionEvent event) throws TransactionWriterException { + + this.transactionWriterOne.beforeCommit(event); + this.transactionWriterTwo.beforeCommit(event); + } + + /** + * @inheritDoc + */ + @Override + public void close() { + + this.transactionWriterOne.close(); + this.transactionWriterTwo.close(); + } + + /** + * @inheritDoc + */ + @Override + public void init(Properties properties) { + + this.transactionWriterOne.init(properties); + this.transactionWriterTwo.init(properties); + } + + /** + * @inheritDoc + */ + @Override + public void initialize(Cache cache, Properties properties) { + + this.transactionWriterOne.initialize(cache, properties); + this.transactionWriterTwo.initialize(cache, properties); + } +} diff --git a/src/main/java/org/springframework/data/gemfire/transaction/event/TransactionApplicationEvent.java b/src/main/java/org/springframework/data/gemfire/transaction/event/TransactionApplicationEvent.java index cf664b0a..1f55daa4 100644 --- a/src/main/java/org/springframework/data/gemfire/transaction/event/TransactionApplicationEvent.java +++ b/src/main/java/org/springframework/data/gemfire/transaction/event/TransactionApplicationEvent.java @@ -37,12 +37,37 @@ public class TransactionApplicationEvent extends ApplicationEvent { protected static final String TIMESTAMP_PATTERN = "yyyy-MM-dd-hh:mm:ss.S"; + /** + * Factory method used to construct a new instance of {@link TransactionApplicationEvent} initialized with + * the given {@link Object source}. + * + * @param source {@link Object} defined as the source of this {@link TransactionApplicationEvent}. + * @return a new instance of {@link TransactionApplicationEvent}. + * @see #TransactionApplicationEvent(Object) + */ + public static TransactionApplicationEvent of(Object source) { + return new TransactionApplicationEvent(source); + } + private String details; + /** + * Constructs a new instance of {@link TransactionApplicationEvent} initialized with the given {@link Object source} + * of this transaction event. + * + * @param source {@link Object} defined as the source of this {@link TransactionApplicationEvent}. + */ public TransactionApplicationEvent(Object source) { this(source, null); } + /** + * Constructs a new instance of {@link TransactionApplicationEvent} initialized with the given {@link Object source} + * of this transaction event and {@link String details} describing the transaction event. + * + * @param source {@link Object} defined as the source of this {@link TransactionApplicationEvent}. + * @param details {@link String} describing the transaction event. + */ public TransactionApplicationEvent(Object source, String details) { super(source); @@ -73,4 +98,9 @@ public class TransactionApplicationEvent extends ApplicationEvent { .map(details -> String.format("%s - %s", getTimestampAsString(), details)) .orElse(String.format("%s[%s]", getClass().getSimpleName(), getTimestampAsString())); } + + public TransactionApplicationEvent with(String details) { + this.details = details; + return this; + } } diff --git a/src/main/java/org/springframework/data/gemfire/transaction/event/TransactionListenerAdapter.java b/src/main/java/org/springframework/data/gemfire/transaction/event/TransactionListenerAdapter.java new file mode 100644 index 00000000..14ca101d --- /dev/null +++ b/src/main/java/org/springframework/data/gemfire/transaction/event/TransactionListenerAdapter.java @@ -0,0 +1,95 @@ +/* + * Copyright 2019 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.data.gemfire.transaction.event; + +import org.apache.geode.cache.TransactionEvent; +import org.apache.geode.cache.TransactionListener; +import org.apache.geode.cache.TransactionWriter; + +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.util.Assert; + +/** + * The {@link TransactionListenerAdapter} class is an Apache Geode {@link TransactionListener} + * and {@link TransactionWriter} implementation that publishes the {@link TransactionEvent} to application components + * and beans declared in the Spring {@link ApplicationContext} using the {@link ApplicationEventPublisher}. + * + * @author John Blum + * @see org.apache.geode.cache.TransactionEvent + * @see org.apache.geode.cache.TransactionListener + * @see org.apache.geode.cache.TransactionWriter + * @see org.springframework.context.ApplicationContext + * @see org.springframework.context.ApplicationEventPublisher + * @since 2.3.0 + */ +public class TransactionListenerAdapter implements TransactionListener, TransactionWriter { + + private final ApplicationEventPublisher applicationEventPublisher; + + /** + * Constructs a new instance of the {@link TransactionListenerAdapter} initialized with the required + * {@link ApplicationEventPublisher} to publish Apache Geode cache {@link TransactionEvent TransactionEvents} + * to application declared components and beans in a Spring {@link ApplicationContext}. + * + * @param applicationEventPublisher {@link ApplicationEventPublisher} used to publish Apache Geode cache + * {@link TransactionEvent TransactionEvents}. + * @throws IllegalArgumentException if the {@link ApplicationEventPublisher} is {@literal null}. + * @see org.springframework.context.ApplicationEventPublisher + */ + public TransactionListenerAdapter(ApplicationEventPublisher applicationEventPublisher) { + + Assert.notNull(applicationEventPublisher, "ApplicationEventPublisher must not be null"); + + this.applicationEventPublisher = applicationEventPublisher; + } + + /** + * @inheritDoc + */ + @Override + public void beforeCommit(TransactionEvent event) { + + // NOTE: this will not work because Apache Geode's cache before commit transaction event is only triggered + // after Spring's AbstractPlatformTransaction.triggerBeforeCommit(:TransactionStatus) method, which is where + // all application @TransactionalEventListener(phase = TransactionPhase.BEFORE_COMMIT) annotated transaction + // event handler methods are invoked. + + //this.applicationEventPublisher.publishEvent(TransactionApplicationEvent.of(event)); + } + + /** + * @inheritDoc + */ + @Override + public void afterCommit(TransactionEvent event) { + this.applicationEventPublisher.publishEvent(TransactionApplicationEvent.of(event)); + } + + /** + * @inheritDoc + */ + @Override + public void afterFailedCommit(TransactionEvent event) { } + + /** + * @inheritDoc + */ + @Override + public void afterRollback(TransactionEvent event) { + this.applicationEventPublisher.publishEvent(TransactionApplicationEvent.of(event)); + } +} diff --git a/src/test/java/org/springframework/data/gemfire/transaction/AbstractTransactionalEventListenerIntegrationTests.java b/src/test/java/org/springframework/data/gemfire/transaction/AbstractTransactionalEventListenerIntegrationTests.java new file mode 100644 index 00000000..6a9b508f --- /dev/null +++ b/src/test/java/org/springframework/data/gemfire/transaction/AbstractTransactionalEventListenerIntegrationTests.java @@ -0,0 +1,207 @@ +/* + * Copyright 2019 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.data.gemfire.transaction; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.data.gemfire.util.RuntimeExceptionFactory.newIllegalStateException; + +import java.io.Serializable; + +import org.junit.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.annotation.Id; +import org.springframework.data.gemfire.mapping.GemfireMappingContext; +import org.springframework.data.gemfire.mapping.annotation.Region; +import org.springframework.data.gemfire.repository.support.GemfireRepositoryFactoryBean; +import org.springframework.data.gemfire.transaction.event.TransactionApplicationEvent; +import org.springframework.data.repository.CrudRepository; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.Assert; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; +import lombok.ToString; + +/** + * {@link AbstractTransactionalEventListenerIntegrationTests} is an abstract base class for writing transactional, + * Apache Geode cache event listener tests. + * + * @author John Blum + * @see org.springframework.context.ApplicationEventPublisher + * @see org.springframework.context.annotation.Bean + * @see org.springframework.context.annotation.Configuration + * @see org.springframework.data.gemfire.transaction.event.TransactionApplicationEvent + * @see org.springframework.transaction.annotation.Transactional + * @since 2.3.0 + */ +@SuppressWarnings("unused") +public abstract class AbstractTransactionalEventListenerIntegrationTests { + + protected static final String GEMFIRE_LOG_LEVEL = "error"; + + @Autowired + private CustomerService customerService; + + protected abstract void assertTransactionEventListenerOnSuccess() throws Exception; + + protected abstract void assertTransactionEventListenerOnFailure() throws Exception; + + @Test + public void successfulEntityTransactionTriggersCommitTransactionEvents() throws Exception { + + Customer jonDoe = this.customerService.save(Customer.newCustomer(1L, "Jon Doe")); + + Customer jonDoeLoaded = this.customerService.findById(jonDoe.getId()); + + assertThat(jonDoeLoaded).isEqualTo(jonDoe); + assertTransactionEventListenerOnSuccess(); + } + + @Test(expected = IllegalStateException.class) + public void failingEntityTransactionTriggersRollbackTransactionEvent() throws Exception { + + Customer janeDoe = Customer.newCustomer(2L, "Jane Doe"); + + try { + this.customerService.saveFailsAndRollsback(janeDoe); + } + catch (IllegalStateException expected) { + + assertTransactionEventListenerOnFailure(); + assertThat(expected).hasMessage("TEST"); + assertThat(expected).hasNoCause(); + + try { + this.customerService.findById(janeDoe.getId()); + } + catch (IllegalStateException alsoExpected) { + + assertThat(alsoExpected).hasMessage("No Customer having ID [2] was found"); + assertThat(alsoExpected).hasNoCause(); + + throw alsoExpected; + } + } + } + + @Data + @EqualsAndHashCode + @Region("Customers") + @ToString(of = "name") + @RequiredArgsConstructor(staticName = "newCustomer") + static class Customer implements Serializable { + + @Id @NonNull + private Long id; + + @NonNull + private String name; + + } + + public interface CustomerRepository extends CrudRepository { } + + @Configuration + static class CustomerRepositoryConfiguration { + + @Bean + GemfireRepositoryFactoryBean customerRepositoryFactoryBean() { + + GemfireRepositoryFactoryBean customerRepositoryFactoryBean + = new GemfireRepositoryFactoryBean<>(CustomerRepository.class); + + customerRepositoryFactoryBean.setGemfireMappingContext(new GemfireMappingContext()); + + return customerRepositoryFactoryBean; + } + } + + @Service + public static class CustomerService { + + private final CustomerRepository customerRepository; + + public CustomerService(CustomerRepository customerRepository) { + + Assert.notNull(customerRepository, "CustomerRepository is required"); + + this.customerRepository = customerRepository; + } + + public Customer findById(Long id) { + return this.customerRepository.findById(id) + .orElseThrow(() -> newIllegalStateException("No Customer having ID [%d] was found", id)); + } + + @Transactional + public Customer save(Customer customer) { + return this.customerRepository.save(customer); + } + + @Transactional + public Customer saveFailsAndRollsback(Customer customer) { + + this.customerRepository.save(customer); + + throw newIllegalStateException("TEST"); + } + } + + @Service + public static class TransactionEventPublishingCustomerService extends CustomerService { + + private final ApplicationEventPublisher eventPublisher; + + public TransactionEventPublishingCustomerService(ApplicationEventPublisher eventPublisher, + CustomerRepository customerRepository) { + + super(customerRepository); + + Assert.notNull(eventPublisher, "ApplicationEventPublisher is required"); + + this.eventPublisher = eventPublisher; + } + + @Override + @Transactional + public Customer save(Customer customer) { + + customer = super.save(customer); + + this.eventPublisher.publishEvent(new TransactionApplicationEvent(this, + String.valueOf(customer.getId()))); + + return customer; + } + + @Override + @Transactional + public Customer saveFailsAndRollsback(Customer customer) { + + this.eventPublisher.publishEvent(new TransactionApplicationEvent(this, + String.valueOf(customer.getId()))); + + return super.saveFailsAndRollsback(customer); + } + } +} diff --git a/src/test/java/org/springframework/data/gemfire/transaction/AutoTransactionalEventListenerIntegrationTests.java b/src/test/java/org/springframework/data/gemfire/transaction/AutoTransactionalEventListenerIntegrationTests.java new file mode 100644 index 00000000..9385429d --- /dev/null +++ b/src/test/java/org/springframework/data/gemfire/transaction/AutoTransactionalEventListenerIntegrationTests.java @@ -0,0 +1,177 @@ +/* + * Copyright 2019 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.data.gemfire.transaction; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import java.util.ArrayList; +import java.util.EventListener; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.junit.runner.RunWith; + +import org.apache.geode.cache.RegionShortcut; +import org.apache.geode.cache.TransactionEvent; +import org.apache.geode.cache.TransactionWriter; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Import; +import org.springframework.data.gemfire.config.annotation.EnableEntityDefinedRegions; +import org.springframework.data.gemfire.config.annotation.PeerCacheApplication; +import org.springframework.data.gemfire.config.annotation.PeerCacheConfigurer; +import org.springframework.data.gemfire.transaction.config.EnableGemfireCacheTransactions; +import org.springframework.data.gemfire.transaction.event.TransactionApplicationEvent; +import org.springframework.stereotype.Component; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.transaction.event.TransactionPhase; +import org.springframework.transaction.event.TransactionalEventListener; + +/** + * Integration Tests for the Spring {@link TransactionalEventListener} in the context of Apache Geode + * cache transactions when auto-publishing of transaction events is enabled. + * + * @author John Blum + * @see org.junit.Test + * @see org.springframework.context.annotation.Bean + * @see org.springframework.context.annotation.Import + * @see org.springframework.data.gemfire.config.annotation.PeerCacheApplication + * @see org.springframework.data.gemfire.config.annotation.PeerCacheConfigurer + * @see org.springframework.data.gemfire.transaction.config.EnableGemfireCacheTransactions + * @see org.springframework.data.gemfire.transaction.event.TransactionApplicationEvent + * @see org.springframework.test.context.ContextConfiguration + * @see org.springframework.test.context.junit4.SpringRunner + * @see org.springframework.transaction.event.TransactionPhase + * @see org.springframework.transaction.event.TransactionalEventListener + * @since 2.3.0 + */ +@RunWith(SpringRunner.class) +@ContextConfiguration +@SuppressWarnings("unused") +public class AutoTransactionalEventListenerIntegrationTests extends AbstractTransactionalEventListenerIntegrationTests { + + @Autowired + private TestTransactionEventListener transactionEventListener; + + @Autowired + @Qualifier("MockTransactionWriter") + private TransactionWriter mockTransactionWriter; + + @Override + protected void assertTransactionEventListenerOnSuccess() throws Exception { + + assertThat(this.transactionEventListener.isBeforeCommitInvoked()).isFalse(); + + assertThat(this.transactionEventListener.getAndClearTransactionPhases()) + .containsExactly(TransactionPhase.AFTER_COMMIT); + + verify(this.mockTransactionWriter, times(1)).beforeCommit(any(TransactionEvent.class)); + } + + @Override + protected void assertTransactionEventListenerOnFailure() { + + assertThat(this.transactionEventListener.isBeforeCommitInvoked()).isFalse(); + + assertThat(this.transactionEventListener.getAndClearTransactionPhases()) + .containsExactly(TransactionPhase.AFTER_ROLLBACK); + } + + @PeerCacheApplication(logLevel = GEMFIRE_LOG_LEVEL) + @EnableEntityDefinedRegions( + basePackageClasses = Customer.class, + serverRegionShortcut = RegionShortcut.LOCAL + ) + @EnableGemfireCacheTransactions(enableAutoTransactionEventPublishing = true) + @Import(CustomerRepositoryConfiguration.class) + static class TestConfiguration { + + @Bean + CustomerService customerService(CustomerRepository customerRepository) { + return new CustomerService(customerRepository); + } + + @Bean + TestTransactionEventListener testTransactionEventListener() { + return new TestTransactionEventListener(); + } + + @Bean("MockTransactionWriter") + TransactionWriter mockTransactionWriter() { + return mock(TransactionWriter.class); + } + + @Bean + PeerCacheConfigurer transactionWriterRegisteringCacheConfigurer( + @Qualifier("MockTransactionWriter") TransactionWriter transactionWriter) { + + return (beanName, bean) -> bean.setTransactionWriter(transactionWriter); + } + } + + @Component + public static class TestTransactionEventListener implements EventListener { + + private AtomicBoolean beforeCommitInvoked = new AtomicBoolean(false); + + private List transactionPhases = new ArrayList<>(); + + public List getAndClearTransactionPhases() { + + List copy = new ArrayList<>(this.transactionPhases); + + this.transactionPhases.clear(); + + return copy; + } + + private boolean isBeforeCommitInvoked() { + return this.beforeCommitInvoked.getAndSet(false); + } + + private void handleTransactionEvent(TransactionApplicationEvent event, TransactionPhase transactionPhase) { + + Optional.ofNullable(event) + .map(TransactionApplicationEvent::getSource) + .filter(TransactionEvent.class::isInstance) + .ifPresent(transactionEvent -> this.transactionPhases.add(transactionPhase)); + } + + @TransactionalEventListener(phase = TransactionPhase.BEFORE_COMMIT) + public void handleTransactionBeforeCommit(TransactionApplicationEvent event) { + this.beforeCommitInvoked.set(true); + handleTransactionEvent(event, TransactionPhase.BEFORE_COMMIT); + } + + @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) + public void handleTransactionAfterCommit(TransactionApplicationEvent event) { + handleTransactionEvent(event, TransactionPhase.AFTER_COMMIT); + } + + @TransactionalEventListener(phase = TransactionPhase.AFTER_ROLLBACK) + public void handleTransactionAfterRollback(TransactionApplicationEvent event) { + handleTransactionEvent(event, TransactionPhase.AFTER_ROLLBACK); + } + } +} diff --git a/src/test/java/org/springframework/data/gemfire/transaction/TransactionalEventListenerIntegrationTests.java b/src/test/java/org/springframework/data/gemfire/transaction/TransactionalEventListenerIntegrationTests.java index 6705d56e..77695f9c 100644 --- a/src/test/java/org/springframework/data/gemfire/transaction/TransactionalEventListenerIntegrationTests.java +++ b/src/test/java/org/springframework/data/gemfire/transaction/TransactionalEventListenerIntegrationTests.java @@ -16,16 +16,13 @@ package org.springframework.data.gemfire.transaction; import static org.assertj.core.api.Assertions.assertThat; -import static org.springframework.data.gemfire.util.RuntimeExceptionFactory.newIllegalStateException; -import java.io.Serializable; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Optional; import java.util.Set; -import org.junit.Test; import org.junit.runner.RunWith; import org.apache.geode.cache.client.ClientRegionShortcut; @@ -34,28 +31,16 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationEvent; import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.annotation.Bean; -import org.springframework.data.annotation.Id; +import org.springframework.context.annotation.Import; import org.springframework.data.gemfire.config.annotation.ClientCacheApplication; import org.springframework.data.gemfire.config.annotation.EnableEntityDefinedRegions; -import org.springframework.data.gemfire.mapping.GemfireMappingContext; -import org.springframework.data.gemfire.mapping.annotation.Region; -import org.springframework.data.gemfire.repository.support.GemfireRepositoryFactoryBean; import org.springframework.data.gemfire.transaction.config.EnableGemfireCacheTransactions; import org.springframework.data.gemfire.transaction.event.TransactionApplicationEvent; -import org.springframework.data.repository.CrudRepository; import org.springframework.stereotype.Component; -import org.springframework.stereotype.Service; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringRunner; -import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.event.TransactionPhase; import org.springframework.transaction.event.TransactionalEventListener; -import org.springframework.util.Assert; - -import lombok.Data; -import lombok.EqualsAndHashCode; -import lombok.NonNull; -import lombok.RequiredArgsConstructor; /** * Integration Tests for the Spring {@link TransactionalEventListener} in the context of Apache Geode @@ -66,9 +51,11 @@ import lombok.RequiredArgsConstructor; * @see org.springframework.context.ApplicationEvent * @see org.springframework.context.ApplicationEventPublisher * @see org.springframework.context.annotation.Bean + * @see org.springframework.context.annotation.Import * @see org.springframework.data.gemfire.config.annotation.ClientCacheApplication - * @see TransactionApplicationEvent + * @see org.springframework.data.gemfire.transaction.AbstractTransactionalEventListenerIntegrationTests * @see org.springframework.data.gemfire.transaction.config.EnableGemfireCacheTransactions + * @see org.springframework.data.gemfire.transaction.event.TransactionApplicationEvent * @see org.springframework.test.context.ContextConfiguration * @see org.springframework.test.context.junit4.SpringRunner * @see org.springframework.transaction.annotation.Transactional @@ -79,24 +66,13 @@ import lombok.RequiredArgsConstructor; @RunWith(SpringRunner.class) @ContextConfiguration @SuppressWarnings("unused") -public class TransactionalEventListenerIntegrationTests { - - private static final String GEMFIRE_LOG_LEVEL = "error"; - - @Autowired - private CustomerService customerService; +public class TransactionalEventListenerIntegrationTests extends AbstractTransactionalEventListenerIntegrationTests { @Autowired private TestTransactionEventListener transactionEventListener; - @Test - public void successfulEntityTransactionTriggersCommitTransactionEvents() { - - Customer jonDoe = this.customerService.save(Customer.newCustomer(1L, "Jon Doe")); - - Customer jonDoeLoaded = this.customerService.findById(jonDoe.getId()); - - assertThat(jonDoeLoaded).isEqualTo(jonDoe); + @Override + protected void assertTransactionEventListenerOnSuccess() { assertThat(this.transactionEventListener.getAndClearTransactionDetails()).containsExactly("1"); @@ -104,35 +80,13 @@ public class TransactionalEventListenerIntegrationTests { .containsExactly(TransactionPhase.BEFORE_COMMIT, TransactionPhase.AFTER_COMMIT); } - @Test(expected = IllegalStateException.class) - public void failingEntityTransactionTriggersRollbackTransactionEvent() { + @Override + protected void assertTransactionEventListenerOnFailure() { - Customer janeDoe = Customer.newCustomer(2L, "Jane Doe"); + assertThat(this.transactionEventListener.getAndClearTransactionDetails()).containsExactly("2"); - try { - this.customerService.saveFailsAndRollsback(janeDoe); - } - catch (IllegalStateException expected) { - - assertThat(this.transactionEventListener.getAndClearTransactionDetails()).containsExactly("2"); - - assertThat(this.transactionEventListener.getAndClearTransactionPhases()) - .containsExactly(TransactionPhase.AFTER_ROLLBACK); - - assertThat(expected).hasMessage("TEST"); - assertThat(expected).hasNoCause(); - - try { - this.customerService.findById(janeDoe.getId()); - } - catch (IllegalStateException alsoExpected) { - - assertThat(alsoExpected).hasMessage("No Customer having ID [2] was found"); - assertThat(alsoExpected).hasNoCause(); - - throw alsoExpected; - } - } + assertThat(this.transactionEventListener.getAndClearTransactionPhases()) + .containsExactly(TransactionPhase.AFTER_ROLLBACK); } @ClientCacheApplication(logLevel = GEMFIRE_LOG_LEVEL) @@ -141,24 +95,14 @@ public class TransactionalEventListenerIntegrationTests { clientRegionShortcut = ClientRegionShortcut.LOCAL ) @EnableGemfireCacheTransactions + @Import(CustomerRepositoryConfiguration.class) static class TestConfiguration { - @Bean - GemfireRepositoryFactoryBean customerRepositoryFactoryBean() { - - GemfireRepositoryFactoryBean customerRepositoryFactoryBean - = new GemfireRepositoryFactoryBean<>(CustomerRepository.class); - - customerRepositoryFactoryBean.setGemfireMappingContext(new GemfireMappingContext()); - - return customerRepositoryFactoryBean; - } - @Bean CustomerService customerService(ApplicationEventPublisher eventPublisher, CustomerRepository customerRepository) { - return new CustomerService(eventPublisher, customerRepository); + return new TransactionEventPublishingCustomerService(eventPublisher, customerRepository); } @Bean @@ -167,66 +111,6 @@ public class TransactionalEventListenerIntegrationTests { } } - @Data - @EqualsAndHashCode - @Region("Customers") - @RequiredArgsConstructor(staticName = "newCustomer") - static class Customer implements Serializable { - - @Id @NonNull - private Long id; - - @NonNull - private String name; - - } - - public interface CustomerRepository extends CrudRepository { } - - @Service - public static class CustomerService { - - private ApplicationEventPublisher eventPublisher; - - private final CustomerRepository customerRepository; - - public CustomerService(ApplicationEventPublisher eventPublisher, CustomerRepository customerRepository) { - - Assert.notNull(eventPublisher, "ApplicationEventPublisher is required"); - Assert.notNull(customerRepository, "CustomerRepository is required"); - - this.eventPublisher = eventPublisher; - this.customerRepository = customerRepository; - } - - public Customer findById(Long id) { - return this.customerRepository.findById(id) - .orElseThrow(() -> newIllegalStateException("No Customer having ID [%d] was found", id)); - } - - @Transactional - public Customer save(Customer customer) { - - customer = this.customerRepository.save(customer); - - this.eventPublisher.publishEvent(new TransactionApplicationEvent(this, - String.valueOf(customer.getId()))); - - return customer; - } - - @Transactional - public Customer saveFailsAndRollsback(Customer customer) { - - this.customerRepository.save(customer); - - this.eventPublisher.publishEvent(new TransactionApplicationEvent(this, - String.valueOf(customer.getId()))); - - throw newIllegalStateException("TEST"); - } - } - @Component public static class TestTransactionEventListener {