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 ad5d7f82..c2a47927 100644 --- a/src/main/java/org/springframework/data/gemfire/support/AnnotationBasedExpiration.java +++ b/src/main/java/org/springframework/data/gemfire/support/AnnotationBasedExpiration.java @@ -22,14 +22,23 @@ import java.util.concurrent.atomic.AtomicReference; 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.data.gemfire.ExpirationActionConverter; import org.springframework.data.gemfire.ExpirationActionType; +import org.springframework.expression.EvaluationContext; +import org.springframework.expression.EvaluationException; import org.springframework.expression.Expression; -import org.springframework.expression.ExpressionParser; +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; @@ -45,6 +54,7 @@ import com.gemstone.gemfire.cache.Region; * 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.ExpirationActionType @@ -60,7 +70,10 @@ import com.gemstone.gemfire.cache.Region; @SuppressWarnings("unused") public class AnnotationBasedExpiration implements BeanFactoryAware, CustomExpiry { - private static final AtomicReference beanFactory = new AtomicReference(null); + protected static final AtomicReference BEAN_FACTORY_REFERENCE = new AtomicReference(null); + + protected static final AtomicReference EVALUATION_CONTEXT_REFERENCE + = new AtomicReference(null); //private ExpirationAttributes defaultExpirationAttributes = ExpirationAttributes.DEFAULT; private ExpirationAttributes defaultExpirationAttributes; @@ -155,13 +168,58 @@ public class AnnotationBasedExpiration implements BeanFactoryAware, Custom }; } - @Override - public void setBeanFactory(final BeanFactory beanFactory) throws BeansException { - AnnotationBasedExpiration.beanFactory.set(beanFactory); + /* (non-Javadoc) */ + 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 BeanFactory managing this 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. + * @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 BeanFactory in which this AnnotationBasedExpiration Bean belongs. + * + * @return a reference to the Spring BeanFactory. + * @throws java.lang.IllegalStateException if the BeanFactory reference was not properly initialized. + * @see org.springframework.beans.factory.BeanFactory + */ protected BeanFactory getBeanFactory() { - BeanFactory localBeanFactory = beanFactory.get(); + BeanFactory localBeanFactory = BEAN_FACTORY_REFERENCE.get(); Assert.state(localBeanFactory != null, "beanFactory was not properly initialized"); return localBeanFactory; } @@ -372,12 +430,21 @@ public class AnnotationBasedExpiration implements BeanFactoryAware, Custom try { return Integer.parseInt(timeout); } - catch (NumberFormatException ignore) { - StandardEvaluationContext evaluationContext = new StandardEvaluationContext(); + 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)); + } - evaluationContext.setBeanResolver(new BeanFactoryResolver(beanFactory.get())); - - return new SpelExpressionParser().parseExpression(timeout).getValue(evaluationContext, Integer.TYPE); + throw cause; + } } } @@ -385,21 +452,44 @@ public class AnnotationBasedExpiration implements BeanFactoryAware, Custom try { return ExpirationActionType.valueOf(EXPIRATION_ACTION_CONVERTER.convert(action)); } - catch (RuntimeException e) { - StandardEvaluationContext evaluationContext = new StandardEvaluationContext(); - evaluationContext.setBeanResolver(new BeanFactoryResolver(beanFactory.get())); - ExpressionParser parser = new SpelExpressionParser(); - Expression expression = parser.parseExpression(action); - Class valueType = expression.getValueType(evaluationContext); + 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); - if (String.class.equals(valueType)) { - return ExpirationActionType.valueOfIgnoreCase(expression.getValue(evaluationContext, String.class)); + 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; } - else if (ExpirationAction.class.equals(valueType)) { - return ExpirationActionType.valueOf(expression.getValue(evaluationContext, ExpirationAction.class)); - } - else { - return expression.getValue(evaluationContext, ExpirationActionType.class); + 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; } } } 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 f1b0ad18..24db16ff 100644 --- a/src/test/java/org/springframework/data/gemfire/support/AnnotationBasedExpirationConfigurationIntegrationTest.java +++ b/src/test/java/org/springframework/data/gemfire/support/AnnotationBasedExpirationConfigurationIntegrationTest.java @@ -55,6 +55,9 @@ import com.gemstone.gemfire.cache.Region; * @see org.springframework.data.gemfire.test.GemfireTestApplicationContextInitializer * @see org.springframework.test.context.ContextConfiguration * @see org.springframework.test.context.junit4.SpringJUnit4ClassRunner + * @see com.gemstone.gemfire.cache.CustomExpiry + * @see com.gemstone.gemfire.cache.ExpirationAction + * @see com.gemstone.gemfire.cache.ExpirationAttributes * @since 1.7.0 */ @RunWith(SpringJUnit4ClassRunner.class) @@ -187,8 +190,8 @@ public class AnnotationBasedExpirationConfigurationIntegrationTest { protected static class ApplicationDomainObjectWithNoExpirationPolicy { } - @TimeToLiveExpiration(timeout = "@expirationProperties['gemfire.region.entry.expiration.timeout']", - action = "@expirationProperties['gemfire.region.entry.expiration.action.string']") + @TimeToLiveExpiration(timeout = "${gemfire.region.entry.expiration.timeout}", + action = "${gemfire.region.entry.expiration.action.string}") protected static class RegionEntryTimeToLiveExpirationPolicy { } @@ -197,7 +200,7 @@ public class AnnotationBasedExpirationConfigurationIntegrationTest { protected static class RegionEntryIdleTimeoutExpirationPolicy { } - @Expiration(timeout = "@expirationProperties['gemfire.region.entry.expiration.timeout']", + @Expiration(timeout = "${gemfire.region.entry.expiration.timeout}", action = "@expirationProperties['gemfire.region.entry.expiration.action.spring.type']") protected static class RegionEntryGenericExpirationPolicy { } diff --git a/src/test/java/org/springframework/data/gemfire/support/AnnotationBasedExpirationTest.java b/src/test/java/org/springframework/data/gemfire/support/AnnotationBasedExpirationTest.java index d381863d..d6853fb9 100644 --- a/src/test/java/org/springframework/data/gemfire/support/AnnotationBasedExpirationTest.java +++ b/src/test/java/org/springframework/data/gemfire/support/AnnotationBasedExpirationTest.java @@ -17,13 +17,16 @@ package org.springframework.data.gemfire.support; import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.instanceOf; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.CoreMatchers.isA; import static org.hamcrest.CoreMatchers.not; import static org.hamcrest.CoreMatchers.nullValue; import static org.hamcrest.CoreMatchers.sameInstance; +import static org.junit.Assert.assertSame; import static org.junit.Assert.assertThat; import static org.mockito.Mockito.atLeast; +import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @@ -31,7 +34,19 @@ import static org.mockito.Mockito.when; import static org.springframework.data.gemfire.support.AnnotationBasedExpiration.ExpirationMetaData; import org.junit.Test; +import org.mockito.Matchers; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; +import org.springframework.beans.factory.config.ConfigurableBeanFactory; +import org.springframework.context.expression.BeanFactoryResolver; +import org.springframework.core.convert.ConversionService; import org.springframework.data.gemfire.ExpirationActionType; +import org.springframework.data.gemfire.TestUtils; +import org.springframework.expression.BeanResolver; +import org.springframework.expression.PropertyAccessor; +import org.springframework.expression.TypeConverter; +import org.springframework.expression.TypeLocator; +import org.springframework.expression.spel.support.StandardEvaluationContext; import com.gemstone.gemfire.cache.ExpirationAction; import com.gemstone.gemfire.cache.ExpirationAttributes; @@ -146,6 +161,51 @@ public class AnnotationBasedExpirationTest { verify(mockRegionEntry, atLeast(3)).getValue(); } + @Test + public void setAndGetBeanFactory() { + final StandardEvaluationContext mockEvaluationContext = mock(StandardEvaluationContext.class, + "MockStandardEvaluationContext"); + + ConversionService mockConversionService = mock(ConversionService.class, "MockConversionService"); + + final ConfigurableBeanFactory mockBeanFactory = mock(ConfigurableBeanFactory.class, "MockBeanFactory"); + + when(mockBeanFactory.getConversionService()).thenReturn(mockConversionService); + when(mockBeanFactory.getBeanClassLoader()).thenReturn(Thread.currentThread().getContextClassLoader()); + + doAnswer(new Answer() { + @Override public Void answer(final InvocationOnMock invocation) throws Throwable { + BeanResolver beanResolver = invocation.getArgumentAt(0, BeanResolver.class); + assertThat(beanResolver, is(instanceOf(BeanFactoryResolver.class))); + assertThat(TestUtils.readField("beanFactory", beanResolver), + is(equalTo(mockBeanFactory))); + return null; + } + }).when(mockEvaluationContext).setBeanResolver(Matchers.any(BeanResolver.class)); + + AnnotationBasedExpiration annotationBasedExpiration = new AnnotationBasedExpiration() { + @Override StandardEvaluationContext newEvaluationContext() { + return mockEvaluationContext; + } + }; + + annotationBasedExpiration.setBeanFactory(mockBeanFactory); + + assertSame(mockBeanFactory, annotationBasedExpiration.getBeanFactory()); + + verify(mockEvaluationContext, times(3)).addPropertyAccessor(Matchers.any(PropertyAccessor.class)); + verify(mockEvaluationContext, times(1)).setTypeConverter(Matchers.any(TypeConverter.class)); + verify(mockEvaluationContext, times(1)).setTypeLocator(Matchers.any(TypeLocator.class)); + verify(mockEvaluationContext, times(1)).setBeanResolver(Matchers.any(BeanResolver.class)); + verify(mockBeanFactory, times(1)).getConversionService(); + verify(mockBeanFactory, times(1)).getBeanClassLoader(); + } + + @Test(expected = IllegalStateException.class) + public void getUninitializedBeanFactory() { + new AnnotationBasedExpiration().getBeanFactory(); + } + @Test public void setAndGetDefaultExpirationAttributes() { ExpirationAttributes expectedExpirationAttributes = new ExpirationAttributes(120, ExpirationAction.INVALIDATE);