diff --git a/src/main/asciidoc/reference/data.adoc b/src/main/asciidoc/reference/data.adoc index 5090b2e2..400f8ea8 100644 --- a/src/main/asciidoc/reference/data.adoc +++ b/src/main/asciidoc/reference/data.adoc @@ -65,15 +65,16 @@ or `` declaration, which acts as an exception translator and i the _Spring_ infrastructure and used accordingly. [[apis:transaction-management]] -== Transaction Management +== Local, Cache Transaction Management One of the most popular features of the _Spring Framework_ is http://docs.spring.io/spring/docs/current/spring-framework-reference/htmlsingle/#transaction[Transaction Management]. If you are not familiar with _Spring's_ transaction abstraction then we strongly recommend http://docs.spring.io/spring/docs/current/spring-framework-reference/htmlsingle/#transaction-motivation[reading] -about it as it offers a _consistent programming model_ that works transparently across multiple APIs -and can be configured either programmatically or declaratively (the most popular choice). +about _Spring's Transaction Management_ infrastructure as it offers a consistent _programming model_ that works +transparently across multiple APIs and can be configured either programmatically or declaratively +(the most popular choice). For Apache Geode, _Spring Data Geode_ provides a dedicated, per-cache, `PlatformTransactionManager` that, once declared, allows Region operations to be executed atomically through _Spring_: @@ -83,21 +84,230 @@ once declared, allows Region operations to be executed atomically through _Sprin ---- -NOTE: The example above can be simplified even further by eliminating the `cache-ref` attribute if the GemFire cache +NOTE: The example above can be simplified even further by eliminating the `cache-ref` attribute if the Geode cache is defined under the default name, `gemfireCache`. As with the other _Spring Data Geode_ namespace elements, if the cache bean name is not configured, the aforementioned naming convention will be used. -Additionally, the transaction manager name is `"gemfireTransactionManager"` if not explicitly specified. +Additionally, the transaction manager name is "`gemfireTransactionManager`" if not explicitly specified. Currently, Apache Geode supports optimistic transactions with *read committed* isolation. Furthermore, to guarantee this isolation, developers should avoid making *in-place* changes that manually modify values present in the cache. -To prevent this from happening, the transaction manager configures the cache to use *copy on read* semantics, +To prevent this from happening, the transaction manager configures the cache to use *copy on read* semantics by default, meaning a clone of the actual value is created each time a read is performed. This behavior can be disabled if needed through the `copyOnRead` property. -For more information on the semantics and bevior of the underlying Geode transaction manager, please refer to the Geode +For more information on the semantics and behavior of the underlying Geode transaction manager, please refer to the Geode http://geode.apache.org/releases/latest/javadoc/org/apache/geode/cache/CacheTransactionManager.html[CacheTransactionManager Javadoc] as well as the http://geode.apache.org/docs/guide/11/developing/transactions/chapter_overview.html[documentation]. +[[apis:global-transaction-management]] +== Global, JTA Transaction Management + +It is also possible for Apache Geode to participate in a Global, JTA based transaction, such as a transaction managed +by an Java EE Application Server (e.g. WebSphere Application Server, a.k.a. WAS) using Container Managed Transactions +(CMT) along with other JTA resources. + +However, unlike many other JTA "compliant" resources (e.g. JMS Message Brokers like ActiveMQ), Apache Geode is *not* +an XA compliant resource. Therefore, Apache Geode must be positioned as the "_Last Resource_" in a JTA transaction +(_prepare phase_) since it does not implement the 2-phase commit protocol, or rather does not handle +distributed transactions. + +Many managed environments with CMT maintain support for "_Last Resource_", non-XA compliant resources in JTA transactions +though it is not actually required in the JTA spec. More information on what a non-XA compliant, "_Last Resource_" means +can be found in Red Hat's https://access.redhat.com/documentation/en-US/JBoss_Enterprise_Application_Platform/5/html/Administration_And_Configuration_Guide/lrco-overview.html[documentation]. +In fact, Red Hat's JBoss project, http://narayana.io/[Narayana] is one such LGPL Open Source implementation. _Narayana_ +refers to this as "_Last Resource Commit Optimization_" (LRCO). More details can be found +http://narayana.io//docs/project/index.html#d0e1859[here]. + +However, whether you are using Apache Geode in a standalone environment with an Open Source JTA Transaction Management +implementation that supports "_Last Resource_", or a managed environment (e.g. Java EE AS such as WAS), +_Spring Data Geode_ has you covered. + +There are a series of steps you must complete to properly use Apache Geode as a "_Last Resource_" in a JTA transaction +involving more than 1 transactional resource. Additionally, there can only be 1 non-XA compliant resource +(e.g. Apache Geode) in such an arrangement. + +1) First, you must complete Steps 1-4 in Geode's documentation +http://geode.apache.org/docs/guide/11/developing/transactions/JTA_transactions.html#task_sln_x3b_wk[here]. + +NOTE: #1 above is independent of your _Spring [Boot] and/or [Data Geode]_ application +and must be completed successfully. + +2) Referring to Step 5 in Geode's http://geode.apache.org/docs/guide/11/developing/transactions/JTA_transactions.html#task_sln_x3b_wk[documentation], +_Spring Data Geode's_ Annotation support will attempt to set the `GemFireCache`, http://geode.apache.org/releases/latest/javadoc/org/apache/geode/cache/GemFireCache.html#setCopyOnRead-boolean-[`copyOnRead`] +property for you when using the `@EnableGemFireAsLastResource` annotation. + +However, if SDG's auto-configuration is unsuccessful then you must explicitly set the `copy-on-read` attribute on the +`` or `` element in XML or the `copyOnRead` property of the SDG `CacheFactoryBean` class +in JavaConfig to *true*. For example... + +Peer Cache XML: + +[source,xml] +---- + +---- + +Peer Cache JavaConfig: + +[source,java] +---- + @Bean + CacheFacatoryBean gemfireCache() { + + CacheFactoryBean gemfireCache = new CacheFactoryBean(); + + gemfireCache.setClose(true); + gemfireCache.setCopyOnRead(true); + + return gemfireCache; + } +---- + +Client Cache XML: + +[source,xml] +---- + +---- + +Client Cache JavaConfig: + +[source,java] +---- + @Bean + ClientCacheFacatoryBean gemfireCache() { + + ClientCacheFactoryBean gemfireCache = new ClientCacheFactoryBean(); + + gemfireCache.setClose(true); + gemfireCache.setCopyOnRead(true); + + return gemfireCache; + } +---- + +NOTE: explicitly setting the `copy-on-read` attribute or optionally the `copyOnRead` property +really should not be necessary. + +3) At this point, you *skip* Steps 6-8 in Geode's http://geode.apache.org/docs/guide/11/developing/transactions/JTA_transactions.html#task_sln_x3b_wk[documentation] +and let _Spring Data Geode_ work its magic. All you need do is annotate your _Spring_ `@Configuration` class +with _Spring Data Geode's_ *new* `@EnableGemFireAsLastResource` annotation and a combination of _Spring's_ +http://docs.spring.io/spring/docs/current/spring-framework-reference/htmlsingle/#transaction[Transaction Management] +infrastructure and _Spring Data Geode's_ `@EnableGemFireAsLastResource` configuration does the trick. + +The configuration looks like this... + +[source,java] +---- +@Configuration +@EnableGemFireAsLastResource +@EnableTransactionManagement(order = 1) +class GeodeConfiguration { + + ... +} +---- + +The only requirements are... + +3.1) The `@EnableGemFireAsLastResource` annotation must be declared on the same _Spring_ `@Configuration` class +where _Spring's_ `@EnableTransactionManagement` annotation is also specified. + +3.2) The `order` attribute of the `@EnableTransactionManagement` annotation must be explicitly set to an integer value +that is not `Integer.MAX_VALUE` or `Integer.MIN_VALUE` (defaults to `Integer.MAX_VALUE`). + +Of course, hopefully you are aware that you also need to configure _Spring's_ `JtaTransactionManager` +when using JTA Transactions like so.. + +[source,java] +---- +@Bean +public JtaTransactionManager transactionManager(UserTransaction userTransaction) { + + JtaTransactionManager transactionManager = new JtaTransactionManager(); + + transactionManager.setUserTransaction(userTransaction); + + return transactionManager; +} +---- + +NOTE: The configuration in section <> does *not* apply here. +The use of _Spring Data Geode's_ `GemfireTransactionManager` is applicable only in "Local", Cache Transactions, +*not* "Global", JTA Transactions. Therefore, you do *not* configure the SDG `GemfireTransactionManager` in this case. +You configure _Spring's_ `JtaTransactionManager` as shown above. + +For more details on using _Spring's Transaction Management_ with JTA, +see http://docs.spring.io/spring/docs/current/spring-framework-reference/htmlsingle/#transaction-application-server-integration[here]. + +Effectively, _Spring Data Geode's_ `@EnableGemFireAsLastResource` annotation imports configuration containing 2 Aspect +bean definitions that handles the Geode `o.a.g.ra.GFConnectionFactory.getConnection()` +and `o.a.g.ra.GFConnection.close()` operations at the appropriate points during the transactional operation. + +Specifically, the correct sequence of events are... + +1. `jtaTransation.begin()` + +2. `GFConnectionFactory.getConnection()` + +3. Call the application's `@Transactional` service method + +4. Either `jtaTransaction.commit()` or `jtaTransaction.rollback()` + +5. Finally, `GFConnection.close()` + +This is consistent with how you, as the application developer, would code this manually if you had to use the JTA API ++ Geode API yourself, as shown in the +Geode http://geode.apache.org/docs/guide/11/developing/transactions/jca_adapter_example.html#concept_swv_z2p_wk[example]. + +Thankfully, _Spring_ does the heavy lifting for you and all you need do after applying the appropriate configuration +(shown above) is... + +[source,java] +---- +@Service +class MyTransactionalService ... { + + @Transactional + public someTransactionalMethod() { + // perform business logic interacting with and accessing multiple JTA resources atomically, here + } + + ... +} +---- + +#1 & #4 above are appropriately handled for you by _Spring's_ JTA based `PlatformTransactionManager` once the +`@Transactional` boundary is entered by your application (i.e. when the `MyTransactionSerivce.someTransactionalMethod()` +is called). + +#2 & #3 are handled by _Spring Data Geode's_ new Aspects enabled with the `@EnableGemFireAsLastResource` annotation. + +#3 of course is the responsibility of your application. + +Indeed, with the appropriate logging configured, you will see the correct sequence of events... + +[source,xml] +---- +2017-Jun-22 11:11:37 TRACE TransactionInterceptor - Getting transaction for [example.app.service.MessageService.send] + +2017-Jun-22 11:11:37 TRACE GemFireAsLastResourceConnectionAcquiringAspect - Acquiring GemFire Connection +from GemFire JCA ResourceAdapter registered at [gfe/jca] + +2017-Jun-22 11:11:37 TRACE MessageService - PRODUCER [ Message : +[{ @type = example.app.domain.Message, id= MSG0000000000, message = SENT }], +JSON : [{"id":"MSG0000000000","message":"SENT"}] ] + +2017-Jun-22 11:11:37 TRACE TransactionInterceptor - Completing transaction for [example.app.service.MessageService.send] + +2017-Jun-22 11:11:37 TRACE GemFireAsLastResourceConnectionClosingAspect - Closed GemFire Connection @ [Reference [...]] +---- + +For more details on using Apache Geode in JTA transactions, see http://geode.apache.org/docs/guide/11/developing/transactions/JTA_transactions.html[here]. + +For more details on configuring Apache Geode as a "_Last Resource_", +see http://geode.apache.org/docs/guide/11/developing/transactions/JTA_transactions.html#concept_csy_vfb_wk[here]. + include::{basedocdir}/reference/cq-container.adoc[] [[apis:declarable]] diff --git a/src/main/java/org/springframework/data/gemfire/config/annotation/EnableGemFireAsLastResource.java b/src/main/java/org/springframework/data/gemfire/config/annotation/EnableGemFireAsLastResource.java new file mode 100644 index 00000000..4881c301 --- /dev/null +++ b/src/main/java/org/springframework/data/gemfire/config/annotation/EnableGemFireAsLastResource.java @@ -0,0 +1,48 @@ +/* + * Copyright 2017 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.data.gemfire.config.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.context.annotation.EnableAspectJAutoProxy; +import org.springframework.context.annotation.Import; + +/** + * The {@link EnableGemFireAsLastResource} annotation is used to enable GemFire as a Last Resource in a Spring, + * CMT/JTA Transaction. + * + * @author John Blum + * @see org.springframework.context.annotation.EnableAspectJAutoProxy + * @see org.springframework.context.annotation.Import + * @see org.springframework.data.gemfire.config.annotation.GemFireAsLastResourceConfiguration + * @since 2.0.0 + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Inherited +@Documented +@EnableAspectJAutoProxy +@Import(GemFireAsLastResourceConfiguration.class) +@SuppressWarnings("unused") +public @interface EnableGemFireAsLastResource { + +} diff --git a/src/main/java/org/springframework/data/gemfire/config/annotation/EnableRegionDataAccessTracing.java b/src/main/java/org/springframework/data/gemfire/config/annotation/EnableRegionDataAccessTracing.java new file mode 100644 index 00000000..de9b712d --- /dev/null +++ b/src/main/java/org/springframework/data/gemfire/config/annotation/EnableRegionDataAccessTracing.java @@ -0,0 +1,44 @@ +/* + * Copyright 2017 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.data.gemfire.config.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.context.annotation.EnableAspectJAutoProxy; +import org.springframework.context.annotation.Import; + +/** + * The EnableRegionDataAccessTracing class... + * + * @author John Blum + * @since 1.0.0 + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Inherited +@Documented +@EnableAspectJAutoProxy +@Import(RegionDataAccessTracingConfiguration.class) +@SuppressWarnings("unused") +public @interface EnableRegionDataAccessTracing { + +} diff --git a/src/main/java/org/springframework/data/gemfire/config/annotation/GemFireAsLastResourceConfiguration.java b/src/main/java/org/springframework/data/gemfire/config/annotation/GemFireAsLastResourceConfiguration.java new file mode 100644 index 00000000..909a2fd8 --- /dev/null +++ b/src/main/java/org/springframework/data/gemfire/config/annotation/GemFireAsLastResourceConfiguration.java @@ -0,0 +1,146 @@ +/* + * Copyright 2017 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.data.gemfire.config.annotation; + +import static org.springframework.data.gemfire.util.RuntimeExceptionFactory.newIllegalArgumentException; +import static org.springframework.data.gemfire.util.RuntimeExceptionFactory.newIllegalStateException; + +import java.util.Map; +import java.util.Optional; + +import org.apache.geode.cache.GemFireCache; +import org.aspectj.lang.annotation.Aspect; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.ImportAware; +import org.springframework.core.annotation.AnnotationAttributes; +import org.springframework.core.type.AnnotationMetadata; +import org.springframework.data.gemfire.config.annotation.support.GemFireAsLastResourceConnectionAcquiringAspect; +import org.springframework.data.gemfire.config.annotation.support.GemFireAsLastResourceConnectionClosingAspect; +import org.springframework.transaction.annotation.EnableTransactionManagement; + +/** + * The {@link GemFireAsLastResourceConfiguration} class is a Spring {@link Configuration @Configuration} + * annotated class used to configure the GemFire "Last Resource" Spring Data GemFire {@link Aspect Aspects}. + * + * @author John Blum + * @see org.apache.geode.cache.GemFireCache + * @see org.aspectj.lang.annotation.Aspect + * @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.AnnotationMetadata + * @see org.springframework.data.gemfire.config.annotation.EnableGemFireAsLastResource + * @see org.springframework.data.gemfire.config.annotation.support.GemFireAsLastResourceConnectionAcquiringAspect + * @see org.springframework.data.gemfire.config.annotation.support.GemFireAsLastResourceConnectionClosingAspect + * @see org.springframework.transaction.annotation.EnableTransactionManagement + * @since 1.0.0 + */ +@Configuration +@SuppressWarnings("unused") +public class GemFireAsLastResourceConfiguration implements ImportAware { + + private Integer enableTransactionManagementOrder; + + /* (non-Javadoc) */ + @Override + public void setImportMetadata(AnnotationMetadata importMetadata) { + this.enableTransactionManagementOrder = resolveEnableTransactionManagementOrder(importMetadata); + } + + /* (non-Javadoc) */ + @SuppressWarnings("all") + protected int resolveEnableTransactionManagementOrder(AnnotationMetadata importMetadata) { + + AnnotationAttributes enableTransactionManagementAttributes = + resolveEnableTransactionManagementAttributes(importMetadata); + + Integer order = enableTransactionManagementAttributes.getNumber("order"); + + return Optional.ofNullable(order) + .filter(it -> !(it == Integer.MAX_VALUE || it == Integer.MIN_VALUE)) + .orElseThrow(() -> newIllegalArgumentException( + "The @%1$s(order) attribute value [%2$s] must be explicitly set to a value" + + " other than Integer.MAX_VALUE or Integer.MIN_VALUE", + EnableTransactionManagement.class.getSimpleName(), String.valueOf(order))); + } + + /* (non-Javadoc) */ + @SuppressWarnings("all") + protected AnnotationAttributes resolveEnableTransactionManagementAttributes( + AnnotationMetadata importMetadata) { + + Map enableTransactionManagementAttributes = + importMetadata.getAnnotationAttributes(EnableTransactionManagement.class.getName()); + + return Optional.ofNullable(enableTransactionManagementAttributes) + .map(AnnotationAttributes::fromMap) + .orElseThrow(() -> newIllegalStateException( + "The @%1$s annotation may only be used on a Spring application @%2$s class" + + " that is also annotated with @%3$s having an explicit [order] set", + EnableGemFireAsLastResource.class.getSimpleName(), Configuration.class.getSimpleName(), + EnableTransactionManagement.class.getSimpleName())); + } + + /* (non-Javadoc) */ + protected Integer getEnableTransactionManagementOrder() { + + return Optional.ofNullable(this.enableTransactionManagementOrder) + .orElseThrow(() -> newIllegalStateException( + "The @%1$s(order) attribute was not properly set [%2$s]; Also, please make your" + + " Spring application @%3$s annotated class is annotated with both @%4$s and @%1$s", + EnableTransactionManagement.class.getSimpleName(), String.valueOf(this.enableTransactionManagementOrder), + Configuration.class.getSimpleName(), EnableGemFireAsLastResource.class.getSimpleName())); + } + + /* (non-Javadoc) */ + @Bean + public Object gemfireCachePostProcessor(@Autowired(required = false) GemFireCache gemfireCache) { + Optional.ofNullable(gemfireCache).ifPresent(cache -> cache.setCopyOnRead(true)); + return null; + } + + /* (non-Javadoc) */ + @Bean + public GemFireAsLastResourceConnectionAcquiringAspect gemfireJcaConnectionAcquiringAspect() { + + GemFireAsLastResourceConnectionAcquiringAspect connectionAcquiringAspect = + new GemFireAsLastResourceConnectionAcquiringAspect(); + + int order = (getEnableTransactionManagementOrder() + 1); + + connectionAcquiringAspect.setOrder(order); + + return connectionAcquiringAspect; + } + + /* (non-Javadoc) */ + @Bean + public GemFireAsLastResourceConnectionClosingAspect gemfireJcaConnectionClosingAspect() { + + GemFireAsLastResourceConnectionClosingAspect connectionClosingAspect = + new GemFireAsLastResourceConnectionClosingAspect(); + + int order = (getEnableTransactionManagementOrder() - 1); + + connectionClosingAspect.setOrder(order); + + return connectionClosingAspect; + } +} diff --git a/src/main/java/org/springframework/data/gemfire/config/annotation/RegionDataAccessTracingConfiguration.java b/src/main/java/org/springframework/data/gemfire/config/annotation/RegionDataAccessTracingConfiguration.java new file mode 100644 index 00000000..a85c6346 --- /dev/null +++ b/src/main/java/org/springframework/data/gemfire/config/annotation/RegionDataAccessTracingConfiguration.java @@ -0,0 +1,38 @@ +/* + * Copyright 2017 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.data.gemfire.config.annotation; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.gemfire.config.annotation.support.RegionDataAccessTracingAspect; + +/** + * The RegionDataAccessTracingConfiguration class... + * + * @author John Blum + * @since 1.0.0 + */ +@Configuration +@SuppressWarnings("unused") +public class RegionDataAccessTracingConfiguration { + + /* (non-Javadoc) */ + @Bean + public RegionDataAccessTracingAspect regionDataAccessTracingAspect() { + return new RegionDataAccessTracingAspect(); + } +} diff --git a/src/main/java/org/springframework/data/gemfire/config/annotation/support/AbstractGemFireAsLastResourceAspectSupport.java b/src/main/java/org/springframework/data/gemfire/config/annotation/support/AbstractGemFireAsLastResourceAspectSupport.java new file mode 100644 index 00000000..0bf52e5d --- /dev/null +++ b/src/main/java/org/springframework/data/gemfire/config/annotation/support/AbstractGemFireAsLastResourceAspectSupport.java @@ -0,0 +1,577 @@ +/* + * Copyright 2017 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.data.gemfire.config.annotation.support; + +import static org.springframework.data.gemfire.util.RuntimeExceptionFactory.newIllegalStateException; +import static org.springframework.data.gemfire.util.RuntimeExceptionFactory.newRuntimeException; + +import java.util.Hashtable; +import java.util.Optional; +import java.util.function.Consumer; +import java.util.function.Supplier; + +import javax.naming.Context; +import javax.naming.InitialContext; +import javax.naming.NamingException; +import javax.resource.ResourceException; + +import org.apache.geode.cache.GemFireCache; +import org.apache.geode.ra.GFConnection; +import org.apache.geode.ra.GFConnectionFactory; +import org.aspectj.lang.annotation.Pointcut; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.slf4j.event.Level; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.core.Ordered; +import org.springframework.data.gemfire.GemfireUtils; +import org.springframework.util.StringUtils; + +/** + * {@link AbstractGemFireAsLastResourceAspectSupport} is an abstract base class encapsulating functionality common + * to all AOP Aspect extensions/implementations involving the GemFire JCA ResourceAdapter object registered in + * the JNDI context of a managed environment. + * + * @author John Blum + * @see javax.naming.Context + * @see org.apache.geode.cache.GemFireCache + * @see org.apache.geode.ra.GFConnection + * @see org.slf4j.Logger + * @see org.springframework.core.Ordered + * @since 1.0.0 + */ +@SuppressWarnings("unused") +public class AbstractGemFireAsLastResourceAspectSupport implements Ordered { + + protected static final boolean DEFAULT_THROW_ON_ERROR = false; + + protected static final int DEFAULT_ORDER = Integer.MAX_VALUE; + + protected static final Consumer NO_OP_LOGGER = message -> {}; + + protected static final String DEFAULT_GEMFIRE_JCA_RESOURCE_ADAPTER_JNDI_NAME = "gfe/jca"; + + private boolean throwOnError = DEFAULT_THROW_ON_ERROR; + + private int order = DEFAULT_ORDER; + + @Autowired(required = false) + private Context context; + + @Autowired(required = false) + private GemFireCache gemfireCache; + + private final Logger logger = newLogger(); + + @Value("${spring.data.gemfire.jca.resource-adapter.jndi.name:" + + DEFAULT_GEMFIRE_JCA_RESOURCE_ADAPTER_JNDI_NAME + "}") + private String gemfireJcaResourceAdapterJndiName; + + @Value("${spring.data.gemfire.naming.context.factory:}") + private String initialContextFactory; + + @Value("${spring.data.gemfire.naming.context.provider-url:}") + private String providerUrl; + + /** + * Returns a reference to the naming {@link Context}. + * + * @return a reference to the naming {@link Context}. + * @see javax.naming.Context + */ + protected synchronized Context getContext() { + return this.context; + } + + /** + * Returns a reference to the {@link GemFireCache} used to interact with GemFire. + * + * @param {@link Class} sub-type of the {@link GemFireCache} in use. + * @return a reference to the {@link GemFireCache}. + * @see org.apache.geode.cache.GemFireCache + */ + @SuppressWarnings("unchecked") + protected synchronized T getGemFireCache() { + return (T) this.gemfireCache; + } + + /** + * Returns the configured reference to GemFire's JCA ResourceAdapter registered in the managed environment's + * JNDI context. + * + * @return a {@link String} containing the configured reference to GemFire's JCA ResourceAdapter + * registered in the managed environment's JNDI context. + */ + public String getGemFireJcaResourceAdapterJndiName() { + return this.gemfireJcaResourceAdapterJndiName; + } + + /** + * Returns the configured, fully-qualified classname of the {@link javax.naming.spi.InitialContextFactory} + * used to construct the {@link InitialContext} that is then used to lookup managed objects registered + * in the JNDI context of the managed environment. + * + * @return the configured, fully-qualified classname of the {@link javax.naming.spi.InitialContextFactory} + * used to construct the {@link InitialContext}. + */ + public String getInitialContextFactory() { + return this.initialContextFactory; + } + + /** + * Returns a reference to the {@link Logger} used to record debug, info, warning and error messages + * logged by the application. + * + * @return a reference to the configured {@link Logger} used by the application for logging purposes. + * @see org.slf4j.Logger + */ + protected Logger getLogger() { + return this.logger; + } + + /** + * Sets the order of this AOP Aspect relative to other Aspects in the chain of Aspects configured + * in Spring's Transaction Management. + * + * @param order int value specifying the relative order of this Aspect. + * @see org.springframework.core.Ordered + */ + public void setOrder(int order) { + this.order = order; + } + + /** + * Returns the order of this AOP Aspect relative to other Aspects in the chain of Aspects configured + * in Spring's Transaction Management. + * + * @return an int value specifying the relative order of this Aspect. + * @see org.springframework.core.Ordered + */ + @Override + public int getOrder() { + + return Optional.of(this.order) + .filter(order -> !(order == Integer.MAX_VALUE || order == Integer.MIN_VALUE)) + .orElseGet(this::getDefaultOrder); + } + + /** + * Returns the default order used by this AOP Aspect in the chain of Aspects configured + * in Spring's Transaction Management. + * + * @return an int value specifying the default order used by this AOP Aspect in the chain of Aspects + * configured in Spring's Transaction Management. + */ + protected Integer getDefaultOrder() { + return DEFAULT_ORDER; + } + + /** + * Returns the URL of the Naming Context Provider as a {@link String}. + * + * @return the URL of the Naming Context Provider. + */ + public String getProviderUrl() { + return this.providerUrl; + } + + /** + * Determines whether an Exception should be thrown when this Aspect is unable to perform its function. + * + * Alternatively (and by default) the error condition is simply logged. + * + * Defaults to {@literal false}. + * + * @return a boolean value indicating whether this Aspect should throw an Exception when an error occurs + * preventing this Aspect from performing its normal function. + */ + public boolean isThrowOnError() { + return this.throwOnError; + } + + /** + * Defines an AOP Pointcut identifying Join Points during the execution of the application's components + * on which this Aspect should be applied. + * + * This particular Pointcut identifies {@link org.springframework.transaction.annotation.Transactional} annotated + * application service {@link Class types}. That is, the {@link org.springframework.transaction.annotation.Transactional} + * annotation is used at the class level. + * + * @see org.aspectj.lang.annotation.Pointcut + */ + @Pointcut("@within(org.springframework.transaction.annotation.Transactional)") + protected void atTransactionalType() {} + + /** + * Defines an AOP Pointcut identifying Join Points during the execution of the application's components + * on which this Aspect should be applied. + * + * This particular Pointcut identifies {@link org.springframework.transaction.annotation.Transactional} annotated + * application service {@link java.lang.reflect.Method methods}. That is, the + * {@link org.springframework.transaction.annotation.Transactional} annotation is used at the service class, + * service {@link java.lang.reflect.Method method} level. + * + * @see org.aspectj.lang.annotation.Pointcut + */ + @Pointcut("@annotation(org.springframework.transaction.annotation.Transactional)") + protected void atTransactionalMethod() {} + + /** + * Formats the given {@link String message} with the provided array of {@link Object arguments}. + * + * @param message {@link String} containing the message to format. + * @param args array of {@link Object arguments} used to format the message. + * @return the {@link String message} formatted with the provided array of {@link Object arguments}. + * @see java.lang.String#format(String, Object...) + */ + protected String format(String message, Object... args) { + return String.format(message, args); + } + + /** + * Logs the given {@link String message} formatted with the given array of {@link Object arguments} + * at {@link Level#DEBUG} level when debugging is enabled. + * + * @param {@link Class} type extension of {@link AbstractGemFireAsLastResourceAspectSupport}. + * @param message {@link String} containing the message to log. + * @param args array of {@link Object arguments} used to format the message. + * @return this aspect. + * @see org.slf4j.Logger#isDebugEnabled() + * @see org.slf4j.Logger#debug(String, Object...) + * @see #format(String, Object...) + */ + @SuppressWarnings("unchecked") + protected T logDebugInfo(String message, Object... args) { + + Logger logger = getLogger(); + + if (logger.isDebugEnabled()) { + logger.debug(format(message, args), args); + } + + return (T) this; + } + + /** + * Logs the given {@link String message} formatted with the given array of {@link Object arguments} + * at {@link Level#INFO} level when info logging is enabled. + * + * @param {@link Class} type extension of {@link AbstractGemFireAsLastResourceAspectSupport}. + * @param message {@link String} containing the message to log. + * @param args array of {@link Object arguments} used to format the message. + * @return this aspect. + * @see org.slf4j.Logger#isInfoEnabled() + * @see org.slf4j.Logger#info(String, Object...) + * @see #format(String, Object...) + */ + @SuppressWarnings("unchecked") + protected T logInfo(String message, Object... args) { + + Logger logger = getLogger(); + + if (logger.isInfoEnabled()) { + logger.info(format(message, args), args); + } + + return (T) this; + } + + /** + * Logs the given {@link String message} formatted with the given array of {@link Object arguments} + * at {@link Level#TRACE} level when tracing is enabled. + * + * @param {@link Class} type extension of {@link AbstractGemFireAsLastResourceAspectSupport}. + * @param message {@link String} containing the message to log. + * @param args array of {@link Object arguments} used to format the message. + * @return this aspect. + * @see java.util.function.Supplier + * @see #logTraceInfo(Supplier) + */ + protected T logTraceInfo(String message, Object... args) { + return logTraceInfo(() -> format(message, args)); + } + + /** + * Logs the given {@link Supplier log message} at {@link Level#TRACE} level when tracing is enabled. + * + * @param {@link Class} type extension of {@link AbstractGemFireAsLastResourceAspectSupport}. + * @param logMessage {@link Supplier} of the message to log. + * @return this aspect. + * @see java.util.function.Supplier + * @see org.slf4j.Logger#isTraceEnabled() + * @see org.slf4j.Logger#trace(String) + */ + @SuppressWarnings("unchecked") + protected T logTraceInfo(Supplier logMessage) { + + Logger logger = getLogger(); + + if (logger.isTraceEnabled()) { + logger.trace(logMessage.get()); + } + + return (T) this; + } + + /** + * Logs the given {@link String message} formatted with the given array of {@link Object arguments} + * at {@link Level#WARN} level when warnings are enabled. + * + * @param {@link Class} type extension of {@link AbstractGemFireAsLastResourceAspectSupport}. + * @param message {@link String} containing the message to log. + * @param args array of {@link Object arguments} used to format the message. + * @return this aspect. + * @see org.slf4j.Logger#isWarnEnabled() + * @see org.slf4j.Logger#warn(String, Object...) + * @see #format(String, Object...) + */ + @SuppressWarnings("unchecked") + protected T logWarning(String message, Object... args) { + + Logger logger = getLogger(); + + if (logger.isWarnEnabled()) { + logger.warn(format(message, args), args); + } + + return (T) this; + } + + /** + * Logs the given {@link String message} formatted with the given array of {@link Object arguments} + * at {@link Level#ERROR} level when error logging is enabled. + * + * @param {@link Class} type extension of {@link AbstractGemFireAsLastResourceAspectSupport}. + * @param message {@link String} containing the message to log. + * @param args array of {@link Object arguments} used to format the message. + * @return this aspect. + * @see org.slf4j.Logger#isErrorEnabled() + * @see org.slf4j.Logger#error(String, Object...) + * @see #format(String, Object...) + */ + @SuppressWarnings("unchecked") + protected T logError(String message, Object... args) { + + Logger logger = getLogger(); + + if (logger.isErrorEnabled()) { + logger.error(format(message, args), args); + } + + return (T) this; + } + + /** + * Constructs a new instance of the {@link InitialContext} configured with the given {@link Hashtable environment}. + * + * @param environment {@link Hashtable} containing environment configuration meta-data used to access + * the JNDI context in a managed environment. + * @return a new instance of the {@link InitialContext} configured with the given {@link Hashtable environment}. + * @throws NamingException if the {@link InitialContext} could not be initialized with + * the provided {@link Hashtable environment}. + * @see javax.naming.InitialContext + * @see java.util.Hashtable + */ + protected InitialContext newInitialContext(Hashtable environment) throws NamingException { + return new InitialContext(environment); + } + + /** + * Constructs a new instance of {@link Logger} used by this application to log messages. + * + * @return a new, configured instance of {@link Logger}. + */ + protected Logger newLogger() { + return LoggerFactory.getLogger(getClass()); + } + + /** + * Resolves the {@link Context} used to perform lookups of registered, managed objects in a management environment. + * + * @return the {@link Context} used to perform lookups of registered, managed objects in a managed environment. + * @throws IllegalStateException if the {@link Context} could not be resolved. + * @see org.apache.geode.cache.GemFireCache#getJNDIContext() + * @see #newInitialContext(Hashtable) + * @see #resolveEnvironment() + * @see #resolveGemFireCache() + * @see #getContext() + * @see javax.naming.Context + */ + protected synchronized Context resolveContext() { + + Context context = getContext(); + + if (context == null) { + + Hashtable resolvedEnvironment = resolveEnvironment(); + + try { + context = this.context = newInitialContext(resolvedEnvironment); + } + catch (NamingException cause) { + context = this.context = Optional.ofNullable(resolveGemFireCache()).map(GemFireCache::getJNDIContext) + .orElseThrow(() -> newIllegalStateException(cause, + "Failed to initialize an %1$s with the provided Environment configuration ['%2$s']", + InitialContext.class.getName(), resolvedEnvironment)); + } + } + + return context; + } + + /** + * Resolves the {@link Hashtable environment} used by the application to configure the {@link InitialContext}. + * + * @return the resolved {@link Hashtable environment} used to configure the {@link InitialContext}. + * @see #getInitialContextFactory() + * @see #getProviderUrl() + * @see java.util.Hashtable + */ + protected Hashtable resolveEnvironment() { + + Hashtable environment = new Hashtable<>(); + + environment.put(Context.INITIAL_CONTEXT_FACTORY, getInitialContextFactory()); + environment.put(Context.PROVIDER_URL, getProviderUrl()); + + return environment; + } + + /** + * Resolves a reference to the {@link GemFireCache} required by this Aspect to perform its function. + * + * This method either returns the configured {@link GemFireCache} instance or looks up + * the {@link GemFireCache} instance using GemFire's API. + * + * @return a reference to the resolved {@link GemFireCache} instance. + * @see org.springframework.data.gemfire.GemfireUtils#resolveGemFireCache() + * @see org.apache.geode.cache.GemFireCache + * @see #getGemFireCache() + */ + protected synchronized GemFireCache resolveGemFireCache() { + + GemFireCache gemfireCache = getGemFireCache(); + + if (gemfireCache == null) { + gemfireCache = this.gemfireCache = GemfireUtils.resolveGemFireCache(); + } + + return gemfireCache; + } + + /** + * Resolves the configured JNDI name used to lookup and resolve the GemFire JCA ResourceAdapter object + * from the JNDI context in the managed environment. + * + * @return a {@link String} containing the JNDI name used to lookup and resolve the GemFire JCA ResourceAdapter + * object from the JNDI context in the managed environment. + * @see #getGemFireJcaResourceAdapterJndiName() + */ + protected String resolveGemFireJcaResourceAdapterJndiName() { + + String gemfireJcaResourceAdapterJndiName = getGemFireJcaResourceAdapterJndiName(); + + return (StringUtils.hasText(gemfireJcaResourceAdapterJndiName) ? gemfireJcaResourceAdapterJndiName + : DEFAULT_GEMFIRE_JCA_RESOURCE_ADAPTER_JNDI_NAME); + } + + /** + * Builder method used to set the {@link #isThrowOnError() throwOnError} property. + * + * The {@link #isThrowOnError() throwOnError} property is used to indicate whether an Exception should be thrown + * when an error occurs during the normal operation and function of this Aspect. By default, the error condition + * is simply logged. + * + * @param {@link Class sub-type} of this Aspect. + * @param throwOnError boolean value used to set the {@link #isThrowOnError() throwOnError} property. + * @return this Aspect. + * @see #isThrowOnError() + */ + @SuppressWarnings("unchecked") + public T withThrowOnError(boolean throwOnError) { + this.throwOnError = throwOnError; + return (T) this; + } + + protected static class GemFireConnectionHolder { + + private static final ThreadLocal gemfireConnection = new ThreadLocal<>(); + + public static GFConnection acquire(GFConnectionFactory connectionFactory) { + return acquire(connectionFactory, DEFAULT_THROW_ON_ERROR, NO_OP_LOGGER); + } + + public static GFConnection acquire(GFConnectionFactory connectionFactory, boolean throwOnError, + Consumer logger) { + + try { + return of(connectionFactory.getConnection()); + } + catch (ResourceException cause) { + + String message = + String.format("Failed to acquire GemFire Connection from GemFire's JCA ResourceAdapter: %s", + cause.getMessage()); + + if (throwOnError) { + throw newRuntimeException(cause, message); + } + else { + logger.accept(message); + return null; + } + } + } + + /* (non-Javadoc) */ + public static GFConnection of(GFConnection connection) { + gemfireConnection.set(connection); + return connection; + } + + /* (non-Javadoc) */ + public static Optional get() { + return Optional.ofNullable(gemfireConnection.get()); + } + + public static void close() { + close(DEFAULT_THROW_ON_ERROR, NO_OP_LOGGER); + } + + /* (non-Javadoc) */ + public static void close(boolean throwOnError, Consumer logger) { + + get().ifPresent(connection -> { + try { + connection.close(); + } + catch (ResourceException cause) { + + String message = String.format("Failed to close GemFire Connection: %s", cause.getMessage()); + + if (throwOnError) { + throw newRuntimeException(cause, message); + } + else { + logger.accept(message); + } + } + }); + } + } +} diff --git a/src/main/java/org/springframework/data/gemfire/config/annotation/support/GemFireAsLastResourceConnectionAcquiringAspect.java b/src/main/java/org/springframework/data/gemfire/config/annotation/support/GemFireAsLastResourceConnectionAcquiringAspect.java new file mode 100644 index 00000000..b505752f --- /dev/null +++ b/src/main/java/org/springframework/data/gemfire/config/annotation/support/GemFireAsLastResourceConnectionAcquiringAspect.java @@ -0,0 +1,107 @@ +/* + * Copyright 2017 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.data.gemfire.config.annotation.support; + +import static org.springframework.data.gemfire.util.RuntimeExceptionFactory.newRuntimeException; + +import javax.naming.NamingException; + +import org.apache.geode.ra.GFConnectionFactory; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Before; +import org.springframework.beans.factory.annotation.Autowired; + +/** + * The {@link GemFireAsLastResourceConnectionAcquiringAspect} class is a {@link AbstractGemFireAsLastResourceAspectSupport} + * implementation responsible for acquiring a GemFire Connection from GemFire's JCA ResourceAdapter, + * {@link GFConnectionFactory} after a CMT/JTA Transaction is began, which is initiated by + * Spring's Transaction infrastructure. + * + * @author John Blum + * @see org.apache.geode.ra.GFConnectionFactory + * @see org.aspectj.lang.annotation.Aspect + * @see org.aspectj.lang.annotation.Before + * @see org.springframework.data.gemfire.config.annotation.support.AbstractGemFireAsLastResourceAspectSupport + * @since 2.0.0 + */ +@SuppressWarnings("unused") +@Aspect +public class GemFireAsLastResourceConnectionAcquiringAspect extends AbstractGemFireAsLastResourceAspectSupport { + + private static final int DEFAULT_ORDER = 2048000; + + @Autowired(required = false) + private GFConnectionFactory gemfireConnectionFactory; + + /** + * Acquires (opens) a GemFire JCA ResourceAdapter Connection after the Spring CMT/JTA Transaction begins. + */ + @Before("atTransactionalType() || atTransactionalMethod()") + public void doGemFireConnectionFactoryGetConnection() { + + logTraceInfo("Acquiring GemFire Connection from GemFire JCA ResourceAdapter registered at [%s]...", + resolveGemFireJcaResourceAdapterJndiName()); + + GemFireConnectionHolder.acquire(resolveGemFireConnectionFactory(), isThrowOnError(), this::logError); + } + + /* (non-Javadoc) */ + synchronized GFConnectionFactory resolveGemFireConnectionFactory() { + + GFConnectionFactory connectionFactory = getGemFireConnectionFactory(); + + if (connectionFactory == null) { + + String resolvedGemFireJcaResourceAdapterJndiName = resolveGemFireJcaResourceAdapterJndiName(); + + try { + connectionFactory = this.gemfireConnectionFactory = + (GFConnectionFactory) resolveContext().lookup(resolvedGemFireJcaResourceAdapterJndiName); + } + catch (NamingException cause) { + throw newRuntimeException(cause, + "Failed to resolve a GFConnectionFactory from the configured JNDI context name [%s]", + resolvedGemFireJcaResourceAdapterJndiName); + } + } + + return connectionFactory; + } + + + /** + * Returns the default order used by this AOP Aspect in the chain of Aspects configured + * in Spring's Transaction Management. + * + * @return an int value specifying the default order used by this AOP Aspect in the chain of Aspects + * configured in Spring's Transaction Management. + */ + @Override + protected Integer getDefaultOrder() { + return DEFAULT_ORDER; + } + + /** + * Returns a reference to the configured {@link GFConnectionFactory} instance. + * + * @return a reference to the configured {@link GFConnectionFactory} instance; may be {@literal null}. + * @see org.apache.geode.ra.GFConnectionFactory + */ + public synchronized GFConnectionFactory getGemFireConnectionFactory() { + return this.gemfireConnectionFactory; + } +} diff --git a/src/main/java/org/springframework/data/gemfire/config/annotation/support/GemFireAsLastResourceConnectionClosingAspect.java b/src/main/java/org/springframework/data/gemfire/config/annotation/support/GemFireAsLastResourceConnectionClosingAspect.java new file mode 100644 index 00000000..8951ceac --- /dev/null +++ b/src/main/java/org/springframework/data/gemfire/config/annotation/support/GemFireAsLastResourceConnectionClosingAspect.java @@ -0,0 +1,62 @@ +/* + * Copyright 2017 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.data.gemfire.config.annotation.support; + +import org.aspectj.lang.annotation.After; +import org.aspectj.lang.annotation.Aspect; + +/** + * The {@link GemFireAsLastResourceConnectionClosingAspect} class is a {@link AbstractGemFireAsLastResourceAspectSupport} + * implementation responsible for closing the GemFire Connection obtained from the GemFire JCA ResourceAdapter + * deployed in a managed environment when using GemFire as the Last Resource in a CMT/JTA Transaction + * initiated from Spring's Transaction infrastructure. + * + * @author John Blum + * @see org.aspectj.lang.annotation.Aspect + * @see org.aspectj.lang.annotation.After + * @see org.springframework.data.gemfire.config.annotation.support.AbstractGemFireAsLastResourceAspectSupport + * @since 2.0.0 + */ +@SuppressWarnings("unused") +@Aspect +public class GemFireAsLastResourceConnectionClosingAspect extends AbstractGemFireAsLastResourceAspectSupport { + + private static final int DEFAULT_ORDER = 1024000; + + /** + * Closes the GemFire JCA ResourceAdapter Connection after the Spring CMT/JTA Transaction completes. + */ + @After("atTransactionalType() || atTransactionalMethod()") + public void doGemFireConnectionClose() { + + logTraceInfo("Closing GemFire Connection..."); + + GemFireConnectionHolder.close(isThrowOnError(), this::logWarning); + } + + /** + * Returns the default order used by this AOP Aspect in the chain of Aspects configured + * in Spring's Transaction Management. + * + * @return an int value specifying the default order used by this AOP Aspect in the chain of Aspects + * configured in Spring's Transaction Management. + */ + @Override + protected Integer getDefaultOrder() { + return DEFAULT_ORDER; + } +} diff --git a/src/main/java/org/springframework/data/gemfire/config/annotation/support/RegionDataAccessTracingAspect.java b/src/main/java/org/springframework/data/gemfire/config/annotation/support/RegionDataAccessTracingAspect.java new file mode 100644 index 00000000..466e7b67 --- /dev/null +++ b/src/main/java/org/springframework/data/gemfire/config/annotation/support/RegionDataAccessTracingAspect.java @@ -0,0 +1,83 @@ +/* + * Copyright 2017 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.data.gemfire.config.annotation.support; + +import java.io.PrintWriter; +import java.io.StringWriter; + +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Before; +import org.aspectj.lang.annotation.Pointcut; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The RegionDataAccessTracingAspect class... + * + * @author John Blum + * @since 1.0.0 + */ +@Aspect +@SuppressWarnings("unused") +public class RegionDataAccessTracingAspect { + + private final Logger logger = newLogger(); + + /* (non-Javadoc) */ + protected Logger newLogger() { + return LoggerFactory.getLogger(getClass()); + } + + /* (non-Javadoc) */ + protected String getCurrentThreadStackTrace() { + + StringWriter writer = new StringWriter(); + + new Throwable().printStackTrace(new PrintWriter(writer)); + + return writer.toString(); + } + + /* (non-Javadoc) */ + protected Logger getLogger() { + return this.logger; + } + + /* (non-Javadoc) */ + @Pointcut("target(com.gemstone.gemfire.cache.Region)") + private void regionPointcut() {} + + /* (non-Javadoc) */ + @Pointcut("execution(* com.gemstone.gemfire.cache.Region.create(..))" + + " || execution(* com.gemstone.gemfire.cache.Region.get(..))" + + " || execution(* com.gemstone.gemfire.cache.Region.getAll(..))" + + " || execution(* com.gemstone.gemfire.cache.Region.put(..))" + + " || execution(* com.gemstone.gemfire.cache.Region.putAll(..))" + + " || execution(* com.gemstone.gemfire.cache.Region.putIfAbsent(..))" + + " || execution(* com.gemstone.gemfire.cache.Region.remove(..))" + + " || execution(* com.gemstone.gemfire.cache.Region.replace(..))" + + " || execution(* com.gemstone.gemfire.cache.Region.selectValue(..))" + + " || execution(* com.gemstone.gemfire.cache.Region.values(..))" + + "") + private void regionDataAccessPointcut() {} + + /* (non-Javadoc) */ + @Before("regionPointcut() && regionDataAccessPointcut()") + public void regionDataAccessTracingAdvice() { + getLogger().trace("Region data access call [{}]", getCurrentThreadStackTrace()); + } +} diff --git a/src/test/java/org/springframework/data/gemfire/config/annotation/GemFireAsLastResourceConfigurationUnitTests.java b/src/test/java/org/springframework/data/gemfire/config/annotation/GemFireAsLastResourceConfigurationUnitTests.java new file mode 100644 index 00000000..c509cab8 --- /dev/null +++ b/src/test/java/org/springframework/data/gemfire/config/annotation/GemFireAsLastResourceConfigurationUnitTests.java @@ -0,0 +1,231 @@ +/* + * Copyright 2017 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.data.gemfire.config.annotation; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import org.apache.geode.cache.GemFireCache; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import org.springframework.context.annotation.AdviceMode; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.annotation.AnnotationAttributes; +import org.springframework.core.type.AnnotationMetadata; +import org.springframework.data.gemfire.config.annotation.support.GemFireAsLastResourceConnectionAcquiringAspect; +import org.springframework.data.gemfire.config.annotation.support.GemFireAsLastResourceConnectionClosingAspect; +import org.springframework.transaction.annotation.EnableTransactionManagement; + +/** + * Unit tests for {@link GemFireAsLastResourceConfiguration}. + * + * @author John Blum + * @see org.junit.Test + * @see org.junit.runner.RunWith + * @see org.mockito.Mock + * @see org.mockito.Mockito + * @see org.mockito.junit.MockitoJUnitRunner + * @see org.springframework.data.gemfire.config.annotation.GemFireAsLastResourceConfiguration + * @since 2.0.0 + */ +@RunWith(MockitoJUnitRunner.class) +public class GemFireAsLastResourceConfigurationUnitTests { + + @Mock + private AnnotationMetadata mockImportMetadata; + + private GemFireAsLastResourceConfiguration configuration; + + @Before + public void setup() { + configuration = new GemFireAsLastResourceConfiguration(); + } + + private AnnotationMetadata mockEnableTransactionManagementWithOrder(Integer order) { + + Map enableTransactionManagementAttributes = Collections.singletonMap("order", order); + + when(mockImportMetadata.getAnnotationAttributes(eq(EnableTransactionManagement.class.getName()))) + .thenReturn(enableTransactionManagementAttributes); + + return mockImportMetadata; + } + + @Test + public void resolveEnableTransactionManagementAttributes() { + + Map enableTransactionManagementAttributesMap = new HashMap<>(); + + enableTransactionManagementAttributesMap.put("mode", AdviceMode.ASPECTJ); + enableTransactionManagementAttributesMap.put("order", 1); + enableTransactionManagementAttributesMap.put("proxyTargetClass", true); + + when(mockImportMetadata.getAnnotationAttributes(eq(EnableTransactionManagement.class.getName()))) + .thenReturn(enableTransactionManagementAttributesMap); + + AnnotationAttributes enableTransactionManagementAttributes = + configuration.resolveEnableTransactionManagementAttributes(mockImportMetadata); + + assertThat(enableTransactionManagementAttributes).isNotNull(); + assertThat(enableTransactionManagementAttributes.getEnum("mode")) + .isEqualTo(AdviceMode.ASPECTJ); + assertThat(enableTransactionManagementAttributes.getBoolean("proxyTargetClass")).isTrue(); + + verify(mockImportMetadata, times(1)) + .getAnnotationAttributes(eq(EnableTransactionManagement.class.getName())); + } + + @Test(expected = IllegalStateException.class) + public void resolveEnableTransactionManagementAttributesThrowsIllegalStateException() { + try { + when(mockImportMetadata.getAnnotationAttributes(anyString())).thenReturn(null); + + configuration.resolveEnableTransactionManagementAttributes(mockImportMetadata); + } + catch (IllegalStateException expected) { + + assertThat(expected).hasMessage(String.format( + "The @%1$s annotation may only be used on a Spring application @%2$s class" + + " that is also annotated with @%3$s having an explicit [order] set", + EnableGemFireAsLastResource.class.getSimpleName(), Configuration.class.getSimpleName(), + EnableTransactionManagement.class.getSimpleName())); + + assertThat(expected).hasNoCause(); + + throw expected; + } + } + + @Test + public void resolveEnableTransactionManagementOrderEqualsOne() { + assertThat(configuration.resolveEnableTransactionManagementOrder( + mockEnableTransactionManagementWithOrder(1))).isEqualTo(1); + } + + @Test(expected = IllegalArgumentException.class) + public void resolveEnableTransactionManagementOrderThrowsIllegalArgumentExceptionForIntegerMaxValue() { + try { + configuration.resolveEnableTransactionManagementOrder( + mockEnableTransactionManagementWithOrder(Integer.MAX_VALUE)); + } + catch (IllegalArgumentException expected) { + + assertThat(expected).hasMessage(String.format( + "The @%1$s(order) attribute value [%2$d] must be explicitly set to a value other than" + + " Integer.MAX_VALUE or Integer.MIN_VALUE", + EnableTransactionManagement.class.getSimpleName(), Integer.MAX_VALUE)); + + assertThat(expected).hasNoCause(); + + throw expected; + } + } + + @Test(expected = IllegalArgumentException.class) + public void resolveEnableTransactionManagementOrderThrowsIllegalArgumentExceptionForIntegerMinValue() { + try { + configuration.resolveEnableTransactionManagementOrder( + mockEnableTransactionManagementWithOrder(Integer.MIN_VALUE)); + } + catch (IllegalArgumentException expected) { + + assertThat(expected).hasMessage(String.format( + "The @%1$s(order) attribute value [%2$d] must be explicitly set to a value other than" + + " Integer.MAX_VALUE or Integer.MIN_VALUE", + EnableTransactionManagement.class.getSimpleName(), Integer.MIN_VALUE)); + + assertThat(expected).hasNoCause(); + + throw expected; + } + } + + @Test + public void getEnableTransactionManagementOrderReturnsValue() { + + configuration.setImportMetadata(mockEnableTransactionManagementWithOrder(101)); + + assertThat(configuration.getEnableTransactionManagementOrder()).isEqualTo(101); + } + + @Test(expected = IllegalStateException.class) + public void getEnableTransactionManagementOrderThrowsIllegalStateException() { + try { + configuration.getEnableTransactionManagementOrder(); + } + catch (IllegalStateException expected) { + + assertThat(expected).hasMessage(String.format("The @%1$s(order) attribute was not properly set [null];" + + " Also, please make your Spring application @%2$s annotated class is annotated with both @%3$s and @%1$s", + EnableTransactionManagement.class.getSimpleName(), Configuration.class.getSimpleName(), + EnableGemFireAsLastResource.class.getSimpleName())); + + assertThat(expected).hasNoCause(); + + throw expected; + } + } + + @Test + public void gemfireCachePostProcessorSetsCopyOnReadToTrue() { + + GemFireCache mockGemFireCache = mock(GemFireCache.class); + + assertThat(configuration.gemfireCachePostProcessor(mockGemFireCache)).isNull(); + + verify(mockGemFireCache, times(1)).setCopyOnRead(eq(true)); + } + + @Test + public void gemfireCachePostProcessorHandlesNull() { + assertThat(configuration.gemfireCachePostProcessor(null)).isNull(); + } + + @Test + public void gemfireJcaConnectionAcquiringAspectIsOrderPlusOne() { + + configuration.setImportMetadata(mockEnableTransactionManagementWithOrder(0)); + + GemFireAsLastResourceConnectionAcquiringAspect aspect = configuration.gemfireJcaConnectionAcquiringAspect(); + + assertThat(aspect).isNotNull(); + assertThat(aspect.getOrder()).isEqualTo(1); + } + + @Test + public void gemfireJcaConnectionClosingAspectIsOrderMinusOne() { + + configuration.setImportMetadata(mockEnableTransactionManagementWithOrder(0)); + + GemFireAsLastResourceConnectionClosingAspect aspect = configuration.gemfireJcaConnectionClosingAspect(); + + assertThat(aspect).isNotNull(); + assertThat(aspect.getOrder()).isEqualTo(-1); + } +} diff --git a/src/test/java/org/springframework/data/gemfire/config/annotation/support/AbstractGemFireAsLastResourceAspectSupportUnitTests.java b/src/test/java/org/springframework/data/gemfire/config/annotation/support/AbstractGemFireAsLastResourceAspectSupportUnitTests.java new file mode 100644 index 00000000..fbdae19d --- /dev/null +++ b/src/test/java/org/springframework/data/gemfire/config/annotation/support/AbstractGemFireAsLastResourceAspectSupportUnitTests.java @@ -0,0 +1,449 @@ +/* + * Copyright 2017 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.data.gemfire.config.annotation.support; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyZeroInteractions; +import static org.mockito.Mockito.when; +import static org.springframework.data.gemfire.util.RuntimeExceptionFactory.newIllegalArgumentException; + +import java.util.Arrays; +import java.util.Hashtable; +import java.util.List; +import java.util.Optional; + +import javax.naming.Context; +import javax.naming.InitialContext; +import javax.naming.NamingException; + +import org.apache.geode.cache.GemFireCache; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentMatcher; +import org.mockito.Mock; +import org.mockito.Spy; +import org.mockito.internal.matchers.VarargMatcher; +import org.mockito.junit.MockitoJUnitRunner; +import org.slf4j.Logger; + +/** + * Unit tests for {@link AbstractGemFireAsLastResourceAspectSupport}. + * + * @author John Blum + * @see javax.naming.Context + * @see javax.naming.InitialContext + * @see org.junit.Test + * @see org.junit.runner.RunWith + * @see org.mockito.Mock + * @see org.mockito.Mockito + * @see org.mockito.Spy + * @see org.mockito.junit.MockitoJUnitRunner + * @see org.apache.geode.cache.GemFireCache + * @see org.springframework.data.gemfire.config.annotation.support.AbstractGemFireAsLastResourceAspectSupport + * @since 2.0.0 + */ +@RunWith(MockitoJUnitRunner.class) +public class AbstractGemFireAsLastResourceAspectSupportUnitTests { + + @Mock + private Logger mockLogger; + + @Spy + private AbstractGemFireAsLastResourceAspectSupport aspectSupport; + + @Before + @SuppressWarnings("all") + public void setup() { + doReturn(mockLogger).when(aspectSupport).getLogger(); + } + + @Test + public void setAndGetOrder() { + + assertThat(aspectSupport.getOrder()).isEqualTo(AbstractGemFireAsLastResourceAspectSupport.DEFAULT_ORDER); + + aspectSupport.setOrder(1); + + assertThat(aspectSupport.getOrder()).isEqualTo(1); + + aspectSupport.setOrder(-2); + + assertThat(aspectSupport.getOrder()).isEqualTo(-2); + + aspectSupport.setOrder(Integer.MAX_VALUE); + + assertThat(aspectSupport.getOrder()).isEqualTo(AbstractGemFireAsLastResourceAspectSupport.DEFAULT_ORDER); + + aspectSupport.setOrder(Integer.MIN_VALUE); + + assertThat(aspectSupport.getOrder()).isEqualTo(AbstractGemFireAsLastResourceAspectSupport.DEFAULT_ORDER); + } + + @Test + public void logDebugInfoWhenDebuggingIsDisabled() { + + when(mockLogger.isDebugEnabled()).thenReturn(false); + + assertThat(aspectSupport.logDebugInfo("message", "arg")) + .isSameAs(aspectSupport); + + verify(mockLogger, times(1)).isDebugEnabled(); + verify(mockLogger, never()).debug(anyString()); + verify(mockLogger, never()).debug(anyString(), any(Object.class)); + verify(mockLogger, never()).debug(anyString(), any(Object.class), any(Object.class)); + verify(mockLogger, never()).debug(anyString(), any(Object[].class)); + } + + @Test + public void logDebugInfoWhenDebuggingIsEnabled() { + + when(mockLogger.isDebugEnabled()).thenReturn(true); + + assertThat(aspectSupport.logDebugInfo("test %s message", + "debug", "test")) .isSameAs(aspectSupport); + + verify(mockLogger, times(1)).isDebugEnabled(); + // TODO why the f#&k does this not work Mockito?! + //verify(mockLogger, times(1)) + // .debug(eq("test debug message"), eq("debug"), eq("test")); + // TODO this ridiculous sh!t works + //verify(mockLogger, times(1)).debug(eq("test debug message"), + // ArgumentMatchers.any()); + // TODO and so does this, but what a hack! + verify(mockLogger, times(1)).debug(eq("test debug message"), + VariableArgumentMatcher.varArgThat("debug", "test")); + } + + @Test + public void logInfoWhenInfoIsDisabled() { + + when(mockLogger.isInfoEnabled()).thenReturn(false); + + assertThat(aspectSupport.logInfo("message", "arg")) + .isSameAs(aspectSupport); + + verify(mockLogger, times(1)).isInfoEnabled(); + verify(mockLogger, never()).info(anyString(), any(Object.class)); + verify(mockLogger, never()).info(anyString(), any(Object.class), any(Object.class)); + verify(mockLogger, never()).info(anyString(), any(Object[].class)); + } + + @Test + public void logInfoWhenInfoIsEnabled() { + + when(mockLogger.isInfoEnabled()).thenReturn(true); + + assertThat(aspectSupport.logInfo("test %s message", "info")) + .isSameAs(aspectSupport); + + verify(mockLogger, times(1)).isInfoEnabled(); + verify(mockLogger, times(1)).info(eq("test info message"), + VariableArgumentMatcher.varArgThat("info")); + } + + @Test + public void logTraceInfoWhenTracingIsDisabled() { + + when(mockLogger.isTraceEnabled()).thenReturn(false); + + assertThat(aspectSupport.logTraceInfo("message", "arg")) + .isSameAs(aspectSupport); + + verify(mockLogger, times(1)).isTraceEnabled(); + verify(mockLogger, never()).trace(anyString()); + verify(mockLogger, never()).trace(anyString(), any(Object.class)); + verify(mockLogger, never()).trace(anyString(), any(Object.class), any(Object.class)); + verify(mockLogger, never()).trace(anyString(), any(Object[].class)); + } + + @Test + public void logTraceInfoWhenTracingIsEnabled() { + + when(mockLogger.isTraceEnabled()).thenReturn(true); + + assertThat(aspectSupport.logTraceInfo("test %s message", "trace")) + .isSameAs(aspectSupport); + + verify(mockLogger, times(1)).isTraceEnabled(); + verify(mockLogger, times(1)).trace(eq("test trace message")); + } + + @Test + public void logTraceInfoUsingMessageSupplierWhenTracingIsEnabled() { + + when(mockLogger.isTraceEnabled()).thenReturn(true); + + assertThat(aspectSupport.logTraceInfo( + () -> "trace test message with Supplier")).isSameAs(aspectSupport); + + verify(mockLogger, times(1)).isTraceEnabled(); + verify(mockLogger, times(1)).trace(eq("trace test message with Supplier")); + } + + @Test + public void logWarningWhenWarningsAreDisabled() { + + when(mockLogger.isWarnEnabled()).thenReturn(false); + + assertThat(aspectSupport.logWarning("message", "arg")) + .isSameAs(aspectSupport); + + verify(mockLogger, times(1)).isWarnEnabled(); + verify(mockLogger, never()).warn(anyString()); + verify(mockLogger, never()).warn(anyString(), any(Object.class)); + verify(mockLogger, never()).warn(anyString(), any(Object.class), any(Object.class)); + verify(mockLogger, never()).warn(anyString(), any(Object[].class)); + } + + @Test + public void logWarningWhenWarningsAreEnabled() { + + when(mockLogger.isWarnEnabled()).thenReturn(true); + + assertThat(aspectSupport.logWarning("test %s message", "warning")) + .isSameAs(aspectSupport); + + verify(mockLogger, times(1)).isWarnEnabled(); + verify(mockLogger, times(1)).warn(eq("test warning message"), + VariableArgumentMatcher.varArgThat("warning")); + } + + @Test + public void logErrorWhenErrorsAreDisabled() { + + when(mockLogger.isErrorEnabled()).thenReturn(false); + + assertThat(aspectSupport.logError("message", "arg")) + .isSameAs(aspectSupport); + + verify(mockLogger, times(1)).isErrorEnabled(); + verify(mockLogger, never()).error(anyString()); + verify(mockLogger, never()).error(anyString(), any(Object.class)); + verify(mockLogger, never()).error(anyString(), any(Object.class), any(Object.class)); + verify(mockLogger, never()).error(anyString(), any(Object[].class)); + } + + @Test + public void logErrorWhenErrorsAreEnabled() { + + when(mockLogger.isErrorEnabled()).thenReturn(true); + + assertThat(aspectSupport.logError("test %s message", "error")) + .isSameAs(aspectSupport); + + verify(mockLogger, times(1)).isErrorEnabled(); + verify(mockLogger, times(1)).error(eq("test error message"), + VariableArgumentMatcher.varArgThat("error")); + } + + @Test + public void resolveContextReturnsConfiguredContext() throws NamingException { + + Context mockContext = mock(Context.class); + + doReturn(mockContext).when(aspectSupport).getContext(); + + assertThat(aspectSupport.resolveContext()).isSameAs(mockContext); + + verify(aspectSupport, never()).newInitialContext(any(Hashtable.class)); + verifyZeroInteractions(mockContext); + } + + @Test + @SuppressWarnings("all") + public void resolveContextReturnsNewInitialContext() throws NamingException { + + Hashtable mockEnvironment = new Hashtable<>(); + + InitialContext mockContext = mock(InitialContext.class); + + doReturn(mockContext).when(aspectSupport).newInitialContext(any(Hashtable.class)); + doReturn(mockEnvironment).when(aspectSupport).resolveEnvironment(); + + assertThat(aspectSupport.resolveContext()).isEqualTo(mockContext); + + verify(aspectSupport, times(1)).getContext(); + verify(aspectSupport, times(1)).newInitialContext(eq(mockEnvironment)); + verifyZeroInteractions(mockContext); + } + + @Test + public void resolveContextHandlesNamingExceptionAndReturnsGemFireJndiContext() throws NamingException { + + Context mockContext = mock(Context.class); + + GemFireCache mockGemFireCache = mock(GemFireCache.class); + + Hashtable mockEnvironment = new Hashtable<>(); + + doThrow(new NamingException("TEST")).when(aspectSupport).newInitialContext(any(Hashtable.class)); + doReturn(mockEnvironment).when(aspectSupport).resolveEnvironment(); + doReturn(mockGemFireCache).when(aspectSupport).resolveGemFireCache(); + doReturn(mockContext).when(mockGemFireCache).getJNDIContext(); + + assertThat(aspectSupport.resolveContext()).isEqualTo(mockContext); + + verify(aspectSupport, times(1)).getContext(); + verify(aspectSupport, times(1)).resolveEnvironment(); + verify(aspectSupport, times(1)).newInitialContext(eq(mockEnvironment)); + verify(aspectSupport, times(1)).resolveGemFireCache(); + verify(mockGemFireCache, times(1)).getJNDIContext(); + } + + @Test(expected = IllegalStateException.class) + public void resolveContextHandlesNamingExceptionAndThrowsIllegalStateException() throws NamingException { + + Hashtable testEnvironment = new Hashtable<>(); + + testEnvironment.put("key", "test"); + + doThrow(new NamingException("TEST")).when(aspectSupport).newInitialContext(any(Hashtable.class)); + doReturn(testEnvironment).when(aspectSupport).resolveEnvironment(); + doReturn(null).when(aspectSupport).resolveGemFireCache(); + + try { + aspectSupport.resolveContext(); + } + catch (IllegalStateException expected) { + + assertThat(expected).hasMessage( + "Failed to initialize an %1$s with the provided Environment configuration [%2$s]", + InitialContext.class.getName(), testEnvironment.toString()); + + assertThat(expected).hasCauseInstanceOf(NamingException.class); + assertThat(expected.getCause()).hasMessage("TEST"); + assertThat(expected.getCause()).hasNoCause(); + + throw expected; + } + finally { + verify(aspectSupport, times(1)).getContext(); + verify(aspectSupport, times(1)).resolveEnvironment(); + verify(aspectSupport, times(1)).newInitialContext(eq(testEnvironment)); + verify(aspectSupport, times(1)).resolveGemFireCache(); + } + } + + @Test + public void resolveEnvironmentContainsInitialContextFactoryAndProviderUrl() { + + doReturn("org.example.test.naming.AppServerContextFactory").when(aspectSupport) + .getInitialContextFactory(); + + doReturn("jndi:rmi://java/comp:jndi/context").when(aspectSupport).getProviderUrl(); + + Hashtable resolvedEnvironment = aspectSupport.resolveEnvironment(); + + assertThat(resolvedEnvironment).isNotNull(); + assertThat(resolvedEnvironment).hasSize(2); + assertThat(resolvedEnvironment.containsKey(Context.INITIAL_CONTEXT_FACTORY)).isTrue(); + assertThat(resolvedEnvironment.get(Context.INITIAL_CONTEXT_FACTORY)) + .isEqualTo("org.example.test.naming.AppServerContextFactory"); + assertThat(resolvedEnvironment.containsKey(Context.PROVIDER_URL)).isTrue(); + assertThat(resolvedEnvironment.get(Context.PROVIDER_URL)).isEqualTo("jndi:rmi://java/comp:jndi/context"); + } + + @Test + public void resolveGemFireCacheReturnsConfiguredGemFireCache() { + + GemFireCache mockGemFireCache = mock(GemFireCache.class); + + doReturn(mockGemFireCache).when(aspectSupport).getGemFireCache(); + + assertThat(aspectSupport.resolveGemFireCache()).isSameAs(mockGemFireCache); + + verify(aspectSupport, times(1)).getGemFireCache(); + } + + @Test + public void resolveGemFireCacheReturnsNull() { + + assertThat(aspectSupport.resolveGemFireCache()).isNull(); + + verify(aspectSupport, times(1)).getGemFireCache(); + } + + @Test + @SuppressWarnings("all") + public void resolveGemFireJcaResourceAdapterJndiNameReturnsConfiguredJndiName() { + + doReturn("java/comp:gemfire/jca/resourceAdapter").when(aspectSupport) + .getGemFireJcaResourceAdapterJndiName(); + + assertThat(aspectSupport.resolveGemFireJcaResourceAdapterJndiName()) + .isEqualTo("java/comp:gemfire/jca/resourceAdapter"); + + verify(aspectSupport, times(1)).getGemFireJcaResourceAdapterJndiName(); + } + + @Test + public void resolveGemFireJcaResourceAdapterJndiNameReturnsDefaultJndiName() { + assertThat(aspectSupport.getGemFireJcaResourceAdapterJndiName()).isNull(); + assertThat(aspectSupport.resolveGemFireJcaResourceAdapterJndiName()) + .isEqualTo(AbstractGemFireAsLastResourceAspectSupport.DEFAULT_GEMFIRE_JCA_RESOURCE_ADAPTER_JNDI_NAME); + } + + @Test + public void withThrowOnErrorAndIsThrowOnError() { + assertThat(aspectSupport.isThrowOnError()).isFalse(); + assertThat(aspectSupport.withThrowOnError(true)).isSameAs(aspectSupport); + assertThat(aspectSupport.isThrowOnError()).isTrue(); + assertThat(aspectSupport.withThrowOnError(false)).isSameAs(aspectSupport); + assertThat(aspectSupport.isThrowOnError()).isFalse(); + } + + // TODO refactor this BS; damn you Mockito for your inability to match Varargs completely/reliably; WTF! + protected static final class VariableArgumentMatcher implements ArgumentMatcher, VarargMatcher { + + protected static Object[] varArgThat(Object... expectedArguments) { + return argThat(new VariableArgumentMatcher<>(expectedArguments)); + } + + private Object[] expectedArguments; + + @SuppressWarnings("unchecked") + protected VariableArgumentMatcher(Object... expectedArguments) { + this.expectedArguments = Optional.ofNullable(expectedArguments) + .orElseThrow(() -> newIllegalArgumentException("Expected arguments must not be null")); + } + + @Override + @SuppressWarnings("unchecked") + public boolean matches(T actualArgument) { + return asList(this.expectedArguments).containsAll(asList(actualArgument)); + } + + private List asList(Object argument) { + return Arrays.asList(toArray(argument)); + } + + private Object[] toArray(Object argument) { + return (argument instanceof Object[] ? (Object[]) argument : new Object[] { argument }); + } + } +} diff --git a/src/test/java/org/springframework/data/gemfire/config/annotation/support/EnableGemFireAsLastResourceIntegrationTests.java b/src/test/java/org/springframework/data/gemfire/config/annotation/support/EnableGemFireAsLastResourceIntegrationTests.java new file mode 100644 index 00000000..c62f93ef --- /dev/null +++ b/src/test/java/org/springframework/data/gemfire/config/annotation/support/EnableGemFireAsLastResourceIntegrationTests.java @@ -0,0 +1,359 @@ +/* + * Copyright 2017 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.data.gemfire.config.annotation.support; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; + +import javax.resource.ResourceException; + +import org.apache.geode.cache.GemFireCache; +import org.apache.geode.ra.GFConnection; +import org.apache.geode.ra.GFConnectionFactory; +import org.junit.Before; +import org.junit.Test; +import org.springframework.beans.factory.BeanCreationException; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.data.gemfire.config.annotation.EnableGemFireAsLastResource; +import org.springframework.data.gemfire.config.annotation.GemFireAsLastResourceConfiguration; +import org.springframework.stereotype.Service; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.TransactionDefinition; +import org.springframework.transaction.TransactionStatus; +import org.springframework.transaction.annotation.EnableTransactionManagement; +import org.springframework.transaction.annotation.Transactional; + +/** + * Integration tests for {@link EnableGemFireAsLastResource}. + * + * @author John Blum + * @see org.junit.Test + * @see org.mockito.Mock + * @see org.mockito.Mockito + * @see org.apache.geode.ra.GFConnection + * @see org.apache.geode.ra.GFConnectionFactory + * @see org.springframework.data.gemfire.config.annotation.EnableGemFireAsLastResource + * @see org.springframework.data.gemfire.config.annotation.GemFireAsLastResourceConfiguration + * @see org.springframework.transaction.PlatformTransactionManager + * @see org.springframework.transaction.annotation.EnableTransactionManagement + * @see org.springframework.transaction.annotation.Transactional + * @since 2.0.0 + */ +public class EnableGemFireAsLastResourceIntegrationTests { + + private static List transactionEvents = new ArrayList<>(); + + @Before + public void setup() { + transactionEvents.clear(); + } + + private ConfigurableApplicationContext newApplicationContext(Class... annotatedClasses) { + ConfigurableApplicationContext applicationContext = new AnnotationConfigApplicationContext(annotatedClasses); + applicationContext.registerShutdownHook(); + return applicationContext; + } + + @Test + public void configurationIsCorrect() { + + ConfigurableApplicationContext applicationContext = + newApplicationContext(TestGemFireAsLastResourceConfiguration.class); + + assertThat(applicationContext).isNotNull(); + + GemFireCache gemfireCache = applicationContext.getBean("gemfireCache", GemFireCache.class); + + assertThat(gemfireCache).isNotNull(); + assertThat(gemfireCache.getCopyOnRead()).isTrue(); + + GemFireAsLastResourceConnectionAcquiringAspect connectionAcquiringAspect = + applicationContext.getBean(GemFireAsLastResourceConnectionAcquiringAspect.class); + + assertThat(connectionAcquiringAspect).isNotNull(); + assertThat(connectionAcquiringAspect.getOrder()).isEqualTo(3); + + GemFireAsLastResourceConnectionClosingAspect connectionClosingAspect = + applicationContext.getBean(GemFireAsLastResourceConnectionClosingAspect.class); + + assertThat(connectionClosingAspect).isNotNull(); + assertThat(connectionClosingAspect.getOrder()).isEqualTo(1); + } + + private void transactionEventsForTransactionalServiceAreCorrect( + Class transactionalServiceType) { + + ConfigurableApplicationContext applicationContext = + newApplicationContext(TestSpringApplicationConfiguration.class); + + assertThat(applicationContext).isNotNull(); + assertThat(applicationContext.containsBean(transactionalServiceType.getSimpleName())).isTrue(); + assertThat(transactionEvents).isEmpty(); + + GFConnectionFactory gemfireConnectionFactory = applicationContext.getBean(GFConnectionFactory.class); + + assertThat(gemfireConnectionFactory).isNotNull(); + + GemFireAsLastResourceConnectionAcquiringAspect connectionAcquiringAspect = + applicationContext.getBean(GemFireAsLastResourceConnectionAcquiringAspect.class); + + assertThat(connectionAcquiringAspect).isNotNull(); + assertThat(connectionAcquiringAspect.getGemFireConnectionFactory()).isSameAs(gemfireConnectionFactory); + + TestTransactionalService service = + applicationContext.getBean(transactionalServiceType.getSimpleName(), transactionalServiceType); + + assertThat(service).isNotNull(); + + service.doInTransactionCommits(); + + assertThat(transactionEvents).containsExactly(SpringGemFireTransactionEvents.BEGIN, + SpringGemFireTransactionEvents.GET_CONNECTION, + SpringGemFireTransactionEvents.TRANSACTION, + SpringGemFireTransactionEvents.COMMIT, + SpringGemFireTransactionEvents.CLOSE_CONNECTION); + + transactionEvents.clear(); + + assertThat(transactionEvents).isEmpty(); + + try { + service.doInTransactionRollsback(); + } + catch (RuntimeException expected) { + assertThat(expected).hasMessage("TEST"); + assertThat(expected).hasNoCause(); + + throw expected; + } + finally { + assertThat(transactionEvents).containsExactly(SpringGemFireTransactionEvents.BEGIN, + SpringGemFireTransactionEvents.GET_CONNECTION, + SpringGemFireTransactionEvents.TRANSACTION, + SpringGemFireTransactionEvents.ROLLBACK, + SpringGemFireTransactionEvents.CLOSE_CONNECTION); + } + } + + @Test(expected = RuntimeException.class) + public void transactionEventsForTransactionalServiceClassAreCorrect() { + transactionEventsForTransactionalServiceAreCorrect(TestTransactionalServiceClass.class); + } + + @Test(expected = RuntimeException.class) + public void transactionEventsForTransactionalServiceMethodsAreCorrect() { + transactionEventsForTransactionalServiceAreCorrect(TestTransactionalServiceMethods.class); + } + + @Test(expected = IllegalStateException.class) + public void missingEnableTransactionManagerAnnotationThrowsIllegalStateException() { + try { + newApplicationContext(TestMissingEnableTransactionManagementAnnotationConfiguration.class); + } + catch (BeanCreationException expected) { + + assertThat(expected).hasMessageStartingWith(String.format("Error creating bean with name '%s'", + GemFireAsLastResourceConfiguration.class.getName())); + + assertThat(expected).hasCauseInstanceOf(IllegalStateException.class); + + assertThat(expected.getCause()).hasMessage("The @EnableGemFireAsLastResource annotation may only be used" + + " on a Spring application @Configuration class that is also annotated with" + + " @EnableTransactionManagement having an explicit [order] set"); + + assertThat(expected.getCause()).hasNoCause(); + + throw (IllegalStateException) expected.getCause(); + } + } + + @Test(expected = IllegalArgumentException.class) + public void missingEnableTransactionManagementOrderAttributeConfigurationThrowsIllegalArgumentException() { + try { + newApplicationContext(TestMissingEnableTransactionManagementOrderAttributeConfiguration.class); + } + catch (BeanCreationException expected) { + + assertThat(expected).hasMessageStartingWith(String.format("Error creating bean with name '%s'", + GemFireAsLastResourceConfiguration.class.getName())); + + assertThat(expected).hasCauseInstanceOf(IllegalArgumentException.class); + + assertThat(expected.getCause()).hasMessage("The @EnableTransactionManagement(order) attribute value" + + " [2147483647] must be explicitly set to a value other than Integer.MAX_VALUE or Integer.MIN_VALUE"); + + assertThat(expected.getCause()).hasNoCause(); + + throw (IllegalArgumentException) expected.getCause(); + } + } + + @Configuration + @EnableGemFireAsLastResource + @EnableTransactionManagement(order = 2) + static class TestGemFireAsLastResourceConfiguration { + + @Bean("gemfireCache") + GemFireCache mockGemFireCache() { + + AtomicBoolean copyOnRead = new AtomicBoolean(false); + + GemFireCache mockGemFireCache = mock(GemFireCache.class); + + doAnswer(invocation -> { + copyOnRead.set(invocation.getArgument(0)); + return null; + }).when(mockGemFireCache).setCopyOnRead(anyBoolean()); + + when(mockGemFireCache.getCopyOnRead()).thenAnswer(invocation -> copyOnRead.get()); + + return mockGemFireCache; + } + + @Bean + GFConnectionFactory mockGemFireConnectionFactory() throws ResourceException { + + GFConnectionFactory mockGemFireConnectionFactory = mock(GFConnectionFactory.class); + + GFConnection mockGemFireConnection = mock(GFConnection.class); + + when(mockGemFireConnectionFactory.getConnection()).thenAnswer(invocation -> { + transactionEvents.add(SpringGemFireTransactionEvents.GET_CONNECTION); + return mockGemFireConnection; + }); + + doAnswer(invocation -> transactionEvents.add(SpringGemFireTransactionEvents.CLOSE_CONNECTION)) + .when(mockGemFireConnection).close(); + + return mockGemFireConnectionFactory; + } + + @Bean("transactionManager") + PlatformTransactionManager mockTransactionManager() { + + PlatformTransactionManager mockTransactionManager = mock(PlatformTransactionManager.class); + + when(mockTransactionManager.getTransaction(any(TransactionDefinition.class))).thenAnswer(invocation -> { + + TransactionStatus mockTransactionStatus = mock(TransactionStatus.class); + + transactionEvents.add(SpringGemFireTransactionEvents.BEGIN); + + return mockTransactionStatus; + }); + + doAnswer(invocation -> transactionEvents.add(SpringGemFireTransactionEvents.COMMIT)) + .when(mockTransactionManager).commit(any(TransactionStatus.class)); + + doAnswer(invocation -> transactionEvents.add(SpringGemFireTransactionEvents.ROLLBACK)) + .when(mockTransactionManager).rollback(any(TransactionStatus.class)); + + return mockTransactionManager; + } + } + + @Configuration + @EnableGemFireAsLastResource + static class TestMissingEnableTransactionManagementAnnotationConfiguration { + } + + @Configuration + @EnableGemFireAsLastResource + @EnableTransactionManagement + static class TestMissingEnableTransactionManagementOrderAttributeConfiguration { + + @Bean("transactionManager") + PlatformTransactionManager mockTransactionManager() { + return mock(PlatformTransactionManager.class); + } + } + + @Configuration + @Import(TestGemFireAsLastResourceConfiguration.class) + static class TestSpringApplicationConfiguration { + + @Bean("TestTransactionalServiceClass") + TestTransactionalServiceClass testTransactionalServiceClass() { + return new TestTransactionalServiceClass(); + } + + @Bean("TestTransactionalServiceMethods") + TestTransactionalServiceMethods testTransactionalServiceMethods() { + return new TestTransactionalServiceMethods(); + } + } + + enum SpringGemFireTransactionEvents { + + BEGIN, + CLOSE_CONNECTION, + COMMIT, + GET_CONNECTION, + ROLLBACK, + TRANSACTION, + + } + + interface TestTransactionalService { + + void doInTransactionCommits(); + + void doInTransactionRollsback(); + + } + + @Service("TestTransactionalServiceClass") + @Transactional + static class TestTransactionalServiceClass implements TestTransactionalService { + + public void doInTransactionCommits() { + transactionEvents.add(SpringGemFireTransactionEvents.TRANSACTION); + } + + public void doInTransactionRollsback() { + transactionEvents.add(SpringGemFireTransactionEvents.TRANSACTION); + throw new RuntimeException("TEST"); + } + } + + @Service("TestTransactionalServiceMethods") + static class TestTransactionalServiceMethods implements TestTransactionalService { + + @Transactional + public void doInTransactionCommits() { + transactionEvents.add(SpringGemFireTransactionEvents.TRANSACTION); + } + + @Transactional + public void doInTransactionRollsback() { + transactionEvents.add(SpringGemFireTransactionEvents.TRANSACTION); + throw new RuntimeException("TEST"); + } + } +} diff --git a/src/test/java/org/springframework/data/gemfire/config/annotation/support/GemFireAsLastResourceConnectionAcquiringAspectUnitTests.java b/src/test/java/org/springframework/data/gemfire/config/annotation/support/GemFireAsLastResourceConnectionAcquiringAspectUnitTests.java new file mode 100644 index 00000000..cc94bbea --- /dev/null +++ b/src/test/java/org/springframework/data/gemfire/config/annotation/support/GemFireAsLastResourceConnectionAcquiringAspectUnitTests.java @@ -0,0 +1,160 @@ +/* + * Copyright 2017 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.data.gemfire.config.annotation.support; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import javax.naming.Context; +import javax.naming.NamingException; +import javax.resource.ResourceException; + +import org.apache.geode.ra.GFConnection; +import org.apache.geode.ra.GFConnectionFactory; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import org.slf4j.Logger; + +/** + * Unit tests for {@link GemFireAsLastResourceConnectionAcquiringAspect}. + * + * @author John Blum + * @see org.junit.Test + * @see org.junit.runner.RunWith + * @see org.mockito.Mock + * @see org.mockito.Mockito + * @see org.mockito.Spy + * @see org.mockito.junit.MockitoJUnitRunner + * @see org.springframework.data.gemfire.config.annotation.support.GemFireAsLastResourceConnectionAcquiringAspect + * @since 2.0.0 + */ +@RunWith(MockitoJUnitRunner.class) +public class GemFireAsLastResourceConnectionAcquiringAspectUnitTests { + + @Mock + private Context mockContext; + + @Mock + private GFConnection mockGemFireConnection; + + @Mock + private GFConnectionFactory mockGemFireConnectionFactory; + + @Mock + private Logger mockLogger; + + private GemFireAsLastResourceConnectionAcquiringAspect aspect; + + @Before + public void setup() { + aspect = spy(new GemFireAsLastResourceConnectionAcquiringAspect()); + when(aspect.getLogger()).thenReturn(mockLogger); + } + + @Test + public void connectionAcquiringAspectHasLowerPriorityThanConnectionClosingAspect() { + assertThat(aspect.getOrder()).isGreaterThan(new GemFireAsLastResourceConnectionClosingAspect().getOrder()); + } + + @Test + public void doConnectionFactoryGetConnectionReturnsConnection() throws ResourceException { + + when(mockLogger.isTraceEnabled()).thenReturn(true); + doReturn(mockGemFireConnectionFactory).when(aspect).resolveGemFireConnectionFactory(); + when(mockGemFireConnectionFactory.getConnection()).thenReturn(mockGemFireConnection); + + aspect.doGemFireConnectionFactoryGetConnection(); + + assertThat(AbstractGemFireAsLastResourceAspectSupport.GemFireConnectionHolder.get().orElse(null)) + .isEqualTo(mockGemFireConnection); + + verify(aspect, times(1)).resolveGemFireConnectionFactory(); + verify(aspect, times(1)).resolveGemFireJcaResourceAdapterJndiName(); + verify(mockGemFireConnectionFactory, times(1)).getConnection(); + verify(mockLogger, times(1)) + .trace(eq(String.format("Acquiring GemFire Connection from GemFire JCA ResourceAdapter registered at [%s]...", + GemFireAsLastResourceConnectionAcquiringAspect.DEFAULT_GEMFIRE_JCA_RESOURCE_ADAPTER_JNDI_NAME))); + } + + @Test + public void resolveGemFireConnectionFactoryFromAutowiring() { + + when(aspect.getGemFireConnectionFactory()).thenReturn(mockGemFireConnectionFactory); + + assertThat(aspect.resolveGemFireConnectionFactory()).isSameAs(mockGemFireConnectionFactory); + + verify(aspect, times(1)).getGemFireConnectionFactory(); + verify(aspect, never()).resolveGemFireJcaResourceAdapterJndiName(); + verify(aspect, never()).resolveContext(); + } + + @Test + public void resolveGemFireConnectionFactoryFromJndiContext() throws NamingException { + + when(aspect.getGemFireConnectionFactory()).thenReturn(null); + doReturn(mockContext).when(aspect).resolveContext(); + when(mockContext.lookup(anyString())).thenReturn(mockGemFireConnectionFactory); + + assertThat(aspect.resolveGemFireConnectionFactory()).isEqualTo(mockGemFireConnectionFactory); + + verify(aspect, times(1)).getGemFireConnectionFactory(); + verify(aspect, times(1)).resolveGemFireJcaResourceAdapterJndiName(); + verify(aspect, times(1)).resolveContext(); + verify(mockContext, times(1)) + .lookup(eq(GemFireAsLastResourceConnectionAcquiringAspect.DEFAULT_GEMFIRE_JCA_RESOURCE_ADAPTER_JNDI_NAME)); + } + + @Test(expected = RuntimeException.class) + public void resolveGemFireConnectionFactoryFromJndiContextThrowsNamingException() throws NamingException { + + when(aspect.getGemFireConnectionFactory()).thenReturn(null); + when(aspect.getGemFireJcaResourceAdapterJndiName()).thenReturn("java:comp/gemfire/jca"); + doReturn(mockContext).when(aspect).resolveContext(); + when(mockContext.lookup(anyString())).thenThrow(new NamingException("TEST")); + + try { + aspect.resolveGemFireConnectionFactory(); + } + catch (RuntimeException expected) { + + assertThat(expected).hasMessage( + "Failed to resolve a GFConnectionFactory from the configured JNDI context name [java:comp/gemfire/jca]"); + + assertThat(expected).hasCauseInstanceOf(NamingException.class); + assertThat(expected.getCause()).hasMessage("TEST"); + assertThat(expected.getCause()).hasNoCause(); + + throw expected; + } + finally { + verify(aspect, times(1)).getGemFireConnectionFactory(); + verify(aspect, times(1)).resolveGemFireJcaResourceAdapterJndiName(); + verify(aspect, times(1)).resolveContext(); + verify(mockContext, times(1)).lookup(eq("java:comp/gemfire/jca")); + } + } +} diff --git a/src/test/java/org/springframework/data/gemfire/config/annotation/support/GemFireAsLastResourceConnectionClosingAspectUnitTests.java b/src/test/java/org/springframework/data/gemfire/config/annotation/support/GemFireAsLastResourceConnectionClosingAspectUnitTests.java new file mode 100644 index 00000000..255c21dd --- /dev/null +++ b/src/test/java/org/springframework/data/gemfire/config/annotation/support/GemFireAsLastResourceConnectionClosingAspectUnitTests.java @@ -0,0 +1,83 @@ +/* + * Copyright 2017 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.data.gemfire.config.annotation.support; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import javax.resource.ResourceException; + +import org.apache.geode.ra.GFConnection; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import org.slf4j.Logger; + +/** + * Unit tests for {@link GemFireAsLastResourceConnectionClosingAspect}. + * + * @author John Blum + * @see org.junit.Test + * @see org.junit.runner.RunWith + * @see org.mockito.Mock + * @see org.mockito.Mockito + * @see org.mockito.Spy + * @see org.mockito.junit.MockitoJUnitRunner + * @see org.springframework.data.gemfire.config.annotation.support.GemFireAsLastResourceConnectionClosingAspect + * @since 2.0.0 + */ +@RunWith(MockitoJUnitRunner.class) +public class GemFireAsLastResourceConnectionClosingAspectUnitTests { + + private GemFireAsLastResourceConnectionClosingAspect aspect; + + @Mock + private GFConnection mockGemFireConnection; + + @Mock + private Logger mockLogger; + + @Before + public void setup() { + aspect = spy(new GemFireAsLastResourceConnectionClosingAspect()); + when(aspect.getLogger()).thenReturn(mockLogger); + } + + @Test + public void connectionClosingAspectHasHigherPriorityThanConnectionAcquiringAspect() { + assertThat(aspect.getOrder()).isLessThan(new GemFireAsLastResourceConnectionAcquiringAspect().getOrder()); + } + + @Test + public void doGemFireConnectionCloseIsSuccessful() throws ResourceException { + + AbstractGemFireAsLastResourceAspectSupport.GemFireConnectionHolder.of(mockGemFireConnection); + + when(mockLogger.isTraceEnabled()).thenReturn(true); + + aspect.doGemFireConnectionClose();; + + verify(mockGemFireConnection, times(1)).close(); + verify(mockLogger, times(1)).trace(eq("Closing GemFire Connection...")); + } +}