/* * 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.expiration.AnnotationBasedExpiration; import org.springframework.data.gemfire.expiration.ExpirationActionType; 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<? extends Annotation> 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<String, Object> 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<Object, Object>) 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 <K> {@link Class type} of the {@link Region} keys. * @param <V> {@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 */ <K, V> Region<K, V> configure(Region<K, V> 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 <a href="https://en.wikipedia.org/wiki/Composite_pattern">Composition Software Design Pattern</a> */ 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<ExpirationPolicyConfigurer> 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 <K, V> Region<K, V> configure(Region<K, V> 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<String> regionNames = new HashSet<String>(); private final Set<ExpirationType> types = new HashSet<ExpirationType>(); /** * 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.<ExpirationActionType>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<String> regionNames, Set<ExpirationType> 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<String> regionNames, Set<ExpirationType> 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 <K, V> Region<K, V> configure(Region<K, V> region) { if (accepts(region)) { AttributesMutator<K, V> regionAttributesMutator = region.getAttributesMutator(); ExpirationAttributes defaultExpirationAttributes = defaultExpirationAttributes(); if (isIdleTimeout()) { regionAttributesMutator.setCustomEntryIdleTimeout( AnnotationBasedExpiration.<K, V>forIdleTimeout(defaultExpirationAttributes)); } if (isTimeToLive()) { regionAttributesMutator.setCustomEntryTimeToLive( AnnotationBasedExpiration.<K, V>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; } } }