diff --git a/spring-data-geode/src/main/java/org/springframework/data/gemfire/config/annotation/AsCacheListener.java b/spring-data-geode/src/main/java/org/springframework/data/gemfire/config/annotation/AsCacheListener.java new file mode 100644 index 000000000..43901a1e7 --- /dev/null +++ b/spring-data-geode/src/main/java/org/springframework/data/gemfire/config/annotation/AsCacheListener.java @@ -0,0 +1,57 @@ +/* + * Copyright 2016-2019 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 + * + * https://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.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.data.gemfire.eventing.config.CacheListenerEventType; +import org.springframework.data.gemfire.eventing.config.RegionCacheListenerEventType; + +/** + * Used to declare a concrete method as a {@link org.apache.geode.cache.CacheListener} event handler + * + * @author Udo Kohlmeyer + * @see org.apache.geode.cache.CacheListener + * @see CacheListenerEventType + * @see org.apache.geode.cache.Region + * @see org.apache.geode.cache.RegionEvent + * @see org.apache.geode.cache.EntryEvent + * @since 2.4.0 + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.METHOD }) +public @interface AsCacheListener { + + /** + * An array of {@link CacheListenerEventType} that control what region CRUD events need to be observed + * {@link CacheListenerEventType} and {@link RegionCacheListenerEventType} cannot be set on the same method. As they + * are mutually exclusive and require that the implementing method uses {@link org.apache.geode.cache.RegionEvent} or + * {@link org.apache.geode.cache.EntryEvent} + */ + CacheListenerEventType[] eventTypes() default {}; + + /** + * An array for {@link org.apache.geode.cache.Region} names which this {@link org.apache.geode.cache.CacheListener} + * will be link to. Not declaring any regions will result in the CacheListener to be configured against all defined + * regions. + */ + String[] regions() default {}; +} diff --git a/spring-data-geode/src/main/java/org/springframework/data/gemfire/config/annotation/AsCacheWriter.java b/spring-data-geode/src/main/java/org/springframework/data/gemfire/config/annotation/AsCacheWriter.java new file mode 100644 index 000000000..d3a1f4ed0 --- /dev/null +++ b/spring-data-geode/src/main/java/org/springframework/data/gemfire/config/annotation/AsCacheWriter.java @@ -0,0 +1,57 @@ +/* + * Copyright 2016-2019 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 + * + * https://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.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.data.gemfire.eventing.config.CacheWriterEventType; +import org.springframework.data.gemfire.eventing.config.RegionCacheWriterEventType; + +/** + * Used to declare a concrete method as a {@link org.apache.geode.cache.CacheWriter} event handler + * + * @author Udo Kohlmeyer + * @see org.apache.geode.cache.CacheWriter + * @see CacheWriterEventType + * @see org.apache.geode.cache.Region + * @see org.apache.geode.cache.RegionEvent + * @see org.apache.geode.cache.EntryEvent + * @since 2.4.0 + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.METHOD }) +public @interface AsCacheWriter { + + /** + * An array of {@link CacheWriterEventType} that control what region CRUD events need to be observed + * {@link CacheWriterEventType} and {@link RegionCacheWriterEventType} cannot be set on the same method. As they + * are mutually exclusive and require that the implementing method uses {@link org.apache.geode.cache.RegionEvent} or + * {@link org.apache.geode.cache.EntryEvent} + */ + CacheWriterEventType[] eventTypes() default {}; + + /** + * An array for {@link org.apache.geode.cache.Region} names which this {@link org.apache.geode.cache.CacheWriter} + * will be link to. Not declaring any regions will result in the CacheWriter to be configured against all defined + * regions. + */ + String[] regions() default {}; +} diff --git a/spring-data-geode/src/main/java/org/springframework/data/gemfire/config/annotation/AsRegionEventHandler.java b/spring-data-geode/src/main/java/org/springframework/data/gemfire/config/annotation/AsRegionEventHandler.java new file mode 100644 index 000000000..e8d50348c --- /dev/null +++ b/spring-data-geode/src/main/java/org/springframework/data/gemfire/config/annotation/AsRegionEventHandler.java @@ -0,0 +1,70 @@ +/* + * Copyright 2016-2019 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 + * + * https://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.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.data.gemfire.eventing.config.CacheListenerEventType; +import org.springframework.data.gemfire.eventing.config.CacheWriterEventType; +import org.springframework.data.gemfire.eventing.config.RegionCacheListenerEventType; +import org.springframework.data.gemfire.eventing.config.RegionCacheWriterEventType; + +/** + * Used to declare a concrete method as a {@link AsRegionEventHandler} event handler, + * which handles {@link org.apache.geode.cache.Region} + * + * @author Udo Kohlmeyer + * @see org.apache.geode.cache.CacheWriter + * @see org.apache.geode.cache.CacheListener + * @see org.apache.geode.cache.Region + * @see RegionCacheListenerEventType + * @see RegionCacheWriterEventType + * @see org.apache.geode.cache.RegionEvent + * @see org.apache.geode.cache.EntryEvent + * @since 2.4.0 + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.METHOD }) +public @interface AsRegionEventHandler { + + /** + * An array of {@link RegionCacheListenerEventType} that control what region events need to be observed + * {@link CacheListenerEventType} and {@link RegionCacheListenerEventType} cannot be set on the same method. As they + * are mutually exclusive and require that the implementing method uses {@link org.apache.geode.cache.RegionEvent} or + * {@link org.apache.geode.cache.EntryEvent} + */ + RegionCacheListenerEventType[] regionListenerEventTypes() default {}; + + /** + * An array of {@link RegionCacheWriterEventType} that control what region events need to be observed. + * {@link CacheWriterEventType} and {@link RegionCacheWriterEventType} cannot be set on the same method. As they + * are mutually exclusive and require that the implementing method uses {@link org.apache.geode.cache.RegionEvent} or + * {@link org.apache.geode.cache.EntryEvent} + */ + RegionCacheWriterEventType[] regionWriterEventTypes() default {}; + + /** + * An array for {@link org.apache.geode.cache.Region} names which this {@link org.apache.geode.cache.CacheWriter} + * will be link to. Not declaring any regions will result in the CacheListener to be configured against all defined + * regions. + */ + String[] regions() default {}; +} diff --git a/spring-data-geode/src/main/java/org/springframework/data/gemfire/config/annotation/EnableEventProcessing.java b/spring-data-geode/src/main/java/org/springframework/data/gemfire/config/annotation/EnableEventProcessing.java new file mode 100644 index 000000000..04381fff6 --- /dev/null +++ b/spring-data-geode/src/main/java/org/springframework/data/gemfire/config/annotation/EnableEventProcessing.java @@ -0,0 +1,55 @@ +/* + * Copyright 2016-2019 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 + * + * https://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.springframework.context.annotation.Import; +import org.springframework.data.gemfire.eventing.config.AsCacheListenerBeanPostProcessorRegistrar; +import org.springframework.data.gemfire.eventing.config.AsCacheWriterBeanPostProcessorRegistrar; +import org.springframework.data.gemfire.eventing.config.PojoCacheListenerWrapper; + +/** + * Enables GemFire annotated EventHandler implementations. These implementation will include {@link org.apache.geode.cache.CacheListener}, + * {@link org.apache.geode.cache.CacheWriter}, {@link org.apache.geode.cache.TransactionListener} and {@link org.apache.geode.cache.CacheLoader} + * + * This annotation results in the container discovering any beans that are annotated with: + * + * + * @author Udo Kohlmeyer + * @since 2.3.0 + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Inherited +@Documented +@Import({ AsCacheListenerBeanPostProcessorRegistrar.class, + AsCacheWriterBeanPostProcessorRegistrar.class}) +@SuppressWarnings("unused") +public @interface EnableEventProcessing { + +} diff --git a/spring-data-geode/src/main/java/org/springframework/data/gemfire/config/support/CacheListenerPostProcessor.java b/spring-data-geode/src/main/java/org/springframework/data/gemfire/config/support/CacheListenerPostProcessor.java new file mode 100644 index 000000000..3a18b5b10 --- /dev/null +++ b/spring-data-geode/src/main/java/org/springframework/data/gemfire/config/support/CacheListenerPostProcessor.java @@ -0,0 +1,122 @@ +/* + * Copyright 2016-2019 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 + * + * https://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.support; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; +import java.util.List; + +import org.apache.geode.cache.CacheEvent; +import org.apache.geode.cache.CacheListener; +import org.apache.geode.cache.EntryEvent; +import org.apache.geode.cache.Region; +import org.apache.geode.cache.RegionEvent; +import org.springframework.beans.factory.config.BeanPostProcessor; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.annotation.AnnotationAttributes; +import org.springframework.data.gemfire.config.annotation.AsCacheListener; +import org.springframework.data.gemfire.config.annotation.AsRegionEventHandler; +import org.springframework.data.gemfire.eventing.EventProcessorUtils; +import org.springframework.data.gemfire.eventing.config.CacheListenerEventType; +import org.springframework.data.gemfire.eventing.config.PojoCacheListenerWrapper; +import org.springframework.data.gemfire.eventing.config.PojoRegionEventCacheListenerWrapper; +import org.springframework.data.gemfire.eventing.config.RegionCacheListenerEventType; + +/** + * A {@link BeanPostProcessor} to create and register {@link CacheListener}, annotated with {@link AsCacheListener} + * and {@link AsRegionEventHandler} onto the configured {@link Region}s + * + * @author Udo Kohlmeyer + * @see BeanPostProcessor + * @see CacheListener + * @see Region + * @see AsCacheListener + * @see AsRegionEventHandler + * @see CallbackPostProcessor + * @since 2.4.0 + */ +@Configuration +public class CacheListenerPostProcessor extends CallbackPostProcessor { + + @Override + protected Class getRegionEventHandlerClass() { + return AsRegionEventHandler.class; + } + + @Override + protected Class getEventHandlerClass() { + return AsCacheListener.class; + } + + @Override + protected void registerEventHandlers(Object bean, Class listenerAnnotationClazz, + Method method, AnnotationAttributes cacheListenerAttributes) { + if (listenerAnnotationClazz.isAssignableFrom(getEventHandlerClass())) { + registerCacheListenerEventHandler(bean, method, cacheListenerAttributes); + } + else if (listenerAnnotationClazz.isAssignableFrom(getRegionEventHandlerClass())) { + registerRegionEventHandler(bean, method, cacheListenerAttributes); + } + } + + /** + * Lookup {@link CacheListenerEventType} from the {@link AsCacheListener} annotation and create a {@link PojoCacheListenerWrapper} + * of type {@link CacheListener} that would register itself onto a {@link Region} for the configured events + */ + private void registerCacheListenerEventHandler(Object bean, Method method, + AnnotationAttributes cacheListenerAttributes) { + CacheListenerEventType[] eventTypes = (CacheListenerEventType[]) cacheListenerAttributes + .get("eventTypes"); + registerEventHandlerToRegion(method, cacheListenerAttributes, + new PojoCacheListenerWrapper(method, bean, eventTypes), EntryEvent.class); + } + + /** + * Lookup {@link RegionCacheListenerEventType} from the {@link AsRegionEventHandler} annotation and + * create a {@link PojoRegionEventCacheListenerWrapper} + * of type {@link CacheListener} that would register itself onto a {@link Region} for the configured + * {@link Region} specific events + */ + private void registerRegionEventHandler(Object bean, Method method, + AnnotationAttributes cacheListenerAttributes) { + RegionCacheListenerEventType[] eventTypes = (RegionCacheListenerEventType[]) cacheListenerAttributes + .get("regionListenerEventTypes"); + registerEventHandlerToRegion(method, cacheListenerAttributes, + new PojoRegionEventCacheListenerWrapper(method, bean, eventTypes), RegionEvent.class); + } + + /** + * Validates the method parameters to be of the correct type dependent on the eventing Annotation. It then registers + * the defined {@link CacheListener} onto the defined set of {@link Region}. + * + * @param method - The event handler callback method for event handling type + * @param cacheListenerAttributes - A set of {@link Annotation} attributes used to get the region names configured + * on the annotation + * @param cacheListener - The {@link CacheListener} to be registered onto the {@link Region} + * @param eventClass - The expected method parameter type. Can be either {@link EntryEvent} or {@link RegionEvent} + */ + private void registerEventHandlerToRegion(Method method, + AnnotationAttributes cacheListenerAttributes, CacheListener cacheListener, Class eventClass) { + List regions = getRegionsForEventRegistration(cacheListenerAttributes.getStringArray("regions"), + getBeanFactory()); + + EventProcessorUtils.validateEventHandlerMethodParameters(method, eventClass); + EventProcessorUtils.registerCacheListenerToRegions(regions, beanFactory, cacheListener); + } + +} diff --git a/spring-data-geode/src/main/java/org/springframework/data/gemfire/config/support/CacheWriterPostProcessor.java b/spring-data-geode/src/main/java/org/springframework/data/gemfire/config/support/CacheWriterPostProcessor.java new file mode 100644 index 000000000..50d526a88 --- /dev/null +++ b/spring-data-geode/src/main/java/org/springframework/data/gemfire/config/support/CacheWriterPostProcessor.java @@ -0,0 +1,134 @@ +/* + * Copyright 2016-2019 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 + * + * https://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.support; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import org.apache.geode.cache.CacheEvent; +import org.apache.geode.cache.CacheWriter; +import org.apache.geode.cache.Region; +import org.springframework.beans.factory.config.BeanPostProcessor; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.annotation.AnnotationAttributes; +import org.springframework.data.gemfire.config.annotation.AsCacheWriter; +import org.springframework.data.gemfire.config.annotation.AsRegionEventHandler; +import org.springframework.data.gemfire.eventing.EventProcessorUtils; +import org.springframework.data.gemfire.eventing.config.ComposableCacheWriterWrapper; + +/** + * A {@link BeanPostProcessor} to create and register {@link org.apache.geode.cache.CacheWriter}, + * annotated with {@link AsCacheWriter} + * and {@link AsRegionEventHandler} onto the configured {@link Region}s + * + * @author Udo Kohlmeyer + * @see Region + * @see CacheWriter + * @see AsCacheWriter + * @see BeanPostProcessor + * @see AsRegionEventHandler + * @see ComposableCacheWriterWrapper + * @since 2.4.0 + */ +@Configuration +public class CacheWriterPostProcessor> extends CallbackPostProcessor { + + private final Map composableCacheWriterWrappers = new HashMap<>(); + private List configuredRegions; + + @Override + protected Class getRegionEventHandlerClass() { + return AsRegionEventHandler.class; + } + + @Override + protected Class getEventHandlerClass() { + return AsCacheWriter.class; + } + + @Override + protected void registerEventHandlers(Object bean, Class writerAnnotationClazz, + Method method, AnnotationAttributes cacheWriterAttributes) { + + String[] regionNames = cacheWriterAttributes.getStringArray("regions"); + + List regions = regionNames.length > 0 ? Arrays.asList(regionNames) : getConfiguredRegions(); + + Optional eventTypes = Optional.ofNullable(getEventTypes(writerAnnotationClazz, cacheWriterAttributes)); + + eventTypes.ifPresent(events -> { + if (events.length > 0) { + for (String region : regions) { + registerCacheWriterEventHandler(bean, method, events, region); + } + } + }); + } + + private E[] getEventTypes(Class writerAnnotationClazz, + AnnotationAttributes cacheWriterAttributes) { + if (writerAnnotationClazz.isAssignableFrom(getEventHandlerClass())) { + return (E[]) cacheWriterAttributes.get("eventTypes"); + } + else if (writerAnnotationClazz.isAssignableFrom(getRegionEventHandlerClass())) { + return (E[]) cacheWriterAttributes.get("regionWriterEventTypes"); + } + return null; + } + + private List getConfiguredRegions() { + if (configuredRegions == null) { + configuredRegions = getRegionsForEventRegistration(getBeanFactory()); + } + return configuredRegions; + } + + + private void registerCacheWriterEventHandler(Object bean, Method method, E[] eventTypes, String region) { + ComposableCacheWriterWrapper composableCacheWriterWrapper = + composableCacheWriterWrappers.getOrDefault(region, new ComposableCacheWriterWrapper()); + + Optional> eventTypeForMethod = Optional + .ofNullable(getEventTypeForMethod(eventTypes)); + eventTypeForMethod.ifPresent(eventTypeClass -> { + EventProcessorUtils.validateEventHandlerMethodParameters(method, eventTypeClass); + composableCacheWriterWrapper.addCacheWriter(method, bean, eventTypes); + + composableCacheWriterWrappers.put(region, composableCacheWriterWrapper); + + registerEventHandlerToRegion(region, composableCacheWriterWrapper); + }); + } + + /** + * Validates the method parameters to be of the correct type dependent on the eventing Annotation. It then registers + * the defined {@link CacheWriter} onto the defined set of {@link Region}. + * + * on the annotation + * + * @param cacheWriter - The {@link CacheWriter} to be registered onto the {@link Region} + */ + private void registerEventHandlerToRegion(String regionName, CacheWriter cacheWriter) { + EventProcessorUtils.registerCacheWriterToRegion(regionName, beanFactory, cacheWriter); + } +} diff --git a/spring-data-geode/src/main/java/org/springframework/data/gemfire/config/support/CallbackPostProcessor.java b/spring-data-geode/src/main/java/org/springframework/data/gemfire/config/support/CallbackPostProcessor.java new file mode 100644 index 000000000..4bdb799a1 --- /dev/null +++ b/spring-data-geode/src/main/java/org/springframework/data/gemfire/config/support/CallbackPostProcessor.java @@ -0,0 +1,177 @@ +package org.springframework.data.gemfire.config.support; + +import static java.util.Arrays.stream; +import static org.springframework.data.gemfire.util.ArrayUtils.isEmpty; +import static org.springframework.data.gemfire.util.ArrayUtils.nullSafeArray; +import static org.springframework.data.gemfire.util.RuntimeExceptionFactory.newIllegalStateException; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; + +import org.apache.geode.cache.CacheEvent; +import org.apache.geode.cache.EntryEvent; +import org.apache.geode.cache.Region; +import org.apache.geode.cache.RegionEvent; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanFactoryAware; +import org.springframework.beans.factory.config.BeanPostProcessor; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.core.annotation.AnnotationAttributes; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.data.gemfire.eventing.config.CacheListenerEventType; +import org.springframework.data.gemfire.eventing.config.CacheWriterEventType; +import org.springframework.data.gemfire.eventing.config.RegionCacheListenerEventType; +import org.springframework.data.gemfire.eventing.config.RegionCacheWriterEventType; +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; +import org.springframework.util.ReflectionUtils; + +/** + * + */ +public abstract class CallbackPostProcessor implements BeanPostProcessor, BeanFactoryAware { + + protected ConfigurableListableBeanFactory beanFactory; + + @Override + public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { + + registerAnyDeclaredCallbackAnnotatedMethods(bean, getEventHandlerClass()); + registerAnyDeclaredCallbackAnnotatedMethods(bean, getRegionEventHandlerClass()); + + return bean; + } + + protected abstract Class getRegionEventHandlerClass(); + + protected abstract Class getEventHandlerClass(); + + protected void registerAnyDeclaredCallbackAnnotatedMethods(Object bean, + Class annotationClazz) { + + Method[] declaredMethods = nullSafeArray(ReflectionUtils.getAllDeclaredMethods(bean.getClass()), Method.class); + + stream(declaredMethods).forEach(method -> { + + Optional optionalCallbackAnnotation = Optional.ofNullable(AnnotationUtils + .getAnnotation(method, annotationClazz)); + + optionalCallbackAnnotation.ifPresent(callback -> { + + Assert.isTrue(Modifier.isPublic(method.getModifiers()), String + .format("The bean [%s] method [%s] annotated with [%s] must be public", bean.getClass().getName(), + method.getName(), annotationClazz.getName())); + + AnnotationAttributes callbackAttributes = resolveAnnotationAttributes(callback); + + registerEventHandlers(bean, annotationClazz, method, callbackAttributes); + + }); + }); + } + + protected abstract void registerEventHandlers(Object bean, Class annotationClazz, + Method method, AnnotationAttributes callbackAttributes); + + /** + * Takes an array of Region names. If empty, returns all configured {@link Region} names, otherwise returns the input + * region name array + * + * @param beanFactory - A {@link org.springframework.data.gemfire.ConfigurableRegionFactoryBean} + * @return An array of {@link Region} names. If the input regions array is empty, the result will be an array with all + * configured {@link Region} names + */ + protected List getRegionsForEventRegistration(ConfigurableListableBeanFactory beanFactory) { + List regionNames = new ArrayList<>(); + stream(beanFactory.getBeanDefinitionNames()).forEach(beanName -> { + Object bean = beanFactory.getBean(beanName); + if (bean instanceof Region) { + Region region = (Region) bean; + regionNames.add(region.getName()); + } + }); + return regionNames; + } + + private AnnotationAttributes resolveAnnotationAttributes(Annotation annotation) { + return AnnotationAttributes.fromMap( + AnnotationUtils.getAnnotationAttributes(annotation, false, true)); + } + + /** + * Takes an array of Region names. If empty, returns all configured {@link Region} names, otherwise returns the input + * region name array + * + * @param regions - An Array of {@link Region} names. This can be empty and thus defaults to all configured {@link Region} + * @param beanFactory - A {@link org.springframework.data.gemfire.ConfigurableRegionFactoryBean} + * @return An array of {@link Region} names. If the input regions array is empty, the result will be an array with all + * configured {@link Region} names + */ + protected List getRegionsForEventRegistration(String[] regions, + ConfigurableListableBeanFactory beanFactory) { + if (isEmpty(regions)) { + return getRegionsForEventRegistration(beanFactory); + } + else { + return Arrays.asList(regions); + } + } + + /** + * Sets a reference to the configured Spring {@link BeanFactory}. + * + * @param beanFactory configured Spring {@link BeanFactory}. + * @throws IllegalArgumentException if the given {@link BeanFactory} is not an instance of + * {@link ConfigurableListableBeanFactory}. + * @see BeanFactoryAware + * @see BeanFactory + */ + @Override + @SuppressWarnings("all") + public final void setBeanFactory(BeanFactory beanFactory) throws BeansException { + + Assert.isInstanceOf(ConfigurableListableBeanFactory.class, beanFactory, String + .format("BeanFactory [%1$s] must be an instance of %2$s", ObjectUtils.nullSafeClassName(beanFactory), + ConfigurableListableBeanFactory.class.getSimpleName())); + + this.beanFactory = (ConfigurableListableBeanFactory) beanFactory; + } + + /** + * Returns a reference to the containing Spring {@link BeanFactory}. + * + * @return a reference to the containing Spring {@link BeanFactory}. + * @throws IllegalStateException if the {@link BeanFactory} was not configured. + * @see BeanFactory + */ + protected ConfigurableListableBeanFactory getBeanFactory() { + return Optional.ofNullable(this.beanFactory) + .orElseThrow(() -> newIllegalStateException("BeanFactory was not properly configured")); + } + + /** + * Returns the correct Event type, either {@link EntryEvent} or {@link RegionEvent}, dependent on the eventType + * of either {@link RegionCacheWriterEventType}, {@link CacheWriterEventType}, {@link CacheListenerEventType} + * or {@link RegionCacheListenerEventType} + * + * @param eventTypes an array of event types + * @return a class type associated with the enum event type. Returns {@literal null} if not association is found + */ + protected Class getEventTypeForMethod(Enum[] eventTypes) { + for (Enum eventType : eventTypes) { + if (eventType instanceof CacheWriterEventType || eventType instanceof CacheListenerEventType) { + return EntryEvent.class; + } + if (eventType instanceof RegionCacheWriterEventType || eventType instanceof RegionCacheListenerEventType) { + return RegionEvent.class; + } + } + return null; + } +} diff --git a/spring-data-geode/src/main/java/org/springframework/data/gemfire/eventing/EventProcessorUtils.java b/spring-data-geode/src/main/java/org/springframework/data/gemfire/eventing/EventProcessorUtils.java new file mode 100644 index 000000000..a0b9f4e4b --- /dev/null +++ b/spring-data-geode/src/main/java/org/springframework/data/gemfire/eventing/EventProcessorUtils.java @@ -0,0 +1,96 @@ +/* + * Copyright 2016-2019 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 + * + * https://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.eventing; + +import java.lang.reflect.Method; +import java.util.List; +import java.util.Optional; + +import org.apache.geode.cache.CacheEvent; +import org.apache.geode.cache.CacheListener; +import org.apache.geode.cache.CacheWriter; +import org.apache.geode.cache.Region; +import org.springframework.beans.factory.BeanFactory; + +/** + * @author Udo Kohlmeyer + * @see BeanFactory + * @see Region + * @see List + * @see CacheListener + * @see CacheWriter + * @see CacheEvent + * @see org.apache.geode.cache.RegionEvent + * @see org.apache.geode.cache.EntryEvent + * @see org.springframework.data.gemfire.eventing.config.ComposableCacheWriterWrapper + * @see org.springframework.data.gemfire.eventing.config.PojoCacheListenerWrapper + * @see org.springframework.data.gemfire.eventing.config.PojoRegionEventCacheListenerWrapper + * @see 2.4.0 + */ +public class EventProcessorUtils { + + /** + * Registers a {@link CacheListener} with a {@link Region} + * @param regions a {@link List} of {@link String}s of region names on which the {@link CacheListener} needs to be registered + * @param beanFactory the Spring {@link BeanFactory} + * @param cacheListener the {@link CacheListener} that needs to be registered onto the {@link Region}(s) + */ + public static void registerCacheListenerToRegions(List regions, BeanFactory beanFactory, + CacheListener cacheListener) { + for (String regionName : regions) { + Optional regionBeanOptional = Optional.of(beanFactory.getBean(regionName, Region.class)); + + regionBeanOptional.ifPresent(region -> region.getAttributesMutator().addCacheListener(cacheListener)); + } + } + + /** + * Registers a {@link CacheWriter} with a {@link Region} + * @param regionName the region name on which the {@link CacheWriter} needs to be registered + * @param beanFactory the Spring {@link BeanFactory} + * @param cacheWriter the {@link CacheWriter} that needs to be registered + */ + public static void registerCacheWriterToRegion(String regionName, BeanFactory beanFactory, + CacheWriter cacheWriter) { + Optional> regionBeanOptional = Optional.of(beanFactory.getBean(regionName, Region.class)); + + regionBeanOptional.ifPresent(region -> region.getAttributesMutator().setCacheWriter(cacheWriter)); + } + + /** + * The {@link Method} method needs to be validated to have only 1 parameter AND of either {@link org.apache.geode.cache.EntryEvent} + * or {@link org.apache.geode.cache.RegionEvent} + * @param method the method to be validated + * @param requireParameterType the required type of the method parameter + */ + public static void validateEventHandlerMethodParameters(Method method, + Class requireParameterType) { + Class[] parameterTypes = method.getParameterTypes(); + if (parameterTypes.length != 1) { + throw new IllegalArgumentException(String + .format("Callback Handler method: %s does not currently support more than one parameter", + method.getName())); + } + if (!parameterTypes[0].isAssignableFrom(requireParameterType)) { + throw new IllegalArgumentException(String + .format("Callback Handler: %s requires an %s parameter type", method.getName(), + requireParameterType.getName())); + } + } + +} diff --git a/spring-data-geode/src/main/java/org/springframework/data/gemfire/eventing/config/AsCacheListenerBeanPostProcessorRegistrar.java b/spring-data-geode/src/main/java/org/springframework/data/gemfire/eventing/config/AsCacheListenerBeanPostProcessorRegistrar.java new file mode 100644 index 000000000..4248d049b --- /dev/null +++ b/spring-data-geode/src/main/java/org/springframework/data/gemfire/eventing/config/AsCacheListenerBeanPostProcessorRegistrar.java @@ -0,0 +1,48 @@ +/* + * Copyright 2016-2019 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 + * + * https://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.eventing.config; + +import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.support.BeanDefinitionReaderUtils; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.context.annotation.ImportBeanDefinitionRegistrar; +import org.springframework.core.type.AnnotationMetadata; +import org.springframework.data.gemfire.config.support.CacheListenerPostProcessor; + +/** + * Spring {@link org.springframework.context.annotation.ImportBeanDefinitionRegistrar} to register the {@link CacheListenerPostProcessor} + * + * @author Udo Kohlmeyer + * @see org.springframework.beans.factory.BeanFactory + * @see org.springframework.beans.factory.support.BeanDefinitionBuilder + * @see org.springframework.beans.factory.support.BeanDefinitionRegistry + * @see org.springframework.context.annotation.ImportBeanDefinitionRegistrar + * @see CacheListenerPostProcessor + * @since 2.4.0 + */ +public class AsCacheListenerBeanPostProcessorRegistrar implements ImportBeanDefinitionRegistrar { + + @Override + public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) { + + BeanDefinitionBuilder builder = + BeanDefinitionBuilder.genericBeanDefinition(CacheListenerPostProcessor.class); + + BeanDefinitionReaderUtils.registerWithGeneratedName(builder.getBeanDefinition(), registry); + } +} diff --git a/spring-data-geode/src/main/java/org/springframework/data/gemfire/eventing/config/AsCacheWriterBeanPostProcessorRegistrar.java b/spring-data-geode/src/main/java/org/springframework/data/gemfire/eventing/config/AsCacheWriterBeanPostProcessorRegistrar.java new file mode 100644 index 000000000..bbbd0220b --- /dev/null +++ b/spring-data-geode/src/main/java/org/springframework/data/gemfire/eventing/config/AsCacheWriterBeanPostProcessorRegistrar.java @@ -0,0 +1,48 @@ +/* + * Copyright 2016-2019 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 + * + * https://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.eventing.config; + +import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.support.BeanDefinitionReaderUtils; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.context.annotation.ImportBeanDefinitionRegistrar; +import org.springframework.core.type.AnnotationMetadata; +import org.springframework.data.gemfire.config.support.CacheWriterPostProcessor; + +/** + * Spring {@link ImportBeanDefinitionRegistrar} to register the {@link CacheWriterPostProcessor} + * + * @author Udo Kohlmeyer + * @see org.springframework.beans.factory.BeanFactory + * @see org.springframework.beans.factory.support.BeanDefinitionBuilder + * @see org.springframework.beans.factory.support.BeanDefinitionRegistry + * @see org.springframework.context.annotation.ImportBeanDefinitionRegistrar + * @see CacheWriterPostProcessor + * @since 2.4.0 + */ +public class AsCacheWriterBeanPostProcessorRegistrar implements ImportBeanDefinitionRegistrar { + + @Override + public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) { + + BeanDefinitionBuilder builder = + BeanDefinitionBuilder.genericBeanDefinition(CacheWriterPostProcessor.class); + + BeanDefinitionReaderUtils.registerWithGeneratedName(builder.getBeanDefinition(), registry); + } +} diff --git a/spring-data-geode/src/main/java/org/springframework/data/gemfire/eventing/config/CacheListenerEventType.java b/spring-data-geode/src/main/java/org/springframework/data/gemfire/eventing/config/CacheListenerEventType.java new file mode 100644 index 000000000..2b0ce157c --- /dev/null +++ b/spring-data-geode/src/main/java/org/springframework/data/gemfire/eventing/config/CacheListenerEventType.java @@ -0,0 +1,40 @@ +/* + * Copyright 2016-2019 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 + * + * https://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.eventing.config; + +/** + * An Enum that represents event types defined within {@link org.apache.geode.cache.CacheListener}. The event types + * defined are CRUD operation orientated. These do not reflect the events that {@link org.apache.geode.cache.Region} have. + * + * @author Udo Kohlmeyer + * @see org.apache.geode.cache.CacheListener + * @see org.apache.geode.cache.Region + * @since 2.4.0 + */ +public enum CacheListenerEventType{ + ALL(0b00001111), + AFTER_CREATE(0b00000001), + AFTER_UPDATE(0b00000010), + AFTER_DESTROY(0b00000100), + AFTER_INVALIDATE(0b00001000); + + CacheListenerEventType(int mask){ + this.mask = mask; + } + + int mask; +} diff --git a/spring-data-geode/src/main/java/org/springframework/data/gemfire/eventing/config/CacheWriterEventType.java b/spring-data-geode/src/main/java/org/springframework/data/gemfire/eventing/config/CacheWriterEventType.java new file mode 100644 index 000000000..eb7a0d753 --- /dev/null +++ b/spring-data-geode/src/main/java/org/springframework/data/gemfire/eventing/config/CacheWriterEventType.java @@ -0,0 +1,42 @@ +/* + * Copyright 2016-2019 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 + * + * https://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.eventing.config; + +/** + * An Enum that represents event types defined within {@link org.apache.geode.cache.CacheWriter}. The event types + * defined are CRUD operation orientated. These do not reflect the managements events that {@link org.apache.geode.cache.Region} have. + * These events are handled by the {@link RegionCacheWriterEventType}. + * + * Due to constraints imposed by the {@link org.apache.geode.cache.Region} that only one {@link org.apache.geode.cache.CacheWriter} + * can be added to a region, the event mask described here must not conflict with the mask described in {@link RegionCacheWriterEventType}. + * + * @author Udo Kohlmeyer + * @see org.apache.geode.cache.CacheWriter + * @since 2.4.0 + */ +public enum CacheWriterEventType { + ALL(0b00000111), + BEFORE_CREATE(0b00000001), + BEFORE_UPDATE(0b00000010), + BEFORE_DESTROY(0b00000100); + + CacheWriterEventType(int mask){ + this.mask = mask; + } + + int mask; +} diff --git a/spring-data-geode/src/main/java/org/springframework/data/gemfire/eventing/config/ComposableCacheWriterWrapper.java b/spring-data-geode/src/main/java/org/springframework/data/gemfire/eventing/config/ComposableCacheWriterWrapper.java new file mode 100644 index 000000000..56e857759 --- /dev/null +++ b/spring-data-geode/src/main/java/org/springframework/data/gemfire/eventing/config/ComposableCacheWriterWrapper.java @@ -0,0 +1,164 @@ +package org.springframework.data.gemfire.eventing.config; + +import java.lang.reflect.Method; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +import org.apache.geode.cache.CacheEvent; +import org.apache.geode.cache.CacheWriterException; +import org.apache.geode.cache.EntryEvent; +import org.apache.geode.cache.RegionEvent; +import org.apache.geode.cache.util.CacheWriterAdapter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A {@link ComposableCacheWriterWrapper} is a {@link org.apache.geode.cache.CacheWriter} that enables the ability to + * include multiple {@link PojoCacheWriterWrapper}s into a single {@link org.apache.geode.cache.CacheWriter}. + * {@link org.apache.geode.cache.Region}s don't allow for multiple {@link org.apache.geode.cache.CacheWriter}s to be + * registered against the same {@link org.apache.geode.cache.Region}. + * + * Given the callback event handling approach, using method annotations for {@link org.apache.geode.cache.CacheWriter} events of + * {@link RegionEvent} and {@link EntryEvent}, could possibly result into the creation of multiple {@link org.apache.geode.cache.CacheWriter} + * instances for the same region for different event types. + * + * @author Udo Kohlmeyer + * @see org.apache.geode.cache.CacheWriter + * @see CacheWriterAdapter + * @see PojoCacheWriterWrapper + * @since 2.4.0 + */ +public class ComposableCacheWriterWrapper> extends CacheWriterAdapter { + + private static final String BEFORE_CREATE = "beforeCreate"; + private static final String BEFORE_UPDATE = "beforeUpdate"; + private static final String BEFORE_DESTROY = "beforeDestroy"; + private static final String BEFORE_REGION_DESTROY = "beforeRegionDestroy"; + private static final String BEFORE_REGION_CLEAR = "beforeRegionClear"; + + private static Logger logger = LoggerFactory.getLogger(ComposableCacheWriterWrapper.class); + + protected Map> cacheWriterWrapperList = new HashMap<>(); + + /** + * Registers a {@link org.springframework.data.gemfire.config.annotation.AsCacheWriter} or + * {@link org.springframework.data.gemfire.config.annotation.AsRegionEventHandler} method, + * as a {@link PojoCacheWriterWrapper} for execution. + * + * In the event that the events are not {@link org.apache.geode.cache.CacheWriter} events, no event handler will + * be registered * + * + * @param method the method that needs to be executed in an event handling callback + * @param targetInvocationClass the target class on which the method was registered + * @param eventTypes the event types that the method needs to be registered for. + */ + public void addCacheWriter(Method method, Object targetInvocationClass, E[] eventTypes) { + int eventMask = createRegionEventTypeMask(eventTypes); + if (eventMask > 0) { + List regionEventCacheWriterList = cacheWriterWrapperList + .getOrDefault(eventMask, new LinkedList<>()); + + regionEventCacheWriterList.add(new PojoCacheWriterWrapper(method, targetInvocationClass)); + + cacheWriterWrapperList.put(eventMask, regionEventCacheWriterList); + } + } + + /** + * Takes either a {@link RegionCacheWriterEventType} or {@link CacheWriterEventType} as an input. + * These enums contain an int that represents a filtering mask that will be applied. In order to create only 1 + * {@link PojoCacheWriterWrapper} instance for each method, the masks need to be combined into a single value to + * represent all the events that the single event handling callback method needs cover. + * + * @param cacheWriterEventTypes an array of Enum EventTypes. Either {@link RegionCacheWriterEventType} or {@link CacheWriterEventType} + * @return an integer representing the combined masks of all registered event types + * @author Udo Kohlmeyer + * @since 2.4.0 + */ + public int createRegionEventTypeMask(E[] cacheWriterEventTypes) { + int mask = 0x0; + for (E cacheWriterEventType : cacheWriterEventTypes) { + if (cacheWriterEventType instanceof CacheWriterEventType) { + CacheWriterEventType writerEventType = (CacheWriterEventType) cacheWriterEventType; + mask |= writerEventType.mask; + } + else if (cacheWriterEventType instanceof RegionCacheWriterEventType) { + RegionCacheWriterEventType writerEventType = (RegionCacheWriterEventType) cacheWriterEventType; + mask |= writerEventType.mask; + } + } + return mask; + } + + @Override + public void beforeCreate(EntryEvent event) { + logDebug(BEFORE_CREATE); + + executeEventHandler(event, CacheWriterEventType.BEFORE_CREATE.mask); + } + + @Override + public void beforeUpdate(EntryEvent event) { + logDebug(BEFORE_UPDATE); + + executeEventHandler(event, CacheWriterEventType.BEFORE_UPDATE.mask); + } + + @Override + public void beforeDestroy(EntryEvent event) { + logDebug(BEFORE_DESTROY); + + executeEventHandler(event, CacheWriterEventType.BEFORE_DESTROY.mask); + } + + @Override + public void beforeRegionDestroy(RegionEvent event) throws CacheWriterException { + logDebug(BEFORE_REGION_DESTROY); + + executeEventHandler(event, RegionCacheWriterEventType.BEFORE_REGION_DESTROY.mask); + } + + @Override + public void beforeRegionClear(RegionEvent event) throws CacheWriterException { + logDebug(BEFORE_REGION_CLEAR); + + executeEventHandler(event, RegionCacheWriterEventType.BEFORE_REGION_CLEAR.mask); + } + + /** + * This method takes a {@link CacheEvent}, which will either be a {@link RegionEvent} or a {@link EntryEvent}, + * an eventType mask integer and tries to execute the event with registered {@link PojoCacheWriterWrapper}. + * + * Given that the registered {@link PojoCacheWriterWrapper} might be registered under a combined event mask, like + * {@link RegionCacheWriterEventType#ALL} or {@link CacheWriterEventType#ALL} each event received needs to be compared + * against each registered {@link PojoCacheWriterWrapper} entry to determine if the event needs to be executed. + * + * @param event either a {@link RegionEvent} or {@link EntryEvent} that needs to be actioned + * @param eventTypeInt a mask for the event that has just fired and needs to be handled + * @author Udo Kohlmeyer + * @see CacheEvent + * @see EntryEvent + * @see RegionEvent + * @since 2.4.0 + */ + private void executeEventHandler(CacheEvent event, int eventTypeInt) { + + for (Map.Entry> cacheWriterWrappers : cacheWriterWrapperList + .entrySet()) { + if ((cacheWriterWrappers.getKey() & eventTypeInt) == eventTypeInt) { + + for (PojoCacheWriterWrapper pojoCacheWriterWrapper : cacheWriterWrappers.getValue()) { + pojoCacheWriterWrapper.executeCacheWriter(event); + } + } + } + } + + private void logDebug(String eventType) { + if (logger.isDebugEnabled()) { + logger.debug("Invoking " + eventType); + } + } +} diff --git a/spring-data-geode/src/main/java/org/springframework/data/gemfire/eventing/config/PojoCacheListenerWrapper.java b/spring-data-geode/src/main/java/org/springframework/data/gemfire/eventing/config/PojoCacheListenerWrapper.java new file mode 100644 index 000000000..768400ff6 --- /dev/null +++ b/spring-data-geode/src/main/java/org/springframework/data/gemfire/eventing/config/PojoCacheListenerWrapper.java @@ -0,0 +1,106 @@ +/* + * Copyright 2016-2019 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 + * + * https://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.eventing.config; + +import java.lang.reflect.Method; + +import org.apache.geode.cache.EntryEvent; +import org.apache.geode.cache.util.CacheListenerAdapter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.util.ReflectionUtils; + +/** + * Invokes a given {@link Object POJO} {@link Method} as a GemFire/Geode {@link org.apache.geode.cache.CacheListener}. + * This proxy will process events triggered from CRUD operations against the {@link org.apache.geode.cache.Region} data. + * + * @author Udo Kohlmeyer + * @see org.apache.geode.cache.CacheListener + * @see org.apache.geode.cache.util.CacheListenerAdapter + * @see org.apache.geode.cache.Region + * @see ReflectionUtils + * @since 2.4.0 + */ +public class PojoCacheListenerWrapper extends CacheListenerAdapter { + + private static final String AFTER_CREATE = "afterCreate"; + private static final String AFTER_UPDATE = "afterUpdate"; + private static final String AFTER_DESTROY = "afterDestroy"; + private static final String AFTER_INVALIDATE = "afterInvalidate"; + + private static transient Logger logger = LoggerFactory.getLogger(PojoCacheListenerWrapper.class); + private final Method method; + private final Object targetInvocationObject; + private final int eventTypeMask; + + public PojoCacheListenerWrapper(Method method, Object targetInvocationClass, CacheListenerEventType[] eventTypes) { + this.method = method; + this.targetInvocationObject = targetInvocationClass; + this.eventTypeMask = createEventTypeMask(eventTypes); + } + + private static int createEventTypeMask(CacheListenerEventType[] eventTypes) { + int mask = 0x0; + for (CacheListenerEventType eventType : eventTypes) { + mask |= eventType.mask; + } + return mask; + } + + @Override + public void afterCreate(EntryEvent event) { + logDebug(AFTER_CREATE); + + executeEventHandler(event, CacheListenerEventType.AFTER_CREATE); + } + + @Override + public void afterUpdate(EntryEvent event) { + logDebug(AFTER_UPDATE); + + executeEventHandler(event, CacheListenerEventType.AFTER_UPDATE); + } + + @Override + public void afterDestroy(EntryEvent event) { + logDebug(AFTER_DESTROY); + + executeEventHandler(event, CacheListenerEventType.AFTER_DESTROY); + } + + @Override + public void afterInvalidate(EntryEvent event) { + logDebug(AFTER_INVALIDATE); + + executeEventHandler(event, CacheListenerEventType.AFTER_INVALIDATE); + } + + private void executeEventHandler(EntryEvent event, CacheListenerEventType eventType) { + + if ((eventTypeMask & eventType.mask) == eventType.mask) { + + ReflectionUtils.invokeMethod(this.method, this.targetInvocationObject, event); + + } + } + + private void logDebug(String eventType) { + if (logger.isDebugEnabled()) { + logger.debug("Invoking " + eventType); + } + } +} diff --git a/spring-data-geode/src/main/java/org/springframework/data/gemfire/eventing/config/PojoCacheWriterWrapper.java b/spring-data-geode/src/main/java/org/springframework/data/gemfire/eventing/config/PojoCacheWriterWrapper.java new file mode 100644 index 000000000..a760a3f77 --- /dev/null +++ b/spring-data-geode/src/main/java/org/springframework/data/gemfire/eventing/config/PojoCacheWriterWrapper.java @@ -0,0 +1,38 @@ +package org.springframework.data.gemfire.eventing.config; + +import java.lang.reflect.Method; + +import org.apache.geode.cache.CacheEvent; +import org.springframework.util.ReflectionUtils; + +/** + * A simple wrapper method that wraps a {@link Method} and its target instance for simple usage in the {@link ComposableCacheWriterWrapper} + * for method execution on a {@link org.apache.geode.cache.Region}'s {@link org.apache.geode.cache.CacheWriter}. + * + * @author Udo Kohlmeyer + * @see org.apache.geode.cache.Region + * @see org.apache.geode.cache.CacheWriter + * @see ComposableCacheWriterWrapper + * @see ReflectionUtils + * @since 2.4.0 + */ +public class PojoCacheWriterWrapper { + + private final Method method; + private final Object targetInvocationObject; + + public PojoCacheWriterWrapper(Method method, Object targetInvocationClass) { + this.method = method; + this.targetInvocationObject = targetInvocationClass; + } + + /** + * Using Reflection the stored {@link Method} and target invocation instance is used to reflectively run the method. + * + * @param event the {@link org.apache.geode.cache.RegionEvent} or {@link org.apache.geode.cache.EntryEvent} that is + * required for the method to execute. + */ + public void executeCacheWriter(CacheEvent event) { + ReflectionUtils.invokeMethod(method, targetInvocationObject, event); + } +} diff --git a/spring-data-geode/src/main/java/org/springframework/data/gemfire/eventing/config/PojoRegionEventCacheListenerWrapper.java b/spring-data-geode/src/main/java/org/springframework/data/gemfire/eventing/config/PojoRegionEventCacheListenerWrapper.java new file mode 100644 index 000000000..61af8f224 --- /dev/null +++ b/spring-data-geode/src/main/java/org/springframework/data/gemfire/eventing/config/PojoRegionEventCacheListenerWrapper.java @@ -0,0 +1,106 @@ +/* + * Copyright 2016-2019 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 + * + * https://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.eventing.config; + +import java.lang.reflect.Method; + +import org.apache.geode.cache.RegionEvent; +import org.apache.geode.cache.util.CacheListenerAdapter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.util.ReflectionUtils; + +/** + * Invokes a given {@link Object POJO} {@link Method} as a GemFire/Geode {@link org.apache.geode.cache.CacheListener}. + * This proxy will specifically handle Region type events. + * + * @author Udo Kohlmeyer + * @see org.apache.geode.cache.CacheListener + * @since 2.4.0 + */ +public class PojoRegionEventCacheListenerWrapper extends CacheListenerAdapter { + + private static final String AFTER_REGION_DESTROY = "afterRegionDestroy"; + private static final String AFTER_REGION_CREATE = "afterRegionCreate"; + private static final String AFTER_REGION_INVALIDATE = "afterRegionInvalidate"; + private static final String AFTER_REGION_CLEAR = "afterRegionClear"; + private static final String AFTER_REGION_LIVE = "afterRegionLive"; + + private static transient Logger logger = LoggerFactory.getLogger(PojoRegionEventCacheListenerWrapper.class); + private final Method method; + private final Object targetInvocationObject; + private final int eventTypeMask; + + public PojoRegionEventCacheListenerWrapper(Method method, Object targetInvocationClass, RegionCacheListenerEventType[] regionEventTypes) { + this.method = method; + this.targetInvocationObject = targetInvocationClass; + this.eventTypeMask = createRegionEventTypeMask(regionEventTypes); + } + + private static int createRegionEventTypeMask(RegionCacheListenerEventType[] regionEventTypes) { + int mask = 0x0; + for (RegionCacheListenerEventType eventType : regionEventTypes) { + mask |= eventType.mask; + } + return mask; + } + + @Override public void afterRegionDestroy(RegionEvent event) { + logDebug(AFTER_REGION_DESTROY); + + executeEventHandler(event, RegionCacheListenerEventType.AFTER_REGION_DESTROY); + } + + @Override public void afterRegionCreate(RegionEvent event) { + logDebug(AFTER_REGION_CREATE); + + executeEventHandler(event, RegionCacheListenerEventType.AFTER_REGION_CREATE); + } + + @Override public void afterRegionInvalidate(RegionEvent event) { + logDebug(AFTER_REGION_INVALIDATE); + + executeEventHandler(event, RegionCacheListenerEventType.AFTER_REGION_INVALIDATE); + } + + @Override public void afterRegionClear(RegionEvent event) { + logDebug(AFTER_REGION_CLEAR); + + executeEventHandler(event, RegionCacheListenerEventType.AFTER_REGION_CLEAR); + } + + @Override public void afterRegionLive(RegionEvent event) { + logDebug(AFTER_REGION_LIVE); + + executeEventHandler(event, RegionCacheListenerEventType.AFTER_REGION_LIVE); + } + + private void executeEventHandler(RegionEvent regionEvent, RegionCacheListenerEventType eventType) { + + if ((eventTypeMask & eventType.mask) == eventType.mask) { + + ReflectionUtils.invokeMethod(this.method, this.targetInvocationObject, regionEvent); + + } + } + + private void logDebug(String eventType) { + if (logger.isDebugEnabled()) { + logger.debug("Invoking " + eventType); + } + } +} diff --git a/spring-data-geode/src/main/java/org/springframework/data/gemfire/eventing/config/RegionCacheListenerEventType.java b/spring-data-geode/src/main/java/org/springframework/data/gemfire/eventing/config/RegionCacheListenerEventType.java new file mode 100644 index 000000000..c107b584b --- /dev/null +++ b/spring-data-geode/src/main/java/org/springframework/data/gemfire/eventing/config/RegionCacheListenerEventType.java @@ -0,0 +1,40 @@ +/* + * Copyright 2016-2019 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 + * + * https://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.eventing.config; + +/** + * An Enum that represents region event types defined within {@link org.apache.geode.cache.CacheListener}. The region event + * types defined events triggered from Region management operations. These do not reflect the events + * that {@link org.apache.geode.cache.Region} have. + * + * @author Udo Kohlmeyer + * @since 2.4.0 + */ +public enum RegionCacheListenerEventType { + ALL(0b0111111), + AFTER_REGION_CREATE(0b00000001), + AFTER_REGION_CLEAR(0b00000010), + AFTER_REGION_DESTROY(0b00000100), + AFTER_REGION_INVALIDATE(0b00001000), + AFTER_REGION_LIVE(0b00010000); + + RegionCacheListenerEventType(int mask){ + this.mask = mask; + } + + int mask; +} diff --git a/spring-data-geode/src/main/java/org/springframework/data/gemfire/eventing/config/RegionCacheWriterEventType.java b/spring-data-geode/src/main/java/org/springframework/data/gemfire/eventing/config/RegionCacheWriterEventType.java new file mode 100644 index 000000000..6294a17d0 --- /dev/null +++ b/spring-data-geode/src/main/java/org/springframework/data/gemfire/eventing/config/RegionCacheWriterEventType.java @@ -0,0 +1,41 @@ +/* + * Copyright 2016-2019 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 + * + * https://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.eventing.config; + +/** + * An Enum that represents region event types defined within {@link org.apache.geode.cache.CacheWriter}. The region event + * types defined events triggered from Region management operations. These do not reflect CRUD operational events + * that {@link org.apache.geode.cache.Region} have. These events are represented by the enum {@link CacheWriterEventType} + * + * * Due to constraints imposed by the {@link org.apache.geode.cache.Region} that only one {@link org.apache.geode.cache.CacheWriter} + * * can be added to a region, the event mask described here must not conflict with the mask described in {@link CacheWriterEventType}. + * + * @author Udo Kohlmeyer + * @see org.apache.geode.cache.CacheWriter + * @since 2.4.0 + */ +public enum RegionCacheWriterEventType { + ALL(0b0011000), + BEFORE_REGION_CLEAR(0b0001000), + BEFORE_REGION_DESTROY(0b0010000); + + RegionCacheWriterEventType(int mask) { + this.mask = mask; + } + + int mask; +} diff --git a/spring-data-geode/src/test/java/org/springframework/data/gemfire/config/annotation/AsCacheListenerCacheServerConfigurationTests.java b/spring-data-geode/src/test/java/org/springframework/data/gemfire/config/annotation/AsCacheListenerCacheServerConfigurationTests.java new file mode 100644 index 000000000..e45f764a4 --- /dev/null +++ b/spring-data-geode/src/test/java/org/springframework/data/gemfire/config/annotation/AsCacheListenerCacheServerConfigurationTests.java @@ -0,0 +1,202 @@ +/* + * Copyright 2016-2019 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 + * + * https://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 org.apache.geode.cache.EntryEvent; +import org.apache.geode.cache.GemFireCache; +import org.apache.geode.cache.RegionEvent; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.gemfire.ReplicatedRegionFactoryBean; +import org.springframework.data.gemfire.eventing.config.CacheListenerEventType; + +/** + * Tests for {@link org.springframework.data.gemfire.config.annotation.AsCacheListener} configured for a + * {@link org.apache.geode.cache.server.CacheServer} + * + * @author Udo Kohlmeyer + * @see org.junit.Test + * @see org.mockito.Mockito + * @see org.apache.geode.cache.server.CacheServer + * @see org.springframework.context.annotation.Configuration + * @see org.springframework.test.context.ContextConfiguration + * @see org.springframework.test.context.junit4.SpringRunner + * @since 2.3.0 + */ +public class AsCacheListenerCacheServerConfigurationTests extends AsCacheListenerConfigurationTests { + + protected static ReplicatedRegionFactoryBean createRegionFactoryBean(GemFireCache cache, + String regionName) { + ReplicatedRegionFactoryBean replicateRegion = new ReplicatedRegionFactoryBean<>(); + replicateRegion.setName(regionName); + replicateRegion.setCache(cache); + return replicateRegion; + } + + @Override + protected Class getCacheListenerWithIncorrectRegionEventParameterConfiguration() { + return TestConfigurationWithIncorrectRegionEventParameter.class; + } + + @Override + protected Class getCacheListenerAnnotationSingleDefaultRegionsConfiguration() { + return TestConfigurationWithSimpleCacheListener.class; + } + + @Override + protected Class getCacheListenerAnnotationWithInvalidRegion() { + return TestConfigurationWithInvalidRegion.class; + } + + @Override + protected Class getCacheListenerAnnotationMultipleRegionsDefault() { + return TestConfigurationWith2RegionsAnd2CacheListenersDefaulted.class; + } + + @Override + protected Class getCacheListenerAnnotationSingleRegionAllEvents() { + return TestConfigurationWithSimpleCacheListenerAllEvents.class; + } + + @Override + protected Class getCacheListenerAnnotationAgainst2NamedRegions() { + return TestConfigurationWithSimpleCacheListenerWith2Regions.class; + } + + @Configuration + @CacheServerApplication + @EnableEventProcessing + public static class TestConfigurationWithIncorrectRegionEventParameter { + + @Bean("TestRegion1") + ReplicatedRegionFactoryBean getTestRegion(GemFireCache cache) { + return createRegionFactoryBean(cache, "TestRegion1"); + } + + @AsCacheListener(eventTypes = CacheListenerEventType.AFTER_CREATE) + public void afterCreateListener(RegionEvent event) { + } + } + + @Configuration + @CacheServerApplication + @EnableEventProcessing + public static class TestConfigurationWithSimpleCacheListener { + + @Bean("TestRegion1") + ReplicatedRegionFactoryBean getTestRegion(GemFireCache cache) { + return createRegionFactoryBean(cache, "TestRegion1"); + } + + @AsCacheListener(eventTypes = CacheListenerEventType.AFTER_CREATE) + public void afterCreateListener(EntryEvent event) { + recordEvent(event); + } + + @AsCacheListener(eventTypes = CacheListenerEventType.AFTER_UPDATE) + public void afterUpdateListener(EntryEvent event) { + recordEvent(event); + + } + } + + @Configuration + @CacheServerApplication + @EnableEventProcessing + public static class TestConfigurationWithInvalidRegion { + + @Bean("TestRegion1") + ReplicatedRegionFactoryBean getTestRegion(GemFireCache cache) { + return createRegionFactoryBean(cache, "TestRegion1"); + } + + @AsCacheListener(eventTypes = CacheListenerEventType.AFTER_CREATE, regions = "TestRegion2") + public void afterCreateListener(EntryEvent event) { + recordEvent(event); + } + + } + + @Configuration + @CacheServerApplication + @EnableEventProcessing + public static class TestConfigurationWithSimpleCacheListenerAllEvents { + + @Bean("TestRegion1") + ReplicatedRegionFactoryBean getTestRegion(GemFireCache cache) { + return createRegionFactoryBean(cache, "TestRegion1"); + } + + @AsCacheListener(eventTypes = CacheListenerEventType.ALL) + public void afterCreateListener(EntryEvent event) { + recordEvent(event); + } + } + + @Configuration + @CacheServerApplication + @EnableEventProcessing + public static class TestConfigurationWithSimpleCacheListenerWith2Regions { + + @Bean("TestRegion1") + ReplicatedRegionFactoryBean getTestRegion1(GemFireCache cache) { + return createRegionFactoryBean(cache, "TestRegion1"); + } + + @Bean("TestRegion2") + ReplicatedRegionFactoryBean getTestRegion2(GemFireCache cache) { + return createRegionFactoryBean(cache, "TestRegion2"); + } + + @AsCacheListener(eventTypes = CacheListenerEventType.AFTER_CREATE, regions = "TestRegion1") + public void afterCreateListener(EntryEvent event) { + recordEvent(event); + } + + @AsCacheListener(eventTypes = CacheListenerEventType.AFTER_CREATE, regions = "TestRegion2") + public void afterUpdateListener(EntryEvent event) { + recordEvent(event); + } + } + + @Configuration + @CacheServerApplication + @EnableEventProcessing + public static class TestConfigurationWith2RegionsAnd2CacheListenersDefaulted { + + @Bean("TestRegion1") + ReplicatedRegionFactoryBean getTestRegion1(GemFireCache cache) { + return createRegionFactoryBean(cache, "TestRegion1"); + } + + @Bean("TestRegion2") + ReplicatedRegionFactoryBean getTestRegion2(GemFireCache cache) { + return createRegionFactoryBean(cache, "TestRegion2"); + } + + @AsCacheListener(eventTypes = CacheListenerEventType.ALL) + public void afterCreateListener1(EntryEvent event) { + recordEvent(event); + } + + @AsCacheListener(eventTypes = CacheListenerEventType.ALL) + public void afterCreateListener2(EntryEvent event) { + recordEvent(event); + } + } +} diff --git a/spring-data-geode/src/test/java/org/springframework/data/gemfire/config/annotation/AsCacheListenerClientCacheConfigurationTests.java b/spring-data-geode/src/test/java/org/springframework/data/gemfire/config/annotation/AsCacheListenerClientCacheConfigurationTests.java new file mode 100644 index 000000000..e5a22c3bf --- /dev/null +++ b/spring-data-geode/src/test/java/org/springframework/data/gemfire/config/annotation/AsCacheListenerClientCacheConfigurationTests.java @@ -0,0 +1,196 @@ +/* + * Copyright 2019 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 + * + * https://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 org.apache.geode.cache.EntryEvent; +import org.apache.geode.cache.GemFireCache; +import org.apache.geode.cache.RegionEvent; +import org.apache.geode.cache.client.ClientRegionShortcut; +import org.junit.Test; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.gemfire.client.ClientRegionFactoryBean; +import org.springframework.data.gemfire.eventing.config.CacheListenerEventType; + +/** + * Tests for {@link AsCacheListener} configured for a {@link org.apache.geode.cache.client.ClientCache} + * + * @author Udo Kohlmeyer + * @see Test + * @see org.mockito.Mockito + * @see org.apache.geode.cache.client.ClientCache + * @see Configuration + * @see org.springframework.test.context.ContextConfiguration + * @see org.springframework.test.context.junit4.SpringRunner + * @since 2.3.0 + */ +public class AsCacheListenerClientCacheConfigurationTests extends AsCacheListenerConfigurationTests { + + protected static ClientRegionFactoryBean createRegionFactoryBean(GemFireCache cache, + String regionName) { + ClientRegionFactoryBean clientRegion = new ClientRegionFactoryBean<>(); + clientRegion.setName(regionName); + clientRegion.setCache(cache); + clientRegion.setShortcut(ClientRegionShortcut.LOCAL); + return clientRegion; + } + + @Override + protected Class getCacheListenerWithIncorrectRegionEventParameterConfiguration() { + return TestConfigurationWithIncorrectEventParameter.class; + } + + protected Class getCacheListenerAnnotationSingleDefaultRegionsConfiguration() { + return TestConfigurationWithSimpleCacheListener.class; + } + + protected Class getCacheListenerAnnotationWithInvalidRegion() { + return TestConfigurationWithInvalidRegion.class; + } + + protected Class getCacheListenerAnnotationMultipleRegionsDefault() { + return TestConfigurationWith2RegionsAnd2CacheListenersDefaulted.class; + } + + protected Class getCacheListenerAnnotationSingleRegionAllEvents() { + return TestConfigurationWithSimpleCacheListenerAllEvents.class; + } + + protected Class getCacheListenerAnnotationAgainst2NamedRegions() { + return TestConfigurationWithSimpleCacheListenerWith2Regions.class; + } + + @Configuration + @ClientCacheApplication + @EnableEventProcessing + public static class TestConfigurationWithIncorrectEventParameter { + + @Bean("TestRegion") + ClientRegionFactoryBean getTestRegion(GemFireCache cache) { + return createRegionFactoryBean(cache, "TestRegion"); + } + + @AsCacheListener(eventTypes = CacheListenerEventType.AFTER_CREATE) + public void afterCreateListener(RegionEvent event) { + } + } + + @Configuration + @ClientCacheApplication + @EnableEventProcessing + public static class TestConfigurationWithSimpleCacheListener { + + @Bean("TestRegion1") + ClientRegionFactoryBean getTestRegion(GemFireCache cache) { + return createRegionFactoryBean(cache, "TestRegion1"); + } + + @AsCacheListener(eventTypes = CacheListenerEventType.AFTER_CREATE) + public void afterCreateListener(EntryEvent event) { + recordEvent(event); + } + + @AsCacheListener(eventTypes = CacheListenerEventType.AFTER_UPDATE) + public void afterUpdateListener(EntryEvent event) { + recordEvent(event); + } + } + + @Configuration + @ClientCacheApplication + @EnableEventProcessing + public static class TestConfigurationWithInvalidRegion { + + @Bean("TestRegion") + ClientRegionFactoryBean getTestRegion(GemFireCache cache) { + return createRegionFactoryBean(cache, "TestRegion"); + } + + @AsCacheListener(eventTypes = CacheListenerEventType.AFTER_CREATE, regions = "TestRegion2") + public void afterCreateListener(EntryEvent event) { + recordEvent(event); + } + } + + @Configuration + @ClientCacheApplication + @EnableEventProcessing + public static class TestConfigurationWithSimpleCacheListenerAllEvents { + + @Bean("TestRegion1") + ClientRegionFactoryBean getTestRegion(GemFireCache cache) { + return createRegionFactoryBean(cache, "TestRegion1"); + } + + @AsCacheListener(eventTypes = CacheListenerEventType.ALL) + public void afterCreateListener(EntryEvent event) { + recordEvent(event); + } + } + + @Configuration + @ClientCacheApplication + @EnableEventProcessing + public static class TestConfigurationWithSimpleCacheListenerWith2Regions { + + @Bean("TestRegion1") + ClientRegionFactoryBean getTestRegion1(GemFireCache cache) { + return createRegionFactoryBean(cache, "TestRegion1"); + } + + @Bean("TestRegion2") + ClientRegionFactoryBean getTestRegion2(GemFireCache cache) { + return createRegionFactoryBean(cache, "TestRegion2"); + } + + @AsCacheListener(eventTypes = CacheListenerEventType.AFTER_CREATE, regions = "TestRegion1") + public void afterCreateListener(EntryEvent event) { + recordEvent(event); + } + + @AsCacheListener(eventTypes = CacheListenerEventType.AFTER_CREATE, regions = "TestRegion2") + public void afterUpdateListener(EntryEvent event) { + recordEvent(event); + } + } + + @Configuration + @ClientCacheApplication + @EnableEventProcessing + public static class TestConfigurationWith2RegionsAnd2CacheListenersDefaulted { + + @Bean("TestRegion1") + ClientRegionFactoryBean getTestRegion1(GemFireCache cache) { + return createRegionFactoryBean(cache, "TestRegion1"); + } + + @Bean("TestRegion2") + ClientRegionFactoryBean getTestRegion2(GemFireCache cache) { + return createRegionFactoryBean(cache, "TestRegion2"); + } + + @AsCacheListener(eventTypes = CacheListenerEventType.ALL) + public void afterCreateListener1(EntryEvent event) { + recordEvent(event); + } + + @AsCacheListener(eventTypes = CacheListenerEventType.ALL) + public void afterCreateListener2(EntryEvent event) { + recordEvent(event); + } + } +} diff --git a/spring-data-geode/src/test/java/org/springframework/data/gemfire/config/annotation/AsCacheListenerConfigurationTests.java b/spring-data-geode/src/test/java/org/springframework/data/gemfire/config/annotation/AsCacheListenerConfigurationTests.java new file mode 100644 index 000000000..573df8452 --- /dev/null +++ b/spring-data-geode/src/test/java/org/springframework/data/gemfire/config/annotation/AsCacheListenerConfigurationTests.java @@ -0,0 +1,170 @@ +package org.springframework.data.gemfire.config.annotation; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +import org.apache.geode.cache.EntryEvent; +import org.apache.geode.cache.Operation; +import org.apache.geode.cache.Region; +import org.assertj.core.api.Assertions; +import org.junit.After; +import org.junit.Test; +import org.springframework.beans.factory.BeanCreationException; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; + +public abstract class AsCacheListenerConfigurationTests { + + protected static final List events = new ArrayList<>(); + protected ConfigurableApplicationContext applicationContext; + + @After public void shutdown() { + Optional.ofNullable(this.applicationContext).ifPresent(ConfigurableApplicationContext::close); + events.clear(); + } + + protected static void recordEvent(EntryEvent event) { + events.add( + new Object[] { + event.getRegion().getName(), + event.getOperation(), + event.getNewValue(), + event.getOldValue() + }); + } + + @Test(expected = BeanCreationException.class) + public void cacheListenerWithIncorrectRegionEventParameter() { + this.applicationContext = newApplicationContext(getCacheListenerWithIncorrectRegionEventParameterConfiguration()); + } + + @Test(expected = BeanCreationException.class) + public void cacheListenerWithIncorrectEntryEventParameter() { + this.applicationContext = newApplicationContext(getCacheListenerWithIncorrectRegionEventParameterConfiguration()); + } + + protected abstract Class getCacheListenerWithIncorrectRegionEventParameterConfiguration(); + + @Test public void cacheListenerAnnotationSingleDefaultRegions() { + + this.applicationContext = newApplicationContext(getCacheListenerAnnotationSingleDefaultRegionsConfiguration()); + + Region testRegion = applicationContext.getBean("TestRegion1", Region.class); + + testRegion.put("1", "1"); + testRegion.put("1", "2"); + + Assertions.assertThat(events.size()).isEqualTo(2); + + assertCreateEvent(0, "1", null, "TestRegion1"); + assertUpdateEvent(1, "2", "1", "TestRegion1"); + } + + abstract protected Class getCacheListenerAnnotationSingleDefaultRegionsConfiguration(); + + @Test(expected = BeanCreationException.class) + public void cacheListenerAnnotationWithInvalidRegion() { + + this.applicationContext = newApplicationContext(getCacheListenerAnnotationWithInvalidRegion()); + } + + abstract protected Class getCacheListenerAnnotationWithInvalidRegion(); + + @Test public void cacheListenerAnnotationMultipleRegionsDefault() { + + this.applicationContext = newApplicationContext(getCacheListenerAnnotationMultipleRegionsDefault()); + + Region testRegion1 = applicationContext.getBean("TestRegion1", Region.class); + Region testRegion2 = applicationContext.getBean("TestRegion2", Region.class); + + testRegion1.put("1", "1"); + testRegion2.put("1", "2"); + testRegion2.put("1", "3"); + + Assertions.assertThat(events.size()).isEqualTo(6); + + assertCreateEvent(0, "1", null, "TestRegion1", "TestRegion2"); + assertCreateEvent(1, "1", null, "TestRegion1", "TestRegion2"); + assertCreateEvent(2, "2", null, "TestRegion1", "TestRegion2"); + assertCreateEvent(3, "2", null, "TestRegion1", "TestRegion2"); + assertUpdateEvent(4, "3", "2", "TestRegion2"); + assertUpdateEvent(5, "3", "2", "TestRegion2"); + } + + abstract protected Class getCacheListenerAnnotationMultipleRegionsDefault(); + + @Test public void cacheListenerAnnotationSingleRegionAllEvents() { + + this.applicationContext = newApplicationContext(getCacheListenerAnnotationSingleRegionAllEvents()); + + Region testRegion = applicationContext.getBean("TestRegion1", Region.class); + + testRegion.put("1", "1"); + testRegion.put("1", "2"); + testRegion.invalidate("1"); + testRegion.destroy("1"); + + Assertions.assertThat(events.size()).isEqualTo(4); + + assertCreateEvent(0, "1", null, "TestRegion1"); + assertUpdateEvent(1, "2", "1", "TestRegion1"); + assertInvalidateEvent(2, null, "2", "TestRegion1"); + assertDestroyEvent(3, null, null, "TestRegion1"); + } + + abstract protected Class getCacheListenerAnnotationSingleRegionAllEvents(); + + @Test public void cacheListenerAnnotationAgainst2NamedRegions() { + + this.applicationContext = newApplicationContext(getCacheListenerAnnotationAgainst2NamedRegions()); + + Region testRegion1 = applicationContext.getBean("TestRegion1", Region.class); + Region testRegion2 = applicationContext.getBean("TestRegion2", Region.class); + + testRegion1.put("1", "1"); + testRegion2.put("1", "2"); + + Assertions.assertThat(events.size()).isEqualTo(2); + + assertCreateEvent(0, "1", null, "TestRegion1"); + assertCreateEvent(1, "2", null, "TestRegion2"); + } + + abstract protected Class getCacheListenerAnnotationAgainst2NamedRegions(); + + private ConfigurableApplicationContext newApplicationContext(Class... annotatedClasses) { + + ConfigurableApplicationContext applicationContext = new AnnotationConfigApplicationContext(annotatedClasses); + + applicationContext.registerShutdownHook(); + + return applicationContext; + } + + private void assetCommonEventProperties(int index, String newValue, String oldValue, String[] regions) { + Assertions.assertThat(events.get(index)[0]).isIn(regions); //regionName + Assertions.assertThat(events.get(index)[2]).isEqualTo(newValue); //newValue + Assertions.assertThat(events.get(index)[3]).isEqualTo(oldValue); //oldValue + } + + private void assertCreateEvent(int index, String newValue, String oldValue, String... regions) { + assetCommonEventProperties(index, newValue, oldValue, regions); + Assertions.assertThat(((Operation) events.get(index)[1]).isCreate()).isEqualTo(true); //isCreate + } + + private void assertUpdateEvent(int index, String newValue, String oldValue, String... regions) { + assetCommonEventProperties(index, newValue, oldValue, regions); + Assertions.assertThat(((Operation) events.get(index)[1]).isUpdate()).isEqualTo(true); //isUpdate + } + + private void assertInvalidateEvent(int index, String newValue, String oldValue, String... regions) { + assetCommonEventProperties(index, newValue, oldValue, regions); + Assertions.assertThat(((Operation) events.get(index)[1]).isInvalidate()).isEqualTo(true); //isInvalidate + } + + private void assertDestroyEvent(int index, String newValue, String oldValue, String... regions) { + assetCommonEventProperties(index, newValue, oldValue, regions); + Assertions.assertThat(((Operation) events.get(index)[1]).isDestroy()).isEqualTo(true); //isDestroy + } +} diff --git a/spring-data-geode/src/test/java/org/springframework/data/gemfire/config/annotation/AsCacheWriterCacheServerConfigurationTests.java b/spring-data-geode/src/test/java/org/springframework/data/gemfire/config/annotation/AsCacheWriterCacheServerConfigurationTests.java new file mode 100644 index 000000000..32ab547df --- /dev/null +++ b/spring-data-geode/src/test/java/org/springframework/data/gemfire/config/annotation/AsCacheWriterCacheServerConfigurationTests.java @@ -0,0 +1,229 @@ +/* + * Copyright 2016-2019 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 + * + * https://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 org.apache.geode.cache.EntryEvent; +import org.apache.geode.cache.GemFireCache; +import org.apache.geode.cache.RegionEvent; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.gemfire.ReplicatedRegionFactoryBean; +import org.springframework.data.gemfire.eventing.config.CacheWriterEventType; +import org.springframework.data.gemfire.eventing.config.RegionCacheWriterEventType; + +/** + * Tests for {@link AsCacheWriter} configured for a + * {@link org.apache.geode.cache.server.CacheServer} + * + * @author Udo Kohlmeyer + * @see org.junit.Test + * @see org.mockito.Mockito + * @see org.apache.geode.cache.server.CacheServer + * @see Configuration + * @see org.springframework.test.context.ContextConfiguration + * @see org.springframework.test.context.junit4.SpringRunner + * @since 2.3.0 + */ +public class AsCacheWriterCacheServerConfigurationTests extends AsCacheWriterConfigurationTests { + + protected static ReplicatedRegionFactoryBean createRegionFactoryBean(GemFireCache cache, + String regionName) { + ReplicatedRegionFactoryBean replicateRegion = new ReplicatedRegionFactoryBean<>(); + replicateRegion.setName(regionName); + replicateRegion.setCache(cache); + return replicateRegion; + } + + @Override + protected Class getCacheWriterWithIncorrectRegionEventParameterConfiguration() { + return TestConfigurationWithIncorrectRegionEventParameter.class; + } + + @Override + protected Class getCacheWriterAnnotationSingleDefaultRegionsConfiguration() { + return TestConfigurationWithSimpleCacheWriter.class; + } + + @Override + protected Class getCacheWriterAnnotationWithInvalidRegion() { + return TestConfigurationWithInvalidRegion.class; + } + + @Override + protected Class getCacheWriterAnnotationMultipleRegionsDefault() { + return TestConfigurationWith2RegionsAnd2CacheWritersDefaulted.class; + } + + @Override + protected Class getCacheWriterAnnotationSingleRegionAllEvents() { + return TestConfigurationWithSimpleCacheWriterAllEvents.class; + } + + @Override + protected Class getCacheWriterAnnotationAgainst2NamedRegions() { + return TestConfigurationWithSimpleCacheWriterWith2Regions.class; + } + + @Override + protected Class getCacheWriterAnnotationWithRegionEventAndCacheWriter() { + return TestConfigurationWithRegionEventAndCacheWriter.class; + } + + @Configuration + @CacheServerApplication + @EnableEventProcessing + public static class TestConfigurationWithIncorrectRegionEventParameter { + + @Bean("TestRegion1") + ReplicatedRegionFactoryBean getTestRegion(GemFireCache cache) { + return createRegionFactoryBean(cache, "TestRegion1"); + } + + @AsCacheWriter(eventTypes = CacheWriterEventType.BEFORE_CREATE) + public void beforeCreateWriter(RegionEvent event) { + } + } + + @Configuration + @CacheServerApplication + @EnableEventProcessing + public static class TestConfigurationWithSimpleCacheWriter { + + @Bean("TestRegion1") + ReplicatedRegionFactoryBean getTestRegion(GemFireCache cache) { + return createRegionFactoryBean(cache, "TestRegion1"); + } + + @AsCacheWriter(eventTypes = CacheWriterEventType.BEFORE_CREATE) + public void beforeCreateWriter(EntryEvent event) { + recordEvent(event); + } + + @AsCacheWriter(eventTypes = CacheWriterEventType.BEFORE_UPDATE) + public void beforeUpdateWriter(EntryEvent event) { + recordEvent(event); + + } + } + + @Configuration + @CacheServerApplication + @EnableEventProcessing + public static class TestConfigurationWithInvalidRegion { + + @Bean("TestRegion1") + ReplicatedRegionFactoryBean getTestRegion(GemFireCache cache) { + return createRegionFactoryBean(cache, "TestRegion1"); + } + + @AsCacheWriter(eventTypes = CacheWriterEventType.BEFORE_CREATE, regions = "TestRegion2") + public void beforeCreateWriter(EntryEvent event) { + recordEvent(event); + } + + } + + @Configuration + @CacheServerApplication + @EnableEventProcessing + public static class TestConfigurationWithSimpleCacheWriterAllEvents { + + @Bean("TestRegion1") + ReplicatedRegionFactoryBean getTestRegion(GemFireCache cache) { + return createRegionFactoryBean(cache, "TestRegion1"); + } + + @AsCacheWriter(eventTypes = CacheWriterEventType.ALL) + public void beforeCreateWriter(EntryEvent event) { + recordEvent(event); + } + } + + @Configuration + @CacheServerApplication + @EnableEventProcessing + public static class TestConfigurationWithSimpleCacheWriterWith2Regions { + + @Bean("TestRegion1") + ReplicatedRegionFactoryBean getTestRegion1(GemFireCache cache) { + return createRegionFactoryBean(cache, "TestRegion1"); + } + + @Bean("TestRegion2") + ReplicatedRegionFactoryBean getTestRegion2(GemFireCache cache) { + return createRegionFactoryBean(cache, "TestRegion2"); + } + + @AsCacheWriter(eventTypes = CacheWriterEventType.BEFORE_CREATE, regions = "TestRegion1") + public void beforeCreateWriter(EntryEvent event) { + recordEvent(event); + } + + @AsCacheWriter(eventTypes = CacheWriterEventType.BEFORE_CREATE, regions = "TestRegion2") + public void beforeUpdateWriter(EntryEvent event) { + recordEvent(event); + } + } + + @Configuration + @CacheServerApplication + @EnableEventProcessing + public static class TestConfigurationWith2RegionsAnd2CacheWritersDefaulted { + + @Bean("TestRegion1") + ReplicatedRegionFactoryBean getTestRegion1(GemFireCache cache) { + return createRegionFactoryBean(cache, "TestRegion1"); + } + + @Bean("TestRegion2") + ReplicatedRegionFactoryBean getTestRegion2(GemFireCache cache) { + return createRegionFactoryBean(cache, "TestRegion2"); + } + + @AsCacheWriter(eventTypes = CacheWriterEventType.ALL) + public void beforeCreateWriter1(EntryEvent event) { + recordEvent(event); + } + + @AsCacheWriter(eventTypes = CacheWriterEventType.ALL) + public void beforeCreateWriter2(EntryEvent event) { + recordEvent(event); + } + } + + @Configuration + @CacheServerApplication + @EnableEventProcessing + public static class TestConfigurationWithRegionEventAndCacheWriter { + + @Bean("TestRegion1") + ReplicatedRegionFactoryBean getTestRegion1(GemFireCache cache) { + return createRegionFactoryBean(cache, "TestRegion1"); + } + + @AsCacheWriter(eventTypes = CacheWriterEventType.BEFORE_CREATE) + public void beforeCreateWriter1(EntryEvent event) { + recordEvent(event); + } + + @AsRegionEventHandler(regionWriterEventTypes = RegionCacheWriterEventType.BEFORE_REGION_CLEAR) + public void beforeClear(RegionEvent event) { + recordEvent(event); + } + } +} diff --git a/spring-data-geode/src/test/java/org/springframework/data/gemfire/config/annotation/AsCacheWriterClientCacheConfigurationTests.java b/spring-data-geode/src/test/java/org/springframework/data/gemfire/config/annotation/AsCacheWriterClientCacheConfigurationTests.java new file mode 100644 index 000000000..44265157c --- /dev/null +++ b/spring-data-geode/src/test/java/org/springframework/data/gemfire/config/annotation/AsCacheWriterClientCacheConfigurationTests.java @@ -0,0 +1,223 @@ +/* + * Copyright 2019 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 + * + * https://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 org.apache.geode.cache.EntryEvent; +import org.apache.geode.cache.GemFireCache; +import org.apache.geode.cache.RegionEvent; +import org.apache.geode.cache.client.ClientRegionShortcut; +import org.junit.Test; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.gemfire.client.ClientRegionFactoryBean; +import org.springframework.data.gemfire.eventing.config.CacheWriterEventType; +import org.springframework.data.gemfire.eventing.config.RegionCacheWriterEventType; + +/** + * Tests for {@link AsCacheWriter} configured for a {@link org.apache.geode.cache.client.ClientCache} + * + * @author Udo Kohlmeyer + * @see Test + * @see org.mockito.Mockito + * @see org.apache.geode.cache.client.ClientCache + * @see Configuration + * @see org.springframework.test.context.ContextConfiguration + * @see org.springframework.test.context.junit4.SpringRunner + * @since 2.3.0 + */ +public class AsCacheWriterClientCacheConfigurationTests extends AsCacheWriterConfigurationTests { + + protected static ClientRegionFactoryBean createRegionFactoryBean(GemFireCache cache, + String regionName) { + ClientRegionFactoryBean clientRegion = new ClientRegionFactoryBean<>(); + clientRegion.setName(regionName); + clientRegion.setCache(cache); + clientRegion.setShortcut(ClientRegionShortcut.LOCAL); + return clientRegion; + } + + @Override + protected Class getCacheWriterWithIncorrectRegionEventParameterConfiguration() { + return TestConfigurationWithIncorrectEventParamter.class; + } + + protected Class getCacheWriterAnnotationSingleDefaultRegionsConfiguration() { + return TestConfigurationWithSimpleCacheWriter.class; + } + + protected Class getCacheWriterAnnotationWithInvalidRegion() { + return TestConfigurationWithInvalidRegion.class; + } + + protected Class getCacheWriterAnnotationMultipleRegionsDefault() { + return TestConfigurationWith2RegionsAnd2CacheWritersDefaulted.class; + } + + protected Class getCacheWriterAnnotationSingleRegionAllEvents() { + return TestConfigurationWithSimpleCacheWriterAllEvents.class; + } + + protected Class getCacheWriterAnnotationAgainst2NamedRegions() { + return TestConfigurationWithSimpleCacheWriterWith2Regions.class; + } + + @Override + protected Class getCacheWriterAnnotationWithRegionEventAndCacheWriter() { + return TestConfigurationWithRegionEventAndCacheWriter.class; + } + + @Configuration + @ClientCacheApplication + @EnableEventProcessing + public static class TestConfigurationWithIncorrectEventParamter { + + @Bean("TestRegion") + ClientRegionFactoryBean getTestRegion(GemFireCache cache) { + return createRegionFactoryBean(cache, "TestRegion"); + } + + @AsCacheWriter(eventTypes = CacheWriterEventType.BEFORE_CREATE) + public void beforeCreateWriter(RegionEvent event) { + } + } + + @Configuration + @ClientCacheApplication + @EnableEventProcessing + public static class TestConfigurationWithSimpleCacheWriter { + + @Bean("TestRegion1") + ClientRegionFactoryBean getTestRegion(GemFireCache cache) { + return createRegionFactoryBean(cache, "TestRegion1"); + } + + @AsCacheWriter(eventTypes = CacheWriterEventType.BEFORE_CREATE) + public void beforeCreateWriter(EntryEvent event) { + recordEvent(event); + } + + @AsCacheWriter(eventTypes = CacheWriterEventType.BEFORE_UPDATE) + public void beforeUpdateWriter(EntryEvent event) { + recordEvent(event); + } + } + + @Configuration + @ClientCacheApplication + @EnableEventProcessing + public static class TestConfigurationWithInvalidRegion { + + @Bean("TestRegion") + ClientRegionFactoryBean getTestRegion(GemFireCache cache) { + return createRegionFactoryBean(cache, "TestRegion"); + } + + @AsCacheWriter(eventTypes = CacheWriterEventType.BEFORE_CREATE, regions = "TestRegion2") + public void beforeCreateWriter(EntryEvent event) { + recordEvent(event); + } + } + + @Configuration + @ClientCacheApplication + @EnableEventProcessing + public static class TestConfigurationWithSimpleCacheWriterAllEvents { + + @Bean("TestRegion1") + public ClientRegionFactoryBean getTestRegion(GemFireCache cache) { + return createRegionFactoryBean(cache, "TestRegion1"); + } + + @AsCacheWriter(eventTypes = CacheWriterEventType.ALL) + public void beforeCreateWriter(EntryEvent event) { + recordEvent(event); + } + } + + @Configuration + @ClientCacheApplication + @EnableEventProcessing + public static class TestConfigurationWithSimpleCacheWriterWith2Regions { + + @Bean("TestRegion1") + ClientRegionFactoryBean getTestRegion1(GemFireCache cache) { + return createRegionFactoryBean(cache, "TestRegion1"); + } + + @Bean("TestRegion2") + ClientRegionFactoryBean getTestRegion2(GemFireCache cache) { + return createRegionFactoryBean(cache, "TestRegion2"); + } + + @AsCacheWriter(eventTypes = CacheWriterEventType.BEFORE_CREATE, regions = "TestRegion1") + public void beforeCreateWriter(EntryEvent event) { + recordEvent(event); + } + + @AsCacheWriter(eventTypes = CacheWriterEventType.BEFORE_CREATE, regions = "TestRegion2") + public void beforeUpdateWriter(EntryEvent event) { + recordEvent(event); + } + } + + @Configuration + @ClientCacheApplication + @EnableEventProcessing + public static class TestConfigurationWith2RegionsAnd2CacheWritersDefaulted { + + @Bean("TestRegion1") + ClientRegionFactoryBean getTestRegion1(GemFireCache cache) { + return createRegionFactoryBean(cache, "TestRegion1"); + } + + @Bean("TestRegion2") + ClientRegionFactoryBean getTestRegion2(GemFireCache cache) { + return createRegionFactoryBean(cache, "TestRegion2"); + } + + @AsCacheWriter(eventTypes = CacheWriterEventType.ALL) + public void beforeCreateWriter1(EntryEvent event) { + recordEvent(event); + } + + @AsCacheWriter(eventTypes = CacheWriterEventType.ALL) + public void beforeCreateWriter2(EntryEvent event) { + recordEvent(event); + } + } + + @Configuration + @ClientCacheApplication + @EnableEventProcessing + public static class TestConfigurationWithRegionEventAndCacheWriter { + + @Bean("TestRegion1") + ClientRegionFactoryBean getTestRegion1(GemFireCache cache) { + return createRegionFactoryBean(cache, "TestRegion1"); + } + + @AsCacheWriter(eventTypes = CacheWriterEventType.BEFORE_CREATE) + public void beforeCreateWriter(EntryEvent event) { + recordEvent(event); + } + + @AsRegionEventHandler(regionWriterEventTypes = RegionCacheWriterEventType.BEFORE_REGION_CLEAR) + public void beforeClear(RegionEvent event) { + recordEvent(event); + } + } +} diff --git a/spring-data-geode/src/test/java/org/springframework/data/gemfire/config/annotation/AsCacheWriterConfigurationTests.java b/spring-data-geode/src/test/java/org/springframework/data/gemfire/config/annotation/AsCacheWriterConfigurationTests.java new file mode 100644 index 000000000..c7b76f140 --- /dev/null +++ b/spring-data-geode/src/test/java/org/springframework/data/gemfire/config/annotation/AsCacheWriterConfigurationTests.java @@ -0,0 +1,204 @@ +package org.springframework.data.gemfire.config.annotation; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +import org.apache.geode.cache.EntryEvent; +import org.apache.geode.cache.Operation; +import org.apache.geode.cache.Region; +import org.apache.geode.cache.RegionEvent; +import org.assertj.core.api.Assertions; +import org.junit.After; +import org.junit.Test; +import org.springframework.beans.factory.BeanCreationException; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; + +public abstract class AsCacheWriterConfigurationTests { + + protected static final List events = new ArrayList<>(); + protected ConfigurableApplicationContext applicationContext; + + @After + public void shutdown() { + Optional.ofNullable(this.applicationContext).ifPresent(ConfigurableApplicationContext::close); + events.clear(); + } + + protected static void recordEvent(EntryEvent event) { + events.add( + new Object[] { + event.getRegion().getName(), + event.getOperation(), + event.getNewValue(), + event.getOldValue() + }); + } + + protected static void recordEvent(RegionEvent event) { + events.add( + new Object[] { + event.getRegion().getName(), + event.getOperation() + }); + } + + @Test(expected = BeanCreationException.class) + public void cacheWriterWithIncorrectRegionEventParameter() { + this.applicationContext = newApplicationContext(getCacheWriterWithIncorrectRegionEventParameterConfiguration()); + } + + @Test(expected = BeanCreationException.class) + public void cacheWriterrWithIncorrectEntryEventParameter() { + this.applicationContext = newApplicationContext(getCacheWriterWithIncorrectRegionEventParameterConfiguration()); + } + + protected abstract Class getCacheWriterWithIncorrectRegionEventParameterConfiguration(); + + @Test + public void cacheWriterAnnotationSingleDefaultRegions() { + + this.applicationContext = newApplicationContext(getCacheWriterAnnotationSingleDefaultRegionsConfiguration()); + + Region testRegion = applicationContext.getBean("TestRegion1", Region.class); + + testRegion.put("1", "1"); + testRegion.put("1", "2"); + + Assertions.assertThat(events.size()).isEqualTo(2); + + assertCreateEvent(0, "1", null, "TestRegion1"); + assertUpdateEvent(1, "2", "1", "TestRegion1"); + } + + abstract protected Class getCacheWriterAnnotationSingleDefaultRegionsConfiguration(); + + @Test(expected = BeanCreationException.class) + public void cacheWriterAnnotationWithInvalidRegion() { + + this.applicationContext = newApplicationContext(getCacheWriterAnnotationWithInvalidRegion()); + } + + abstract protected Class getCacheWriterAnnotationWithInvalidRegion(); + + @Test + public void cacheWriterAnnotationMultipleRegionsDefault() { + + this.applicationContext = newApplicationContext(getCacheWriterAnnotationMultipleRegionsDefault()); + + Region testRegion1 = applicationContext.getBean("TestRegion1", Region.class); + Region testRegion2 = applicationContext.getBean("TestRegion2", Region.class); + + testRegion1.put("1", "1"); + testRegion2.put("1", "2"); + testRegion2.put("1", "3"); + + Assertions.assertThat(events.size()).isEqualTo(6); + + assertCreateEvent(0, "1", null, "TestRegion1", "TestRegion2"); + assertCreateEvent(1, "1", null, "TestRegion1", "TestRegion2"); + assertCreateEvent(2, "2", null, "TestRegion1", "TestRegion2"); + assertCreateEvent(3, "2", null, "TestRegion1", "TestRegion2"); + assertUpdateEvent(4, "3", "2", "TestRegion2"); + assertUpdateEvent(5, "3", "2", "TestRegion2"); + } + + abstract protected Class getCacheWriterAnnotationMultipleRegionsDefault(); + + @Test + public void cacheWriterAnnotationSingleRegionAllEvents() { + + this.applicationContext = newApplicationContext(getCacheWriterAnnotationSingleRegionAllEvents()); + + Region testRegion = applicationContext.getBean("TestRegion1", Region.class); + + testRegion.put("1", "1"); + testRegion.put("1", "2"); + testRegion.destroy("1"); + + Assertions.assertThat(events.size()).isEqualTo(3); + + assertCreateEvent(0, "1", null, "TestRegion1"); + assertUpdateEvent(1, "2", "1", "TestRegion1"); + assertDestroyEvent(2, null, "2", "TestRegion1"); + } + + abstract protected Class getCacheWriterAnnotationSingleRegionAllEvents(); + + @Test + public void cacheWriterAnnotationAgainst2NamedRegions() { + + this.applicationContext = newApplicationContext(getCacheWriterAnnotationAgainst2NamedRegions()); + + Region testRegion1 = applicationContext.getBean("TestRegion1", Region.class); + Region testRegion2 = applicationContext.getBean("TestRegion2", Region.class); + + testRegion1.put("1", "1"); + testRegion2.put("1", "2"); + + Assertions.assertThat(events.size()).isEqualTo(2); + + assertCreateEvent(0, "1", null, "TestRegion1"); + assertCreateEvent(1, "2", null, "TestRegion2"); + } + + abstract protected Class getCacheWriterAnnotationAgainst2NamedRegions(); + + @Test + public void cacheWriterAnnotationWithRegionEventSingleRegionAllEvents() { + + this.applicationContext = newApplicationContext(getCacheWriterAnnotationWithRegionEventAndCacheWriter()); + + Region testRegion = applicationContext.getBean("TestRegion1", Region.class); + + testRegion.put("1", "1"); + testRegion.clear(); + + Assertions.assertThat(events.size()).isEqualTo(2); + + assertCreateEvent(0, "1", null, "TestRegion1"); + assertRegionClearEvent(1, "TestRegion1"); + } + + abstract protected Class getCacheWriterAnnotationWithRegionEventAndCacheWriter(); + + private ConfigurableApplicationContext newApplicationContext(Class... annotatedClasses) { + + ConfigurableApplicationContext applicationContext = new AnnotationConfigApplicationContext(annotatedClasses); + + applicationContext.registerShutdownHook(); + + return applicationContext; + } + + private void assetCommonEventProperties(int index, String newValue, String oldValue, String[] regions) { + Assertions.assertThat(events.get(index)[0]).isIn(regions); //regionName + Assertions.assertThat(events.get(index)[2]).isEqualTo(newValue); //newValue + Assertions.assertThat(events.get(index)[3]).isEqualTo(oldValue); //oldValue + } + + private void assetCommonRegionEventProperties(int index, String[] regions) { + Assertions.assertThat(events.get(index)[0]).isIn(regions); //regionName + } + + private void assertCreateEvent(int index, String newValue, String oldValue, String... regions) { + assetCommonEventProperties(index, newValue, oldValue, regions); + Assertions.assertThat(((Operation) events.get(index)[1]).isCreate()).isEqualTo(true); //isCreate + } + + private void assertUpdateEvent(int index, String newValue, String oldValue, String... regions) { + assetCommonEventProperties(index, newValue, oldValue, regions); + Assertions.assertThat(((Operation) events.get(index)[1]).isUpdate()).isEqualTo(true); //isUpdate + } + + private void assertDestroyEvent(int index, String newValue, String oldValue, String... regions) { + assetCommonEventProperties(index, newValue, oldValue, regions); + Assertions.assertThat(((Operation) events.get(index)[1]).isDestroy()).isEqualTo(true); //isDestroy + } + + private void assertRegionClearEvent(int index, String... regions) { + assetCommonRegionEventProperties(index, regions); + Assertions.assertThat(((Operation) events.get(index)[1]).isClear()).isEqualTo(true); //isClear + } +} diff --git a/spring-data-geode/src/test/java/org/springframework/data/gemfire/config/annotation/AsRegionEventCacheServerConfigurationTests.java b/spring-data-geode/src/test/java/org/springframework/data/gemfire/config/annotation/AsRegionEventCacheServerConfigurationTests.java new file mode 100644 index 000000000..0a88d2ada --- /dev/null +++ b/spring-data-geode/src/test/java/org/springframework/data/gemfire/config/annotation/AsRegionEventCacheServerConfigurationTests.java @@ -0,0 +1,272 @@ +/* + * Copyright 2016-2019 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 + * + * https://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 org.apache.geode.cache.EntryEvent; +import org.apache.geode.cache.GemFireCache; +import org.apache.geode.cache.RegionEvent; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.gemfire.ReplicatedRegionFactoryBean; +import org.springframework.data.gemfire.eventing.config.RegionCacheListenerEventType; +import org.springframework.data.gemfire.eventing.config.RegionCacheWriterEventType; + +/** + * Tests for {@link AsCacheListener} configured for a + * {@link org.apache.geode.cache.server.CacheServer} + * + * @author Udo Kohlmeyer + * @see org.junit.Test + * @see org.mockito.Mockito + * @see org.apache.geode.cache.server.CacheServer + * @see Configuration + * @see org.springframework.test.context.ContextConfiguration + * @see org.springframework.test.context.junit4.SpringRunner + * @since 2.3.0 + */ +public class AsRegionEventCacheServerConfigurationTests extends AsRegionEventConfigurationTests { + + protected static ReplicatedRegionFactoryBean createRegionFactoryBean(GemFireCache cache, + String regionName) { + ReplicatedRegionFactoryBean replicateRegion = new ReplicatedRegionFactoryBean<>(); + replicateRegion.setName(regionName); + replicateRegion.setCache(cache); + return replicateRegion; + } + + @Override + protected Class getRegionEventWithIncorrectRegionEventParameterConfiguration() { + return TestConfigurationWithIncorrectRegionEventParameter.class; + } + + @Override + protected Class getCacheListenerAnnotationRegionEventClearConfiguration() { + return TestConfigurationForCacheListenerRegionClear.class; + } + + @Override + protected Class getCacheListenerAnnotationWithInvalidRegion() { + return TestConfigurationWithInvalidRegionNameConfiguration.class; + } + + @Override + protected Class getCacheListenerAnnotationRegionEventDestroyConfiguration() { + return TestConfigurationForCacheListenerRegionDestroy.class; + } + + @Override + protected Class getCacheWriterAnnotationRegionDestroyConfiguration() { + return TestConfigurationForCacheWriterRegionDestroy.class; + } + + @Override + protected Class getCacheWriterAnnotationRegionClearConfiguration() { + return TestConfigurationForCacheWriterRegionClear.class; + } + + @Override + protected Class getCacheListenerAnnotationRegionEventInvalidateConfiguration() { + return TestConfigurationForRegionInvalidate.class; + } + + @Override + protected Class getRegionClearWithBothWriterAndListenerConfiguration() { + return AsRegionEventClientCacheConfigurationTests.TestConfigurationForWithBothListenerAndWriterSingleHandler.class; + } + + @Override + protected Class getRegionClearWithNoEventHandlersConfiguration() { + return AsRegionEventClientCacheConfigurationTests.TestConfigurationForWithNoEventHandlers.class; + } + + @Configuration + @CacheServerApplication + @EnableEventProcessing + public static class TestConfigurationWithIncorrectRegionEventParameter { + + @Bean("TestRegion1") + ReplicatedRegionFactoryBean getTestRegion(GemFireCache cache) { + return createRegionFactoryBean(cache, "TestRegion1"); + } + + @AsRegionEventHandler(regionListenerEventTypes = RegionCacheListenerEventType.AFTER_REGION_CREATE) + public void afterCreateListener(EntryEvent event) { + } + } + + @Configuration + @CacheServerApplication + @EnableEventProcessing + public static class TestConfigurationForCacheListenerRegionClear { + + @Bean("TestRegion1") + ReplicatedRegionFactoryBean getTestRegion(GemFireCache cache) { + return createRegionFactoryBean(cache, "TestRegion1"); + } + + @AsRegionEventHandler(regionListenerEventTypes = RegionCacheListenerEventType.AFTER_REGION_CLEAR) + public void afterCreateListener(RegionEvent event) { + recordEvent(event); + } + } + + @Configuration + @CacheServerApplication + @EnableEventProcessing + public static class TestConfigurationWithInvalidRegionNameConfiguration { + + @Bean("TestRegion1") + ReplicatedRegionFactoryBean getTestRegion(GemFireCache cache) { + return createRegionFactoryBean(cache, "TestRegion1"); + } + + @AsRegionEventHandler(regions = "TestRegion2", regionListenerEventTypes = RegionCacheListenerEventType.ALL) + public void afterCreateListener(RegionEvent event) { + recordEvent(event); + } + } + + @Configuration + @CacheServerApplication + @EnableEventProcessing + public static class TestConfigurationForRegionCreate { + + @Bean("TestRegion1") + ReplicatedRegionFactoryBean getTestRegion(GemFireCache cache) { + return createRegionFactoryBean(cache, "TestRegion1"); + } + + @AsRegionEventHandler(regionListenerEventTypes = RegionCacheListenerEventType.AFTER_REGION_CREATE) + public void afterCreateListener(RegionEvent event) { + recordEvent(event); + } + } + + @Configuration + @CacheServerApplication + @EnableEventProcessing + public static class TestConfigurationForRegionInvalidate { + + @Bean("TestRegion1") + ReplicatedRegionFactoryBean getTestRegion(GemFireCache cache) { + return createRegionFactoryBean(cache, "TestRegion1"); + } + + @AsRegionEventHandler(regionListenerEventTypes = RegionCacheListenerEventType.AFTER_REGION_INVALIDATE) + public void afterCreateListener(RegionEvent event) { + recordEvent(event); + } + } + + @Configuration + @CacheServerApplication + @EnableEventProcessing + public static class TestConfigurationForCacheListenerRegionDestroy { + + @Bean("TestRegion1") + ReplicatedRegionFactoryBean getTestRegion(GemFireCache cache) { + return createRegionFactoryBean(cache, "TestRegion1"); + } + + @AsRegionEventHandler(regionListenerEventTypes = RegionCacheListenerEventType.AFTER_REGION_DESTROY) + public void afterCreateListener(RegionEvent event) { + recordEvent(event); + } + } + + @Configuration + @CacheServerApplication + @EnableEventProcessing + public static class TestConfigurationForRegionLive { + + @Bean("TestRegion1") + ReplicatedRegionFactoryBean getTestRegion(GemFireCache cache) { + return createRegionFactoryBean(cache, "TestRegion1"); + } + + @AsRegionEventHandler(regionListenerEventTypes = RegionCacheListenerEventType.AFTER_REGION_LIVE) + public void afterCreateListener(RegionEvent event) { + recordEvent(event); + } + } + + @Configuration + @CacheServerApplication + @EnableEventProcessing + public static class TestConfigurationForCacheWriterRegionClear { + + @Bean("TestRegion1") + ReplicatedRegionFactoryBean getTestRegion(GemFireCache cache) { + return createRegionFactoryBean(cache, "TestRegion1"); + } + + @AsRegionEventHandler(regionWriterEventTypes = RegionCacheWriterEventType.BEFORE_REGION_CLEAR) + public void beforeRegionClear(RegionEvent event) { + recordEvent(event); + } + } + + @Configuration + @CacheServerApplication + @EnableEventProcessing + public static class TestConfigurationForCacheWriterRegionDestroy { + + @Bean("TestRegion1") + ReplicatedRegionFactoryBean getTestRegion(GemFireCache cache) { + return createRegionFactoryBean(cache, "TestRegion1"); + } + + @AsRegionEventHandler(regionWriterEventTypes = RegionCacheWriterEventType.BEFORE_REGION_DESTROY) + public void beforeRegionWriter(RegionEvent event) { + recordEvent(event); + } + } + + @Configuration + @CacheServerApplication + @EnableEventProcessing + public static class TestConfigurationForWithBothListenerAndWriterSingleHandler { + + @Bean("TestRegion1") + ReplicatedRegionFactoryBean getTestRegion(GemFireCache cache) { + return createRegionFactoryBean(cache, "TestRegion1"); + } + + @AsRegionEventHandler(regionWriterEventTypes = RegionCacheWriterEventType.BEFORE_REGION_CLEAR, + regionListenerEventTypes = RegionCacheListenerEventType.AFTER_REGION_CLEAR) + public void regionClear(RegionEvent event) { + recordEvent(event); + } + } + + @Configuration + @CacheServerApplication + @EnableEventProcessing + public static class TestConfigurationForWithNoEventHandlers { + + @Bean("TestRegion1") + ReplicatedRegionFactoryBean getTestRegion(GemFireCache cache) { + return createRegionFactoryBean(cache, "TestRegion1"); + } + + @AsRegionEventHandler() + public void regionClear(RegionEvent event) { + recordEvent(event); + } + } +} diff --git a/spring-data-geode/src/test/java/org/springframework/data/gemfire/config/annotation/AsRegionEventClientCacheConfigurationTests.java b/spring-data-geode/src/test/java/org/springframework/data/gemfire/config/annotation/AsRegionEventClientCacheConfigurationTests.java new file mode 100644 index 000000000..49ab9cfa3 --- /dev/null +++ b/spring-data-geode/src/test/java/org/springframework/data/gemfire/config/annotation/AsRegionEventClientCacheConfigurationTests.java @@ -0,0 +1,273 @@ +/* + * Copyright 2016-2019 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 + * + * https://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 org.apache.geode.cache.EntryEvent; +import org.apache.geode.cache.GemFireCache; +import org.apache.geode.cache.RegionEvent; +import org.apache.geode.cache.client.ClientRegionShortcut; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.gemfire.client.ClientRegionFactoryBean; +import org.springframework.data.gemfire.eventing.config.RegionCacheListenerEventType; +import org.springframework.data.gemfire.eventing.config.RegionCacheWriterEventType; + +/** + * Tests for {@link AsRegionEventHandler} configured for a + * {@link org.apache.geode.cache.server.CacheServer} to process all {@link RegionEvent} + * + * @author Udo Kohlmeyer + * @see org.junit.Test + * @see org.mockito.Mockito + * @see org.apache.geode.cache.server.CacheServer + * @see Configuration + * @see org.springframework.test.context.ContextConfiguration + * @see org.springframework.test.context.junit4.SpringRunner + * @since 2.3.0 + */ +public class AsRegionEventClientCacheConfigurationTests extends AsRegionEventConfigurationTests { + + protected static ClientRegionFactoryBean createRegionFactoryBean(GemFireCache cache, + String regionName) { + ClientRegionFactoryBean clientRegion = new ClientRegionFactoryBean<>(); + clientRegion.setName(regionName); + clientRegion.setCache(cache); + clientRegion.setShortcut(ClientRegionShortcut.LOCAL); + return clientRegion; + } + + @Override + protected Class getRegionEventWithIncorrectRegionEventParameterConfiguration() { + return TestConfigurationWithIncorrectRegionEventParameter.class; + } + + @Override + protected Class getCacheListenerAnnotationRegionEventClearConfiguration() { + return TestConfigurationForCacheListenerRegionClear.class; + } + + @Override + protected Class getCacheListenerAnnotationWithInvalidRegion() { + return TestConfigurationWithInvalidRegionNameConfiguration.class; + } + + @Override + protected Class getCacheListenerAnnotationRegionEventDestroyConfiguration() { + return TestConfigurationForCacheListenerRegionDestroy.class; + } + + @Override + protected Class getCacheListenerAnnotationRegionEventInvalidateConfiguration() { + return TestConfigurationForRegionInvalidate.class; + } + + @Override + protected Class getCacheWriterAnnotationRegionDestroyConfiguration() { + return TestConfigurationForCacheWriterRegionDestroy.class; + } + + @Override + protected Class getCacheWriterAnnotationRegionClearConfiguration() { + return TestConfigurationForCacheWriterRegionClear.class; + } + + @Override + protected Class getRegionClearWithBothWriterAndListenerConfiguration() { + return TestConfigurationForWithBothListenerAndWriterSingleHandler.class; + } + + @Override + protected Class getRegionClearWithNoEventHandlersConfiguration() { + return TestConfigurationForWithNoEventHandlers.class; + } + + @Configuration + @ClientCacheApplication + @EnableEventProcessing + public static class TestConfigurationWithIncorrectRegionEventParameter { + + @Bean("TestRegion1") + ClientRegionFactoryBean getTestRegion(GemFireCache cache) { + return createRegionFactoryBean(cache, "TestRegion1"); + } + + @AsRegionEventHandler(regionListenerEventTypes = RegionCacheListenerEventType.AFTER_REGION_CREATE) + public void afterCreateListener(EntryEvent event) { + } + } + + @Configuration + @ClientCacheApplication + @EnableEventProcessing + public static class TestConfigurationForCacheListenerRegionClear { + + @Bean("TestRegion1") + ClientRegionFactoryBean getTestRegion(GemFireCache cache) { + return createRegionFactoryBean(cache, "TestRegion1"); + } + + @AsRegionEventHandler(regionListenerEventTypes = RegionCacheListenerEventType.AFTER_REGION_CLEAR) + public void afterCreateListener(RegionEvent event) { + recordEvent(event); + } + } + + @Configuration + @ClientCacheApplication + @EnableEventProcessing + public static class TestConfigurationWithInvalidRegionNameConfiguration { + + @Bean("TestRegion1") + ClientRegionFactoryBean getTestRegion(GemFireCache cache) { + return createRegionFactoryBean(cache, "TestRegion1"); + } + + @AsRegionEventHandler(regions = "TestRegion2", regionListenerEventTypes = RegionCacheListenerEventType.ALL) + public void afterCreateListener(RegionEvent event) { + recordEvent(event); + } + } + + @Configuration + @ClientCacheApplication + @EnableEventProcessing + public static class TestConfigurationForRegionCreate { + + @Bean("TestRegion1") + ClientRegionFactoryBean getTestRegion(GemFireCache cache) { + return createRegionFactoryBean(cache, "TestRegion1"); + } + + @AsRegionEventHandler(regionListenerEventTypes = RegionCacheListenerEventType.AFTER_REGION_CREATE) + public void afterCreateListener(RegionEvent event) { + recordEvent(event); + } + } + + @Configuration + @ClientCacheApplication + @EnableEventProcessing + public static class TestConfigurationForRegionInvalidate { + + @Bean("TestRegion1") + ClientRegionFactoryBean getTestRegion(GemFireCache cache) { + return createRegionFactoryBean(cache, "TestRegion1"); + } + + @AsRegionEventHandler(regionListenerEventTypes = RegionCacheListenerEventType.AFTER_REGION_INVALIDATE) + public void afterCreateListener(RegionEvent event) { + recordEvent(event); + } + } + + @Configuration + @ClientCacheApplication + @EnableEventProcessing + public static class TestConfigurationForCacheListenerRegionDestroy { + + @Bean("TestRegion1") + ClientRegionFactoryBean getTestRegion(GemFireCache cache) { + return createRegionFactoryBean(cache, "TestRegion1"); + } + + @AsRegionEventHandler(regionListenerEventTypes = RegionCacheListenerEventType.AFTER_REGION_DESTROY) + public void beforeRegionDestroy(RegionEvent event) { + recordEvent(event); + } + } + + @Configuration + @ClientCacheApplication + @EnableEventProcessing + public static class TestConfigurationForRegionLive { + + @Bean("TestRegion1") + ClientRegionFactoryBean getTestRegion(GemFireCache cache) { + return createRegionFactoryBean(cache, "TestRegion1"); + } + + @AsRegionEventHandler(regionListenerEventTypes = RegionCacheListenerEventType.AFTER_REGION_LIVE) + public void afterCreateListener(RegionEvent event) { + recordEvent(event); + } + } + + @Configuration + @ClientCacheApplication + @EnableEventProcessing + public static class TestConfigurationForCacheWriterRegionDestroy { + + @Bean("TestRegion1") + ClientRegionFactoryBean getTestRegion(GemFireCache cache) { + return createRegionFactoryBean(cache, "TestRegion1"); + } + + @AsRegionEventHandler(regionWriterEventTypes = RegionCacheWriterEventType.BEFORE_REGION_DESTROY) + public void beforeRegionDestroy(RegionEvent event) { + recordEvent(event); + } + } + + @Configuration + @ClientCacheApplication + @EnableEventProcessing + public static class TestConfigurationForCacheWriterRegionClear { + + @Bean("TestRegion1") + ClientRegionFactoryBean getTestRegion(GemFireCache cache) { + return createRegionFactoryBean(cache, "TestRegion1"); + } + + @AsRegionEventHandler(regionWriterEventTypes = RegionCacheWriterEventType.BEFORE_REGION_CLEAR) + public void beforeRegionClear(RegionEvent event) { + recordEvent(event); + } + } + + @Configuration + @ClientCacheApplication + @EnableEventProcessing + public static class TestConfigurationForWithBothListenerAndWriterSingleHandler { + + @Bean("TestRegion1") + ClientRegionFactoryBean getTestRegion(GemFireCache cache) { + return createRegionFactoryBean(cache, "TestRegion1"); + } + + @AsRegionEventHandler(regionWriterEventTypes = RegionCacheWriterEventType.BEFORE_REGION_CLEAR, regionListenerEventTypes = RegionCacheListenerEventType.AFTER_REGION_CLEAR) + public void regionClear(RegionEvent event) { + recordEvent(event); + } + } + + @Configuration + @ClientCacheApplication + @EnableEventProcessing + public static class TestConfigurationForWithNoEventHandlers { + + @Bean("TestRegion1") + ClientRegionFactoryBean getTestRegion(GemFireCache cache) { + return createRegionFactoryBean(cache, "TestRegion1"); + } + + @AsRegionEventHandler() + public void regionClear(RegionEvent event) { + recordEvent(event); + } + } +} diff --git a/spring-data-geode/src/test/java/org/springframework/data/gemfire/config/annotation/AsRegionEventConfigurationTests.java b/spring-data-geode/src/test/java/org/springframework/data/gemfire/config/annotation/AsRegionEventConfigurationTests.java new file mode 100644 index 000000000..1f154fbe2 --- /dev/null +++ b/spring-data-geode/src/test/java/org/springframework/data/gemfire/config/annotation/AsRegionEventConfigurationTests.java @@ -0,0 +1,170 @@ +package org.springframework.data.gemfire.config.annotation; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +import org.apache.geode.cache.Operation; +import org.apache.geode.cache.Region; +import org.apache.geode.cache.RegionEvent; +import org.assertj.core.api.Assertions; +import org.junit.After; +import org.junit.Test; +import org.springframework.beans.factory.BeanCreationException; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; + +public abstract class AsRegionEventConfigurationTests { + + protected static final List events = new ArrayList<>(); + protected ConfigurableApplicationContext applicationContext; + + protected static void recordEvent(RegionEvent event) { + events.add( + new Object[] { + event.getRegion().getName(), + event.getOperation() + }); + } + + @After + public void shutdown() { + Optional.ofNullable(this.applicationContext).ifPresent(ConfigurableApplicationContext::close); + events.clear(); + } + + @Test(expected = BeanCreationException.class) + public void cacheListenerWithIncorrectRegionEventParameter() { + this.applicationContext = newApplicationContext(getRegionEventWithIncorrectRegionEventParameterConfiguration()); + } + + protected abstract Class getRegionEventWithIncorrectRegionEventParameterConfiguration(); + + @Test + public void cacheListenerAnnotationRegionEventClear() { + + this.applicationContext = newApplicationContext(getCacheListenerAnnotationRegionEventClearConfiguration()); + + Region testRegion = applicationContext.getBean("TestRegion1", Region.class); + + testRegion.clear(); + + Assertions.assertThat(events.size()).isEqualTo(1); + + Assertions.assertThat(((Operation) events.get(0)[1]).isClear()).isEqualTo(true); //isClear + } + + abstract protected Class getCacheListenerAnnotationRegionEventClearConfiguration(); + + @Test(expected = BeanCreationException.class) + public void cacheListenerAnnotationWithInvalidRegion() { + + this.applicationContext = newApplicationContext(getCacheListenerAnnotationWithInvalidRegion()); + } + + abstract protected Class getCacheListenerAnnotationWithInvalidRegion(); + + @Test + public void cacheListenerAnnotationRegionEventInvalidate() { + + this.applicationContext = newApplicationContext(getCacheListenerAnnotationRegionEventInvalidateConfiguration()); + + Region testRegion1 = applicationContext.getBean("TestRegion1", Region.class); + + testRegion1.invalidateRegion(); + + Assertions.assertThat(events.size()).isEqualTo(1); + + Assertions.assertThat(((Operation) events.get(0)[1]).isRegionInvalidate()).isEqualTo(true); //isInvalid + } + + abstract protected Class getCacheListenerAnnotationRegionEventInvalidateConfiguration(); + + @Test + public void cacheListenerAnnotationRegionEventDestroy() { + + this.applicationContext = newApplicationContext(getCacheListenerAnnotationRegionEventDestroyConfiguration()); + + Region testRegion1 = applicationContext.getBean("TestRegion1", Region.class); + + testRegion1.destroyRegion(); + + Assertions.assertThat(events.size()).isEqualTo(1); + + Assertions.assertThat(((Operation) events.get(0)[1]).isRegionDestroy()).isEqualTo(true); //isDestroy + } + + abstract protected Class getCacheListenerAnnotationRegionEventDestroyConfiguration(); + + @Test + public void cacheWriterAnnotationRegionDestroy() { + + this.applicationContext = newApplicationContext(getCacheWriterAnnotationRegionDestroyConfiguration()); + + Region testRegion1 = applicationContext.getBean("TestRegion1", Region.class); + + testRegion1.destroyRegion(); + + Assertions.assertThat(events.size()).isEqualTo(1); + + Assertions.assertThat(((Operation) events.get(0)[1]).isRegionDestroy()).isEqualTo(true); //isDestroy + } + + abstract protected Class getCacheWriterAnnotationRegionDestroyConfiguration(); + + @Test + public void cacheWriterAnnotationRegionClear() { + + this.applicationContext = newApplicationContext(getCacheWriterAnnotationRegionClearConfiguration()); + + Region testRegion1 = applicationContext.getBean("TestRegion1", Region.class); + + testRegion1.clear(); + + Assertions.assertThat(events.size()).isEqualTo(1); + + Assertions.assertThat(((Operation) events.get(0)[1]).isClear()).isEqualTo(true); //isDestroy + } + + protected abstract Class getCacheWriterAnnotationRegionClearConfiguration(); + + @Test + public void regionClearWithBothWriterAndListener() { + + this.applicationContext = newApplicationContext(getRegionClearWithBothWriterAndListenerConfiguration()); + + Region testRegion1 = applicationContext.getBean("TestRegion1", Region.class); + + testRegion1.clear(); + + Assertions.assertThat(events.size()).isEqualTo(2); + + Assertions.assertThat(((Operation) events.get(0)[1]).isClear()).isEqualTo(true); //isDestroy + Assertions.assertThat(((Operation) events.get(1)[1]).isClear()).isEqualTo(true); //isDestroy + } + + protected abstract Class getRegionClearWithBothWriterAndListenerConfiguration(); + + @Test + public void regionClearWithNoEventHandlers() { + + this.applicationContext = newApplicationContext(getRegionClearWithNoEventHandlersConfiguration()); + + Region testRegion1 = applicationContext.getBean("TestRegion1", Region.class); + + testRegion1.clear(); + + Assertions.assertThat(events.size()).isEqualTo(0); + } + + protected abstract Class getRegionClearWithNoEventHandlersConfiguration(); + + private ConfigurableApplicationContext newApplicationContext(Class... annotatedClasses) { + + ConfigurableApplicationContext applicationContext = new AnnotationConfigApplicationContext(annotatedClasses); + + applicationContext.registerShutdownHook(); + + return applicationContext; + } +} diff --git a/spring-data-geode/src/test/java/org/springframework/data/gemfire/eventing/config/CacheWriterEventTypeTest.java b/spring-data-geode/src/test/java/org/springframework/data/gemfire/eventing/config/CacheWriterEventTypeTest.java new file mode 100644 index 000000000..43908d55d --- /dev/null +++ b/spring-data-geode/src/test/java/org/springframework/data/gemfire/eventing/config/CacheWriterEventTypeTest.java @@ -0,0 +1,31 @@ +package org.springframework.data.gemfire.eventing.config; + +import java.util.HashMap; +import java.util.Map; + +import org.assertj.core.api.Assertions; +import org.junit.Test; + +public class CacheWriterEventTypeTest { + + @Test + public void testNoMaskConflictBetweenEntryAndRegionEvents() { + HashMap eventCount = new HashMap<>(); + + for (RegionCacheWriterEventType value : RegionCacheWriterEventType.values()) { + Integer maskCount = eventCount.getOrDefault(value.mask, 0); + eventCount.put(value.mask, maskCount + 1); + } + + for (CacheWriterEventType value : CacheWriterEventType.values()) { + Integer maskCount = eventCount.getOrDefault(value.mask, 0); + eventCount.put(value.mask, maskCount + 1); + } + + for (Map.Entry maskCountEntry : eventCount.entrySet()) { + Assertions.assertThat(maskCountEntry.getValue()) + .as("Event mask for value %d. Please check CacheWriterEventType and RegionCacheWriterEventType for conflicts", + maskCountEntry.getKey()).isEqualTo(1); + } + } +} \ No newline at end of file diff --git a/src/main/asciidoc/index.adoc b/src/main/asciidoc/index.adoc index ee8a102ab..8621930c5 100644 --- a/src/main/asciidoc/index.adoc +++ b/src/main/asciidoc/index.adoc @@ -3,7 +3,7 @@ Costin Leau; David Turanski; John Blum; Oliver Gierke; Jay Bryant :revdate: {localdate} :revnumber: {version} :toclevels: 2 -:apache-geode-version: 19 +:apache-geode-version: 112 :apache-geode-docs: https://geode.apache.org/docs/guide/{apache-geode-version} :apache-geode-javadoc: https://geode.apache.org/releases/latest/javadoc :apache-geode-website: https://geode.apache.org @@ -11,8 +11,8 @@ Costin Leau; David Turanski; John Blum; Oliver Gierke; Jay Bryant :data-store-name-symbolic: geode :data-store-name-simple: Geode :data-store-name: Apache {data-store-name-simple} -:data-store-version: 1.9.0 -:pivotal-gemfire-version: 98 +:data-store-version: 1.12.0 +:pivotal-gemfire-version: 910 :pivotal-gemfire-docs: https://gemfire.docs.pivotal.io/{pivotal-gemfire-version} :pivotal-gemfire-javadoc: https://gemfire-{pivotal-gemfire-version}-javadocs.docs.pivotal.io/ :pivotal-gemfire-website: https://pivotal.io/pivotal-gemfire @@ -39,11 +39,9 @@ Costin Leau; David Turanski; John Blum; Oliver Gierke; Jay Bryant :x-data-store-wiki: {apache-geode-wiki} ifdef::backend-epub3[:front-cover-image: image:epub-cover.png[Front Cover,1050,1600]] -(C) 2010-2019 The original authors. +(C) 2010-2020 The original authors. -NOTE: Copies of this document may be made for your own use and for distribution to others provided that you do not -charge any fee for such copies and further provided that each copy contains this Copyright Notice -whether distributed in print or electronically. +NOTE: Copies of this document may be made for your own use and for distribution to others provided that you do not charge any fee for such copies and further provided that each copy contains this Copyright Notice whether distributed in print or electronically. [[preface]] include::{basedocdir}/preface.adoc[] @@ -63,6 +61,7 @@ include::{basedocdir}/reference/serialization.adoc[leveloffset=+1] include::{basedocdir}/reference/mapping.adoc[leveloffset=+1] include::{basedocdir}/reference/repositories.adoc[leveloffset=+1] include::{basedocdir}/reference/function-annotations.adoc[leveloffset=+1] +include::{basedocdir}/reference/event-handling-annotations.adoc[leveloffset=+1] include::{basedocdir}/reference/lucene.adoc[leveloffset=+1] include::{basedocdir}/reference/gemfire-bootstrap.adoc[leveloffset=+1] include::{basedocdir}/reference/samples.adoc[leveloffset=+1] diff --git a/src/main/asciidoc/reference/event-handling-annotations.adoc b/src/main/asciidoc/reference/event-handling-annotations.adoc new file mode 100644 index 000000000..bdea226c9 --- /dev/null +++ b/src/main/asciidoc/reference/event-handling-annotations.adoc @@ -0,0 +1,134 @@ +[[bootstrap:eventhandling]] += Event Handling + +{sdg-name} includes annotation support for event handling. +There are multiple types of event handlers in {data-store-name}. +The most common event handlers are {x-data-store-javadoc}/org/apache/geode/cache/CacheListener.html[CacheListeners] and +{x-data-store-javadoc}/org/apache/geode/cache/CacheWriter.html[CacheWriters]. + +To enable the processing of annotation driven event handlers the following annotation has to be included: + +[source,java] +---- +@EnableEventProcessing +---- + +Once enabled,methods annotated with `@AsCacheWriter`, `@AsCacheListener` or `@AsRegionEventHandler` will be configured within the system. + +It is imporatant to note, all methods annotated with: + +- `AsCacheListener` will be post-commit event handling methods for Region data specific events +- `AsCacheWriter` will be pre-commit event handling methods for Region data specific events +- `AsRegionEventHandler` can be both pre and post commit event handling methods for Region specific events + +[Note] +==== +It is important to note here that methods annotated with `AsCacheWriter`, `AsCacheLister` or `AsRegionEventHandler` only support one parameter. +This parameter will either be of type `EntryEvent` or `RegionEvent`. +==== + +== CacheListeners + +CacheListeners are post-commit event handlers, thus every event handler method is denoted with the `_after_` prefix. +To configure an event handling method, the method has to be annotated with `@AsCacheListener`. +The `AsCacheListener` annotation deals only with post-commit data operational events. +Every method annotated with `AsCacheListener` requires a single parameter of type `EntryEvent`. + +[source,java] +---- +@AsCacheListener(eventTypes = ... + ,regions = ...) +public void afterEventHandler(EntryEvent event) { + ... + ... +} +---- + +<1> The event types that need to handled by this method need to be configured here. +This is a mandatory field and will require a value. +Multiple values are accepted here. +<2> The regions on which this event handler needs to be registered on. +This is an optional field. +If this field is not set, this event handler will be registered with all configured Regions. +<3> Every event handler needs to be configured to with an Event. +In the case of a Region operation, and CRUD operation, this event is of type EntryEvent. +<4> Write the event handling code for the received event. + +The supported event types are described by +{sdg-javadoc}/org/springframework/data/gemfire/eventing/config/CacheListenerEventType.java[CacheListenerEventType] + +== CacheWriters + +CacheWriters are pre-commit event handlers, thus every event handler method is denoted with the `_before_` prefix. +To configure an event handling method, the method has to be annotated with `@AsCacheWriter`. +The `AsCacheWriter` annotation deals only with pre-commit data operational events. +Every method annotated with `AsCacheWriter` requires a single parameter of type `EntryEvent`. + +[source,java] +---- +@AsCacheWriter(eventTypes = ... + ,regions = ...) +public void beforeEventHandler(EntryEvent event) { + ... + ... +} +---- + +<1> The event types that need to handled by this method need to be configured here. +This is a mandatory field and will require a value. +Multiple values are accepted here. +<2> The regions on which this event handler needs to be registered on. +This is an optional field. +If this field is not set, this event handler will be registered with all configured Regions. +<3> Every event handler needs to be configured to with an Event. +In the case of a Region operation, and CRUD operation, this event is of type EntryEvent. +<4> Write the event handling code for the received event. + +The supported event types are described by +{sdg-javadoc}/org/springframework/data/gemfire/eventing/config/CacheWriterEventType.java[CacheWriterEventType] + +[Note] +==== +A single method can be annotated with both `AsCacheWriter` and `AsCacheListener`. +A common usecase for this is possible +`Fine` system logging of events. +==== + +== Region Event Handlers + +Region Event Handlers are event handlers that handle Region management events, not Region data operational events like CacheWriters or CacheListeners. +Common Region events are, REGION_CLEAR, REGION_CREATE, REGION_DESTROY or REGION_LIVE. + +Similarly to `AsCacheListener` or `AsCacheWriter`, Region event handlers are annotated with `@AsRegionEventHandler`. +A method marked for Region event handling has a single parameter, `RegionEvent`. + +[Note] +==== +It is important to note that Region event handler methods have to use `RegionEvent`. `CacheWriter` and `CacheListener` +event handlers use `EntryEntries`. +It is important to note this distinction. +==== + +Unlike `CacheListeners` and `CacheWriters`, a method annotated with `@AsRegionEventHandler`, can be used for both the pre and post-commit events. +Meaning a single method can be configured to fire before or after the event has occured. + +[source,java] +---- +@AsRegionEventHandler(regionWriterEventTypes = .... + ,regionListenerEventTypes = ... + ,regions = ... ) +public void beforeRegionDestroy(RegionEvent event) { + .... +} +---- + +<1> The post-commit Region event type is configured here. +Multiple values are accepted. +<2> The pre-commit Region event type is configured here. +Multiple values are accepted. +<3> The regions on which the RegionEventHandler needs to be registered on. +This is an optional field. +If not populated all defined Regions will be configured to use the RegionEventHandler. +<4> The region event that is provided. +Note: In the case of the a RegionEventHandler, this type is a RegionEvent +<5> The custom event handling code that is to be executed upon receiving the event. \ No newline at end of file