From 420df4d2e3c1e8463056a67ad365e257192216ac Mon Sep 17 00:00:00 2001 From: John Blum Date: Fri, 2 Dec 2016 17:51:08 -0800 Subject: [PATCH] SGF-516 - Configure Expiration with annotations. (cherry picked from commit bfb3347056f4f87ab7f218c8a7618ea5c5378d32) Signed-off-by: John Blum --- src/main/asciidoc/reference/cache.adoc | 2 +- src/main/asciidoc/reference/function.adoc | 2 +- src/main/asciidoc/reference/region.adoc | 6 +- .../config/annotation/EnableExpiration.java | 187 +++++++ .../annotation/ExpirationConfiguration.java | 529 ++++++++++++++++++ .../support/AnnotationBasedExpiration.java | 204 ++++--- ...nableExpirationConfigurationUnitTests.java | 294 ++++++++++ ...xpirationConfigurationIntegrationTest.java | 3 +- 8 files changed, 1157 insertions(+), 70 deletions(-) create mode 100644 src/main/java/org/springframework/data/gemfire/config/annotation/EnableExpiration.java create mode 100644 src/main/java/org/springframework/data/gemfire/config/annotation/ExpirationConfiguration.java create mode 100644 src/test/java/org/springframework/data/gemfire/config/annotation/EnableExpirationConfigurationUnitTests.java diff --git a/src/main/asciidoc/reference/cache.adoc b/src/main/asciidoc/reference/cache.adoc index a59d6964..50569b28 100644 --- a/src/main/asciidoc/reference/cache.adoc +++ b/src/main/asciidoc/reference/cache.adoc @@ -156,7 +156,7 @@ no harm in setting this value to false. The example above includes a number of attributes related to GemFire's enhanced serialization framework, PDX. While a complete discussion of PDX is beyond the scope of this reference guide, it is important to note that PDX is enabled by registering a PDX serializer which is done via the `pdx-serializer` attribute. GemFire provides -an implementation class `com.gemstone.gemfire.pdx.ReflectionBasedAutoSerializer`, however it is common for developers +an implementation class `org.apache.geode.pdx.ReflectionBasedAutoSerializer`, however it is common for developers to provide their own implementation. The value of the attribute is simply a reference to a Spring bean that implements the required interface. More information on serialization support can be found in <> diff --git a/src/main/asciidoc/reference/function.adoc b/src/main/asciidoc/reference/function.adoc index 41c1096d..6b5b152e 100644 --- a/src/main/asciidoc/reference/function.adoc +++ b/src/main/asciidoc/reference/function.adoc @@ -1,7 +1,7 @@ [[bootstrap:function]] = Configuring GemFire's Function Service -As of Release 1.3.0, Spring Data GemFire provides <> support for implementing and registering functions. Spring Data GemFire also provides namespace support for registering GemFire http://data-docs-samples.cfapps.io/docs-gemfire/latest/javadocs/japi/com/gemstone/gemfire/cache/execute/Function.html[Functions] for remote function execution. Please refer to the GemFire documentation for more information on the function execution framework. Functions are declared as Spring beans and must implement the `com.gemstone.gemfire.cache.execute.Function` interface or extend `com.gemstone.gemfire.cache.execute.FunctionAdapter`. The namespace uses a familiar pattern to declare functions: +As of Release 1.3.0, Spring Data GemFire provides <> support for implementing and registering functions. Spring Data GemFire also provides namespace support for registering GemFire http://data-docs-samples.cfapps.io/docs-gemfire/latest/javadocs/japi/com/gemstone/gemfire/cache/execute/Function.html[Functions] for remote function execution. Please refer to the GemFire documentation for more information on the function execution framework. Functions are declared as Spring beans and must implement the `org.apache.geode.cache.execute.Function` interface or extend `org.apache.geode.cache.execute.FunctionAdapter`. The namespace uses a familiar pattern to declare functions: [source,xml] ---- diff --git a/src/main/asciidoc/reference/region.adoc b/src/main/asciidoc/reference/region.adoc index e312a523..a00767a2 100644 --- a/src/main/asciidoc/reference/region.adoc +++ b/src/main/asciidoc/reference/region.adoc @@ -728,9 +728,9 @@ Expiration attributes on the `AnnotationBasedExpiration` bean by doing the follo [source,xml] ---- - + - + 600 #{T(org.springframework.data.gemfire.ExpirationActionType).DESTROY} - #{T(com.gemstone.gemfire.cache.ExpirationAction).INVALIDATE} + #{T(org.apache.geode.cache.ExpirationAction).INVALIDATE} ... diff --git a/src/main/java/org/springframework/data/gemfire/config/annotation/EnableExpiration.java b/src/main/java/org/springframework/data/gemfire/config/annotation/EnableExpiration.java new file mode 100644 index 00000000..c47dd4ef --- /dev/null +++ b/src/main/java/org/springframework/data/gemfire/config/annotation/EnableExpiration.java @@ -0,0 +1,187 @@ +/* + * Copyright 2016 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.config.annotation.EnableExpiration.ExpirationType.IDLE_TIMEOUT; + +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.apache.geode.cache.Region; +import org.springframework.context.annotation.Import; +import org.springframework.data.gemfire.ExpirationActionType; +import org.springframework.data.gemfire.support.Expiration; +import org.springframework.data.gemfire.support.IdleTimeoutExpiration; +import org.springframework.data.gemfire.support.TimeToLiveExpiration; + +/** + * The {@link EnableExpiration} annotation marks a Spring {@link org.springframework.context.annotation.Configuration @Configuration} + * annotated class to enable {@link Region} entry expiration for individual entries. Note, this annotation does not + * cover {@link Region} expiration; {@link Region} expiration must be configure on the {@link Region} definition itself. + * + * @author John Blum + * @see org.springframework.context.annotation.Import + * @see org.springframework.data.gemfire.config.annotation.ExpirationConfiguration + * @see org.springframework.data.gemfire.support.Expiration + * @see org.springframework.data.gemfire.support.IdleTimeoutExpiration + * @see org.springframework.data.gemfire.support.TimeToLiveExpiration + * @see org.apache.geode.cache.Region + * @see Annotation-based Data Expiration + * @see GemFire Expiration + * @see Geode Expiration + * @since 1.9.0 + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Inherited +@Documented +@Import(ExpirationConfiguration.class) +@SuppressWarnings({ "unused" }) +public @interface EnableExpiration { + + /** + * Defines individual {@link Region} Expiration policies or customizes the default Expiration policy + * for all {@link Region Regions}. + * + * Defaults to empty. + */ + ExpirationPolicy[] policies() default {}; + + /** + * Definition for a specific Expiration policy that can be applied to 1 or more {@link Region Regions}. + * + * An Expiration policy defines the expiration timeout and expiration action to take when + * an {@link Region} entry times out. + * + * Additionally, the Expiration policy defines the algorithm to use (e.g. Idle Timeout (TTI) or Time-To-Live (TTL), + * or both) to determine if and when an {@link Region} entry has timed out. + */ + @interface ExpirationPolicy { + + /** + * Specifies the timeout used to determine when a {@link Region} entry expires. + * + * This value of this attribute determines the "default" timeout used if no specific timeout was specified. + * A specific timeout is determined by {@link Expiration#timeout()}, {@link IdleTimeoutExpiration#timeout()} + * or {@link TimeToLiveExpiration#timeout()} attribute on the application domain object. + * + * See the SDG Reference Guide for more details... + * + * @see Annotation-based Data Expiration + */ + int timeout(); + + /** + * Specifies the action taken when a {@link Region} entry expires. + * + * This value of this attribute determines the "default" action taken if no specific action was specified. + * The specific action is determined by {@link Expiration#action()}, {@link IdleTimeoutExpiration#action()} + * or {@link TimeToLiveExpiration#action()} attribute on the application domain object. + * + * See the SDG Reference Guide for more details... + * + * @see Annotation-based Data Expiration + */ + ExpirationActionType action(); + + /** + * Names of specific {@link Region Regions} on which this Expiration policy is applied. + * + * If no {@link Region} names are specified then this Expiration policy will apply to + * all {@link Region Regions} declared in the Spring context. + * + * Defaults to all {@link Region Regions}. + */ + String[] regionNames() default {}; + + /** + * Types of Expiration algorithms (Idle Timeout (TTI) or Time to Live (TTL)) configured and used by + * {@link Region Region(s)} to expire entries. + * + * Defaults to both Idle Timeout (TTI). + * + * @see org.springframework.data.gemfire.config.annotation.EnableExpiration.ExpirationType + */ + ExpirationType[] types() default { IDLE_TIMEOUT }; + + } + + /** + * {@link ExpirationType} defines different types of GemFire/Geode Expiration policies such as + * (Entry) Idle Timeout (TTI) and (Entry) Time to Live (TTL). + * + * @see GemFire Expiration + * @see Geode Expiration + */ + enum ExpirationType { + IDLE_TIMEOUT("TTI"), + TIME_TO_LIVE("TTL"); + + private final String abbreviation; + + /** + * Factory method to lookup an appropriate {@link ExpirationType} based on an abbreviation. + * + * @param abbreviation abbreviation used to lookup the appropriate {@link ExpirationType}. + * @return an {@link ExpirationType} matching the abbreviation or {@literal null} if the abbreviation + * does not match an {@link ExpirationType}. + * @see org.springframework.data.gemfire.config.annotation.EnableExpiration.ExpirationType + * @see #values() + * @see #abbreviation() + */ + static ExpirationType valueOfAbbreviation(String abbreviation) { + for (ExpirationType expirationType : values()) { + if (expirationType.abbreviation().equalsIgnoreCase(abbreviation)) { + return expirationType; + } + } + + return null; + } + + /** + * Constructs a new instance of {@link ExpirationType} initialized with the given abbreviation. + * + * @param abbreviation {@link String} indicating the {@link ExpirationType} abbreviation. + */ + ExpirationType(String abbreviation) { + this.abbreviation = abbreviation; + } + + /** + * Returns the abbreviation for this {@link ExpirationType}. + * + * @return a {@link String} with the {@link ExpirationType} abbreviation. + */ + protected String abbreviation() { + return this.abbreviation; + } + + /** + * @inheritDoc + */ + @Override + public String toString() { + return String.format("%1$s (%2$s)", name(), abbreviation()); + } + } +} diff --git a/src/main/java/org/springframework/data/gemfire/config/annotation/ExpirationConfiguration.java b/src/main/java/org/springframework/data/gemfire/config/annotation/ExpirationConfiguration.java new file mode 100644 index 00000000..d547b0c0 --- /dev/null +++ b/src/main/java/org/springframework/data/gemfire/config/annotation/ExpirationConfiguration.java @@ -0,0 +1,529 @@ +/* + * Copyright 2016 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.config.annotation.EnableExpiration.ExpirationPolicy; +import static org.springframework.data.gemfire.config.annotation.EnableExpiration.ExpirationType; + +import java.lang.annotation.Annotation; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import org.apache.geode.cache.AttributesMutator; +import org.apache.geode.cache.ExpirationAction; +import org.apache.geode.cache.ExpirationAttributes; +import org.apache.geode.cache.Region; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.config.BeanPostProcessor; +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.ExpirationActionType; +import org.springframework.data.gemfire.support.AnnotationBasedExpiration; +import org.springframework.data.gemfire.util.ArrayUtils; +import org.springframework.data.gemfire.util.CollectionUtils; +import org.springframework.data.gemfire.util.SpringUtils; +import org.springframework.util.Assert; + +/** + * {@link ExpirationConfiguration} is a Spring {@link Configuration} class used to configure expiration policies + * for GemFire/Geode {@link Region Regions}. + * + * @author John Blum + * @see org.springframework.context.annotation.Configuration + * @see org.springframework.context.annotation.ImportAware + * @see org.springframework.data.gemfire.config.annotation.EnableExpiration + * @see org.apache.geode.cache.ExpirationAttributes + * @see org.apache.geode.cache.Region + * @since 1.9.0 + */ +@Configuration +public class ExpirationConfiguration implements ImportAware { + + protected static final int DEFAULT_TIMEOUT = 0; + + protected static final ExpirationActionType DEFAULT_ACTION = ExpirationActionType.DEFAULT; + + protected static final ExpirationType[] DEFAULT_EXPIRATION_TYPES = { ExpirationType.IDLE_TIMEOUT }; + + private ExpirationPolicyConfigurer expirationPolicyConfigurer; + + /** + * Returns the {@link Annotation} {@link Class type} that enables and configures Expiration. + * + * @return {@link Annotation} {@link Class type} that enables and configures Expiration. + * @see java.lang.annotation.Annotation + * @see java.lang.Class + */ + protected Class getAnnotationType() { + return EnableExpiration.class; + } + + /** + * Returns the name of the {@link Annotation} type that enables and configures Expiration. + * + * @return the name of the {@link Annotation} type that enables and configures Expiration. + * @see java.lang.Class#getName() + * @see #getAnnotationType() + */ + protected String getAnnotationTypeName() { + return getAnnotationType().getName(); + } + + /** + * Returns the simple name of the {@link Annotation} type that enables and configures Expiration. + * + * @return the simple name of the {@link Annotation} type that enables and configures Expiration. + * @see java.lang.Class#getSimpleName() + * @see #getAnnotationType() + */ + @SuppressWarnings("unused") + protected String getAnnotationTypeSimpleName() { + return getAnnotationType().getSimpleName(); + } + + /** + * @inheritDoc + */ + @Override + public void setImportMetadata(AnnotationMetadata importMetadata) { + if (importMetadata.hasAnnotation(getAnnotationTypeName())) { + Map enableExpirationAttributes = + importMetadata.getAnnotationAttributes(getAnnotationTypeName()); + + AnnotationAttributes[] policies = + (AnnotationAttributes[]) enableExpirationAttributes.get("policies"); + + for (AnnotationAttributes expirationPolicyAttributes : + ArrayUtils.nullSafeArray(policies, AnnotationAttributes.class)) { + + this.expirationPolicyConfigurer = ComposableExpirationPolicyConfigurer.compose( + this.expirationPolicyConfigurer, ExpirationPolicyMetaData.from(expirationPolicyAttributes)); + } + + this.expirationPolicyConfigurer = (this.expirationPolicyConfigurer != null ? this.expirationPolicyConfigurer + : ExpirationPolicyMetaData.fromDefaults()); + } + } + + /** + * Determines whether the given bean is a {@link Region}. + * + * @param bean {@link Object} to evaluate. + * @return a boolean value indicating whether the given bean is a {@link Region}. + * @see org.apache.geode.cache.Region + */ + protected boolean isRegion(Object bean) { + return (bean instanceof Region); + } + + protected ExpirationPolicyConfigurer getExpirationPolicyConfigurer() { + Assert.state(this.expirationPolicyConfigurer != null, + "ExpirationPolicyConfigurer was not properly configured and initialized"); + + return expirationPolicyConfigurer; + } + + @Bean + @SuppressWarnings("unused") + public BeanPostProcessor expirationBeanPostProcessor() { + return new BeanPostProcessor() { + @Override + public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { + return bean; + } + + @Override + @SuppressWarnings("unchecked") + public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { + return (isRegion(bean) ? getExpirationPolicyConfigurer().configure((Region) bean) + : bean); + } + + }; + } + + /** + * Interface defining a contract for implementations that configure a {@link Region Region's} expiration policy. + */ + protected interface ExpirationPolicyConfigurer { + + /** + * Configures the expiration policy for the given {@link Region}. + * + * @param {@link Class type} of the {@link Region} keys. + * @param {@link Class type} of the {@link Region} values. + * @param region {@link Region} who's expiration policy will be configured. + * @return the given {@link Region}. + * @see org.apache.geode.cache.Region + */ + Region configure(Region region); + + } + + /** + * {@link ComposableExpirationPolicyConfigurer} is a {@link ExpirationPolicyConfigurer} implementation + * that additionally implements the Composition Software Design Pattern to treat a collection of + * {@link ExpirationPolicyConfigurer} objects as a single instace of the {@link ExpirationPolicyConfigurer}. + * + * @see org.springframework.data.gemfire.config.annotation.ExpirationConfiguration.ExpirationPolicyConfigurer + * @see Composition Software Design Pattern + */ + protected static class ComposableExpirationPolicyConfigurer implements ExpirationPolicyConfigurer { + + private final ExpirationPolicyConfigurer one; + private final ExpirationPolicyConfigurer two; + + /** + * Factory method to compose an array of {@link ExpirationPolicyConfigurer} objects. + * + * @param array array of {@link ComposableExpirationPolicyConfigurer} objects to compose. + * @return a composition containing all the {@link ExpirationPolicyConfigurer} objects in the array. + * @see org.springframework.data.gemfire.config.annotation.ExpirationConfiguration.ExpirationPolicyConfigurer + * @see #compose(Iterable) + */ + protected static ExpirationPolicyConfigurer compose(ExpirationPolicyConfigurer[] array) { + return compose(Arrays.asList(ArrayUtils.nullSafeArray(array, ExpirationPolicyConfigurer.class))); + } + + /** + * Factory method to compose an {@link Iterable} of {@link ExpirationPolicyConfigurer} objects. + * + * @param iterable {@link Iterable} of {@link ComposableExpirationPolicyConfigurer} objects to compose. + * @return a composition containing all the {@link ExpirationPolicyConfigurer} objects in the {@link Iterable}. + * @see org.springframework.data.gemfire.config.annotation.ExpirationConfiguration.ExpirationPolicyConfigurer + * @see #compose(ExpirationPolicyConfigurer, ExpirationPolicyConfigurer) + */ + protected static ExpirationPolicyConfigurer compose(Iterable iterable) { + ExpirationPolicyConfigurer current = null; + + for (ExpirationPolicyConfigurer configurer : CollectionUtils.nullSafeIterable(iterable)) { + current = compose(current, configurer); + } + + return current; + } + + /** + * Factory method to compose 2 {@link ExpirationPolicyConfigurer} objects. + * + * @param one first {@link ComposableExpirationPolicyConfigurer} to compose. + * @param two second {@link ComposableExpirationPolicyConfigurer} to compose. + * @return a composition of the 2 {@link ExpirationPolicyConfigurer} objects. + * Returns {@code one} if {@code two} is {@literal null} or {@code two} if {@code one} is {@literal null}. + * @see org.springframework.data.gemfire.config.annotation.ExpirationConfiguration.ExpirationPolicyConfigurer + */ + protected static ExpirationPolicyConfigurer compose(ExpirationPolicyConfigurer one, + ExpirationPolicyConfigurer two) { + + return (one == null ? two : (two == null ? one : new ComposableExpirationPolicyConfigurer(one, two))); + } + + /** + * Constructs an instance of the {@link ComposableExpirationPolicyConfigurer} initialized with + * 2 {@link ExpirationPolicyConfigurer} objects. + * + * @param one first {@link ComposableExpirationPolicyConfigurer} to compose. + * @param two second {@link ComposableExpirationPolicyConfigurer} to compose. + * @see org.springframework.data.gemfire.config.annotation.ExpirationConfiguration.ExpirationPolicyConfigurer + */ + private ComposableExpirationPolicyConfigurer(ExpirationPolicyConfigurer one, ExpirationPolicyConfigurer two) { + this.one = one; + this.two = two; + } + + /** + * @inheritDoc + */ + @Override + public Region configure(Region region) { + return this.two.configure(this.one.configure(region)); + } + } + + /** + * {@link ExpirationPolicyMetaData} is a {@link ExpirationPolicyConfigurer} implementation that encapsulates + * the expiration configuration meta-data (e.g. expiration timeout and action) necessary to configure + * a {@link Region Regions's} expiration policy and behavior. + * + * This class is meant to capture the expiration configuration meta-data specified in the {@link ExpirationPolicy} + * nested annotation in the application-level {@link EnableExpiration} annotation. + + * @see org.springframework.data.gemfire.config.annotation.ExpirationConfiguration.ExpirationPolicyConfigurer + */ + protected static class ExpirationPolicyMetaData implements ExpirationPolicyConfigurer { + + protected static final String[] ALL_REGIONS = new String[0]; + + private final ExpirationAttributes defaultExpirationAttributes; + + private final Set regionNames = new HashSet(); + + private final Set types = new HashSet(); + + /** + * Factory method to construct an instance of {@link ExpirationPolicyMetaData} initialized with + * the given {@link AnnotationAttributes} from the nested {@link ExpirationPolicy} annotation + * specified in an application-level {@link EnableExpiration} annotation. + * + * @param expirationPolicyAttributes {@link AnnotationAttributes} from a {@link ExpirationPolicy} annotation. + * @return an instance of the {@link ExpirationPolicyMetaData} initialized from + * {@link ExpirationPolicy} {@link AnnotationAttributes}. + * @throws IllegalArgumentException if {@link AnnotationAttributes#annotationType()} is not assignable to + * {@link ExpirationPolicy}. + * @see #newExpirationPolicyMetaData(int, ExpirationActionType, String[], ExpirationType[]) + * @see org.springframework.core.annotation.AnnotationAttributes + */ + protected static ExpirationPolicyMetaData from(AnnotationAttributes expirationPolicyAttributes) { + Assert.isAssignable(ExpirationPolicy.class, expirationPolicyAttributes.annotationType()); + + return newExpirationPolicyMetaData((Integer) expirationPolicyAttributes.get("timeout"), + expirationPolicyAttributes.getEnum("action"), + expirationPolicyAttributes.getStringArray("regionNames"), + (ExpirationType[]) expirationPolicyAttributes.get("types")); + } + + /** + * Factory method to construct an instance of {@link ExpirationPolicyMetaData} initialized with + * the given attribute values from the nested {@link ExpirationPolicy} annotation specified in + * an application-level {@link EnableExpiration} annotation. + * + * @param expirationPolicy {@link ExpirationPolicy} annotation containing the attribute values + * used to initialize the {@link ExpirationPolicyMetaData} instance. + * @return an instance of the {@link ExpirationPolicyMetaData} initialized from + * {@link ExpirationPolicy} attributes values. + * @see org.springframework.data.gemfire.config.annotation.EnableExpiration.ExpirationPolicy + * @see #newExpirationPolicyMetaData(int, ExpirationActionType, String[], ExpirationType[]) + */ + protected static ExpirationPolicyMetaData from(ExpirationPolicy expirationPolicy) { + return newExpirationPolicyMetaData(expirationPolicy.timeout(), expirationPolicy.action(), + expirationPolicy.regionNames(), expirationPolicy.types()); + } + + /** + * Factory method to construct an instance of {@link ExpirationPolicyMetaData} using default expiration policy + * settings. + * + * @see #newExpirationPolicyMetaData(int, ExpirationActionType, String[], ExpirationType[]) + */ + protected static ExpirationPolicyMetaData fromDefaults() { + return newExpirationPolicyMetaData(DEFAULT_TIMEOUT, DEFAULT_ACTION, ALL_REGIONS, DEFAULT_EXPIRATION_TYPES); + } + + /** + * Factory method used to construct a new instance of the {@link ExpirationAttributes} initialized with + * the given expiration timeout and action that is taken when an {@link Region} entry times out. + * + * @param timeout int value indicating the expiration timeout in seconds. + * @param action expiration action to take when the {@link Region} entry times out. + * @return a new instance of {@link ExpirationAttributes} initialized with the given expiration timeout + * and action. + * @see org.apache.geode.cache.ExpirationAttributes + * @see #newExpirationAttributes(int, ExpirationAction) + */ + protected static ExpirationAttributes newExpirationAttributes(int timeout, ExpirationActionType action) { + return newExpirationAttributes(timeout, action.getExpirationAction()); + } + + /** + * Factory method used to construct a new instance of the {@link ExpirationAttributes} initialized with + * the given expiration timeout and action that is taken when an {@link Region} entry times out. + * + * @param timeout int value indicating the expiration timeout in seconds. + * @param action expiration action to take when the {@link Region} entry times out. + * @return a new instance of {@link ExpirationAttributes} initialized with the given expiration timeout + * and action. + * @see org.apache.geode.cache.ExpirationAttributes + */ + protected static ExpirationAttributes newExpirationAttributes(int timeout, ExpirationAction action) { + return new ExpirationAttributes(timeout, action); + } + + /** + * Factory method used to construct an instance of {@link ExpirationPolicyMetaData} initialized with + * the given expiration policy meta-data. + * + * @param timeout int value indicating the expiration timeout in seconds. + * @param action expiration action taken when the {@link Region} entry expires. + * @param regionNames names of {@link Region Regions} configured with the expiration policy meta-data. + * @param types type of expiration algorithm/behavior (TTI/TTL) configured for the {@link Region}. + * @return an instance of {@link ExpirationPolicyMetaData} initialized with the given expiration policy + * meta-data. + * @throws IllegalArgumentException if the {@link ExpirationType} array is empty. + * @see org.springframework.data.gemfire.config.annotation.EnableExpiration.ExpirationType + * @see org.springframework.data.gemfire.ExpirationActionType + * @see #ExpirationPolicyMetaData(ExpirationAttributes, Set, Set) + * @see #newExpirationAttributes(int, ExpirationActionType) + */ + protected static ExpirationPolicyMetaData newExpirationPolicyMetaData(int timeout, ExpirationActionType action, + String[] regionNames, ExpirationType[] types) { + + return new ExpirationPolicyMetaData(newExpirationAttributes(timeout, action), + CollectionUtils.asSet(ArrayUtils.nullSafeArray(regionNames, String.class)), + CollectionUtils.asSet(ArrayUtils.nullSafeArray(types, ExpirationType.class))); + } + + /** + * Resolves the {@link ExpirationAction} used in the expiration policy. Defaults to + * {@link ExpirationActionType#INVALIDATE} if {@code action} is {@literal null}. + * + * @param action given {@link ExpirationActionType} to evaluate. + * @return the resolved {@link ExpirationActionType} or the default if {@code action} is {@literal null}. + * @see org.springframework.data.gemfire.ExpirationActionType + */ + protected static ExpirationActionType resolveAction(ExpirationActionType action) { + return SpringUtils.defaultIfNull(action, DEFAULT_ACTION); + } + + /** + * Resolves the expiration timeout used in the expiration policy. Defaults to {@literal 0} if {@code timeout} + * is less than {@literal 0}. + * + * @param timeout int value expressing the expiration timeout in seconds. + * @return the resolved expiration policy timeout. + */ + protected static int resolveTimeout(int timeout) { + return Math.max(timeout, DEFAULT_TIMEOUT); + } + + /** + * Constructs an instance of {@link ExpirationPolicyMetaData} initialized with the given expiration policy + * configuraiton meta-data and {@link Region} expiration settings. + * + * @param timeout int value indicating the expiration timeout in seconds. + * @param action expiration action taken when the {@link Region} entry expires. + * @param regionNames names of {@link Region Regions} configured with the expiration policy meta-data. + * @param types type of expiration algorithm/behavior (TTI/TTL) configured for the {@link Region}. + * @throws IllegalArgumentException if the {@link ExpirationType} {@link Set} is empty. + * @see org.springframework.data.gemfire.config.annotation.EnableExpiration.ExpirationType + * @see org.springframework.data.gemfire.ExpirationActionType + * @see #ExpirationPolicyMetaData(ExpirationAttributes, Set, Set) + * @see #newExpirationAttributes(int, ExpirationActionType) + * @see #resolveAction(ExpirationActionType) + * @see #resolveTimeout(int) + */ + protected ExpirationPolicyMetaData(int timeout, ExpirationActionType action, Set regionNames, + Set types) { + + this(newExpirationAttributes(resolveTimeout(timeout), resolveAction(action)), regionNames, types); + } + + /** + * Constructs an instance of {@link ExpirationPolicyMetaData} initialized with the given expiration policy + * configuraiton meta-data and {@link Region} expiration settings. + * + * @param expirationAttributes {@link ExpirationAttributes} specifying the expiration timeout in seconds + * and expiration action taken when the {@link Region} entry expires. + * @param regionNames names of {@link Region Regions} configured with the expiration policy meta-data. + * @param types type of expiration algorithm/behaviors (TTI/TTL) configured for the {@link Region}. + * @throws IllegalArgumentException if the {@link ExpirationType} {@link Set} is empty. + * @see org.springframework.data.gemfire.config.annotation.EnableExpiration.ExpirationType + * @see org.apache.geode.cache.ExpirationAttributes + */ + protected ExpirationPolicyMetaData(ExpirationAttributes expirationAttributes, Set regionNames, + Set types) { + + Assert.notEmpty(types, "At least one ExpirationPolicy type [TTI, TTL] must be specified"); + + this.defaultExpirationAttributes = expirationAttributes; + this.regionNames.addAll(CollectionUtils.nullSafeSet(regionNames)); + this.types.addAll(CollectionUtils.nullSafeSet(types)); + } + + /** + * Determines whether to apply this expiration policy to the given {@link Region}. + * + * @param region {@link Region} to evaluate. + * @return a boolean value indicating whether the expiration policy applies to the given {@link Region}. + * @see org.apache.geode.cache.Region + * @see #accepts(String) + */ + protected boolean accepts(Region region) { + return (region != null && accepts(region.getName())); + } + + /** + * Determines whether to apply this expiration policy to the given {@link Region} identified by name. + * + * @param regionName name of the {@link Region} to evaluate. + * @return a boolean value indicating whether the expiration policy applies to the given {@link Region} + * identified by name. + */ + protected boolean accepts(String regionName) { + return (this.regionNames.isEmpty() || this.regionNames.contains(regionName)); + } + + /** + * Determines whether Idle Timeout Expiration (TTI) was configured for this expiration policy. + * + * @return a boolean value indicating whether Idle Timeout Expiration (TTI) was configuration for + * this expiration policy. + */ + protected boolean isIdleTimeout() { + return this.types.contains(ExpirationType.IDLE_TIMEOUT); + } + + /** + * Determines whether Time-To-Live Expiration (TTL) was configured for this expiration policy. + * + * @return a boolean value indicating whether Time-To-Live Expiration (TTL) was configuration for + * this expiration policy. + */ + protected boolean isTimeToLive() { + return this.types.contains(ExpirationType.TIME_TO_LIVE); + } + + /** + * @inheritDoc + */ + @Override + public Region configure(Region region) { + if (accepts(region)) { + AttributesMutator regionAttributesMutator = region.getAttributesMutator(); + + ExpirationAttributes defaultExpirationAttributes = defaultExpirationAttributes(); + + if (isIdleTimeout()) { + regionAttributesMutator.setCustomEntryIdleTimeout( + AnnotationBasedExpiration.forIdleTimeout(defaultExpirationAttributes)); + } + + if (isTimeToLive()) { + regionAttributesMutator.setCustomEntryTimeToLive( + AnnotationBasedExpiration.forTimeToLive(defaultExpirationAttributes)); + } + } + + return region; + } + + /** + * Returns the default, fallback {@link ExpirationAttributes}. + * + * @return an {@link ExpirationAttributes} containing the defaults. + * @see org.apache.geode.cache.ExpirationAttributes + */ + protected ExpirationAttributes defaultExpirationAttributes() { + return this.defaultExpirationAttributes; + } + } +} diff --git a/src/main/java/org/springframework/data/gemfire/support/AnnotationBasedExpiration.java b/src/main/java/org/springframework/data/gemfire/support/AnnotationBasedExpiration.java index 1887802e..f58b7d1e 100644 --- a/src/main/java/org/springframework/data/gemfire/support/AnnotationBasedExpiration.java +++ b/src/main/java/org/springframework/data/gemfire/support/AnnotationBasedExpiration.java @@ -47,10 +47,10 @@ import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; /** - * The AnnotationBasedExpiration class is an implementation of GemFire's CustomExpiry interface that determines - * the Time-To-Live (TTL) or Idle-Timeout (TTI) expiration policy of a Region entry by introspecting the Region - * entry's class type and reflecting on any Region entries annotated with Spring Data GemFire's Expiration-based - * Annotations. + * The {@link AnnotationBasedExpiration} class is an implementation of the {@link CustomExpiry} interface + * that determines the Time-To-Live (TTL) or Idle-Timeout (TTI) expiration policy of a {@link Region} entry + * by introspecting the {@link Region} entry's class type and reflecting on any {@link Region} entries annotated + * with SDG's Expiration-based Annotations. * * @author John Blum * @see java.lang.annotation.Annotation @@ -60,19 +60,28 @@ import org.springframework.util.ObjectUtils; * @see org.springframework.data.gemfire.support.Expiration * @see org.springframework.data.gemfire.support.IdleTimeoutExpiration * @see org.springframework.data.gemfire.support.TimeToLiveExpiration +<<<<<<< HEAD * @see org.apache.geode.cache.CustomExpiry * @see org.apache.geode.cache.ExpirationAction * @see org.apache.geode.cache.ExpirationAttributes * @see org.apache.geode.cache.Region +======= + * @see org.apache.geode.cache.CustomExpiry + * @see org.apache.geode.cache.ExpirationAction + * @see org.apache.geode.cache.ExpirationAttributes + * @see org.apache.geode.cache.Region + * @see Annotation-based Data Expiration +>>>>>>> bfb3347... SGF-516 - Configure Expiration with annotations. * @since 1.7.0 */ @SuppressWarnings("unused") public class AnnotationBasedExpiration implements BeanFactoryAware, CustomExpiry { - protected static final AtomicReference BEAN_FACTORY_REFERENCE = new AtomicReference(null); + protected static final AtomicReference BEAN_FACTORY_REFERENCE = + new AtomicReference<>(null); protected static final AtomicReference EVALUATION_CONTEXT_REFERENCE - = new AtomicReference(null); + = new AtomicReference<>(null); //private ExpirationAttributes defaultExpirationAttributes = ExpirationAttributes.DEFAULT; private ExpirationAttributes defaultExpirationAttributes; @@ -85,7 +94,7 @@ public class AnnotationBasedExpiration implements BeanFactoryAware, Custom } /** - * Constructs a new instance of the AnnotationBasedExpiration class initialized with a specific, default + * Constructs a new instance of {@link AnnotationBasedExpiration} initialized with a specific, default * expiration policy. * * @param defaultExpirationAttributes expiration settings used as the default expiration policy. @@ -96,12 +105,14 @@ public class AnnotationBasedExpiration implements BeanFactoryAware, Custom } /** - * Constructs an AnnotationBasedExpiration instance with no default ExpirationAttributes to process - * Idle Timeout (TTI) Expiration annotated Region Entries. + * Factory method used to construct an instance of {@link AnnotationBasedExpiration} having no default + * {@link ExpirationAttributes} to process expired annotated {@link Region} entries + * using Idle Timeout (TTI) Expiration. * - * @param the class type of the Region Entry Key. - * @param the class type of the Region Entry Value. - * @return an AnnotationBasedExpiration instance to process Idle Timeout Expiration annotated Region Entries. + * @param {@link Class} type of the {@link Region} entry key. + * @param {@link Class} type of the {@link Region} entry value. + * @return an {@link AnnotationBasedExpiration} instance to process expired annotated {@link Region} entries + * using Idle Timeout expiration. * @see org.springframework.data.gemfire.support.AnnotationBasedExpiration * @see org.springframework.data.gemfire.support.IdleTimeoutExpiration * @see #forIdleTimeout(org.apache.geode.cache.ExpirationAttributes) @@ -111,20 +122,23 @@ public class AnnotationBasedExpiration implements BeanFactoryAware, Custom } /** - * Constructs an AnnotationBasedExpiration instance with default ExpirationAttributes to process - * Idle Timeout (TTI) Expiration annotated Region Entries. + * Factory method used to construct an instance of {@link AnnotationBasedExpiration} initialized with + * default {@link ExpirationAttributes} to process expired annotated {@link Region} entries + * using Idle Timeout (TTI) expiration. * - * @param the class type of the Region Entry Key. - * @param the class type of the Region Entry Value. - * @param defaultExpirationAttributes ExpirationAttributes used by default if no Expiration policy was specified - * on the Region Entry. - * @return an AnnotationBasedExpiration instance to process Idle Timeout Expiration annotated Region Entries. + * @param {@link Class} type of the {@link Region} entry key. + * @param {@link Class} type of the {@link Region} entry value. + * @param defaultExpirationAttributes {@link ExpirationAttributes} used by default if no expiration policy + * was specified on the {@link Region}. + * @return an {@link AnnotationBasedExpiration} instance to process expired annotated {@link Region} entries + * using Idle Timeout expiration. * @see org.springframework.data.gemfire.support.AnnotationBasedExpiration * @see org.springframework.data.gemfire.support.IdleTimeoutExpiration + * @see #AnnotationBasedExpiration(ExpirationAttributes) */ public static AnnotationBasedExpiration forIdleTimeout(ExpirationAttributes defaultExpirationAttributes) { return new AnnotationBasedExpiration(defaultExpirationAttributes) { - @Override protected ExpirationMetaData getExpirationMetaData(final Region.Entry entry) { + @Override protected ExpirationMetaData getExpirationMetaData(Region.Entry entry) { return (isIdleTimeoutConfigured(entry) ? ExpirationMetaData.from(getIdleTimeout(entry)) : super.getExpirationMetaData(entry)); } @@ -132,12 +146,14 @@ public class AnnotationBasedExpiration implements BeanFactoryAware, Custom } /** - * Constructs an AnnotationBasedExpiration instance with no default ExpirationAttributes to process - * Time-To-Live (TTL) Expiration annotated Region Entries. + * Factory method used to construct an instance of {@link AnnotationBasedExpiration} having no default + * {@link ExpirationAttributes} to process expired annotated {@link Region} entries + * using Time-To-Live (TTL) Expiration. * - * @param the class type of the Region Entry Key. - * @param the class type of the Region Entry Value. - * @return an AnnotationBasedExpiration instance to process Time-To-Live Expiration annotated Region Entries. + * @param {@link Class} type of the {@link Region} entry key. + * @param {@link Class} type of the {@link Region} entry value. + * @return an {@link AnnotationBasedExpiration} instance to process expired annotated {@link Region} entries + * using Time-To-Live expiration. * @see org.springframework.data.gemfire.support.AnnotationBasedExpiration * @see org.springframework.data.gemfire.support.TimeToLiveExpiration * @see #forTimeToLive(org.apache.geode.cache.ExpirationAttributes) @@ -147,27 +163,33 @@ public class AnnotationBasedExpiration implements BeanFactoryAware, Custom } /** - * Constructs an AnnotationBasedExpiration instance with default ExpirationAttributes to process - * Time-To-Live (TTL) Expiration annotated Region Entries. + * Factory method used to construct an instance of {@link AnnotationBasedExpiration} initialized with + * default {@link ExpirationAttributes} to process expired annotated {@link Region} entries + * using Time-To-Live (TTL) expiration. * - * @param the class type of the Region Entry Key. - * @param the class type of the Region Entry Value. - * @param defaultExpirationAttributes ExpirationAttributes used by default if no Expiration policy was specified - * on the Region Entry. - * @return an AnnotationBasedExpiration instance to process Time-To-Live Expiration annotated Region Entries. + * @param {@link Class} type of the {@link Region} entry key. + * @param {@link Class} type of the {@link Region} entry value. + * @param defaultExpirationAttributes {@link ExpirationAttributes} used by default if no expiration policy + * was specified on the {@link Region}. + * @return an {@link AnnotationBasedExpiration} instance to process expired annotated {@link Region} entries + * using Time-To-Live expiration. * @see org.springframework.data.gemfire.support.AnnotationBasedExpiration * @see org.springframework.data.gemfire.support.TimeToLiveExpiration + * @see #AnnotationBasedExpiration(ExpirationAttributes) */ public static AnnotationBasedExpiration forTimeToLive(ExpirationAttributes defaultExpirationAttributes) { return new AnnotationBasedExpiration(defaultExpirationAttributes) { - @Override protected ExpirationMetaData getExpirationMetaData(final Region.Entry entry) { + @Override protected ExpirationMetaData getExpirationMetaData(Region.Entry entry) { return (isTimeToLiveConfigured(entry) ? ExpirationMetaData.from(getTimeToLive(entry)) : super.getExpirationMetaData(entry)); } }; } - /* (non-Javadoc) */ + /** + * Initializes the Spring Expression Language (SpEL) {@link EvaluationContext} used to parse property placeholder + * and SpEL expressions in the Expiration annotation attribute values. + */ protected void initEvaluationContext() { BeanFactory beanFactory = getBeanFactory(); @@ -181,9 +203,11 @@ public class AnnotationBasedExpiration implements BeanFactoryAware, Custom if (beanFactory instanceof ConfigurableBeanFactory) { ConfigurableBeanFactory configurableBeanFactory = (ConfigurableBeanFactory) beanFactory; ConversionService conversionService = configurableBeanFactory.getConversionService(); + if (conversionService != null) { evaluationContext.setTypeConverter(new StandardTypeConverter(conversionService)); } + evaluationContext.setTypeLocator(new StandardTypeLocator(configurableBeanFactory.getBeanClassLoader())); } } @@ -197,10 +221,10 @@ public class AnnotationBasedExpiration implements BeanFactoryAware, Custom } /** - * Sets the BeanFactory managing this AnnotationBasedExpiration bean in the Spring context. + * Sets the {@link BeanFactory} managing this {@link AnnotationBasedExpiration} bean in the Spring context. * - * @param beanFactory the Spring BeanFactory to which this bean belongs. - * @throws BeansException if the BeanFactory reference cannot be initialized. + * @param beanFactory the Spring {@link BeanFactory} to which this bean belongs. + * @throws BeansException if the {@link BeanFactory} reference cannot be initialized. * @see org.springframework.beans.factory.BeanFactory * @see #initEvaluationContext() */ @@ -211,10 +235,11 @@ public class AnnotationBasedExpiration implements BeanFactoryAware, Custom } /** - * Gets a reference to the BeanFactory in which this AnnotationBasedExpiration Bean belongs. + * Gets a reference to the Spring {@link BeanFactory} in which this {@link AnnotationBasedExpiration} bean + * is managed. * - * @return a reference to the Spring BeanFactory. - * @throws java.lang.IllegalStateException if the BeanFactory reference was not properly initialized. + * @return a reference to the Spring {@link BeanFactory}. + * @throws java.lang.IllegalStateException if the {@link BeanFactory} reference was not properly initialized. * @see org.springframework.beans.factory.BeanFactory */ protected BeanFactory getBeanFactory() { @@ -249,12 +274,16 @@ public class AnnotationBasedExpiration implements BeanFactoryAware, Custom } /** - * Calculate the expiration for a given entry. Returning null indicates that the default for the Region - * should be used. The entry parameter should not be used after this method invocation completes. + * Calculate the expiration for a given entry. Returning {@literal null} indicates that the default + * for the {@link Region} should be used. The entry parameter should not be used after this method + * invocation completes. * - * @param entry the entry to calculate the expiration for. - * @return the expiration to be used, null if the Region's defaults should be used. + * @param entry the entry used to determine the appropriate expiration policy. + * @return the expiration configuration to be used or {@literal null} if the Region's defaults should be used. * @see org.apache.geode.cache.ExpirationAttributes + * @see org.apache.geode.cache.Region + * @see #getExpirationMetaData(Region.Entry) + * @see #newExpirationAttributes(ExpirationMetaData) */ @Override public ExpirationAttributes getExpiry(Region.Entry entry) { @@ -262,11 +291,11 @@ public class AnnotationBasedExpiration implements BeanFactoryAware, Custom } /** - * Gets custom expiration (Annotation-based) meta-data for the given Region entry. + * Gets custom expiration (Annotation-based) policy meta-data for the given {@link Region} entry. * - * @param entry the Region entry used as the source of the expiration meta-data. - * @return ExpirationMetaData extracted from the Region entry. - * @throws NullPointerException if the Region.Entry, Region or the Region's attributes are null. + * @param entry {@link Region} entry used as the source of the expiration policy meta-data. + * @return {@link ExpirationMetaData} extracted from the {@link Region} entry or {@literal null} + * if the expiration policy meta-data could not be determined from the {@link Region} entry. * @see org.springframework.data.gemfire.support.AnnotationBasedExpiration.ExpirationMetaData */ protected ExpirationMetaData getExpirationMetaData(Region.Entry entry) { @@ -274,15 +303,15 @@ public class AnnotationBasedExpiration implements BeanFactoryAware, Custom } /** - * Constructs a new instance of ExpirationAttributes configured with the application domain object specific - * expiration policy. If the application domain object type has not been annotated with custom expiration - * meta-data, then the default expiration settings are used. + * Constructs a new instance of {@link ExpirationAttributes} configured with the application domain object + * specific expiration policy. If the application domain object type has not been annotated with + * custom expiration meta-data, then the default expiration settings are used. * * @param expirationMetaData application domain object specific expiration policy meta-data used to construct - * the ExpirationAttributes. - * @return a custom ExpirationAttributes configured with the application domain object specific expiration policy - * or the default expiration settings if the application domain object has not been annotated with custom - * expiration meta-data. + * the {@link ExpirationAttributes}. + * @return custom {@link ExpirationAttributes} configured from the application domain object specific + * expiration policy or the default expiration settings if the application domain object has not been + * annotated with custom expiration meta-data. * @see org.springframework.data.gemfire.support.AnnotationBasedExpiration.ExpirationMetaData * @see org.apache.geode.cache.ExpirationAttributes * @see #getDefaultExpirationAttributes() @@ -301,7 +330,12 @@ public class AnnotationBasedExpiration implements BeanFactoryAware, Custom * @see #isAnnotationPresent(Object, Class) */ protected boolean isExpirationConfigured(Region.Entry entry) { - return isAnnotationPresent(entry.getValue(), Expiration.class); + return (entry != null && isExpirationConfigured(entry.getValue())); + } + + /* (non-Javadoc) */ + private boolean isExpirationConfigured(Object obj) { + return isAnnotationPresent(obj, Expiration.class); } /** @@ -314,7 +348,12 @@ public class AnnotationBasedExpiration implements BeanFactoryAware, Custom * @see #getAnnotation(Object, Class) */ protected Expiration getExpiration(Region.Entry entry) { - return getAnnotation(entry.getValue(), Expiration.class); + return getExpiration(entry.getValue()); + } + + /* (non-Javadoc) */ + private Expiration getExpiration(Object obj) { + return getAnnotation(obj, Expiration.class); } /** @@ -326,7 +365,12 @@ public class AnnotationBasedExpiration implements BeanFactoryAware, Custom * @see #isAnnotationPresent(Object, Class) */ protected boolean isIdleTimeoutConfigured(Region.Entry entry) { - return isAnnotationPresent(entry.getValue(), IdleTimeoutExpiration.class); + return (entry != null && isIdleTimeoutConfigured(entry.getValue())); + } + + /* (non-Javadoc) */ + private boolean isIdleTimeoutConfigured(Object obj) { + return isAnnotationPresent(obj, IdleTimeoutExpiration.class); } /** @@ -339,7 +383,12 @@ public class AnnotationBasedExpiration implements BeanFactoryAware, Custom * @see #getAnnotation(Object, Class) */ protected IdleTimeoutExpiration getIdleTimeout(Region.Entry entry) { - return getAnnotation(entry.getValue(), IdleTimeoutExpiration.class); + return getIdleTimeout(entry.getValue()); + } + + /* (non-Javadoc) */ + private IdleTimeoutExpiration getIdleTimeout(Object obj) { + return getAnnotation(obj, IdleTimeoutExpiration.class); } /** @@ -351,7 +400,12 @@ public class AnnotationBasedExpiration implements BeanFactoryAware, Custom * @see #isAnnotationPresent(Object, Class) */ protected boolean isTimeToLiveConfigured(Region.Entry entry) { - return isAnnotationPresent(entry.getValue(), TimeToLiveExpiration.class); + return (entry != null && isTimeToLiveConfigured(entry.getValue())); + } + + /* (non-Javadoc) */ + private boolean isTimeToLiveConfigured(Object value) { + return isAnnotationPresent(value, TimeToLiveExpiration.class); } /** @@ -364,7 +418,12 @@ public class AnnotationBasedExpiration implements BeanFactoryAware, Custom * @see #getAnnotation(Object, Class) */ protected TimeToLiveExpiration getTimeToLive(Region.Entry entry) { - return getAnnotation(entry.getValue(), TimeToLiveExpiration.class); + return getTimeToLive(entry.getValue()); + } + + /* (non-Javadoc) */ + private TimeToLiveExpiration getTimeToLive(Object obj) { + return getAnnotation(obj, TimeToLiveExpiration.class); } /* (non-Javadoc) */ @@ -399,32 +458,39 @@ public class AnnotationBasedExpiration implements BeanFactoryAware, Custom private final ExpirationActionType action; + /* (non-Javadoc) */ protected ExpirationMetaData(int timeout, ExpirationActionType action) { this.timeout = timeout; this.action = action; } + /* (non-Javadoc) */ protected static ExpirationMetaData from(ExpirationAttributes expirationAttributes) { return new ExpirationMetaData(expirationAttributes.getTimeout(), ExpirationActionType.valueOf( expirationAttributes.getAction())); } + /* (non-Javadoc) */ protected static ExpirationMetaData from(Expiration expiration) { return new ExpirationMetaData(parseTimeout(expiration.timeout()), parseAction(expiration.action())); } + /* (non-Javadoc) */ protected static ExpirationMetaData from(IdleTimeoutExpiration expiration) { return new ExpirationMetaData(parseTimeout(expiration.timeout()), parseAction(expiration.action())); } + /* (non-Javadoc) */ protected static ExpirationMetaData from(TimeToLiveExpiration expiration) { return new ExpirationMetaData(parseTimeout(expiration.timeout()), parseAction(expiration.action())); } + /* (non-Javadoc) */ public ExpirationAttributes toExpirationAttributes() { return new ExpirationAttributes(timeout(), expirationAction()); } + /* (non-Javadoc) */ protected static int parseTimeout(String timeout) { try { return Integer.parseInt(timeout); @@ -447,6 +513,7 @@ public class AnnotationBasedExpiration implements BeanFactoryAware, Custom } } + /* (non-Javadoc) */ protected static ExpirationActionType parseAction(String action) { try { return ExpirationActionType.valueOf(EXPIRATION_ACTION_CONVERTER.convert(action)); @@ -454,7 +521,7 @@ public class AnnotationBasedExpiration implements BeanFactoryAware, Custom catch (IllegalArgumentException cause) { // Next, try to parse the 'action' as a Spring Expression using SpEL. EvaluationException evaluationException = new EvaluationException(String.format( - "'%1$s' is not resolvable as a valid ExpirationAction(Type)", action), cause); + "[%s] is not resolvable as an ExpirationAction(Type)", action), cause); EvaluationContext evaluationContext = EVALUATION_CONTEXT_REFERENCE.get(); @@ -493,18 +560,24 @@ public class AnnotationBasedExpiration implements BeanFactoryAware, Custom } } + /* (non-Javadoc) */ public ExpirationActionType action() { return action; } + /* (non-Javadoc) */ public ExpirationAction expirationAction() { return action().getExpirationAction(); } + /* (non-Javadoc) */ public int timeout() { return timeout; } + /** + * @inheritDoc + */ @Override public boolean equals(final Object obj) { if (obj == this) { @@ -521,6 +594,9 @@ public class AnnotationBasedExpiration implements BeanFactoryAware, Custom && ObjectUtils.nullSafeEquals(this.action(), that.action())); } + /** + * @inheritDoc + */ @Override public int hashCode() { int hashValue = 17; @@ -529,11 +605,13 @@ public class AnnotationBasedExpiration implements BeanFactoryAware, Custom return hashValue; } + /** + * @inheritDoc + */ @Override public String toString() { return String.format("{ @type = %1$s, timeout = %2$d, action = %3$s }", getClass().getName(), timeout(), action()); } } - } diff --git a/src/test/java/org/springframework/data/gemfire/config/annotation/EnableExpirationConfigurationUnitTests.java b/src/test/java/org/springframework/data/gemfire/config/annotation/EnableExpirationConfigurationUnitTests.java new file mode 100644 index 00000000..20361645 --- /dev/null +++ b/src/test/java/org/springframework/data/gemfire/config/annotation/EnableExpirationConfigurationUnitTests.java @@ -0,0 +1,294 @@ +/* + * Copyright 2016 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.Matchers.any; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static org.springframework.data.gemfire.config.annotation.EnableExpiration.ExpirationPolicy; +import static org.springframework.data.gemfire.config.annotation.EnableExpiration.ExpirationType; + +import java.util.concurrent.atomic.AtomicReference; + +import org.apache.geode.cache.AttributesMutator; +import org.apache.geode.cache.CustomExpiry; +import org.apache.geode.cache.ExpirationAction; +import org.apache.geode.cache.ExpirationAttributes; +import org.apache.geode.cache.Region; +import org.apache.geode.cache.RegionAttributes; +import org.junit.After; +import org.junit.Test; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; +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.data.gemfire.ExpirationActionType; +import org.springframework.data.gemfire.util.ArrayUtils; + +/** + * Unit tests for the {@link EnableExpiration} annotation and {@link ExpirationConfiguration} class. + * + * @author John Blum + * @see org.junit.Test + * @see org.mockito.Mockito + * @see org.springframework.context.ConfigurableApplicationContext + * @see org.springframework.data.gemfire.config.annotation.EnableExpiration + * @see org.springframework.data.gemfire.config.annotation.ExpirationConfiguration + * @see org.apache.geode.cache.CustomExpiry + * @see org.apache.geode.cache.ExpirationAttributes + * @see org.apache.geode.cache.Region + * @since 1.9.0 + */ +public class EnableExpirationConfigurationUnitTests { + + private ConfigurableApplicationContext applicationContext; + + @After + public void tearDown() { + if (applicationContext != null) { + applicationContext.close(); + } + } + + protected void assertRegionExpiration(ExpirationAttributes expectedExpirationAttributes, + Region region, V... applicationDomainObjects) { + + assertIdleTimeoutExpiration(expectedExpirationAttributes, region, applicationDomainObjects); + assertTimeToLiveExpiration(expectedExpirationAttributes, region, applicationDomainObjects); + } + + protected void assertIdleTimeoutExpiration(ExpirationAttributes expectedExpirationAttributes, + Region region, V... applicationDomainObjects) { + + assertExpiration(expectedExpirationAttributes, region.getAttributes().getCustomEntryIdleTimeout(), + applicationDomainObjects); + } + + protected void assertNoIdleTimeoutExpiration(Region region) { + assertThat(region.getAttributes().getCustomEntryIdleTimeout()).isNull(); + } + + protected void assertTimeToLiveExpiration(ExpirationAttributes expectedExpirationAttributes, + Region region, V... applicationDomainObjects) { + + assertExpiration(expectedExpirationAttributes, region.getAttributes().getCustomEntryTimeToLive(), + applicationDomainObjects); + } + + protected void assertNoTimeToLiveExpiration(Region region) { + assertThat(region.getAttributes().getCustomEntryTimeToLive()).isNull(); + } + + @SuppressWarnings("unchecked") + protected void assertExpiration(ExpirationAttributes expectedExpirationAttributes, + CustomExpiry customExpiry, V... applicationDomainObjects) { + + Region.Entry regionEntry = mockRegionEntry(ArrayUtils.getFirst(applicationDomainObjects)); + + assertExpiration(customExpiry.getExpiry(regionEntry), expectedExpirationAttributes); + } + + @SuppressWarnings({ "unchecked", "unused" }) + protected void assertExpiration(ExpirationAttributes actualExpirationAttributes, + ExpirationAttributes expectedExpirationAttributes) { + + assertThat(actualExpirationAttributes).isEqualTo(expectedExpirationAttributes); + } + + @SuppressWarnings("unchecked") + protected Region getRegion(String beanName) { + return applicationContext.getBean(beanName, Region.class); + } + + protected AnnotationConfigApplicationContext newApplicationContext(Class... annotatedClasses) { + return new AnnotationConfigApplicationContext(annotatedClasses); + } + + protected ExpirationAttributes newExpirationAttributes(int timeout, ExpirationActionType action) { + return newExpirationAttributes(timeout, action.getExpirationAction()); + } + + protected ExpirationAttributes newExpirationAttributes(int timeout, ExpirationAction action) { + return new ExpirationAttributes(timeout, action); + } + + @Test + public void usesDefaultExpirationPolicyConfiguration() { + applicationContext = newApplicationContext(DefaultExpirationPolicyConfiguration.class); + + ExpirationAttributes expectedExpiration = newExpirationAttributes(0, ExpirationActionType.INVALIDATE); + + Region one = getRegion("One"); + Region two = getRegion("Two"); + + assertIdleTimeoutExpiration(expectedExpiration, one); + assertIdleTimeoutExpiration(expectedExpiration, two); + + assertNoTimeToLiveExpiration(one); + assertNoTimeToLiveExpiration(two); + } + + @Test + public void usesCustomExpirationPolicyConfiguration() { + applicationContext = newApplicationContext(CustomExpirationPolicyConfiguration.class); + + ExpirationAttributes expectedExpiration = newExpirationAttributes(300, ExpirationActionType.LOCAL_DESTROY); + + Region one = getRegion("One"); + Region two = getRegion("Two"); + + assertIdleTimeoutExpiration(expectedExpiration, one); + assertIdleTimeoutExpiration(expectedExpiration, two); + + assertNoTimeToLiveExpiration(one); + assertNoTimeToLiveExpiration(two); + } + + @Test + public void usesRegionSpecificExpirationPolicyConfiguration() { + applicationContext = newApplicationContext(RegionSpecificExpirationPolicyConfiguration.class); + + ExpirationAttributes expectedIdleTimeout = newExpirationAttributes(180, ExpirationActionType.INVALIDATE); + ExpirationAttributes expectedTimeToLive = newExpirationAttributes(360, ExpirationActionType.DESTROY); + + Region one = getRegion("One"); + Region two = getRegion("Two"); + + assertIdleTimeoutExpiration(expectedIdleTimeout, one); + assertNoIdleTimeoutExpiration(two); + + assertNoTimeToLiveExpiration(one); + assertTimeToLiveExpiration(expectedTimeToLive, two); + } + + @Test + public void usesMixedExpirationPolicyConfiguration() { + applicationContext = newApplicationContext(MixedExpirationPolicyConfiguration.class); + + ExpirationAttributes expectedIdleTimeout = newExpirationAttributes(120, ExpirationActionType.LOCAL_INVALIDATE); + ExpirationAttributes expectedTimeToLive = newExpirationAttributes(600, ExpirationActionType.DESTROY); + + Region one = getRegion("One"); + Region two = getRegion("Two"); + + assertIdleTimeoutExpiration(expectedIdleTimeout, one); + assertNoIdleTimeoutExpiration(two); + + assertTimeToLiveExpiration(expectedTimeToLive, one); + assertTimeToLiveExpiration(expectedTimeToLive, two); + } + + @SuppressWarnings("unchecked") + static Region mockRegion(String name) { + Region mockRegion = mock(Region.class); + + when(mockRegion.getName()).thenReturn(name); + when(mockRegion.getFullPath()).thenReturn(String.format("%1$s%2$s", Region.SEPARATOR, name)); + + final AtomicReference> entryIdleTimeout = new AtomicReference>(null); + final AtomicReference> entryTimeToLive = new AtomicReference>(null); + + AttributesMutator mockAttributesMutator = mock(AttributesMutator.class); + + when(mockRegion.getAttributesMutator()).thenReturn(mockAttributesMutator); + + doAnswer(new Answer>() { + @Override + public CustomExpiry answer(InvocationOnMock invocation) throws Throwable { + CustomExpiry customExpiry = invocation.getArgumentAt(0, CustomExpiry.class); + entryIdleTimeout.set(customExpiry); + return customExpiry; + } + }).when(mockAttributesMutator).setCustomEntryIdleTimeout(any(CustomExpiry.class)); + + doAnswer(new Answer>() { + @Override + public CustomExpiry answer(InvocationOnMock invocation) throws Throwable { + CustomExpiry customExpiry = invocation.getArgumentAt(0, CustomExpiry.class); + entryTimeToLive.set(customExpiry); + return customExpiry; + } + }).when(mockAttributesMutator).setCustomEntryTimeToLive(any(CustomExpiry.class)); + + RegionAttributes mockRegionAttributes = mock(RegionAttributes.class); + + when(mockRegion.getAttributes()).thenReturn(mockRegionAttributes); + + when(mockRegionAttributes.getCustomEntryIdleTimeout()).thenAnswer(new Answer>() { + @Override public CustomExpiry answer(InvocationOnMock invocation) throws Throwable { + return entryIdleTimeout.get(); + } + }); + + when(mockRegionAttributes.getCustomEntryTimeToLive()).thenAnswer(new Answer>() { + @Override public CustomExpiry answer(InvocationOnMock invocation) throws Throwable { + return entryTimeToLive.get(); + } + }); + + return mockRegion; + } + + @SuppressWarnings("unchecked") + static Region.Entry mockRegionEntry(V applicationDomainObject) { + Region.Entry mockRegionEntry = mock(Region.Entry.class); + when(mockRegionEntry.getValue()).thenReturn(applicationDomainObject); + return mockRegionEntry; + } + + @Configuration + @SuppressWarnings("unused") + static class RegionConfiguration { + + @Bean("One") + Region regionOne() { + return mockRegion("One"); + } + + @Bean("Two") + Region regionTwo() { + return mockRegion("Two"); + } + } + + @EnableExpiration + static class DefaultExpirationPolicyConfiguration extends RegionConfiguration { + } + + @EnableExpiration(policies = { @ExpirationPolicy(timeout = 300, action = ExpirationActionType.LOCAL_DESTROY) }) + static class CustomExpirationPolicyConfiguration extends RegionConfiguration { + } + + @EnableExpiration(policies = { + @ExpirationPolicy(timeout = 180, action = ExpirationActionType.INVALIDATE, regionNames = "One"), + @ExpirationPolicy(timeout = 360, action = ExpirationActionType.DESTROY, regionNames = "Two", types = ExpirationType.TIME_TO_LIVE) + }) + static class RegionSpecificExpirationPolicyConfiguration extends RegionConfiguration { + } + + @EnableExpiration(policies = { + @ExpirationPolicy(timeout = 120, action = ExpirationActionType.LOCAL_INVALIDATE, regionNames = "One", types = ExpirationType.IDLE_TIMEOUT), + @ExpirationPolicy(timeout = 600, action = ExpirationActionType.DESTROY, types = ExpirationType.TIME_TO_LIVE) + }) + static class MixedExpirationPolicyConfiguration extends RegionConfiguration { + } +} diff --git a/src/test/java/org/springframework/data/gemfire/support/AnnotationBasedExpirationConfigurationIntegrationTest.java b/src/test/java/org/springframework/data/gemfire/support/AnnotationBasedExpirationConfigurationIntegrationTest.java index 5cd58698..6cca8ca9 100644 --- a/src/test/java/org/springframework/data/gemfire/support/AnnotationBasedExpirationConfigurationIntegrationTest.java +++ b/src/test/java/org/springframework/data/gemfire/support/AnnotationBasedExpirationConfigurationIntegrationTest.java @@ -184,7 +184,7 @@ public class AnnotationBasedExpirationConfigurationIntegrationTest { public void invalidExpirationAction() { expectedException.expect(EvaluationException.class); expectedException.expectCause(isA(IllegalArgumentException.class)); - expectedException.expectMessage(String.format("'%1$s' is not resolvable as a valid ExpirationAction(Type)", + expectedException.expectMessage(String.format("[%s] is not resolvable as an ExpirationAction(Type)", "@expirationProperties['gemfire.region.entry.expiration.invalid.action.string']")); genericExpiration.getExpiry(mockRegionEntry(new RegionEntryWithInvalidExpirationAction())); } @@ -236,5 +236,4 @@ public class AnnotationBasedExpirationConfigurationIntegrationTest { action = "@expirationProperties['gemfire.region.entry.expiration.action.spring.type']") protected static class RegionEntryWithInvalidExpirationTimeout { } - }