/*
* Copyright 2010-2013 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.expiration;
import java.lang.annotation.Annotation;
import java.util.concurrent.atomic.AtomicReference;
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.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.context.expression.BeanFactoryAccessor;
import org.springframework.context.expression.BeanFactoryResolver;
import org.springframework.context.expression.EnvironmentAccessor;
import org.springframework.context.expression.MapAccessor;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.core.convert.ConversionService;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.EvaluationException;
import org.springframework.expression.Expression;
import org.springframework.expression.ParseException;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.expression.spel.support.StandardTypeConverter;
import org.springframework.expression.spel.support.StandardTypeLocator;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
/**
* 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
* @see org.springframework.beans.factory.BeanFactory
* @see org.springframework.beans.factory.BeanFactoryAware
* @see org.springframework.data.gemfire.expiration.Expiration
* @see org.springframework.data.gemfire.expiration.ExpirationActionType
* @see org.springframework.data.gemfire.expiration.IdleTimeoutExpiration
* @see org.springframework.data.gemfire.expiration.TimeToLiveExpiration
* @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 <a href="http://docs.spring.io/spring-data-gemfire/docs/current/reference/html/#bootstrap:region:expiration:annotation">Annotation-based Data Expiration</a>
* @since 1.7.0
*/
@SuppressWarnings("unused")
public class AnnotationBasedExpiration<K, V> implements BeanFactoryAware, CustomExpiry<K, V> {
protected static final AtomicReference<BeanFactory> BEAN_FACTORY_REFERENCE =
new AtomicReference<BeanFactory>(null);
protected static final AtomicReference<StandardEvaluationContext> EVALUATION_CONTEXT_REFERENCE
= new AtomicReference<StandardEvaluationContext>(null);
//private ExpirationAttributes defaultExpirationAttributes = ExpirationAttributes.DEFAULT;
private ExpirationAttributes defaultExpirationAttributes;
/**
* Constructs a new instance of the AnnotationBasedExpiration class with no default expiration policy.
*/
public AnnotationBasedExpiration() {
this(null);
}
/**
* Constructs a new instance of {@link AnnotationBasedExpiration} initialized with a specific, default
* expiration policy.
*
* @param defaultExpirationAttributes expiration settings used as the default expiration policy.
* @see org.apache.geode.cache.ExpirationAttributes
*/
public AnnotationBasedExpiration(ExpirationAttributes defaultExpirationAttributes) {
this.defaultExpirationAttributes = defaultExpirationAttributes;
}
/**
* 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 <K> {@link Class} type of the {@link Region} entry key.
* @param <V> {@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.expiration.AnnotationBasedExpiration
* @see org.springframework.data.gemfire.expiration.IdleTimeoutExpiration
* @see #forIdleTimeout(org.apache.geode.cache.ExpirationAttributes)
*/
public static <K, V> AnnotationBasedExpiration<K, V> forIdleTimeout() {
return forIdleTimeout(null);
}
/**
* 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 <K> {@link Class} type of the {@link Region} entry key.
* @param <V> {@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.expiration.AnnotationBasedExpiration
* @see org.springframework.data.gemfire.expiration.IdleTimeoutExpiration
* @see #AnnotationBasedExpiration(ExpirationAttributes)
*/
public static <K, V> AnnotationBasedExpiration<K, V> forIdleTimeout(ExpirationAttributes defaultExpirationAttributes) {
return new AnnotationBasedExpiration<K, V>(defaultExpirationAttributes) {
@Override protected ExpirationMetaData getExpirationMetaData(Region.Entry<K, V> entry) {
return (isIdleTimeoutConfigured(entry) ? ExpirationMetaData.from(getIdleTimeout(entry))
: super.getExpirationMetaData(entry));
}
};
}
/**
* 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 <K> {@link Class} type of the {@link Region} entry key.
* @param <V> {@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.expiration.AnnotationBasedExpiration
* @see org.springframework.data.gemfire.expiration.TimeToLiveExpiration
* @see #forTimeToLive(ExpirationAttributes)
*/
public static <K, V> AnnotationBasedExpiration<K, V> forTimeToLive() {
return forTimeToLive(null);
}
/**
* 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 <K> {@link Class} type of the {@link Region} entry key.
* @param <V> {@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.expiration.AnnotationBasedExpiration
* @see org.springframework.data.gemfire.expiration.TimeToLiveExpiration
* @see #AnnotationBasedExpiration(ExpirationAttributes)
*/
public static <K, V> AnnotationBasedExpiration<K, V> forTimeToLive(ExpirationAttributes defaultExpirationAttributes) {
return new AnnotationBasedExpiration<K, V>(defaultExpirationAttributes) {
@Override protected ExpirationMetaData getExpirationMetaData(Region.Entry<K, V> entry) {
return (isTimeToLiveConfigured(entry) ? ExpirationMetaData.from(getTimeToLive(entry))
: super.getExpirationMetaData(entry));
}
};
}
/**
* 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();
if (EVALUATION_CONTEXT_REFERENCE.compareAndSet(null, newEvaluationContext())) {
StandardEvaluationContext evaluationContext = EVALUATION_CONTEXT_REFERENCE.get();
evaluationContext.addPropertyAccessor(new BeanFactoryAccessor());
evaluationContext.addPropertyAccessor(new EnvironmentAccessor());
evaluationContext.addPropertyAccessor(new MapAccessor());
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()));
}
}
EVALUATION_CONTEXT_REFERENCE.get().setBeanResolver(new BeanFactoryResolver(beanFactory));
}
/* (non-Javadoc) */
StandardEvaluationContext newEvaluationContext() {
return new StandardEvaluationContext();
}
/**
* Sets the {@link BeanFactory} managing this {@link AnnotationBasedExpiration} bean in the Spring context.
*
* @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()
*/
@Override
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
BEAN_FACTORY_REFERENCE.set(beanFactory);
initEvaluationContext();
}
/**
* Gets a reference to the Spring {@link BeanFactory} in which this {@link AnnotationBasedExpiration} bean
* is managed.
*
* @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() {
BeanFactory localBeanFactory = BEAN_FACTORY_REFERENCE.get();
Assert.state(localBeanFactory != null, "beanFactory was not properly initialized");
return localBeanFactory;
}
/**
* Sets the expiration policy to use by default when no application domain object specific expiration meta-data
* has been specified.
*
* @param defaultExpirationAttributes expiration settings used as the default expiration policy.
* @see #getDefaultExpirationAttributes()
* @see org.apache.geode.cache.ExpirationAttributes
*/
public void setDefaultExpirationAttributes(ExpirationAttributes defaultExpirationAttributes) {
this.defaultExpirationAttributes = defaultExpirationAttributes;
}
/**
* Gets the expiration policy used by default when no application domain object specific expiration meta-data
* has been specified.
*
* @return an instance of ExpirationAttributes with expiration settings defining the default expiration policy.
* @see #setDefaultExpirationAttributes(org.apache.geode.cache.ExpirationAttributes)
* @see org.apache.geode.cache.ExpirationAttributes
*/
protected ExpirationAttributes getDefaultExpirationAttributes() {
//return (defaultExpirationAttributes != null ? defaultExpirationAttributes : ExpirationAttributes.DEFAULT);
return defaultExpirationAttributes;
}
/**
* 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 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<K, V> entry) {
return newExpirationAttributes(getExpirationMetaData(entry));
}
/**
* Gets custom expiration (Annotation-based) policy meta-data for the given {@link Region} entry.
*
* @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.expiration.AnnotationBasedExpiration.ExpirationMetaData
*/
protected ExpirationMetaData getExpirationMetaData(Region.Entry<K, V> entry) {
return (isExpirationConfigured(entry) ? ExpirationMetaData.from(getExpiration(entry)) : null);
}
/**
* 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 {@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.expiration.AnnotationBasedExpiration.ExpirationMetaData
* @see org.apache.geode.cache.ExpirationAttributes
* @see #getDefaultExpirationAttributes()
*/
protected ExpirationAttributes newExpirationAttributes(ExpirationMetaData expirationMetaData) {
return (expirationMetaData != null ? expirationMetaData.toExpirationAttributes()
: getDefaultExpirationAttributes());
}
/**
* Determines whether the Region Entry has been annotated with the Expiration Annotation.
*
* @param entry the Region.Entry to evaluate for the presence of the Expiration Annotation.
* @return a boolean value indicating whether the Region Entry has been annotated with @Expiration.
* @see Expiration
* @see #isAnnotationPresent(Object, Class)
*/
protected boolean isExpirationConfigured(Region.Entry<K, V> entry) {
return (entry != null && isExpirationConfigured(entry.getValue()));
}
/* (non-Javadoc) */
private boolean isExpirationConfigured(Object obj) {
return isAnnotationPresent(obj, Expiration.class);
}
/**
* Gets the Expiration Annotation meta-data from the Region Entry.
*
* @param entry the Region.Entry from which to extract the Expiration Annotation meta-data.
* @return the Expiration Annotation meta-data for the given Region Entry or {@code null}
* if the Region Entry has not been annotated with @Expiration.
* @see Expiration
* @see #getAnnotation(Object, Class)
*/
protected Expiration getExpiration(Region.Entry<K, V> entry) {
return getExpiration(entry.getValue());
}
/* (non-Javadoc) */
private Expiration getExpiration(Object obj) {
return getAnnotation(obj, Expiration.class);
}
/**
* Determines whether the Region Entry has been annotated with the IdleTimeoutExpiration Annotation.
*
* @param entry the Region.Entry to evaluate for the presence of the IdleTimeoutExpiration Annotation.
* @return a boolean value indicating whether the Region Entry has been annotated with @IdleTimeoutExpiration.
* @see IdleTimeoutExpiration
* @see #isAnnotationPresent(Object, Class)
*/
protected boolean isIdleTimeoutConfigured(Region.Entry<K, V> entry) {
return (entry != null && isIdleTimeoutConfigured(entry.getValue()));
}
/* (non-Javadoc) */
private boolean isIdleTimeoutConfigured(Object obj) {
return isAnnotationPresent(obj, IdleTimeoutExpiration.class);
}
/**
* Gets the IdleTimeoutExpiration Annotation meta-data from the Region Entry.
*
* @param entry the Region.Entry from which to extract the IdleTimeoutExpiration Annotation meta-data.
* @return the IdleTimeoutExpiration Annotation meta-data for the given Region Entry or {@code null}
* if the Region Entry has not been annotated with @IdleTimeoutExpiration.
* @see IdleTimeoutExpiration
* @see #getAnnotation(Object, Class)
*/
protected IdleTimeoutExpiration getIdleTimeout(Region.Entry<K, V> entry) {
return getIdleTimeout(entry.getValue());
}
/* (non-Javadoc) */
private IdleTimeoutExpiration getIdleTimeout(Object obj) {
return getAnnotation(obj, IdleTimeoutExpiration.class);
}
/**
* Determines whether the Region Entry has been annotated with the TimeToLiveExpiration Annotation.
*
* @param entry the Region.Entry to evaluate for the presence of the TimeToLiveExpiration Annotation.
* @return a boolean value indicating whether the Region Entry has been annotated with @TimeToLiveExpiration.
* @see TimeToLiveExpiration
* @see #isAnnotationPresent(Object, Class)
*/
protected boolean isTimeToLiveConfigured(Region.Entry<K, V> entry) {
return (entry != null && isTimeToLiveConfigured(entry.getValue()));
}
/* (non-Javadoc) */
private boolean isTimeToLiveConfigured(Object value) {
return isAnnotationPresent(value, TimeToLiveExpiration.class);
}
/**
* Gets the TimeToLiveExpiration Annotation meta-data from the Region Entry.
*
* @param entry the Region.Entry from which to extract the TimeToLiveExpiration Annotation meta-data.
* @return the TimeToLiveExpiration Annotation meta-data for the given Region Entry or {@code null}
* if the Region Entry has not been annotated with @TimeToLiveExpiration.
* @see TimeToLiveExpiration
* @see #getAnnotation(Object, Class)
*/
protected TimeToLiveExpiration getTimeToLive(Region.Entry<K, V> entry) {
return getTimeToLive(entry.getValue());
}
/* (non-Javadoc) */
private TimeToLiveExpiration getTimeToLive(Object obj) {
return getAnnotation(obj, TimeToLiveExpiration.class);
}
/* (non-Javadoc) */
private <T extends Annotation> boolean isAnnotationPresent(Object obj, Class<T> annotationType) {
return (obj != null && obj.getClass().isAnnotationPresent(annotationType));
}
/* (non-Javadoc) */
private <T extends Annotation> T getAnnotation(Object obj, Class<T> annotationType) {
return AnnotationUtils.getAnnotation(obj.getClass(), annotationType);
}
/**
* Called when the Region containing this callback is closed or destroyed, when the Cache is closed,
* or when a callback is removed from a Region using an AttributesMutator.
*/
@Override
public void close() {
}
/**
* The ExpirationMetaData class encapsulates the settings constituting the expiration policy including
* the expiration timeout and the action performed when expiration occurs.
*
* @see org.apache.geode.cache.ExpirationAttributes
*/
protected static class ExpirationMetaData {
private static final ExpirationActionConverter EXPIRATION_ACTION_CONVERTER = new ExpirationActionConverter();
private final int timeout;
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);
}
catch (NumberFormatException cause) {
try {
// Next, try to parse the 'timeout' as a Spring Expression using SpEL.
return new SpelExpressionParser().parseExpression(timeout).getValue(
EVALUATION_CONTEXT_REFERENCE.get(), Integer.TYPE);
}
catch (ParseException e) {
// Finally, try to process the 'timeout' as a Spring Property Placeholder.
if (BEAN_FACTORY_REFERENCE.get() instanceof ConfigurableBeanFactory) {
return Integer.parseInt(((ConfigurableBeanFactory) BEAN_FACTORY_REFERENCE.get())
.resolveEmbeddedValue(timeout));
}
throw cause;
}
}
}
/* (non-Javadoc) */
protected static ExpirationActionType parseAction(String action) {
try {
return ExpirationActionType.valueOf(EXPIRATION_ACTION_CONVERTER.convert(action));
}
catch (IllegalArgumentException cause) {
// Next, try to parse the 'action' as a Spring Expression using SpEL.
EvaluationException evaluationException = new EvaluationException(String.format(
"[%s] is not resolvable as an ExpirationAction(Type)", action), cause);
EvaluationContext evaluationContext = EVALUATION_CONTEXT_REFERENCE.get();
try {
Expression expression = new SpelExpressionParser().parseExpression(action);
Class<?> valueType = expression.getValueType(evaluationContext);
if (String.class.equals(valueType)) {
return ExpirationActionType.valueOf(EXPIRATION_ACTION_CONVERTER.convert(expression.getValue(
evaluationContext, String.class)));
}
else if (ExpirationAction.class.equals(valueType)) {
return ExpirationActionType.valueOf(expression.getValue(evaluationContext, ExpirationAction.class));
}
else if (ExpirationActionType.class.equals(valueType)) {
return expression.getValue(evaluationContext, ExpirationActionType.class);
}
throw evaluationException;
}
catch (ParseException e) {
// Finally, try to process the 'action' as a Spring Property Placeholder.
if (BEAN_FACTORY_REFERENCE.get() instanceof ConfigurableBeanFactory) {
try {
String resolvedValue = ((ConfigurableBeanFactory) BEAN_FACTORY_REFERENCE.get())
.resolveEmbeddedValue(action);
return ExpirationActionType.valueOf(EXPIRATION_ACTION_CONVERTER.convert(resolvedValue));
}
catch (IllegalArgumentException ignore) {
}
}
throw evaluationException;
}
}
}
/* (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) {
return true;
}
if (!(obj instanceof ExpirationMetaData)) {
return false;
}
ExpirationMetaData that = (ExpirationMetaData) obj;
return (this.timeout() == that.timeout()
&& ObjectUtils.nullSafeEquals(this.action(), that.action()));
}
/**
* @inheritDoc
*/
@Override
public int hashCode() {
int hashValue = 17;
hashValue = 37 * hashValue + ObjectUtils.nullSafeHashCode(timeout());
hashValue = 37 * hashValue + ObjectUtils.nullSafeHashCode(action());
return hashValue;
}
/**
* @inheritDoc
*/
@Override
public String toString() {
return String.format("{ @type = %1$s, timeout = %2$d, action = %3$s }", getClass().getName(),
timeout(), action());
}
}
}