diff --git a/src/main/java/org/springframework/data/gemfire/EvictionAttributesFactoryBean.java b/src/main/java/org/springframework/data/gemfire/EvictionAttributesFactoryBean.java index 2b0c3301..8457e0ca 100644 --- a/src/main/java/org/springframework/data/gemfire/EvictionAttributesFactoryBean.java +++ b/src/main/java/org/springframework/data/gemfire/EvictionAttributesFactoryBean.java @@ -52,6 +52,9 @@ public class EvictionAttributesFactoryBean implements FactoryBean getObjectType() { return (evictionAttributes != null ? evictionAttributes.getClass() : EvictionAttributes.class); } - /* non-Javadoc */ + /** + * @inheritDoc + */ public boolean isSingleton() { return true; } @@ -168,5 +177,4 @@ public class EvictionAttributesFactoryBean implements FactoryBean, - InitializingBean { +@SuppressWarnings({ "unused" }) +public class RegionAttributesFactoryBean extends AttributesFactory + implements FactoryBean, InitializingBean { private RegionAttributes attributes; + /** + * @inheritDoc + */ + @Override + public void afterPropertiesSet() throws Exception { + attributes = super.create(); + } + + /** + * @inheritDoc + */ @Override public RegionAttributes getObject() throws Exception { return attributes; } + /** + * @inheritDoc + */ @Override public Class getObjectType() { return (attributes != null ? attributes.getClass() : RegionAttributes.class); } + /** + * @inheritDoc + */ @Override public boolean isSingleton() { return true; @@ -55,10 +72,4 @@ public class RegionAttributesFactoryBean extends AttributesFactory implements Fa public void setIndexUpdateType(final IndexMaintenancePolicyType indexUpdateType) { indexUpdateType.setIndexMaintenance(this); } - - @Override - public void afterPropertiesSet() throws Exception { - attributes = super.create(); - } - } diff --git a/src/main/java/org/springframework/data/gemfire/RegionFactoryBean.java b/src/main/java/org/springframework/data/gemfire/RegionFactoryBean.java index 6877df59..456a68de 100644 --- a/src/main/java/org/springframework/data/gemfire/RegionFactoryBean.java +++ b/src/main/java/org/springframework/data/gemfire/RegionFactoryBean.java @@ -27,6 +27,7 @@ import org.apache.geode.cache.CacheLoader; import org.apache.geode.cache.CacheWriter; import org.apache.geode.cache.DataPolicy; import org.apache.geode.cache.EvictionAction; +import org.apache.geode.cache.EvictionAttributes; import org.apache.geode.cache.GemFireCache; import org.apache.geode.cache.PartitionAttributes; import org.apache.geode.cache.PartitionAttributesFactory; @@ -43,6 +44,7 @@ import org.springframework.context.SmartLifecycle; import org.springframework.core.io.Resource; import org.springframework.data.gemfire.client.ClientRegionFactoryBean; import org.springframework.data.gemfire.support.RegionShortcutWrapper; +import org.springframework.data.gemfire.util.ArrayUtils; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; import org.springframework.util.ReflectionUtils; @@ -73,6 +75,8 @@ public abstract class RegionFactoryBean extends RegionLookupFactoryBean[] cacheListeners; private CacheLoader cacheLoader; @@ -81,8 +85,9 @@ public abstract class RegionFactoryBean extends RegionLookupFactoryBean attributes; @@ -94,15 +99,21 @@ public abstract class RegionFactoryBean extends RegionLookupFactoryBean lookupFallback(GemFireCache gemfireCache, String regionName) throws Exception { + @SuppressWarnings({ "unchecked" }) + protected Region lookupRegion(GemFireCache gemfireCache, String regionName) throws Exception { Assert.isTrue(gemfireCache instanceof Cache, String.format("Unable to create Regions from '%1$s'.", gemfireCache)); @@ -110,22 +121,12 @@ public abstract class RegionFactoryBean extends RegionLookupFactoryBean regionFactory = createRegionFactory(cache); - if (!ObjectUtils.isEmpty(asyncEventQueues)) { - for (Object asyncEventQueue : asyncEventQueues) { - regionFactory.addAsyncEventQueueId(((AsyncEventQueue) asyncEventQueue).getId()); - } + for (AsyncEventQueue asyncEventQueue : ArrayUtils.nullSafeArray(asyncEventQueues, AsyncEventQueue.class)) { + regionFactory.addAsyncEventQueueId(asyncEventQueue.getId()); } - if (!ObjectUtils.isEmpty(gatewaySenders)) { - for (Object gatewaySender : gatewaySenders) { - regionFactory.addGatewaySenderId(((GatewaySender) gatewaySender).getId()); - } - } - - if (!ObjectUtils.isEmpty(cacheListeners)) { - for (CacheListener listener : cacheListeners) { - regionFactory.addCacheListener(listener); - } + for (CacheListener listener : ArrayUtils.nullSafeArray(cacheListeners, CacheListener.class)) { + regionFactory.addCacheListener(listener); } if (cacheLoader != null) { @@ -142,6 +143,14 @@ public abstract class RegionFactoryBean extends RegionLookupFactoryBean extends RegionLookupFactoryBean createRegionFactory(final Cache cache) { + protected RegionFactory createRegionFactory(Cache cache) { if (shortcut != null) { RegionFactory regionFactory = mergeRegionAttributes( cache.createRegionFactory(shortcut), attributes); @@ -218,14 +227,14 @@ public abstract class RegionFactoryBean extends RegionLookupFactoryBean T getFieldValue(final Object source, final String fieldName, final Class targetType) { + private T getFieldValue(Object source, String fieldName, Class targetType) { Field field = ReflectionUtils.findField(source.getClass(), fieldName, targetType); ReflectionUtils.makeAccessible(field); return (T) ReflectionUtils.getField(field, source); @@ -251,8 +260,8 @@ public abstract class RegionFactoryBean extends RegionLookupFactoryBean RegionFactory mergeRegionAttributes(final RegionFactory regionFactory, - final RegionAttributes regionAttributes) { + protected RegionFactory mergeRegionAttributes(RegionFactory regionFactory, + RegionAttributes regionAttributes) { if (regionAttributes != null) { // NOTE this validation may not be strictly required depending on how the RegionAttributes were "created", @@ -297,6 +306,7 @@ public abstract class RegionFactoryBean extends RegionLookupFactoryBean void mergePartitionAttributes(final RegionFactory regionFactory, final RegionAttributes regionAttributes) { @@ -334,7 +344,7 @@ public abstract class RegionFactoryBean extends RegionLookupFactoryBean extends RegionLookupFactoryBean region) { + protected Region postProcess(Region region) { + return region; } /** @@ -531,7 +542,7 @@ public abstract class RegionFactoryBean extends RegionLookupFactoryBean extends RegionLookupFactoryBean extends RegionLookupFactoryBean extends RegionLookupFactoryBean extends RegionLookupFactoryBean implements FactoryBean parent; + private volatile Region region; private String beanName; private String name; private String regionName; + /** + * @inheritDoc + */ + @Override + @SuppressWarnings("all") public void afterPropertiesSet() throws Exception { - Assert.notNull(cache, "the 'cache' reference property must be set"); + Assert.notNull(this.cache, "A 'Cache' reference must be set"); - String regionName = (StringUtils.hasText(this.regionName) ? this.regionName - : (StringUtils.hasText(name) ? name : beanName)); + String regionName = resolveRegionName(); Assert.hasText(regionName, "'regionName', 'name' or 'beanName' property must be set"); - synchronized (cache) { - //region = (getParent() != null ? getParent().getSubregion(regionName) : cache.getRegion(regionName)); + synchronized (this.cache) { if (isLookupEnabled()) { if (getParent() != null) { - region = getParent().getSubregion(regionName); + this.region = getParent().getSubregion(regionName); } else { - region = cache.getRegion(regionName); + this.region = this.cache.getRegion(regionName); } } if (region != null) { - log.info(String.format("found Region (%1$s) in Cache (%2$s)", regionName, cache.getName())); + log.info(String.format("Found Region [%1$s] in Cache [%2$s]", regionName, cache.getName())); } else { - region = lookupFallback(cache, regionName); + region = lookupRegion(cache, regionName); } } } /** - * Fallback method in case the named Region does not exist. By default, this implementation throws an exception. + * Method to perform a lookup when the named {@link Region} does not exist. By default, this implementation + * throws an exception. * - * @param cache a reference to the GemFire Cache. - * @param regionName the name of the GemFire Cache Region. - * @return the Region in the GemFire Cache with the given name. - * @throws Exception if the lookup operation fails. + * @param cache reference to the GemFire cache. + * @param regionName name of the GemFire {@link Region}. + * @return the {@link Region} in the GemFire cache with the given name. + * @throws BeanInitializationException if the lookup operation fails. + * @see org.apache.geode.cache.Region */ - protected Region lookupFallback(GemFireCache cache, String regionName) throws Exception { - throw new BeanInitializationException(String.format("Cannot find Region [%1$s] in Cache [%2$s].", - regionName, cache)); + protected Region lookupRegion(GemFireCache cache, String regionName) throws Exception { + throw new BeanInitializationException(String.format( + "Region [%1$s] in Cache [%2$s] not found", regionName, cache)); } + /** + * @inheritDoc + */ + @Override public Region getObject() throws Exception { return getRegion(); } + /** + * @inheritDoc + */ + @Override public Class getObjectType() { - Region localRegion = getRegion(); - return (localRegion != null ? localRegion.getClass() : Region.class); + Region region = getRegion(); + return (region != null ? region.getClass() : Region.class); } + /** + * @inheritDoc + */ + @Override public boolean isSingleton() { return true; } /** - * Sets the name of the Cache Region based on the bean 'id' attribute. If no Region is found for the given name, - * a new one will be created. + * Resolves the name of the GemFire {@link Region}. * - * @param name the name of this bean (Region) in the application context (bean factory). + * @return a {@link String} indicating the name of the GemFire {@link Region}. + * @see org.apache.geode.cache.Region#getName() + */ + public String resolveRegionName() { + return (StringUtils.hasText(this.regionName) ? this.regionName + : (StringUtils.hasText(this.name) ? this.name : this.beanName)); + } + + /** + * Sets the name of the {@link Region} based on the bean 'id' attribute. If no {@link Region} is found + * with the given name, a new one will be created. + * + * @param name name of this {@link Region} bean in the Spring {@link org.springframework.context.ApplicationContext}. * @see org.springframework.beans.factory.BeanNameAware#setBeanName(String) */ public void setBeanName(String name) { @@ -120,9 +149,9 @@ public abstract class RegionLookupFactoryBean implements FactoryBean implements FactoryBean parent) { - this.parent = parent; - } - - /** - * Gets a reference to the parent Region if this FactoryBean represents a GemFire Cache Sub-Region. - * - * @return a reference to the parent Region or null if this Region is not a Sub-Region. - * @see org.apache.geode.cache.Region - */ - protected Region getParent() { - return parent; - } - - /** - * Gets the reference to the GemFire Region obtained by this Spring FactoryBean during the lookup operation. - * - * @return a reference to the GemFire Region found during lookup. - * @see org.apache.geode.cache.Region - */ - protected Region getRegion() { - return region; - } - - /** - * Sets the name of the Cache Region as expected by GemFire. If no Region is found with the given name, a new one - * will be created. If no name is given, the value of the 'name' property will be used. - * - * @param regionName a String indicating the name of the Region in GemFire. - * @see #setName(String) - * @see org.apache.geode.cache.Region#getName() - */ - public void setRegionName(String regionName) { - this.regionName = regionName; - } - /* (non-Javadoc) */ boolean isLookupEnabled() { return Boolean.TRUE.equals(getLookupEnabled()); @@ -199,4 +174,65 @@ public abstract class RegionLookupFactoryBean implements FactoryBean>>>>>> fffb2b0... SGF-547 - Configure Eviction with annotations. + */ + public void setName(String name) { + this.name = name; + } + + /** + * Sets a reference to the parent {@link Region} to indicated this {@link FactoryBean} represents a GemFire cache + * {@link Region Sub-Region}. + * + * @param parent reference to the parent {@link Region}. + * @see org.apache.geode.cache.Region + */ + public void setParent(Region parent) { + this.parent = parent; + } + + /** + * Returns a reference to the parent {@link Region} indicating this {@link FactoryBean} represents a GemFire cache + * {@link Region Sub-Region}. + * + * @return a reference to the parent {@link Region} or {@literal null} if this {@link Region} + * is not a {@link Region Sub-Region}. + * @see org.apache.geode.cache.Region + */ + protected Region getParent() { + return this.parent; + } + + /** + * Returns a reference to the GemFire {@link Region} resolved by this Spring {@link FactoryBean} + * during the lookup operation; maybe a new {@link Region}. + * + * @return a reference to the GemFire {@link Region} resolved during lookup. + * @see org.apache.geode.cache.Region + */ + protected Region getRegion() { + return this.region; + } + + /** + * Sets the name of the cache {@link Region}. If no {@link Region} is found with the given name, + * a new one will be created. If no name is given, the value of the 'name' property will be used. + * + * @param regionName name of the {@link Region}. + * @see #setName(String) + * @see org.apache.geode.cache.Region#getName() + */ + public void setRegionName(String regionName) { + this.regionName = regionName; + } } diff --git a/src/main/java/org/springframework/data/gemfire/client/ClientRegionFactoryBean.java b/src/main/java/org/springframework/data/gemfire/client/ClientRegionFactoryBean.java index a55d95f6..b46a2d80 100644 --- a/src/main/java/org/springframework/data/gemfire/client/ClientRegionFactoryBean.java +++ b/src/main/java/org/springframework/data/gemfire/client/ClientRegionFactoryBean.java @@ -22,6 +22,7 @@ import org.apache.geode.cache.CacheListener; import org.apache.geode.cache.CacheLoader; import org.apache.geode.cache.CacheWriter; import org.apache.geode.cache.DataPolicy; +import org.apache.geode.cache.EvictionAttributes; import org.apache.geode.cache.GemFireCache; import org.apache.geode.cache.Region; import org.apache.geode.cache.RegionAttributes; @@ -33,17 +34,19 @@ import org.springframework.beans.BeansException; import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.BeanFactoryAware; import org.springframework.beans.factory.DisposableBean; +import org.springframework.beans.factory.FactoryBean; import org.springframework.core.io.Resource; import org.springframework.data.gemfire.DataPolicyConverter; import org.springframework.data.gemfire.GemfireUtils; import org.springframework.data.gemfire.RegionLookupFactoryBean; import org.springframework.data.gemfire.config.xml.GemfireConstants; +import org.springframework.data.gemfire.util.ArrayUtils; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; /** - * Client extension for GemFire Regions. + * Spring {@link FactoryBean} used to create a GemFire client {@link Region}. * * @author Costin Leau * @author David Turanski @@ -86,6 +89,8 @@ public class ClientRegionFactoryBean extends RegionLookupFactoryBean private DataPolicy dataPolicy; + private EvictionAttributes evictionAttributes; + private Interest[] interests; private RegionAttributes attributes; @@ -95,75 +100,61 @@ public class ClientRegionFactoryBean extends RegionLookupFactoryBean private String diskStoreName; private String poolName; + /** + * @inheritDoc + */ @Override public void afterPropertiesSet() throws Exception { super.afterPropertiesSet(); postProcess(getRegion()); } + /** + * @inheritDoc + */ @Override - @SuppressWarnings("all") - protected Region lookupFallback(GemFireCache cache, String regionName) throws Exception { + protected Region lookupRegion(GemFireCache cache, String regionName) throws Exception { Assert.isTrue(GemfireUtils.isClient(cache), "A ClientCache is required to create a client Region"); - ClientRegionFactory clientRegionFactory = ((ClientCache) cache).createClientRegionFactory( - resolveClientRegionShortcut()); - - if (attributes != null) { - clientRegionFactory.setCloningEnabled(attributes.getCloningEnabled()); - clientRegionFactory.setCompressor(attributes.getCompressor()); - clientRegionFactory.setConcurrencyChecksEnabled(attributes.getConcurrencyChecksEnabled()); - clientRegionFactory.setConcurrencyLevel(attributes.getConcurrencyLevel()); - clientRegionFactory.setCustomEntryIdleTimeout(attributes.getCustomEntryIdleTimeout()); - clientRegionFactory.setCustomEntryTimeToLive(attributes.getCustomEntryTimeToLive()); - clientRegionFactory.setDiskStoreName(attributes.getDiskStoreName()); - clientRegionFactory.setDiskSynchronous(attributes.isDiskSynchronous()); - clientRegionFactory.setEntryIdleTimeout(attributes.getEntryIdleTimeout()); - clientRegionFactory.setEntryTimeToLive(attributes.getEntryTimeToLive()); - clientRegionFactory.setEvictionAttributes(attributes.getEvictionAttributes()); - clientRegionFactory.setInitialCapacity(attributes.getInitialCapacity()); - clientRegionFactory.setKeyConstraint(attributes.getKeyConstraint()); - clientRegionFactory.setLoadFactor(attributes.getLoadFactor()); - clientRegionFactory.setPoolName(attributes.getPoolName()); - clientRegionFactory.setRegionIdleTimeout(attributes.getRegionIdleTimeout()); - clientRegionFactory.setRegionTimeToLive(attributes.getRegionTimeToLive()); - clientRegionFactory.setStatisticsEnabled(attributes.getStatisticsEnabled()); - clientRegionFactory.setValueConstraint(attributes.getValueConstraint()); - } - - String poolName = resolvePoolName(); - - if (StringUtils.hasText(poolName)) { - clientRegionFactory.setPoolName(eagerlyInitializePool(poolName)); - } + ClientRegionFactory clientRegionFactory = + ((ClientCache) cache).createClientRegionFactory(resolveClientRegionShortcut()); + setAttributes(clientRegionFactory); addCacheListeners(clientRegionFactory); + setDiskStoreName(clientRegionFactory); + setEvictionAttributes(clientRegionFactory); + setPoolName(clientRegionFactory); - if (diskStoreName != null) { - clientRegionFactory.setDiskStoreName(diskStoreName); - } - - Region clientRegion = (getParent() != null ? clientRegionFactory.createSubregion(getParent(), regionName) - : clientRegionFactory.create(regionName)); - - if (log.isInfoEnabled()) { - if (getParent() != null) { - log.info(String.format("Created new Client Cache sub-Region [%1$s] under parent Region [%2$s].", - regionName, getParent().getName())); - } - else { - log.info(String.format("Created new Client Cache Region [%1$s].", regionName)); - } - } - - if (snapshot != null) { - clientRegion.loadSnapshot(snapshot.getInputStream()); - } - - return clientRegion; + return logCreateRegionEvent(create(clientRegionFactory, regionName)); } /* (non-Javadoc) */ + private Region create(ClientRegionFactory clientRegionFactory, String regionName) { + return (getParent() != null ? clientRegionFactory.createSubregion(getParent(), regionName) + : clientRegionFactory.create(regionName)); + } + + /* (non-Javadoc) */ + private Region logCreateRegionEvent(Region region) { + if (log.isInfoEnabled()) { + if (getParent() != null) { + log.info(String.format("Created new client cache Sub-Region [%1$s] under parent Region [%2$s].", + region.getName(), getParent().getName())); + } + else { + log.info(String.format("Created new client cache Region [%s].", region.getName())); + } + } + + return region; + } + + /** + * Resolves the {@link ClientRegionShortcut} used to configure the data policy of the client {@link Region}. + * + * @return a {@link ClientRegionShortcut} used to configure the data policy of the client {@link Region}. + * @see org.apache.geode.cache.client.ClientRegionShortcut + */ ClientRegionShortcut resolveClientRegionShortcut() { ClientRegionShortcut resolvedShortcut = this.shortcut; @@ -181,10 +172,10 @@ public class ClientRegionFactoryBean extends RegionLookupFactoryBean resolvedShortcut = ClientRegionShortcut.LOCAL_PERSISTENT; } else { - // NOTE the Data Policy validation is based on the ClientRegionShortcut initialization logic + // NOTE the DataPolicy validation is based on the ClientRegionShortcut initialization logic // in org.apache.geode.internal.cache.GemFireCacheImpl.initializeClientRegionShortcuts - throw new IllegalArgumentException(String.format( - "Data Policy '%1$s' is invalid for Client Regions.", this.dataPolicy)); + throw new IllegalArgumentException(String.format("Data Policy '%s' is invalid for Client Regions", + this.dataPolicy)); } } else { @@ -193,43 +184,13 @@ public class ClientRegionFactoryBean extends RegionLookupFactoryBean } } - // NOTE the ClientRegionShortcut and Persistent attribute will be compatible if the shortcut was derived from - // the Data Policy. + // NOTE the ClientRegionShortcut and Persistent attribute will be compatible if the shortcut + // was derived from the Data Policy. assertClientRegionShortcutAndPersistentAttributeAreCompatible(resolvedShortcut); return resolvedShortcut; } - /* (non-Javadoc) */ - String resolvePoolName() { - String poolName = this.poolName; - - if (!StringUtils.hasText(poolName)) { - String defaultPoolName = GemfireConstants.DEFAULT_GEMFIRE_POOL_NAME; - poolName = (beanFactory.containsBean(defaultPoolName) ? defaultPoolName : poolName); - } - - return poolName; - } - - /* (non-Javadoc) */ - String eagerlyInitializePool(String poolName) { - try { - if (beanFactory.isTypeMatch(poolName, Pool.class)) { - if (log.isDebugEnabled()) { - log.debug(String.format("Found bean definition for Pool [%1$s]; Eagerly initializing...", poolName)); - } - - beanFactory.getBean(poolName, Pool.class); - } - } - catch (BeansException ignore) { - log.warn(ignore.getMessage()); - } - - return poolName; - } - /** * Validates that the settings for ClientRegionShortcut and the 'persistent' attribute in <gfe:*-region> elements * are compatible. @@ -240,17 +201,17 @@ public class ClientRegionFactoryBean extends RegionLookupFactoryBean * @see #isNotPersistent() * @see org.apache.geode.cache.client.ClientRegionShortcut */ - protected void assertClientRegionShortcutAndPersistentAttributeAreCompatible(final ClientRegionShortcut resolvedShortcut) { + private void assertClientRegionShortcutAndPersistentAttributeAreCompatible(ClientRegionShortcut resolvedShortcut) { final boolean persistentNotSpecified = (this.persistent == null); if (ClientRegionShortcut.LOCAL_PERSISTENT.equals(resolvedShortcut) - || ClientRegionShortcut.LOCAL_PERSISTENT_OVERFLOW.equals(resolvedShortcut)) { + || ClientRegionShortcut.LOCAL_PERSISTENT_OVERFLOW.equals(resolvedShortcut)) { Assert.isTrue(persistentNotSpecified || isPersistent(), String.format( - "Client Region Shortcut '%1$s' is invalid when persistent is false.", resolvedShortcut)); + "Client Region Shortcut '%s' is invalid when persistent is false", resolvedShortcut)); } else { Assert.isTrue(persistentNotSpecified || isNotPersistent(), String.format( - "Client Region Shortcut '%1$s' is invalid when persistent is true.", resolvedShortcut)); + "Client Region Shortcut '%s' is invalid when persistent is true", resolvedShortcut)); } } @@ -264,43 +225,147 @@ public class ClientRegionFactoryBean extends RegionLookupFactoryBean * @see #isNotPersistent() * @see org.apache.geode.cache.DataPolicy */ - protected void assertDataPolicyAndPersistentAttributeAreCompatible(final DataPolicy resolvedDataPolicy) { + private void assertDataPolicyAndPersistentAttributeAreCompatible(DataPolicy resolvedDataPolicy) { if (resolvedDataPolicy.withPersistence()) { Assert.isTrue(isPersistentUnspecified() || isPersistent(), String.format( - "Data Policy '%1$s' is invalid when persistent is false.", resolvedDataPolicy)); + "Data Policy '%s' is invalid when persistent is false", resolvedDataPolicy)); } else { // NOTE otherwise, the Data Policy is without persistence, so... Assert.isTrue(isPersistentUnspecified() || isNotPersistent(), String.format( - "Data Policy '%1$s' is invalid when persistent is true.", resolvedDataPolicy)); + "Data Policy '%s' is invalid when persistent is true", resolvedDataPolicy)); } } - private void addCacheListeners(ClientRegionFactory factory) { - if (attributes != null) { - CacheListener[] cacheListeners = attributes.getCacheListeners(); + /* (non-Javadoc) */ + private ClientRegionFactory setAttributes(ClientRegionFactory clientRegionFactory) { + RegionAttributes localAttributes = this.attributes; - if (!ObjectUtils.isEmpty(cacheListeners)) { - for (CacheListener cacheListener : cacheListeners) { - factory.addCacheListener(cacheListener); + if (localAttributes != null) { + clientRegionFactory.setCloningEnabled(localAttributes.getCloningEnabled()); + clientRegionFactory.setCompressor(localAttributes.getCompressor()); + clientRegionFactory.setConcurrencyChecksEnabled(localAttributes.getConcurrencyChecksEnabled()); + clientRegionFactory.setConcurrencyLevel(localAttributes.getConcurrencyLevel()); + clientRegionFactory.setCustomEntryIdleTimeout(localAttributes.getCustomEntryIdleTimeout()); + clientRegionFactory.setCustomEntryTimeToLive(localAttributes.getCustomEntryTimeToLive()); + clientRegionFactory.setDiskStoreName(localAttributes.getDiskStoreName()); + clientRegionFactory.setDiskSynchronous(localAttributes.isDiskSynchronous()); + clientRegionFactory.setEntryIdleTimeout(localAttributes.getEntryIdleTimeout()); + clientRegionFactory.setEntryTimeToLive(localAttributes.getEntryTimeToLive()); + clientRegionFactory.setEvictionAttributes(localAttributes.getEvictionAttributes()); + clientRegionFactory.setInitialCapacity(localAttributes.getInitialCapacity()); + clientRegionFactory.setKeyConstraint(localAttributes.getKeyConstraint()); + clientRegionFactory.setLoadFactor(localAttributes.getLoadFactor()); + clientRegionFactory.setPoolName(localAttributes.getPoolName()); + clientRegionFactory.setRegionIdleTimeout(localAttributes.getRegionIdleTimeout()); + clientRegionFactory.setRegionTimeToLive(localAttributes.getRegionTimeToLive()); + clientRegionFactory.setStatisticsEnabled(localAttributes.getStatisticsEnabled()); + clientRegionFactory.setValueConstraint(localAttributes.getValueConstraint()); + } + + return clientRegionFactory; + } + + /* (non-Javadoc) */ + @SuppressWarnings("unchecked") + private ClientRegionFactory addCacheListeners(ClientRegionFactory clientRegionFactory) { + for (CacheListener cacheListener : this.attributesCacheListeners()) { + clientRegionFactory.addCacheListener(cacheListener); + } + + for (CacheListener cacheListener : ArrayUtils.nullSafeArray(this.cacheListeners, CacheListener.class)) { + clientRegionFactory.addCacheListener(cacheListener); + } + + return clientRegionFactory; + } + + /* (non-Javadoc) */ + @SuppressWarnings("unchecked") + private CacheListener[] attributesCacheListeners() { + CacheListener[] cacheListeners = (this.attributes != null ? this.attributes.getCacheListeners() : null); + + return ArrayUtils.nullSafeArray(cacheListeners, CacheListener.class); + } + + /* (non-Javadoc) */ + private ClientRegionFactory setDiskStoreName(ClientRegionFactory clientRegionFactory) { + if (StringUtils.hasText(this.diskStoreName)) { + clientRegionFactory.setDiskStoreName(this.diskStoreName); + } + + return clientRegionFactory; + } + + /* (non-Javadoc) */ + private ClientRegionFactory setEvictionAttributes(ClientRegionFactory clientRegionFactory) { + if (this.evictionAttributes != null) { + clientRegionFactory.setEvictionAttributes(this.evictionAttributes); + } + + return clientRegionFactory; + } + + /* (non-Javadoc) */ + private ClientRegionFactory setPoolName(ClientRegionFactory clientRegionFactory) { + String poolName = resolvePoolName(); + + if (StringUtils.hasText(poolName)) { + clientRegionFactory.setPoolName(eagerlyInitializePool(poolName)); + } + + return clientRegionFactory; + } + + /* (non-Javadoc) */ + private String resolvePoolName() { + String poolName = this.poolName; + + if (!StringUtils.hasText(poolName)) { + String defaultPoolName = GemfireConstants.DEFAULT_GEMFIRE_POOL_NAME; + poolName = (this.beanFactory.containsBean(defaultPoolName) ? defaultPoolName : poolName); + } + + return poolName; + } + + /* (non-Javadoc) */ + private String eagerlyInitializePool(String poolName) { + try { + if (this.beanFactory.isTypeMatch(poolName, Pool.class)) { + if (log.isDebugEnabled()) { + log.debug(String.format("Found bean definition for Pool [%1$s]; Eagerly initializing...", poolName)); } + + this.beanFactory.getBean(poolName, Pool.class); } } + catch (BeansException ignore) { + log.warn(ignore.getMessage()); + } - if (!ObjectUtils.isEmpty(cacheListeners)) { - for (CacheListener cacheListener : cacheListeners) { - factory.addCacheListener(cacheListener); - } - } + return poolName; } - protected void postProcess(final Region region) { + /* (non-Javadoc) */ + protected void postProcess(Region region) throws Exception { + loadSnapshot(region); registerInterests(region); setCacheLoader(region); setCacheWriter(region); } - private void registerInterests(final Region region) { + /* (non-Javadoc) */ + private Region loadSnapshot(Region region) throws Exception { + if (snapshot != null) { + region.loadSnapshot(snapshot.getInputStream()); + } + + return region; + } + + /* (non-Javadoc) */ + private Region registerInterests(Region region) { if (!ObjectUtils.isEmpty(interests)) { for (Interest interest : interests) { if (interest instanceof RegexInterest) { @@ -313,20 +378,31 @@ public class ClientRegionFactoryBean extends RegionLookupFactoryBean } } } + + return region; } - private void setCacheLoader(final Region region) { + /* (non-Javadoc) */ + private Region setCacheLoader(Region region) { if (cacheLoader != null) { region.getAttributesMutator().setCacheLoader(this.cacheLoader); } + + return region; } - private void setCacheWriter(final Region region) { + /* (non-Javadoc) */ + private Region setCacheWriter(Region region) { if (cacheWriter != null) { region.getAttributesMutator().setCacheWriter(this.cacheWriter); } + + return region; } + /** + * @inheritDoc + */ @Override public void destroy() throws Exception { Region region = getObject(); @@ -367,43 +443,7 @@ public class ClientRegionFactoryBean extends RegionLookupFactoryBean this.beanFactory = beanFactory; } - /** - * Set the interests for this client region. Both key and regex interest are - * supported. - * - * @param interests the interests to set - */ - public void setInterests(Interest[] interests) { - this.interests = interests; - } - - /** - * @return the interests - */ - Interest[] getInterests() { - return interests; - } - - /** - * Sets the pool used by this client. - * - * @param pool the GemFire client pool. - */ - public void setPool(Pool pool) { - Assert.notNull(pool, "pool cannot be null"); - setPoolName(pool.getName()); - } - - /** - * Sets the pool name used by this client. - * - * @param poolName a String specify the name of the GemFire client pool. - */ - public void setPoolName(String poolName) { - Assert.hasText(poolName, "pool name is required"); - this.poolName = poolName; - } - + /* (non-Javadoc) */ final boolean isClose() { return close; } @@ -421,23 +461,6 @@ public class ClientRegionFactoryBean extends RegionLookupFactoryBean this.destroy = (this.destroy && !close); // retain previous value iff close is false. } - final boolean isDestroy() { - return destroy; - } - - /** - * Indicates whether the region referred by this factory bean will be - * destroyed on shutdown (default false). Note: destroy and close are - * mutually exclusive. Enabling one will automatically disable the other. - * - * @param destroy whether or not to destroy the region - * @see #setClose(boolean) - */ - public void setDestroy(boolean destroy) { - this.destroy = destroy; - this.close = (this.close && !destroy); // retain previous value iff destroy is false; - } - /** * Sets the cache listeners used for the region used by this factory. Used * only when a new region is created.Overrides the settings specified @@ -494,6 +517,24 @@ public class ClientRegionFactoryBean extends RegionLookupFactoryBean setDataPolicy(resolvedDataPolicy); } + /* (non-Javadoc) */ + final boolean isDestroy() { + return destroy; + } + + /** + * Indicates whether the region referred by this factory bean will be + * destroyed on shutdown (default false). Note: destroy and close are + * mutually exclusive. Enabling one will automatically disable the other. + * + * @param destroy whether or not to destroy the region + * @see #setClose(boolean) + */ + public void setDestroy(boolean destroy) { + this.destroy = destroy; + this.close = (this.close && !destroy); // retain previous value iff destroy is false; + } + /** * Sets the name of disk store to use for overflow and persistence * @@ -503,6 +544,25 @@ public class ClientRegionFactoryBean extends RegionLookupFactoryBean this.diskStoreName = diskStoreName; } + public void setEvictionAttributes(EvictionAttributes evictionAttributes) { + this.evictionAttributes = evictionAttributes; + } + + /** + * Set the interests for this client region. Both key and regex interest are + * supported. + * + * @param interests the interests to set + */ + public void setInterests(Interest[] interests) { + this.interests = interests; + } + + /* (non-Javadoc) */ + Interest[] getInterests() { + return this.interests; + } + protected boolean isPersistentUnspecified() { return (persistent == null); } @@ -520,26 +580,44 @@ public class ClientRegionFactoryBean extends RegionLookupFactoryBean } /** - * Initializes the client using a GemFire {@link ClientRegionShortcut}. The - * recommended way for creating clients since it covers all the major - * scenarios with minimal configuration. + * Sets the {@link Pool} used by this client {@link Region}. * - * @param shortcut the ClientRegionShortcut to use. + * @param pool GemFire client {@link Pool}. + * @see org.apache.geode.cache.client.Pool + */ + public void setPool(Pool pool) { + Assert.notNull(pool, "Pool cannot be null"); + setPoolName(pool.getName()); + } + + /** + * Sets the {@link Pool} name used by this client {@link Region}. + * + * @param poolName String specifying the name of the GemFire client {@link Pool}. + */ + public void setPoolName(String poolName) { + Assert.hasText(poolName, "Pool name is required"); + this.poolName = poolName; + } + + /** + * Initializes the client {@link Region} using a GemFire {@link ClientRegionShortcut}. + * + * @param shortcut {@link ClientRegionShortcut} used to initialize this client {@link Region}. */ public void setShortcut(ClientRegionShortcut shortcut) { this.shortcut = shortcut; } /** - * Sets the snapshots used for loading a newly created region. That - * is, the snapshot will be used only when a new region is created - - * if the region already exists, no loading will be performed. + * Specifies the data snapshots used for loading a newly created {@link Region}. + * The snapshot will be used only when a new {@link Region} is created. + * If the {@link Region} already exists, no loading will be performed. * - * @param snapshot the snapshot to set - * @see #setName(String) + * @param snapshot {@link Resource} referencing the snapshot used to load the {@link Region} with data. + * @see org.springframework.core.io.Resource */ public void setSnapshot(Resource snapshot) { this.snapshot = snapshot; } - } diff --git a/src/main/java/org/springframework/data/gemfire/config/annotation/EnableEviction.java b/src/main/java/org/springframework/data/gemfire/config/annotation/EnableEviction.java new file mode 100644 index 00000000..1db955ba --- /dev/null +++ b/src/main/java/org/springframework/data/gemfire/config/annotation/EnableEviction.java @@ -0,0 +1,114 @@ +/* + * 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 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.EvictionAttributes; +import org.apache.geode.cache.Region; +import org.apache.geode.cache.util.ObjectSizer; +import org.springframework.context.annotation.Import; +import org.springframework.data.gemfire.EvictionActionType; +import org.springframework.data.gemfire.EvictionPolicyType; + +/** + * The {@link EnableEviction} annotation marks a Spring {@link org.springframework.context.annotation.Configuration @Configuration} + * annotated class to enable {@link Region} Eviction. + * + * @author John Blum + * @see org.springframework.context.annotation.Import + * @see org.springframework.data.gemfire.config.annotation.EvictionConfiguration + * @see org.apache.geode.cache.Region + * @since 1.9.0 + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Inherited +@Documented +@Import(EvictionConfiguration.class) +@SuppressWarnings({ "unused" }) +public @interface EnableEviction { + + /** + * Defines individual {@link Region} Eviction policies or customizes the default Eviction policy applied + * to all {@link Region Regions}. + * + * Defaults to empty. + */ + EvictionPolicy[] policies() default {}; + + /** + * Definition for a specific Eviction policy that can be applied to 1 or more {@link Region Regions}. + * + * An Eviction policy defines the maximum (a.k.a. threshold) along with {@link ObjectSizer} used to size + * {@link Region} entry values and the action applied when {@link Region} entries are to be evicted. + * + * Additionally, the Eviction policy defines the algorithm used (eviction based on entry count, JVM Heap percentage + * or system memory size used) to determine when an Eviction should occur. + */ + @interface EvictionPolicy { + + /** + * Action to take on an {@link Region} entry when evicted. + * + * Defaults to {@link EvictionActionType#LOCAL_DESTROY}. + * + * @see org.springframework.data.gemfire.EvictionActionType + */ + EvictionActionType action() default EvictionActionType.LOCAL_DESTROY; + + /** + * Threshold applied for entry count Eviction. + * + * Defaults to {@link EvictionAttributes#DEFAULT_ENTRIES_MAXIMUM} + */ + int maximum() default EvictionAttributes.DEFAULT_ENTRIES_MAXIMUM; + + /** + * Name of a Spring bean of type {@link ObjectSizer} defined in the Spring context used + * to size {@link Region} entry values. + * + * Defaults to empty. + * + * @see org.apache.geode.cache.util.ObjectSizer + */ + String objectSizerName() default ""; + + /** + * Names of {@link Region Regions} for which this Eviction policy applies. + * + * Defaults to empty. + */ + String[] regionNames() default {}; + + /** + * Eviction alorithm used during Eviction. + * + * Defaults to {@link EvictionPolicyType#ENTRY_COUNT}. + * + * @see org.springframework.data.gemfire.EvictionPolicyType + */ + EvictionPolicyType type() default EvictionPolicyType.ENTRY_COUNT; + + } +} diff --git a/src/main/java/org/springframework/data/gemfire/config/annotation/EvictionConfiguration.java b/src/main/java/org/springframework/data/gemfire/config/annotation/EvictionConfiguration.java new file mode 100644 index 00000000..38e477d4 --- /dev/null +++ b/src/main/java/org/springframework/data/gemfire/config/annotation/EvictionConfiguration.java @@ -0,0 +1,465 @@ +/* + * 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.EnableEviction.EvictionPolicy; + +import java.lang.annotation.Annotation; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import org.apache.geode.cache.EvictionAttributes; +import org.apache.geode.cache.Region; +import org.apache.geode.cache.util.ObjectSizer; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.config.BeanPostProcessor; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +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.EvictionActionType; +import org.springframework.data.gemfire.EvictionAttributesFactoryBean; +import org.springframework.data.gemfire.EvictionPolicyType; +import org.springframework.data.gemfire.RegionFactoryBean; +import org.springframework.data.gemfire.RegionLookupFactoryBean; +import org.springframework.data.gemfire.client.ClientRegionFactoryBean; +import org.springframework.data.gemfire.util.ArrayUtils; +import org.springframework.data.gemfire.util.CollectionUtils; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +/** + * The {@link EvictionConfiguration} class is a Spring {@link Configuration @Configuration} annotated class to enable + * Eviction policy configuration on GemFire/Geode {@link Region Regions}. + * + * @author John Blum + * @see org.springframework.beans.factory.config.BeanPostProcessor + * @see org.springframework.context.ApplicationContext + * @see org.springframework.context.ApplicationContextAware + * @see org.springframework.context.annotation.Bean + * @see org.springframework.context.annotation.Configuration + * @see org.springframework.context.annotation.ImportAware + * @see org.springframework.data.gemfire.EvictionActionType + * @see org.springframework.data.gemfire.EvictionAttributesFactoryBean + * @see org.springframework.data.gemfire.EvictionPolicyType + * @see org.springframework.data.gemfire.RegionFactoryBean + * @see org.springframework.data.gemfire.client.ClientRegionFactoryBean + * @see org.apache.geode.cache.EvictionAttributes + * @see org.apache.geode.cache.Region + * @since 1.9.0 + */ +@Configuration +public class EvictionConfiguration implements ApplicationContextAware, ImportAware { + + private ApplicationContext applicationContext; + + private EvictionPolicyConfigurer evictionPolicyConfigurer; + + /** + * Determines whether the Spring bean is an instance of {@link RegionFactoryBean} + * or {@link ClientRegionFactoryBean}. + * + * @param bean Spring bean to evaluate. + * @return a boolean value indicating whether the Spring bean is an instance of {@link RegionFactoryBean}. + * @see org.springframework.data.gemfire.RegionFactoryBean + * @see org.springframework.data.gemfire.client.ClientRegionFactoryBean + */ + protected static boolean isRegionFactoryBean(Object bean) { + return (bean instanceof RegionFactoryBean || bean instanceof ClientRegionFactoryBean); + } + + /** + * Returns the {@link Annotation} {@link Class type} that enables and configures Eviction. + * + * @return the {@link Annotation} {@link Class type} to enable and configure Eviction. + * @see java.lang.annotation.Annotation + * @see java.lang.Class + */ + protected Class getAnnotationType() { + return EnableEviction.class; + } + + /** + * Returns the name of the {@link Annotation} type that enables and configures Eviction. + * + * @return the name of the {@link Annotation} type that enables and configures Eviction. + * @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 Eviction. + * + * @return the simple name of the {@link Annotation} type that enables and configures Eviction. + * @see java.lang.Class#getSimpleName() + * @see #getAnnotationType() + */ + @SuppressWarnings("unused") + protected String getAnnotationTypeSimpleName() { + return getAnnotationType().getSimpleName(); + } + + /** + * @inheritDoc + */ + @Override + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + this.applicationContext = applicationContext; + } + + /** + * @inheritDoc + */ + @Override + public void setImportMetadata(AnnotationMetadata importMetadata) { + if (importMetadata.hasAnnotation(getAnnotationTypeName())) { + Map enableEvictionAttributes = + importMetadata.getAnnotationAttributes(getAnnotationTypeName()); + + AnnotationAttributes[] policies = (AnnotationAttributes[]) enableEvictionAttributes.get("policies"); + + for (AnnotationAttributes evictionPolicyAttributes + : ArrayUtils.nullSafeArray(policies, AnnotationAttributes.class)) { + + this.evictionPolicyConfigurer = ComposableEvictionPolicyConfigurer.compose( + this.evictionPolicyConfigurer, EvictionPolicyMetaData.from(evictionPolicyAttributes, + this.applicationContext)); + } + + this.evictionPolicyConfigurer = (this.evictionPolicyConfigurer != null ? this.evictionPolicyConfigurer + : EvictionPolicyMetaData.fromDefaults()); + } + } + + /** + * Returns a reference to the configured {@link EvictionPolicyConfigurer} used to configure the Eviction policy + * of a {@link Region}. + * + * @return a reference to the configured {@link EvictionPolicyConfigurer}. + * @see org.springframework.data.gemfire.config.annotation.EvictionConfiguration.EvictionPolicyConfigurer + */ + protected EvictionPolicyConfigurer getEvictionPolicyConfigurer() { + Assert.state(this.evictionPolicyConfigurer != null, + "EvictionPolicyConfigurer was not properly configured and initialized"); + + return this.evictionPolicyConfigurer; + } + + @Bean + @SuppressWarnings("unused") + public BeanPostProcessor evictionBeanPostProcessor() { + return new BeanPostProcessor() { + @Override + public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { + return (isRegionFactoryBean(bean) ? getEvictionPolicyConfigurer().configure(bean) : bean); + } + + @Override + public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { + return bean; + } + }; + } + + /** + * {@link EvictionPolicyConfigurer} configures the Eviction policy of a GemFire {@link Region}. + */ + protected interface EvictionPolicyConfigurer { + + /** + * Configure the Eviction policy on the given SDG {@link RegionFactoryBean} or {@link ClientRegionFactoryBean} + * used to create a GemFire {@link Region}. + * + * @param regionFactoryBean {@link RegionFactoryBean} or {@link ClientRegionFactoryBean} used to create + * a GemFire {@link Region}. + * @return the given {@code regionFactoryBean}. + * @see org.springframework.data.gemfire.RegionFactoryBean + * @see org.springframework.data.gemfire.client.ClientRegionFactoryBean + */ + Object configure(Object regionFactoryBean); + + } + + /** + * {@link ComposableEvictionPolicyConfigurer} is a {@link EvictionPolicyConfigurer} implementation that composes + * multiple {@link EvictionPolicyConfigurer} objects into a composition using the Composite Software Design Pattern + * making the composition appear as a single {@link EvictionPolicyConfigurer}. + * + * @see org.springframework.data.gemfire.config.annotation.EvictionConfiguration.EvictionPolicyConfigurer + */ + protected static class ComposableEvictionPolicyConfigurer implements EvictionPolicyConfigurer { + + private final EvictionPolicyConfigurer one; + private final EvictionPolicyConfigurer two; + + /** + * Composes the array of {@link EvictionPolicyConfigurer} objects into a single + * {@link EvictionPolicyConfigurer} implementation using the Composite Software Design Pattern. + * + * @param array array of {@link EvictionPolicyConfigurer} objects to compose. + * @return an {@link EvictionPolicyConfigurer} implementation composed from the array + * of {@link EvictionPolicyConfigurer} objects. + * @see org.springframework.data.gemfire.config.annotation.EvictionConfiguration.EvictionPolicyConfigurer + * @see #compose(Iterable) + */ + @SuppressWarnings("unused") + protected static EvictionPolicyConfigurer compose(EvictionPolicyConfigurer[] array) { + return compose(Arrays.asList(ArrayUtils.nullSafeArray(array, EvictionPolicyConfigurer.class))); + } + + /** + * Composes the {@link Iterable} of {@link EvictionPolicyConfigurer} objects into a single + * {@link EvictionPolicyConfigurer} implementation using the Composite Software Design Pattern. + * + * @param iterable {@link Iterable} of {@link EvictionPolicyConfigurer} objects to compose. + * @return an {@link EvictionPolicyConfigurer} implementation composed from the {@link Iterable} + * of {@link EvictionPolicyConfigurer} objects. + * @see org.springframework.data.gemfire.config.annotation.EvictionConfiguration.EvictionPolicyConfigurer + * @see #compose(EvictionPolicyConfigurer, EvictionPolicyConfigurer) + */ + protected static EvictionPolicyConfigurer compose(Iterable iterable) { + EvictionPolicyConfigurer current = null; + + for (EvictionPolicyConfigurer evictionPolicyConfigurer : CollectionUtils.nullSafeIterable(iterable)) { + current = compose(current, evictionPolicyConfigurer); + } + + return current; + } + + /** + * Composes two {@link EvictionPolicyConfigurer} objects into a composition object + * implementing the {@link EvictionPolicyConfigurer} interface. + * + * @param one first {@link EvictionPolicyConfigurer} object to compose. + * @param two second {@link EvictionPolicyConfigurer} object to compose. + * @return an {@link EvictionPolicyConfigurer} object implementation composed of + * multiple {@link EvictionPolicyConfigurer} objects using the Composite Software Design Pattern. + */ + protected static EvictionPolicyConfigurer compose(EvictionPolicyConfigurer one, EvictionPolicyConfigurer two) { + return (one == null ? two : (two == null ? one : new ComposableEvictionPolicyConfigurer(one, two))); + } + + /** + * Constructs a new instance of the {@link ComposableEvictionPolicyConfigurer} initialized with the two + * {@link EvictionPolicyConfigurer} objects. + * + * @param one first {@link EvictionPolicyConfigurer} object to compose. + * @param two second {@link EvictionPolicyConfigurer} object to compose. + */ + private ComposableEvictionPolicyConfigurer(EvictionPolicyConfigurer one, EvictionPolicyConfigurer two) { + this.one = one; + this.two = two; + } + + /** + * @inheritDoc + */ + @Override + public Object configure(Object regionFactoryBean) { + return two.configure(one.configure(regionFactoryBean)); + } + } + + protected static class EvictionPolicyMetaData implements EvictionPolicyConfigurer { + + protected static final String[] ALL_REGIONS = new String[0]; + + private final EvictionAttributes evictionAttributes; + + private final Set regionNames = new HashSet(); + + protected static EvictionPolicyMetaData from(AnnotationAttributes evictionPolicyAttributes, + ApplicationContext applicationContext) { + + return from((Integer) evictionPolicyAttributes.get("maximum"), + evictionPolicyAttributes.getEnum("type"), + evictionPolicyAttributes.getEnum("action"), + resolveObjectSizer(evictionPolicyAttributes.getString("objectSizerName"), applicationContext), + evictionPolicyAttributes.getStringArray("regionNames")); + } + + protected static EvictionPolicyMetaData from(EvictionPolicy evictionPolicy, + ApplicationContext applicationContext) { + + return from(evictionPolicy.maximum(), evictionPolicy.type(), evictionPolicy.action(), + resolveObjectSizer(evictionPolicy.objectSizerName(), applicationContext), evictionPolicy.regionNames()); + } + + protected static EvictionPolicyMetaData from(int maximum, EvictionPolicyType type, EvictionActionType action, + ObjectSizer objectSizer, String... regionNames) { + + EvictionAttributesFactoryBean factoryBean = new EvictionAttributesFactoryBean(); + + factoryBean.setAction(action.getEvictionAction()); + factoryBean.setObjectSizer(objectSizer); + factoryBean.setThreshold(resolveThreshold(maximum, type)); + factoryBean.setType(type); + factoryBean.afterPropertiesSet(); + + return new EvictionPolicyMetaData(factoryBean.getObject(), regionNames); + } + + protected static EvictionPolicyMetaData fromDefaults() { + return new EvictionPolicyMetaData(EvictionAttributes.createLRUEntryAttributes()); + } + + protected static ObjectSizer resolveObjectSizer(String objectSizerName, ApplicationContext applicationContext) { + boolean resolvable = StringUtils.hasText(objectSizerName) + && applicationContext.containsBean(objectSizerName); + + return (resolvable ? applicationContext.getBean(objectSizerName, ObjectSizer.class) : null); + } + + /** + * Resolves the Eviction policy threshold (a.k.a. maximum) based on the {@link EvictionPolicyType}. + * + * For instance {@link EvictionPolicyType#HEAP_PERCENTAGE} does not support maximum/threshold since + * the settings are determined by the GemFire/Geode cache critical heap percentage and eviction heap percentage + * System property settings. + * + * @param maximum integer value specifying the configured Eviction threshold. + * @param type {@link EvictionPolicyType} specifying the type of Eviction algorithm. + * @return a resolved value for the Eviction maximum/threshold. + * @see org.springframework.data.gemfire.EvictionPolicyType + */ + protected static Integer resolveThreshold(int maximum, EvictionPolicyType type) { + return (EvictionPolicyType.HEAP_PERCENTAGE.equals(type) ? null : maximum); + } + + /** + * Constructs an instance of {@link EvictionPolicyMetaData} initialized with the given + * {@link EvictionAttributes} applying to all {@link Region Regions}. + * + * @param evictionAttributes {@link EvictionAttributes} specifying the Eviction policy configuration + * for a {@link Region}. + * @see org.apache.geode.cache.EvictionAttributes + * @see #EvictionPolicyMetaData(EvictionAttributes, String[]) + */ + protected EvictionPolicyMetaData(EvictionAttributes evictionAttributes) { + this(evictionAttributes, ALL_REGIONS); + } + + /** + * Constructs an instance of {@link EvictionPolicyMetaData} initialized with the given + * {@link EvictionAttributes} to apply to the specific {@link Region Regions}. + * + * @param evictionAttributes {@link EvictionAttributes} specifying the Eviction policy configuration + * for a {@link Region}. + * @param regionNames names of {@link Region Regions} on which the Eviction policy is applied. + * @see org.apache.geode.cache.EvictionAttributes + */ + protected EvictionPolicyMetaData(EvictionAttributes evictionAttributes, String[] regionNames) { + Assert.notNull(evictionAttributes, "EvictionAttributes must not be null"); + + this.evictionAttributes = evictionAttributes; + Collections.addAll(this.regionNames, ArrayUtils.nullSafeArray(regionNames, String.class)); + } + + /** + * Returns an instance of the {@link EvictionAttributes} specifying the Eviction policy configuration + * captured in this Eviction policy meta-data. + * + * @return an instance of the {@link EvictionAttributes} specifying the {@link Region} + * Eviction policy configuration. + * @throws IllegalStateException if the {@link EvictionAttributes} were not properly initialized. + * @see org.apache.geode.cache.EvictionAttributes + */ + protected EvictionAttributes getEvictionAttributes() { + Assert.state(this.evictionAttributes != null, + "EvictionAttributes was not properly configured and initialized"); + + return this.evictionAttributes; + } + + /** + * Determines whether the given {@link Object} (e.g. Spring bean) is accepted for Eviction policy configuration. + * + * @param regionFactoryBean {@link Object} being evaluated as an Eviction policy configuration candidate. + * @return a boolean value indicating whether the {@link Object} is accepted for Eviction policy configuration. + * @see #isRegionFactoryBean(Object) + * @see #resolveRegionName(Object) + * @see #accepts(String) + */ + protected boolean accepts(Object regionFactoryBean) { + return (isRegionFactoryBean(regionFactoryBean) && accepts(resolveRegionName(regionFactoryBean))); + } + + /** + * Determine whether the {@link Region} identified by name is accepted for Eviction policy configuration. + * + * @param regionName name of the {@link Region} targeted for Eviction policy configuration. + * @return a boolean value if the named {@link Region} is accepted for Eviction policy configuration. + */ + protected boolean accepts(String regionName) { + return (this.regionNames.isEmpty() || this.regionNames.contains(regionName)); + } + + /** + * Resolves the name of a given {@link Region} from the corresponding {@link RegionLookupFactoryBean} object. + * + * @param regionFactoryBean {@link RegionLookupFactoryBean} from which to resolve the {@link Region} name. + * @return the resolved name of the {@link Region} created from the given {@link RegionLookupFactoryBean}. + * @see org.springframework.data.gemfire.RegionLookupFactoryBean#resolveRegionName() + */ + protected String resolveRegionName(Object regionFactoryBean) { + return (regionFactoryBean instanceof RegionLookupFactoryBean + ? ((RegionLookupFactoryBean) regionFactoryBean).resolveRegionName() : null); + } + + /** + * Sets the {@link EvictionAttributes} on the {@link RegionFactoryBean} or {@link ClientRegionFactoryBean} + * used to create the targeted {@link Region}. + * + * @param regionFactoryBean {@link RegionFactoryBean} or {@link ClientRegionFactoryBean} on which to + * set the {@link EvictionAttributes} encapsulating the Eviction policy for the targeted {@link Region}. + * @return the {@code regionFactoryBean}. + * @see org.springframework.data.gemfire.RegionFactoryBean#setEvictionAttributes(EvictionAttributes) + * @see org.springframework.data.gemfire.client.ClientRegionFactoryBean#setEvictionAttributes(EvictionAttributes) + * @see org.apache.geode.cache.EvictionAttributes + * @see #getEvictionAttributes() + */ + protected Object setEvictionAttributes(Object regionFactoryBean) { + if (regionFactoryBean instanceof RegionFactoryBean) { + ((RegionFactoryBean) regionFactoryBean).setEvictionAttributes(getEvictionAttributes()); + } + else { + ((ClientRegionFactoryBean) regionFactoryBean).setEvictionAttributes(getEvictionAttributes()); + } + + return regionFactoryBean; + } + + /** + * @inheritDoc + */ + @Override + public Object configure(Object regionFactoryBean) { + return (accepts(regionFactoryBean) ? setEvictionAttributes(regionFactoryBean) : regionFactoryBean); + } + } +} diff --git a/src/test/java/org/springframework/data/gemfire/CacheClusterConfigurationIntegrationTest.java b/src/test/java/org/springframework/data/gemfire/CacheClusterConfigurationIntegrationTest.java index 075b85d7..c0f24e0c 100644 --- a/src/test/java/org/springframework/data/gemfire/CacheClusterConfigurationIntegrationTest.java +++ b/src/test/java/org/springframework/data/gemfire/CacheClusterConfigurationIntegrationTest.java @@ -46,7 +46,6 @@ import org.springframework.context.support.ClassPathXmlApplicationContext; import org.springframework.core.io.ClassPathResource; import org.springframework.data.gemfire.fork.LocatorProcess; import org.springframework.data.gemfire.process.ProcessExecutor; -import org.springframework.data.gemfire.process.ProcessInputStreamListener; import org.springframework.data.gemfire.process.ProcessWrapper; import org.springframework.data.gemfire.test.support.FileUtils; import org.springframework.data.gemfire.test.support.ThreadUtils; @@ -81,15 +80,15 @@ public class CacheClusterConfigurationIntegrationTest { @Rule public TestRule watchman = new TestWatcher() { @Override - protected void failed(final Throwable t, final Description description) { + protected void failed(Throwable throwable, Description description) { System.err.println(String.format("Test '%1$s' failed...", description.getDisplayName())); - System.err.println(ThrowableUtils.toString(t)); + System.err.println(ThrowableUtils.toString(throwable)); System.err.println("Locator process log file contents were..."); System.err.println(getLocatorProcessOutput(description)); } @Override - protected void finished(final Description description) { + protected void finished(Description description) { if (Boolean.valueOf(System.getProperty("spring.gemfire.fork.clean", Boolean.TRUE.toString()))) { try { FileUtils.write(new File(locatorWorkingDirectory.getParent(), @@ -102,7 +101,7 @@ public class CacheClusterConfigurationIntegrationTest { } } - private String getLocatorProcessOutput(final Description description) { + private String getLocatorProcessOutput(Description description) { try { String locatorProcessOutputString = StringUtils.collectionToDelimitedString(locatorProcessOutput, FileUtils.LINE_SEPARATOR, String.format("[%1$s] - ", description.getMethodName()), ""); @@ -128,20 +127,18 @@ public class CacheClusterConfigurationIntegrationTest { ZipUtils.unzip(new ClassPathResource("/cluster_config.zip"), locatorWorkingDirectory); - List arguments = new ArrayList(); + List arguments = new ArrayList<>(); arguments.add("-Dgemfire.name=" + locatorName); + arguments.add("-Dgemfire.mcast-port=0"); + arguments.add("-Dgemfire.log-level=error"); arguments.add("-Dspring.gemfire.enable-cluster-configuration=true"); arguments.add("-Dspring.gemfire.load-cluster-configuration=true"); locatorProcess = ProcessExecutor.launch(locatorWorkingDirectory, LocatorProcess.class, arguments.toArray(new String[arguments.size()])); - locatorProcess.register(new ProcessInputStreamListener() { - @Override public void onInput(final String input) { - locatorProcessOutput.add(input); - } - }); + locatorProcess.register(input -> locatorProcessOutput.add(input)); locatorProcess.registerShutdownHook(); @@ -191,11 +188,11 @@ public class CacheClusterConfigurationIntegrationTest { return baseLocation.concat(File.separator).concat(configLocation); } - protected Region getRegion(final ConfigurableApplicationContext applicationContext, final String regionBeanName) { + protected Region getRegion(ConfigurableApplicationContext applicationContext, String regionBeanName) { return applicationContext.getBean(regionBeanName, Region.class); } - protected ConfigurableApplicationContext newApplicationContext(final String... configLocations) { + protected ConfigurableApplicationContext newApplicationContext(String... configLocations) { ConfigurableApplicationContext applicationContext = new ClassPathXmlApplicationContext(configLocations); applicationContext.registerShutdownHook(); return applicationContext; @@ -204,7 +201,7 @@ public class CacheClusterConfigurationIntegrationTest { @Test @Ignore // TODO re-enable the test once the GemFire Cluster Configuration Service race condition has been properly fixed! - public void testClusterConfiguration() { + public void clusterConfigurationTest() { ConfigurableApplicationContext applicationContext = newApplicationContext( getLocation("cacheUsingClusterConfigurationIntegrationTest.xml")); @@ -225,17 +222,18 @@ public class CacheClusterConfigurationIntegrationTest { } @Test - public void testLocalOnlyConfiguration() { + public void localConfigurationTest() { try { newApplicationContext(getLocation("cacheUsingLocalOnlyConfigurationIntegrationTest.xml")); + fail("Loading the 'cacheUsingLocalOnlyConfigurationIntegrationTest.xml' Spring ApplicationContext" + " configuration file should have resulted in an Exception due to the Region lookup on" + " 'ClusterConfigRegion' when GemFire Cluster Configuration is disabled!"); } catch (BeanCreationException expected) { assertTrue(expected.getCause() instanceof BeanInitializationException); - assertTrue(expected.getCause().getMessage().contains("Cannot find Region [ClusterConfigRegion] in Cache")); + assertTrue(expected.getCause().getMessage().matches( + "Region \\[ClusterConfigRegion\\] in Cache \\[.*\\] not found")); } } - } diff --git a/src/test/java/org/springframework/data/gemfire/client/ClientRegionFactoryBeanTest.java b/src/test/java/org/springframework/data/gemfire/client/ClientRegionFactoryBeanTest.java index aea2388c..e171c2df 100644 --- a/src/test/java/org/springframework/data/gemfire/client/ClientRegionFactoryBeanTest.java +++ b/src/test/java/org/springframework/data/gemfire/client/ClientRegionFactoryBeanTest.java @@ -15,9 +15,9 @@ */ package org.springframework.data.gemfire.client; -import static org.hamcrest.CoreMatchers.equalTo; -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.CoreMatchers.notNullValue; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; @@ -55,6 +55,7 @@ import org.springframework.beans.factory.BeanFactory; import org.springframework.core.io.Resource; import org.springframework.data.gemfire.TestUtils; import org.springframework.data.gemfire.config.xml.GemfireConstants; +import org.springframework.data.gemfire.util.ArrayUtils; /** * @author David Turanski @@ -66,7 +67,7 @@ public class ClientRegionFactoryBeanTest { @Before public void setup() { - factoryBean = new ClientRegionFactoryBean(); + factoryBean = new ClientRegionFactoryBean<>(); } @After @@ -78,7 +79,7 @@ public class ClientRegionFactoryBeanTest { @Test @SuppressWarnings({ "deprecation", "unchecked" }) public void testLookupFallbackUsingDefaultShortcut() throws Exception { - final String testRegionName = "TestRegion"; + String testRegionName = "TestRegion"; ClientCache mockClientCache = mock(ClientCache.class); ClientRegionFactory mockClientRegionFactory = mock(ClientRegionFactory.class); @@ -118,15 +119,18 @@ public class ClientRegionFactoryBeanTest { when(mockPool.getName()).thenReturn("TestPoolTwo"); when(mockSnapshot.getInputStream()).thenReturn(mock(InputStream.class)); + EvictionAttributes evictionAttributes = EvictionAttributes.createLRUEntryAttributes(); + factoryBean.setAttributes(mockRegionAttributes); factoryBean.setBeanFactory(mockBeanFactory); factoryBean.setDiskStoreName("TestDiskStoreTwo"); + factoryBean.setEvictionAttributes(evictionAttributes); factoryBean.setPersistent(false); factoryBean.setPoolName("TestPoolTwo"); factoryBean.setSnapshot(mockSnapshot); factoryBean.setShortcut(null); - Region actualRegion = factoryBean.lookupFallback(mockClientCache, testRegionName); + Region actualRegion = factoryBean.lookupRegion(mockClientCache, testRegionName); assertSame(mockRegion, actualRegion); @@ -141,7 +145,7 @@ public class ClientRegionFactoryBeanTest { verify(mockClientRegionFactory, times(1)).setDiskSynchronous(eq(false)); verify(mockClientRegionFactory, times(1)).setEntryIdleTimeout(any(ExpirationAttributes.class)); verify(mockClientRegionFactory, times(1)).setEntryTimeToLive(any(ExpirationAttributes.class)); - verify(mockClientRegionFactory, times(1)).setEvictionAttributes(any(EvictionAttributes.class)); + verify(mockClientRegionFactory, times(1)).setEvictionAttributes(eq(evictionAttributes)); verify(mockClientRegionFactory, times(1)).setInitialCapacity(eq(101)); verify(mockClientRegionFactory, times(1)).setKeyConstraint(eq(Long.class)); verify(mockClientRegionFactory, times(1)).setLoadFactor(eq(0.75f)); @@ -153,7 +157,7 @@ public class ClientRegionFactoryBeanTest { verify(mockClientRegionFactory, times(1)).setPoolName(eq("TestPoolTwo")); verify(mockClientRegionFactory, times(1)).setDiskStoreName(eq("TestDiskStoreTwo")); verify(mockClientRegionFactory, times(1)).create(eq(testRegionName)); - verify(mockRegion, times(1)).loadSnapshot(any(InputStream.class)); + verify(mockRegion, never()).loadSnapshot(any(InputStream.class)); } @Test @@ -176,7 +180,7 @@ public class ClientRegionFactoryBeanTest { factoryBean.setPoolName("TestPool"); factoryBean.setShortcut(null); - Region actualRegion = factoryBean.lookupFallback(mockClientCache, "TestRegion"); + Region actualRegion = factoryBean.lookupRegion(mockClientCache, "TestRegion"); assertSame(mockRegion, actualRegion); @@ -204,7 +208,7 @@ public class ClientRegionFactoryBeanTest { factoryBean.setBeanFactory(mockBeanFactory); factoryBean.setShortcut(ClientRegionShortcut.CACHING_PROXY); - Region actualRegion = factoryBean.lookupFallback(mockClientCache, "TestRegion"); + Region actualRegion = factoryBean.lookupRegion(mockClientCache, "TestRegion"); assertSame(mockRegion, actualRegion); @@ -233,7 +237,7 @@ public class ClientRegionFactoryBeanTest { factoryBean.setParent(mockRegion); factoryBean.setShortcut(ClientRegionShortcut.PROXY); - Region actualRegion = factoryBean.lookupFallback(mockClientCache, "TestSubRegion"); + Region actualRegion = factoryBean.lookupRegion(mockClientCache, "TestSubRegion"); assertSame(mockSubRegion, actualRegion); @@ -259,7 +263,7 @@ public class ClientRegionFactoryBeanTest { factoryBean.setBeanFactory(mockBeanFactory); factoryBean.setShortcut(ClientRegionShortcut.LOCAL_HEAP_LRU); - Region actualRegion = factoryBean.lookupFallback(mockClientCache, "TestRegion"); + Region actualRegion = factoryBean.lookupRegion(mockClientCache, "TestRegion"); assertSame(mockRegion, actualRegion); @@ -323,7 +327,7 @@ public class ClientRegionFactoryBeanTest { @Test public void testCloseDestroySettings() { - final ClientRegionFactoryBean factory = new ClientRegionFactoryBean(); + final ClientRegionFactoryBean factory = new ClientRegionFactoryBean<>(); assertNotNull(factory); assertFalse(factory.isClose()); @@ -429,7 +433,7 @@ public class ClientRegionFactoryBeanTest { factoryBean.resolveClientRegionShortcut(); } catch (IllegalArgumentException expected) { - assertEquals("Client Region Shortcut 'CACHING_PROXY' is invalid when persistent is true.", + assertEquals("Client Region Shortcut 'CACHING_PROXY' is invalid when persistent is true", expected.getMessage()); throw expected; } @@ -456,7 +460,7 @@ public class ClientRegionFactoryBeanTest { factoryBean.resolveClientRegionShortcut(); } catch (IllegalArgumentException expected) { - assertEquals("Client Region Shortcut 'LOCAL_PERSISTENT' is invalid when persistent is false.", + assertEquals("Client Region Shortcut 'LOCAL_PERSISTENT' is invalid when persistent is false", expected.getMessage()); throw expected; } @@ -503,7 +507,7 @@ public class ClientRegionFactoryBeanTest { factoryBean.resolveClientRegionShortcut(); } catch (IllegalArgumentException expected) { - assertEquals("Data Policy 'NORMAL' is invalid when persistent is true.", expected.getMessage()); + assertEquals("Data Policy 'NORMAL' is invalid when persistent is true", expected.getMessage()); throw expected; } } @@ -529,7 +533,7 @@ public class ClientRegionFactoryBeanTest { factoryBean.resolveClientRegionShortcut(); } catch (IllegalArgumentException expected) { - assertEquals("Data Policy 'PERSISTENT_REPLICATE' is invalid when persistent is false.", expected.getMessage()); + assertEquals("Data Policy 'PERSISTENT_REPLICATE' is invalid when persistent is false", expected.getMessage()); throw expected; } } @@ -548,10 +552,6 @@ public class ClientRegionFactoryBeanTest { return new Interest(key); } - protected Interest[] toArray(Interest... interests) { - return interests; - } - @Test @SuppressWarnings("unchecked") public void destroyCallsRegionClose() throws Exception { @@ -569,7 +569,7 @@ public class ClientRegionFactoryBeanTest { }; clientRegionFactoryBean.setClose(true); - clientRegionFactoryBean.setInterests(toArray(newInterest("test"))); + clientRegionFactoryBean.setInterests(ArrayUtils.asArray(newInterest("test"))); assertThat(clientRegionFactoryBean.isClose(), is(true)); assertThat(clientRegionFactoryBean.isDestroy(), is(false)); @@ -604,7 +604,7 @@ public class ClientRegionFactoryBeanTest { clientRegionFactoryBean.setClose(false); clientRegionFactoryBean.setDestroy(true); - clientRegionFactoryBean.setInterests(toArray(newInterest("test"))); + clientRegionFactoryBean.setInterests(ArrayUtils.asArray(newInterest("test"))); assertThat(clientRegionFactoryBean.isClose(), is(false)); assertThat(clientRegionFactoryBean.isDestroy(), is(true)); @@ -638,7 +638,7 @@ public class ClientRegionFactoryBeanTest { }; clientRegionFactoryBean.setClose(true); - clientRegionFactoryBean.setInterests(toArray(newInterest("test"))); + clientRegionFactoryBean.setInterests(ArrayUtils.asArray(newInterest("test"))); assertThat(clientRegionFactoryBean.isClose(), is(true)); assertThat(clientRegionFactoryBean.isDestroy(), is(false)); @@ -685,5 +685,4 @@ public class ClientRegionFactoryBeanTest { clientRegionFactoryBean.destroy(); } - } diff --git a/src/test/java/org/springframework/data/gemfire/config/annotation/EnableEvictionConfigurationUnitTests.java b/src/test/java/org/springframework/data/gemfire/config/annotation/EnableEvictionConfigurationUnitTests.java new file mode 100644 index 00000000..11f2ee1c --- /dev/null +++ b/src/test/java/org/springframework/data/gemfire/config/annotation/EnableEvictionConfigurationUnitTests.java @@ -0,0 +1,276 @@ +/* + * 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.Matchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static org.springframework.data.gemfire.config.annotation.EnableEviction.EvictionPolicy; + +import java.util.concurrent.atomic.AtomicReference; + +import org.apache.geode.cache.Cache; +import org.apache.geode.cache.EvictionAttributes; +import org.apache.geode.cache.Region; +import org.apache.geode.cache.RegionAttributes; +import org.apache.geode.cache.RegionFactory; +import org.apache.geode.cache.util.ObjectSizer; +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.EvictionActionType; +import org.springframework.data.gemfire.EvictionAttributesFactoryBean; +import org.springframework.data.gemfire.EvictionPolicyType; +import org.springframework.data.gemfire.PartitionedRegionFactoryBean; +import org.springframework.data.gemfire.ReplicatedRegionFactoryBean; +import org.springframework.data.gemfire.util.ArrayUtils; + +/** + * Unit tests for the {@link EnableEviction} annotation and {@link EvictionConfiguration} class. + * + * @author John Blum + * @see org.junit.Test + * @see org.mockito.Mockito + * @see org.springframework.data.gemfire.config.annotation.EnableEviction + * @see org.springframework.data.gemfire.config.annotation.EvictionConfiguration + * @see org.springframework.data.gemfire.EvictionAttributesFactoryBean + * @see org.apache.geode.cache.Region + * @see org.apache.geode.cache.EvictionAttributes + * @since 1.9.0 + */ +public class EnableEvictionConfigurationUnitTests { + + private ConfigurableApplicationContext applicationContext; + + @After + public void tearDown() { + if (applicationContext != null) { + applicationContext.close(); + } + } + + protected void assertEvictionAttributes(Region region, EvictionAttributes expectedEvictionAttributes) { + assertThat(region).isNotNull(); + assertThat(region.getAttributes()).isNotNull(); + assertEvictionAttributes(region.getAttributes().getEvictionAttributes(), expectedEvictionAttributes); + } + + protected void assertEvictionAttributes(EvictionAttributes actualEvictionAttributes, + EvictionAttributes expectedEvictionAttributes) { + + assertThat(actualEvictionAttributes).isNotNull(); + assertThat(actualEvictionAttributes.getAction()).isEqualTo(expectedEvictionAttributes.getAction()); + assertThat(actualEvictionAttributes.getAlgorithm()).isEqualTo(expectedEvictionAttributes.getAlgorithm()); + assertThat(actualEvictionAttributes.getObjectSizer()).isEqualTo(expectedEvictionAttributes.getObjectSizer()); + + if (!EvictionPolicyType.HEAP_PERCENTAGE.equals( + EvictionPolicyType.valueOf(actualEvictionAttributes.getAlgorithm()))) { + assertThat(actualEvictionAttributes.getMaximum()).isEqualTo(expectedEvictionAttributes.getMaximum()); + } + } + + @SuppressWarnings("unchecked") + protected Region getRegion(String beanName) { + return applicationContext.getBean(beanName, Region.class); + } + + protected AnnotationConfigApplicationContext newApplicationContext(Class... annotatedClasses) { + return new AnnotationConfigApplicationContext(annotatedClasses); + } + + protected EvictionAttributes newEvictionAttributes(Integer maximum, EvictionPolicyType type, EvictionActionType action, + ObjectSizer... objectSizer) { + + EvictionAttributesFactoryBean evictionAttributesFactory = new EvictionAttributesFactoryBean(); + + evictionAttributesFactory.setAction(action.getEvictionAction()); + evictionAttributesFactory.setObjectSizer(ArrayUtils.getFirst(objectSizer)); + evictionAttributesFactory.setThreshold(maximum); + evictionAttributesFactory.setType(type); + evictionAttributesFactory.afterPropertiesSet(); + + return evictionAttributesFactory.getObject(); + } + + @Test + public void usesDefaultEvictionPolicyConfiguration() { + applicationContext = newApplicationContext(DefaultEvictionPolicyConfiguration.class); + + EvictionAttributes defaultEvictionAttributes = EvictionAttributes.createLRUEntryAttributes(); + + assertEvictionAttributes(applicationContext.getBean("PartitionRegion", Region.class), defaultEvictionAttributes); + assertEvictionAttributes(applicationContext.getBean("ReplicateRegion", Region.class), defaultEvictionAttributes); + } + + @Test + public void usesCustomEvictionPolicyConfiguration() { + applicationContext = newApplicationContext(CustomEvictionPolicyConfiguration.class); + + ObjectSizer mockObjectSizer = applicationContext.getBean("mockObjectSizer", ObjectSizer.class); + + EvictionAttributes customEvictionAttributes = newEvictionAttributes(65536, EvictionPolicyType.MEMORY_SIZE, + EvictionActionType.OVERFLOW_TO_DISK, mockObjectSizer); + + assertEvictionAttributes(applicationContext.getBean("PartitionRegion", Region.class), customEvictionAttributes); + assertEvictionAttributes(applicationContext.getBean("ReplicateRegion", Region.class), customEvictionAttributes); + } + + @Test + public void usesRegionSpecificEvictionPolicyConfiguration() { + applicationContext = newApplicationContext(RegionSpecificEvictionPolicyConfiguration.class); + + ObjectSizer mockObjectSizer = applicationContext.getBean("mockObjectSizer", ObjectSizer.class); + + EvictionAttributes partitionRegionEvictionAttributes = newEvictionAttributes(null, + EvictionPolicyType.HEAP_PERCENTAGE, EvictionActionType.OVERFLOW_TO_DISK, mockObjectSizer); + + EvictionAttributes replicateRegionEvictionAttributes = newEvictionAttributes(10000, + EvictionPolicyType.ENTRY_COUNT, EvictionActionType.LOCAL_DESTROY); + + assertEvictionAttributes(applicationContext.getBean("PartitionRegion", Region.class), + partitionRegionEvictionAttributes); + + assertEvictionAttributes(applicationContext.getBean("ReplicateRegion", Region.class), + replicateRegionEvictionAttributes); + } + + @Test + public void usesLastMatchingEvictionPolicyConfiguration() { + applicationContext = newApplicationContext(LastMatchingWinsEvictionPolicyConfiguration.class); + + EvictionAttributes lastMatchingEvictionAttributes = newEvictionAttributes(99, EvictionPolicyType.ENTRY_COUNT, + EvictionActionType.OVERFLOW_TO_DISK); + + assertEvictionAttributes(applicationContext.getBean("PartitionRegion", Region.class), + lastMatchingEvictionAttributes); + + assertEvictionAttributes(applicationContext.getBean("ReplicateRegion", Region.class), + lastMatchingEvictionAttributes); + } + + @Configuration + @SuppressWarnings("unused") + static class CacheRegionConfiguration { + + @Bean("mockCache") + @SuppressWarnings("unchecked") + Cache mockCache() { + Cache mockCache = mock(Cache.class); + + RegionFactory mockRegionFactory = mock(RegionFactory.class); + + final AtomicReference evictionAttributes = + new AtomicReference<>(null); + + when(mockCache.createRegionFactory()).thenReturn(mockRegionFactory); + + when(mockRegionFactory.setEvictionAttributes(any(EvictionAttributes.class))).thenAnswer( + new Answer() { + @Override + public RegionFactory answer(InvocationOnMock invocation) throws Throwable { + evictionAttributes.set(invocation.getArgumentAt(0, EvictionAttributes.class)); + return (RegionFactory) invocation.getMock(); + } + } + ); + + when(mockRegionFactory.create(anyString())).thenAnswer(new Answer() { + @Override + public Region answer(InvocationOnMock invocation) throws Throwable { + String regionName = invocation.getArgumentAt(0, String.class); + + Region mockRegion = mock(Region.class, regionName); + + RegionAttributes mockRegionAttributes = + mock(RegionAttributes.class, regionName.concat("Attributes")); + + when(mockRegion.getName()).thenReturn(regionName); + when(mockRegion.getFullPath()).thenReturn(String.format("%1$s%2$s", Region.SEPARATOR, regionName)); + when(mockRegion.getAttributes()).thenReturn(mockRegionAttributes); + when(mockRegionAttributes.getEvictionAttributes()).thenReturn(evictionAttributes.get()); + + return mockRegion; + } + }); + + return mockCache; + } + + @Bean("PartitionRegion") + PartitionedRegionFactoryBean mockPartitionRegion(Cache gemfireCache) { + PartitionedRegionFactoryBean partitionRegion = + new PartitionedRegionFactoryBean<>(); + + partitionRegion.setCache(gemfireCache); + partitionRegion.setClose(false); + partitionRegion.setPersistent(false); + + return partitionRegion; + } + + @Bean("ReplicateRegion") + ReplicatedRegionFactoryBean mockReplicateRegion(Cache gemfireCache) { + ReplicatedRegionFactoryBean replicateRegion = + new ReplicatedRegionFactoryBean<>(); + + replicateRegion.setCache(gemfireCache); + replicateRegion.setClose(false); + replicateRegion.setPersistent(false); + + return replicateRegion; + } + + @Bean + ObjectSizer mockObjectSizer() { + return mock(ObjectSizer.class); + } + } + + @EnableEviction + static class DefaultEvictionPolicyConfiguration extends CacheRegionConfiguration { + } + + @EnableEviction(policies = @EvictionPolicy(maximum = 65536, type = EvictionPolicyType.MEMORY_SIZE, + action = EvictionActionType.OVERFLOW_TO_DISK, objectSizerName = "mockObjectSizer")) + static class CustomEvictionPolicyConfiguration extends CacheRegionConfiguration { + } + + @EnableEviction(policies = { + @EvictionPolicy(maximum = 85, type = EvictionPolicyType.HEAP_PERCENTAGE, action = EvictionActionType.OVERFLOW_TO_DISK, + objectSizerName = "mockObjectSizer", regionNames = "PartitionRegion"), + @EvictionPolicy(maximum = 10000, type = EvictionPolicyType.ENTRY_COUNT, action = EvictionActionType.LOCAL_DESTROY, + regionNames = "ReplicateRegion") + }) + static class RegionSpecificEvictionPolicyConfiguration extends CacheRegionConfiguration { + } + + @EnableEviction(policies = { + @EvictionPolicy(maximum = 1, type = EvictionPolicyType.ENTRY_COUNT, action = EvictionActionType.LOCAL_DESTROY, + objectSizerName = "mockObjectSizer", regionNames = "ReplicateRegion"), + @EvictionPolicy(maximum = 99, type = EvictionPolicyType.ENTRY_COUNT, action = EvictionActionType.OVERFLOW_TO_DISK) + }) + static class LastMatchingWinsEvictionPolicyConfiguration extends CacheRegionConfiguration { + } +}