SGF-668 - Add Annotation configuration support for Continuous Queries.

This commit is contained in:
John Blum
2017-08-21 15:50:26 -07:00
parent 74a86cab19
commit 7e1bf4a9c5
10 changed files with 994 additions and 120 deletions

View File

@@ -0,0 +1,243 @@
/*
* Copyright 2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.gemfire.config.annotation;
import static java.util.Arrays.stream;
import static org.springframework.data.gemfire.util.CollectionUtils.nullSafeMap;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.Executor;
import java.util.stream.Collectors;
import org.apache.geode.cache.GemFireCache;
import org.apache.shiro.util.Assert;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.ListableBeanFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.ImportAware;
import org.springframework.core.annotation.AnnotationAttributes;
import org.springframework.core.type.AnnotationMetadata;
import org.springframework.data.gemfire.config.annotation.support.AbstractAnnotationConfigSupport;
import org.springframework.data.gemfire.listener.ContinuousQueryDefinition;
import org.springframework.data.gemfire.listener.ContinuousQueryListenerContainer;
import org.springframework.data.gemfire.listener.annotation.ContinuousQuery;
import org.springframework.data.gemfire.util.CacheUtils;
import org.springframework.lang.Nullable;
import org.springframework.util.ErrorHandler;
import org.springframework.util.StringUtils;
/**
* The {@link ContinuousQueryConfiguration} class is a Spring {@link Configuration @Configuration} class enabling
* Continuous Query (CQ) GemFire/Geode capabilities in this cache client application.
*
* @author John Blum
* @see org.apache.geode.cache.GemFireCache
* @see org.springframework.beans.factory.config.BeanPostProcessor
* @see org.springframework.context.annotation.Bean
* @see org.springframework.context.annotation.Configuration
* @see org.springframework.context.annotation.ImportAware
* @see org.springframework.core.annotation.AnnotationAttributes
* @see org.springframework.core.type.AnnotationMetadata
* @see org.springframework.data.gemfire.config.annotation.support.AbstractAnnotationConfigSupport
* @see org.springframework.data.gemfire.listener.ContinuousQueryDefinition
* @see org.springframework.data.gemfire.listener.ContinuousQueryListener
* @see org.springframework.data.gemfire.listener.ContinuousQueryListenerContainer
* @since 2.0.0
*/
@Configuration
@SuppressWarnings("unused")
public class ContinuousQueryConfiguration extends AbstractAnnotationConfigSupport implements ImportAware {
protected static final String ORG_SPRINGFRAMEWORK_DATA_GEMFIRE_PACKAGE_NAME = "org.springframework.data.gemfire";
protected static final String ORG_SPRINGFRAMEWORK_PACKAGE_NAME = "org.springframework";
private int phase;
@Autowired(required = false)
private List<ContinuousQueryListenerContainerConfigurer> configurers = Collections.emptyList();
private String errorHandlerBeanName;
private String poolName;
private String taskExecutorBeanName;
@Override
protected Class getAnnotationType() {
return EnableContinuousQueries.class;
}
@Override
public void setImportMetadata(AnnotationMetadata importMetadata) {
if (importMetadata.hasAnnotation(getAnnotationType().getName())) {
AnnotationAttributes enableContinuousQueriesAttributes =
AnnotationAttributes.fromMap(importMetadata.getAnnotationAttributes(getAnnotationType().getName()));
setErrorHandlerBeanName(enableContinuousQueriesAttributes.getString("errorHandlerBeanName"));
setPhase(enableContinuousQueriesAttributes.<Integer>getNumber("phase"));
setPoolName(enableContinuousQueriesAttributes.getString("poolName"));
setTaskExecutorBeanName(enableContinuousQueriesAttributes.getString("taskExecutorBeanName"));
}
}
@Bean
public BeanPostProcessor continuousQueryBeanPostProcessor() {
return new BeanPostProcessor() {
private ContinuousQueryListenerContainer container;
private List<ContinuousQueryDefinition> continuousQueryDefinitions = new ArrayList<>();
@Nullable @Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
if (bean instanceof ContinuousQueryListenerContainer) {
this.container = (ContinuousQueryListenerContainer) bean;
this.continuousQueryDefinitions.forEach(definition -> this.container.addListener(definition));
this.continuousQueryDefinitions.clear();
}
else if (isApplicationBean(bean)) {
List<ContinuousQueryDefinition> definitions = stream(bean.getClass().getMethods())
.filter(method -> method.isAnnotationPresent(ContinuousQuery.class))
.map(method -> ContinuousQueryDefinition.from(bean, method))
.collect(Collectors.toList());
Optional.ofNullable(this.container).map(container -> {
definitions.forEach(container::addListener);
return container;
}).orElseGet(() -> {
this.continuousQueryDefinitions.addAll(definitions);
return null;
});
}
return bean;
}
};
}
private boolean isApplicationBean(Object bean) {
return Optional.ofNullable(bean)
.map(Object::getClass)
.filter(type -> type.getPackage().getName().startsWith(ORG_SPRINGFRAMEWORK_DATA_GEMFIRE_PACKAGE_NAME)
|| !type.getPackage().getName().startsWith(ORG_SPRINGFRAMEWORK_PACKAGE_NAME))
.isPresent();
}
@Bean
public ContinuousQueryListenerContainer continuousQueryListenerContainer(GemFireCache gemfireCache) {
Assert.state(CacheUtils.isClient(gemfireCache),
"Continuous Queries (CQ) may only be used in a ClientCache application");
ContinuousQueryListenerContainer container = new ContinuousQueryListenerContainer();
container.setCache(gemfireCache);
container.setContinuousQueryListenerContainerConfigurers(resolveContinuousQueryListenerContainerConfigurers());
resolveErrorHandler().ifPresent(container::setErrorHandler);
resolvePhase().ifPresent(container::setPhase);
resolvePoolName().ifPresent(container::setPoolName);
resolveTaskExecutor().ifPresent(container::setTaskExecutor);
return container;
}
protected List<ContinuousQueryListenerContainerConfigurer> resolveContinuousQueryListenerContainerConfigurers() {
return Optional.ofNullable(this.configurers)
.filter(configurers -> !configurers.isEmpty())
.orElseGet(() ->
Optional.of(this.beanFactory())
.filter(beanFactory -> beanFactory instanceof ListableBeanFactory)
.map(beanFactory -> {
Map<String, ContinuousQueryListenerContainerConfigurer> beansOfType =
((ListableBeanFactory) beanFactory).getBeansOfType(ContinuousQueryListenerContainerConfigurer.class,
true, true);
return nullSafeMap(beansOfType).values().stream().collect(Collectors.toList());
})
.orElseGet(Collections::emptyList)
);
}
protected Optional<ErrorHandler> resolveErrorHandler() {
return Optional.ofNullable(getErrorHandlerBeanName())
.filter(StringUtils::hasText)
.map(errorHandlerBeanName -> beanFactory().getBean(errorHandlerBeanName, ErrorHandler.class));
}
protected Optional<Integer> resolvePhase() {
return Optional.of(getPhase()).filter(phase -> phase != 0);
}
protected Optional<String> resolvePoolName() {
return Optional.ofNullable(getPoolName()).filter(StringUtils::hasText);
}
protected Optional<Executor> resolveTaskExecutor() {
return Optional.ofNullable(getTaskExecutorBeanName())
.filter(StringUtils::hasText)
.map(taskExecutorBeanName -> beanFactory().getBean(taskExecutorBeanName, Executor.class));
}
public void setErrorHandlerBeanName(String errorHandlerBeanName) {
this.errorHandlerBeanName = errorHandlerBeanName;
}
protected String getErrorHandlerBeanName() {
return errorHandlerBeanName;
}
public void setPhase(int phase) {
this.phase = phase;
}
protected int getPhase() {
return phase;
}
public void setPoolName(String poolName) {
this.poolName = poolName;
}
protected String getPoolName() {
return poolName;
}
public void setTaskExecutorBeanName(String taskExecutorBeanName) {
this.taskExecutorBeanName = taskExecutorBeanName;
}
protected String getTaskExecutorBeanName() {
return taskExecutorBeanName;
}
}

View File

@@ -0,0 +1,41 @@
/*
* Copyright 2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.gemfire.config.annotation;
import org.springframework.data.gemfire.listener.ContinuousQueryListenerContainer;
/**
* The {@link ContinuousQueryListenerContainerConfigurer} interfaces defines a contract for implementations to customize
* the configuration of SDG's {@link ContinuousQueryListenerContainer} when enabling Continuous Query (CQ) functionality
* in Spring Boot, GemFire/Geode cache client applications.
*
* @author John Blum
* @see org.springframework.data.gemfire.listener.ContinuousQueryListenerContainer
* @since 2.0.0
*/
public interface ContinuousQueryListenerContainerConfigurer {
/**
* Applies addditional configuration to the declared/defined {@link ContinuousQueryListenerContainer}.
*
* @param beanName {@link String name} of the {@link ContinuousQueryListenerContainer} bean definition.
* @param container reference to the {@link ContinuousQueryListenerContainer} instance.
* @see org.springframework.data.gemfire.listener.ContinuousQueryListenerContainer
*/
void configure(String beanName, ContinuousQueryListenerContainer container);
}

View File

@@ -0,0 +1,89 @@
/*
* Copyright 2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.gemfire.config.annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.concurrent.Executor;
import org.apache.geode.cache.client.Pool;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.data.gemfire.config.xml.GemfireConstants;
import org.springframework.data.gemfire.listener.ContinuousQueryListenerContainer;
import org.springframework.util.ErrorHandler;
/**
* The {@link EnableContinuousQueries} annotation marks a Spring {@link Configuration @Configuration} annotated
* application configuration class to enable Pivotal GemFire / Apache Geode Continuous Queries (CQ) feature.
*
* @author John Blum
* @see java.lang.annotation.Documented
* @see java.lang.annotation.Inherited
* @see java.lang.annotation.Retention
* @see java.lang.annotation.Target
* @see java.util.concurrent.Executor
* @see org.apache.geode.cache.client.Pool
* @see org.springframework.context.annotation.Configuration
* @see org.springframework.context.annotation.Import
* @see org.springframework.data.gemfire.config.annotation.ContinuousQueryConfiguration
* @see org.springframework.data.gemfire.listener.ContinuousQueryListenerContainer
* @since @.0.0
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
@Import(ContinuousQueryConfiguration.class)
@SuppressWarnings("unused")
public @interface EnableContinuousQueries {
/**
* Refers to the {@link String name} of the declared {@link ErrorHandler} bean that will handle errors
* thrown during CQ event processing by CQ listeners.
*
* Defaults to unset.
*/
String errorHandlerBeanName() default "";
/**
* Defines the Spring container lifecycle phase in which the SDG {@link ContinuousQueryListenerContainer}
* will be started on auto-start.
*
* Defaults to {@literal 0}.
*/
int phase() default 0;
/**
* Refers to the name of the {@link Pool} over which CQs are registered and CQ events are received.
*
* Defaults to {@literal gemfirePool}.
*/
String poolName() default GemfireConstants.DEFAULT_GEMFIRE_POOL_NAME;
/**
* Refers to the name of the {@link Executor} bean used to process CQ events asynchronously.
*
* Defaults to unset.
*/
String taskExecutorBeanName() default "";
}

View File

@@ -16,6 +16,8 @@
package org.springframework.data.gemfire.listener;
import java.lang.reflect.Method;
import java.util.Optional;
import java.util.function.Function;
import org.apache.geode.cache.query.CqAttributes;
@@ -24,6 +26,8 @@ import org.apache.geode.cache.query.CqListener;
import org.apache.geode.cache.query.CqQuery;
import org.apache.shiro.util.StringUtils;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.data.gemfire.listener.adapter.ContinuousQueryListenerAdapter;
import org.springframework.data.gemfire.listener.annotation.ContinuousQuery;
import org.springframework.util.Assert;
/**
@@ -43,6 +47,30 @@ public class ContinuousQueryDefinition implements InitializingBean {
private final String name;
private final String query;
public static ContinuousQueryDefinition from(Object delegate, Method method) {
Assert.notNull(method, "Method must not be null");
ContinuousQuery continuousQuery = method.getAnnotation(ContinuousQuery.class);
Assert.notNull(continuousQuery, () -> String.format("Method [%1$s] must be annotated with [%2$s]",
method, ContinuousQuery.class.getName()));
String name = Optional.of(continuousQuery.name())
.filter(org.springframework.util.StringUtils::hasText)
.orElseGet(() -> String.format("%1$s.%2$s", method.getDeclaringClass().getName(), method.getName()));
String query = continuousQuery.query();
ContinuousQueryListenerAdapter listener = new ContinuousQueryListenerAdapter(delegate);
listener.setDefaultListenerMethod(method.getName());
boolean durable = continuousQuery.durable();
return new ContinuousQueryDefinition(name, query, listener, durable);
}
public ContinuousQueryDefinition(String query, ContinuousQueryListener listener) {
this(query, listener, false);
}

View File

@@ -16,10 +16,17 @@
package org.springframework.data.gemfire.listener;
import static java.util.stream.StreamSupport.stream;
import static org.springframework.data.gemfire.util.ArrayUtils.nullSafeArray;
import static org.springframework.data.gemfire.util.CollectionUtils.nullSafeIterable;
import static org.springframework.data.gemfire.util.CollectionUtils.nullSafeList;
import static org.springframework.data.gemfire.util.CollectionUtils.nullSafeSet;
import static org.springframework.data.gemfire.util.RuntimeExceptionFactory.newIllegalArgumentException;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Optional;
import java.util.Queue;
import java.util.Set;
@@ -52,6 +59,7 @@ import org.springframework.data.gemfire.GemfireQueryException;
import org.springframework.data.gemfire.GemfireUtils;
import org.springframework.data.gemfire.client.support.DefaultableDelegatingPoolAdapter;
import org.springframework.data.gemfire.client.support.DelegatingPoolAdapter;
import org.springframework.data.gemfire.config.annotation.ContinuousQueryListenerContainerConfigurer;
import org.springframework.data.gemfire.config.xml.GemfireConstants;
import org.springframework.util.Assert;
import org.springframework.util.ErrorHandler;
@@ -82,13 +90,13 @@ import org.springframework.util.StringUtils;
* @see org.springframework.data.gemfire.client.support.DefaultableDelegatingPoolAdapter
* @see org.springframework.data.gemfire.client.support.DelegatingPoolAdapter
* @see org.springframework.util.ErrorHandler
* @since 1.1
* @since 1.1.0
*/
@SuppressWarnings("unused")
public class ContinuousQueryListenerContainer implements BeanFactoryAware, BeanNameAware,
InitializingBean, DisposableBean, SmartLifecycle {
// Default Thread name prefix is "ContinuousQueryListenerContainer-".
// Default Thread name prefix is "ContinuousQueryListenerContainer-"
public static final String DEFAULT_THREAD_NAME_PREFIX =
String.format("%s-", ContinuousQueryListenerContainer.class.getSimpleName());
@@ -106,6 +114,12 @@ public class ContinuousQueryListenerContainer implements BeanFactoryAware, BeanN
private Executor taskExecutor;
private List<ContinuousQueryListenerContainerConfigurer> cqListenerContainerConfigurers = Collections.emptyList();
private ContinuousQueryListenerContainerConfigurer compositeCqListenerContainerConfigurer =
(beanName, container) -> nullSafeList(cqListenerContainerConfigurers).forEach(configurer ->
configurer.configure(beanName, container));
protected final Log logger = LogFactory.getLog(getClass());
private Queue<CqQuery> continuousQueries = new ConcurrentLinkedQueue<>();
@@ -120,6 +134,7 @@ public class ContinuousQueryListenerContainer implements BeanFactoryAware, BeanN
@Override
public void afterPropertiesSet() {
applyContinuousQueryListenerContainerConfigurers();
validateQueryService(initQueryService(eagerlyInitializePool(resolvePoolName())));
initExecutor();
initContinuousQueries();
@@ -127,6 +142,11 @@ public class ContinuousQueryListenerContainer implements BeanFactoryAware, BeanN
this.initialized = true;
}
/* (non-Javadoc) */
private void applyContinuousQueryListenerContainerConfigurers() {
applyContinuousQueryListenerContainerConfigurers(getCompositeContinuousQueryListenerContainerConfigurer());
}
/* (non-Javadoc) */
private QueryService validateQueryService(QueryService queryService) {
@@ -135,6 +155,36 @@ public class ContinuousQueryListenerContainer implements BeanFactoryAware, BeanN
return queryService;
}
/**
* Applies the array of {@link ContinuousQueryListenerContainerConfigurer} objects to customize the configuration
* of this {@link ContinuousQueryListenerContainer}.
*
* @param configurers array of {@link ContinuousQueryListenerContainerConfigurer} used to customize
* the configuration of this {@link ContinuousQueryListenerContainer}.
* @see org.springframework.data.gemfire.config.annotation.ContinuousQueryListenerContainerConfigurer
*/
protected void applyContinuousQueryListenerContainerConfigurers(
ContinuousQueryListenerContainerConfigurer... configurers) {
applyContinuousQueryListenerContainerConfigurers(Arrays.asList(
nullSafeArray(configurers, ContinuousQueryListenerContainerConfigurer.class)));
}
/**
* Applies the {@link Iterable} of {@link ContinuousQueryListenerContainerConfigurer} objects to customize
* the configuration of this {@link ContinuousQueryListenerContainer}.
*
* @param configurers {@link Iterable} of {@link ContinuousQueryListenerContainerConfigurer} used to customize
* the configuration of this {@link ContinuousQueryListenerContainer}.
* @see org.springframework.data.gemfire.config.annotation.ContinuousQueryListenerContainerConfigurer
*/
protected void applyContinuousQueryListenerContainerConfigurers(
Iterable<ContinuousQueryListenerContainerConfigurer> configurers) {
stream(nullSafeIterable(configurers).spliterator(), false)
.forEach(configurer -> configurer.configure(getBeanName(), this));
}
/**
* Resolves the name of the {@link Pool} configured to handle the registered Continuous Queries.
*
@@ -175,6 +225,7 @@ public class ContinuousQueryListenerContainer implements BeanFactoryAware, BeanN
};
return Optional.ofNullable(getBeanFactory())
.filter(it -> it.containsBean(poolName))
.filter(it -> it.isTypeMatch(poolName, Pool.class))
.map(it -> {
try {
@@ -374,6 +425,45 @@ public class ContinuousQueryListenerContainer implements BeanFactoryAware, BeanN
return this.continuousQueries;
}
/**
* Null-safe operation setting an array of {@link ContinuousQueryListenerContainerConfigurer} objects used to
* customize the configuration of this {@link ContinuousQueryListenerContainer}.
*
* @param configurers array of {@link ContinuousQueryListenerContainerConfigurer} objects used to customize
* the configuration of this {@link ContinuousQueryListenerContainer}.
* @see org.springframework.data.gemfire.config.annotation.ContinuousQueryListenerContainerConfigurer
* @see #setContinuousQueryListenerContainerConfigurers(List)
*/
public void setContinuousQueryListenerContainerConfigurers(ContinuousQueryListenerContainerConfigurer... configurers) {
setContinuousQueryListenerContainerConfigurers(Arrays.asList(
nullSafeArray(configurers, ContinuousQueryListenerContainerConfigurer.class)));
}
/**
* Null-safe operation setting an {@link Iterable} of {@link ContinuousQueryListenerContainerConfigurer} objects
* used to customize the configuration of this {@link ContinuousQueryListenerContainer}.
*
* @param configurers {@link Iterable} of {@link ContinuousQueryListenerContainerConfigurer} objects used to
* customize the configuration of this {@link ContinuousQueryListenerContainer}.
* @see org.springframework.data.gemfire.config.annotation.ContinuousQueryListenerContainerConfigurer
*/
public void setContinuousQueryListenerContainerConfigurers(List<ContinuousQueryListenerContainerConfigurer> configurers) {
this.cqListenerContainerConfigurers = Optional.ofNullable(configurers).orElseGet(Collections::emptyList);
}
/**
* Returns a <a href="https://en.wikipedia.org/wiki/Composite_pattern">Composite</a> object containing
* the collection of {@link ContinuousQueryListenerContainerConfigurer} objects used to customize the configuration
* of this {@link ContinuousQueryListenerContainer}.
*
* @return a Composite object containing a collection of {@link ContinuousQueryListenerContainerConfigurer} objects
* used to customize the configuration of this {@link ContinuousQueryListenerContainer}.
* @see org.springframework.data.gemfire.config.annotation.ContinuousQueryListenerContainerConfigurer
*/
protected ContinuousQueryListenerContainerConfigurer getCompositeContinuousQueryListenerContainerConfigurer() {
return this.compositeCqListenerContainerConfigurer;
}
/**
* Set an {@link ErrorHandler} to be invoked in case of any uncaught {@link Exception Exceptions} thrown
* while processing a CQ event.

View File

@@ -21,6 +21,7 @@ import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
@@ -33,34 +34,31 @@ import org.springframework.data.gemfire.listener.ContinuousQueryListener;
import org.springframework.data.gemfire.listener.GemfireListenerExecutionFailedException;
import org.springframework.util.Assert;
import org.springframework.util.ReflectionUtils;
import org.springframework.util.ReflectionUtils.MethodCallback;
import org.springframework.util.ReflectionUtils.MethodFilter;
import org.springframework.util.StringUtils;
/**
* Event listener adapter that delegates the handling of messages to target
* listener methods via reflection, with flexible event type conversion.
* Allows listener methods to operate on event content types, completely
* independent from the GemFire API.
* Event listener adapter that delegates the handling of messages to target listener methods via reflection,
* with flexible event type conversion.
*
* <p>Modeled as much as possible after the JMS MessageListenerAdapter in
* Spring Framework.
* Allows listener methods to operate on event content types, completely independent from the GemFire/Geode API.
*
* <p>By default, the content of incoming GemFire events gets extracted before
* being passed into the target listener method, to let the target method
* operate on event content types such as Object or Operation instead of
* the raw {@link CqEvent}.</p>
* <p>Modeled as much as possible after the JMS MessageListenerAdapter in the core Spring Framework.
*
* <p>Find below some examples of method signatures compliant with this
* adapter class. This first example handles all <code>CqEvent</code> types
* and gets passed the contents of each <code>event</code> type as an
* argument.</p>
* <p>By default, the content of incoming GemFire/Geode CQ events gets extracted before being passed into
* the target listener method, to let the target method operate on event content types such as Object or Operation
* instead of the raw {@link CqEvent}.</p>
*
* <p>Find below some examples of method signatures compliant with this adapter class.
*
* This first example handles all <code>CqEvent</code> types and gets passed the contents of each
* <code>event</code> type as an argument.</p>
*
* <pre class="code">public interface PojoListener {
* void handleEvent(CqEvent event);
* void handleEvent(Operation baseOp);
* void handleEvent(Object key);
* void handleEvent(Object key, Object newValue);
* void handleEvent(Throwable th);
* void handleEvent(Throwable cause);
* void handleEvent(CqEvent event, Operation baseOp, byte[] deltaValue);
* void handleEvent(CqEvent event, Operation baseOp, Operation queryOp, Object key, Object newValue);
* }</pre>
@@ -69,6 +67,12 @@ import org.springframework.util.ReflectionUtils.MethodFilter;
* @author Costin Leau
* @author Oliver Gierke
* @author John Blum
* @see java.lang.reflect.Method
* @see org.apache.geode.cache.Operation
* @see org.apache.geode.cache.query.CqEvent
* @see org.apache.geode.cache.query.CqQuery
* @see org.springframework.data.gemfire.listener.ContinuousQueryListener
* @since 1.1.0
*/
public class ContinuousQueryListenerAdapter implements ContinuousQueryListener {
@@ -84,40 +88,45 @@ public class ContinuousQueryListenerAdapter implements ContinuousQueryListener {
private String defaultListenerMethod = DEFAULT_LISTENER_METHOD_NAME;
/**
* Create a new {@link ContinuousQueryListenerAdapter} with default settings.
* Constructs a new instance of {@link ContinuousQueryListenerAdapter} with default settings.
*/
public ContinuousQueryListenerAdapter() {
setDelegate(this);
}
/**
* Create a new {@link ContinuousQueryListenerAdapter} for the given delegate.
* Constructs a new instance of {@link ContinuousQueryListenerAdapter} initialized with
* the given {@link Object delegate}.
*
* @param delegate the delegate object
* @param delegate {@link Object delegate} of the listener method event callback.
* @see #setDelegate(Object)
*/
public ContinuousQueryListenerAdapter(Object delegate) {
setDelegate(delegate);
}
/**
* Set a target object to delegate events listening to.
* Specified listener methods have to be present on this target object.
* <p>If no explicit delegate object has been specified, listener
* methods are expected to present on this adapter instance, that is,
* on a custom subclass of this adapter, defining listener methods.
* Sets the target object to which CQ events are delegated.
*
* @param delegate delegate object
* Specified listener methods have to be present on this target object. If no explicit delegate object
* has been configured then listener methods are expected to present on this adapter instance.
* In other words, on a custom subclass of this adapter, defining listener methods.
*
* @param delegate {@link Object} to delegate listening for CQ events.
* @throws IllegalArgumentException if {@link Object delegate} is {@literal null}.
*/
public void setDelegate(Object delegate) {
Assert.notNull(delegate, "'delegate' must not be null");
public final void setDelegate(Object delegate) {
Assert.notNull(delegate, "Delegate is required");
this.delegate = delegate;
this.invoker = null;
}
/**
* Returns the target object to delegate event listening to.
* Returns a reference to the target object used to listen for and handle CQ events.
*
* @return event listening delegation
* @return a reference to the target object used to listen for and handle CQ events.
*/
public Object getDelegate() {
return this.delegate;
@@ -144,43 +153,6 @@ public class ContinuousQueryListenerAdapter implements ContinuousQueryListener {
return this.defaultListenerMethod;
}
/**
* Standard {@link ContinuousQueryListener} entry point.
* <p>Delegates the event to the target listener method, with appropriate
* conversion of the event argument. In case of an exception, the
* {@link #handleListenerException(Throwable)} method will be invoked.
*
* @param event the incoming GemFire event
* @see #handleListenerException
*/
public void onEvent(CqEvent event) {
try {
// Check whether the delegate is a ContinuousQueryListener implementation itself.
// If so, this adapter will simply act as a pass-through.
if (delegate != this && delegate instanceof ContinuousQueryListener) {
((ContinuousQueryListener) delegate).onEvent(event);
}
// Else... find the listener handler method reflectively.
else {
String methodName = getListenerMethodName(event);
if (methodName == null) {
throw new InvalidDataAccessApiUsageException("No default listener method specified."
+ " Either specify a non-null value for the 'defaultListenerMethod' property"
+ " or override the 'getListenerMethodName' method.");
}
invoker = (invoker != null ? invoker : new MethodInvoker(delegate, methodName));
invokeListenerMethod(event, methodName);
}
}
catch (Throwable cause) {
handleListenerException(cause);
}
}
/**
* Determine the name of the listener method that is supposed to
* handle the given event.
@@ -195,13 +167,52 @@ public class ContinuousQueryListenerAdapter implements ContinuousQueryListener {
return getDefaultListenerMethod();
}
/**
* Standard {@link ContinuousQueryListener} callback method for handling CQ events.
*
* <p>Delegates the CQ event to the target listener method, with appropriate conversion of the event arguments.
* In case of an exception, the {@link #handleListenerException(Throwable)} method will be invoked.
*
* @param event incoming {@link CqEvent CQ event}.
* @see #handleListenerException
*/
@Override
public void onEvent(CqEvent event) {
try {
// Determine whether the delegate is a ContinuousQueryListener implementation;
// If so, this adapter will simply act as a pass-through
if (this.delegate != this && this.delegate instanceof ContinuousQueryListener) {
((ContinuousQueryListener) this.delegate).onEvent(event);
}
// Else, find the listener method handler reflectively
else {
String methodName = Optional.ofNullable(getListenerMethodName(event))
.filter(StringUtils::hasText)
.orElseThrow(() -> new InvalidDataAccessApiUsageException("No default listener method specified;"
+ " Either specify a non-null value for the 'defaultListenerMethod' property"
+ " or override the 'getListenerMethodName' method."));
this.invoker = Optional.ofNullable(this.invoker)
.orElseGet(() -> new MethodInvoker(this.delegate, methodName));
invokeListenerMethod(event, methodName);
}
}
catch (Throwable cause) {
handleListenerException(cause);
}
}
/**
* Handle the given exception that arose during listener execution.
* The default implementation logs the exception at error level.
* @param cause the exception to handle
*/
protected void handleListenerException(Throwable cause) {
logger.error("Listener execution failed...", cause);
logger.error("Listener method execution failed", cause);
}
/**
@@ -212,20 +223,20 @@ public class ContinuousQueryListenerAdapter implements ContinuousQueryListener {
*/
protected void invokeListenerMethod(CqEvent event, String methodName) {
try {
invoker.invoke(event);
this.invoker.invoke(event);
}
catch (InvocationTargetException e) {
if (e.getTargetException() instanceof DataAccessException) {
throw (DataAccessException) e.getTargetException();
catch (InvocationTargetException cause) {
if (cause.getTargetException() instanceof DataAccessException) {
throw (DataAccessException) cause.getTargetException();
}
else {
throw new GemfireListenerExecutionFailedException(
String.format("Listener method [%1$s] threw Exception...", methodName), e.getTargetException());
String.format("Listener method [%s] threw Exception...", methodName), cause.getTargetException());
}
}
catch (Throwable e) {
catch (Throwable cause) {
throw new GemfireListenerExecutionFailedException(
String.format("Failed to invoke the target listener method [%1$s]", methodName), e);
String.format("Failed to invoke the target listener method [%s]", methodName), cause);
}
}
@@ -233,33 +244,30 @@ public class ContinuousQueryListenerAdapter implements ContinuousQueryListener {
private final Object delegate;
List<Method> methods;
private final List<Method> methods;
MethodInvoker(Object delegate, final String methodName) {
Class<?> c = delegate.getClass();
MethodInvoker(Object delegate, String methodName) {
Class<?> delegateType = delegate.getClass();
this.delegate = delegate;
methods = new ArrayList<Method>();
this.methods = new ArrayList<>();
ReflectionUtils.doWithMethods(c, new MethodCallback() {
public void doWith(Method method) throws IllegalArgumentException, IllegalAccessException {
ReflectionUtils.makeAccessible(method);
methods.add(method);
}
}, new MethodFilter() {
public boolean matches(Method method) {
return isValidEventMethodSignature(method, methodName);
}
});
ReflectionUtils.doWithMethods(delegateType, method -> {
ReflectionUtils.makeAccessible(method);
this.methods.add(method);
}, method -> isValidEventMethodSignature(method, methodName));
Assert.isTrue(!methods.isEmpty(), String.format(
"Cannot find a suitable method named [%1$s#%2$s] - is the method public and does it have the proper arguments?",
c.getName(), methodName));
Assert.isTrue(!this.methods.isEmpty(), String.format("Cannot find a suitable method named [%1$s#%2$s];"
+ " Is the method public and does it have the proper arguments?",
delegateType.getName(), methodName));
}
@SuppressWarnings("all")
boolean isValidEventMethodSignature(Method method, String methodName) {
if (Modifier.isPublic(method.getModifiers()) && methodName.equals(method.getName())) {
private boolean isValidEventMethodSignature(Method method, String methodName) {
if (isEventHandlerMethod(method, methodName)) {
Class<?>[] parameterTypes = method.getParameterTypes();
int objects = 0;
@@ -297,20 +305,32 @@ public class ContinuousQueryListenerAdapter implements ContinuousQueryListener {
return false;
}
void invoke(CqEvent event) throws InvocationTargetException, IllegalAccessException {
for (Method method : methods) {
method.invoke(delegate, getMethodArguments(method, event));
private boolean isEventHandlerMethod(Method method, String methodName) {
return Optional.ofNullable(method)
.filter(it -> Modifier.isPublic(it.getModifiers()))
.filter(it -> method.getName().equals(methodName))
.isPresent();
}
void invoke(CqEvent event) throws IllegalAccessException, InvocationTargetException {
for (Method method : this.methods) {
method.invoke(this.delegate, getMethodArguments(method, event));
}
}
Object[] getMethodArguments(Method method, CqEvent event) {
private Object[] getMethodArguments(Method method, CqEvent event) {
Class<?>[] parameterTypes = method.getParameterTypes();
Object[] args = new Object[parameterTypes.length];
boolean query = false;
boolean value = false;
for (int index = 0; index < parameterTypes.length; index++) {
Class<?> parameterType = parameterTypes[index];
if (Object.class.equals(parameterType)) {
@@ -338,5 +358,4 @@ public class ContinuousQueryListenerAdapter implements ContinuousQueryListener {
return args;
}
}
}

View File

@@ -0,0 +1,62 @@
/*
* Copyright 2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.gemfire.listener.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;
/**
* The {@link ContinuousQuery} annotation to define a GemFire/Geode Continuous Query (CQ) on a POJO method
* which handles all CQ events and errors.
*
* @author John Blum
* @see java.lang.annotation.Documented
* @see java.lang.annotation.Inherited
* @see java.lang.annotation.Retention
* @see java.lang.annotation.Target
* @since 2.0.0
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface ContinuousQuery {
/**
* Determines whether the CQ is durable.
*
* Defaults to {@literal false}.
*/
boolean durable() default false;
/**
* {@link String Name} assigned to the registered CQ.
*
* Defaults to the fully-qualified method name.
*/
String name() default "";
/**
* Defines the OQL query used by the CQ to determine CQ events.
*/
String query();
}

View File

@@ -0,0 +1,295 @@
/*
* Copyright 2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.gemfire.config.annotation;
import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.data.gemfire.util.ArrayUtils.asArray;
import java.io.Serializable;
import java.util.Collections;
import java.util.concurrent.atomic.AtomicInteger;
import javax.annotation.Resource;
import org.apache.geode.cache.CacheListener;
import org.apache.geode.cache.CacheLoader;
import org.apache.geode.cache.CacheLoaderException;
import org.apache.geode.cache.EntryEvent;
import org.apache.geode.cache.GemFireCache;
import org.apache.geode.cache.LoaderHelper;
import org.apache.geode.cache.Operation;
import org.apache.geode.cache.Region;
import org.apache.geode.cache.client.ClientRegionShortcut;
import org.apache.geode.cache.query.CqEvent;
import org.apache.geode.cache.util.CacheListenerAdapter;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.data.gemfire.PartitionedRegionFactoryBean;
import org.springframework.data.gemfire.client.ClientRegionFactoryBean;
import org.springframework.data.gemfire.listener.annotation.ContinuousQuery;
import org.springframework.data.gemfire.process.ProcessWrapper;
import org.springframework.data.gemfire.support.ConnectionEndpoint;
import org.springframework.data.gemfire.test.support.ClientServerIntegrationTestsSupport;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringRunner;
import lombok.Data;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
/**
* Integration tests for {@link EnableContinuousQueries}, {@link ContinuousQueryConfiguration}
* and {@link ContinuousQuery}.
*
* @author John Blum
* @see org.junit.Test
* @see org.apache.geode.cache.GemFireCache
* @see org.apache.geode.cache.Region
* @see org.apache.geode.cache.query.CqEvent
* @see org.springframework.data.gemfire.config.annotation.ContinuousQueryConfiguration
* @see org.springframework.data.gemfire.config.annotation.EnableContinuousQueries
* @see org.springframework.data.gemfire.listener.annotation.ContinuousQuery
* @see org.springframework.data.gemfire.process.ProcessWrapper
* @see org.springframework.data.gemfire.test.support.ClientServerIntegrationTestsSupport
* @see org.springframework.test.context.ContextConfiguration
* @see org.springframework.test.context.junit4.SpringRunner
* @since 2.0.0
*/
@RunWith(SpringRunner.class)
@ContextConfiguration(classes = EnableContinuousQueriesConfigurationIntegrationTests.TestConfiguration.class)
@SuppressWarnings("unused")
public class EnableContinuousQueriesConfigurationIntegrationTests extends ClientServerIntegrationTestsSupport {
private static AtomicInteger boilingTemperatureReadingsCounter = new AtomicInteger(0);
private static AtomicInteger freezingTemperatureReadingsCounter = new AtomicInteger(0);
private static AtomicInteger totalTemperatureReadingsCounter = new AtomicInteger(0);
private static ProcessWrapper gemfireServer;
@BeforeClass
public static void startGemFireServer() throws Exception {
int availablePort = findAvailablePort();
gemfireServer = run(GemFireServerConfiguration.class,
String.format("-D%s=%d", GEMFIRE_CACHE_SERVER_PORT_PROPERTY, availablePort));
waitForServerToStart("localhost", availablePort);
System.setProperty(GEMFIRE_CACHE_SERVER_PORT_PROPERTY, String.valueOf(availablePort));
}
@AfterClass
public static void stopGemFireServer() {
System.clearProperty(GEMFIRE_CACHE_SERVER_PORT_PROPERTY);
stop(gemfireServer);
}
@Resource(name = "TemperatureReadings")
private Region<Long, TemperatureReading> temperatureReadings;
@Before
public void setup() {
TemperatureReading temperatureReading = this.temperatureReadings.get(1L);
assertThat(temperatureReading).isNotNull();
assertThat(temperatureReading.getTemperature()).isEqualTo(99);
//System.err.printf("Number of Temperature Readings [%d] on Server [%d]%n",
// this.temperatureReadings.size(), this.temperatureReadings.sizeOnServer());
assertThat(this.temperatureReadings.sizeOnServer()).isEqualTo(10);
//waitOn(() -> totalTemperatureReadingsCounter.get() >= 5, 100L);
//assertThat(totalTemperatureReadingsCounter.get()).isNotZero();
//System.err.printf("Total Temperature Readings [%d]%n", totalTemperatureReadingsCounter.get());
}
@Test
public void boilingTemperatureReadingsEqualsThree() {
assertThat(boilingTemperatureReadingsCounter.get()).isEqualTo(3);
}
@Test
public void freezingTemperatureReadingsEqualsTwo() {
assertThat(freezingTemperatureReadingsCounter.get()).isEqualTo(2);
}
@Configuration
@EnableContinuousQueries(poolName = "DEFAULT")
@Import(GemFireClientConfiguration.class)
static class TestConfiguration {
@Bean
TemperatureReadingQueryListeners temperatureReadingQueryListeners() {
return new TemperatureReadingQueryListeners();
}
}
static class TemperatureReadingQueryListeners {
@ContinuousQuery(name = "BoilingTemperatures", query = "SELECT * FROM /TemperatureReadings r WHERE r.temperature >= 212")
public void boilingTemperatures(CqEvent event) {
boilingTemperatureReadingsCounter.incrementAndGet();
}
@ContinuousQuery(name = "FreezingTemperatures", query = "SELECT * FROM /TemperatureReadings r WHERE r.temperature <= 32")
public void freezingTemperatures(CqEvent event) {
freezingTemperatureReadingsCounter.incrementAndGet();
}
}
@ClientCacheApplication(logLevel = "warning", subscriptionEnabled = true)
static class GemFireClientConfiguration {
@Bean
ClientCacheConfigurer clientCachePoolPortConfigurer(
@Value("${" + GEMFIRE_CACHE_SERVER_PORT_PROPERTY + ":40404}") int port) {
return (bean, clientCacheFactoryBean) -> clientCacheFactoryBean.setServers(
Collections.singletonList(new ConnectionEndpoint("localhost", port)));
}
@Bean(name = "TemperatureReadings")
ClientRegionFactoryBean<Long, TemperatureReading> temperatureReadingsRegion(GemFireCache gemfireCache) {
ClientRegionFactoryBean<Long, TemperatureReading> temperatureReadings =
new ClientRegionFactoryBean<>();
temperatureReadings.setCache(gemfireCache);
temperatureReadings.setCacheListeners(asArray(temperatureReadingCounterListener()));
temperatureReadings.setClose(false);
temperatureReadings.setShortcut(ClientRegionShortcut.PROXY);
return temperatureReadings;
}
private CacheListener<Long, TemperatureReading> temperatureReadingCounterListener() {
return new CacheListenerAdapter<Long, TemperatureReading>() {
@Override
public void afterCreate(EntryEvent<Long, TemperatureReading> event) {
if (Operation.LOCAL_LOAD_CREATE.equals(event.getOperation())) {
totalTemperatureReadingsCounter.incrementAndGet();
}
}
};
}
}
@CacheServerApplication(name = "EnableContinuousQueriesConfigurationIntegrationTests", logLevel = "warning")
static class GemFireServerConfiguration {
public static void main(String[] args) {
AnnotationConfigApplicationContext applicationContext =
new AnnotationConfigApplicationContext(GemFireServerConfiguration.class);
applicationContext.registerShutdownHook();
}
@Bean
CacheServerConfigurer cacheServerPortConfigurer(
@Value("${" + GEMFIRE_CACHE_SERVER_PORT_PROPERTY + ":40404}") int port) {
return (bean, cacheServerFactoryBean) -> cacheServerFactoryBean.setPort(port);
}
@Bean(name = "TemperatureReadings")
PartitionedRegionFactoryBean<Long, TemperatureReading> temperatureReadingsRegion(GemFireCache gemfireCache) {
PartitionedRegionFactoryBean<Long, TemperatureReading> temperatureReadings =
new PartitionedRegionFactoryBean<>();
temperatureReadings.setCache(gemfireCache);
temperatureReadings.setCacheLoader(temperatureReadingsLoader());
temperatureReadings.setClose(false);
temperatureReadings.setPersistent(false);
return temperatureReadings;
}
private CacheLoader<Long, TemperatureReading> temperatureReadingsLoader() {
return new CacheLoader<Long, TemperatureReading>() {
@Override
public TemperatureReading load(LoaderHelper<Long, TemperatureReading> helper) throws CacheLoaderException {
long key = helper.getKey();
Region<Long, TemperatureReading> temperatureReadings = helper.getRegion();
recordTemperature(temperatureReadings, ++key, 213);
recordTemperature(temperatureReadings, ++key, 72);
recordTemperature(temperatureReadings, ++key, 400);
recordTemperature(temperatureReadings, ++key, 1024);
recordTemperature(temperatureReadings, ++key, 43);
recordTemperature(temperatureReadings, ++key, 0);
recordTemperature(temperatureReadings, ++key, 33);
recordTemperature(temperatureReadings, ++key, -45);
recordTemperature(temperatureReadings, ++key, 67);
return TemperatureReading.newTemperatureReading(99);
}
private void recordTemperature(Region<Long, TemperatureReading> temperatureReadings,
long key, int temperature) {
sleep(50);
temperatureReadings.put(key, TemperatureReading.newTemperatureReading(temperature));
}
private void sleep(long milliseconds) {
try {
Thread.sleep(milliseconds);
}
catch (InterruptedException ignore) {
}
}
@Override
public void close() {
}
};
}
}
@Data
@RequiredArgsConstructor(staticName = "newTemperatureReading")
public static class TemperatureReading implements Serializable {
@NonNull
private Integer temperature;
@Override
public String toString() {
return String.format("%d °F", getTemperature());
}
}
}

View File

@@ -50,19 +50,18 @@ import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.util.ErrorHandler;
/**
* The ContinuousQueryListenerContainerNamespaceTest class is a test suite of test cases testing the SDG XML namespace
* for proper configuration and initialization of a ContinuousQueryListenerContainer bean component
* in the Spring context.
* Integration tests for the configuration and initialization of the SDG {@link ContinuousQueryListenerContainer}
* using the SDG XML namespace.
*
* @author John Blum
* @see org.junit.Test
* @see org.junit.runner.RunWith
* @see org.springframework.data.gemfire.listener.ContinuousQueryListenerContainer
* @see org.springframework.test.context.ContextConfiguration
* @see org.springframework.test.context.junit4.SpringRunner
* @see org.apache.geode.cache.client.ClientCache
* @see org.apache.geode.cache.query.CqListener
* @see org.apache.geode.cache.query.CqQuery
* @see org.springframework.data.gemfire.listener.ContinuousQueryListenerContainer
* @see org.springframework.test.context.ContextConfiguration
* @see org.springframework.test.context.junit4.SpringRunner
* @since 1.4.0
*/
@RunWith(SpringRunner.class)
@@ -74,6 +73,7 @@ public class ContinuousQueryListenerContainerNamespaceTest extends ClientServerI
@BeforeClass
public static void startGemFireServer() throws Exception {
int availablePort = findAvailablePort();
gemfireServer = run(CqCacheServerProcess.class,
@@ -104,6 +104,7 @@ public class ContinuousQueryListenerContainerNamespaceTest extends ClientServerI
@Test
public void testContainerConfiguration() throws Exception {
assertNotNull("The ContinuousQueryListenerContainer was not properly configured!", container);
assertTrue("The CQ Listener Container should be active (initialized)!", container.isActive());
assertFalse("The CQ Listener container should not be configured to auto-start!", container.isAutoStartup());
@@ -122,6 +123,7 @@ public class ContinuousQueryListenerContainerNamespaceTest extends ClientServerI
List<String> actualNames = new ArrayList<>(3);
for (CqQuery query : queries) {
actualNames.add(query.getName());
assertEquals("SELECT * FROM /test-cq", query.getQueryString());
@@ -129,12 +131,9 @@ public class ContinuousQueryListenerContainerNamespaceTest extends ClientServerI
CqListener cqListener = query.getCqAttributes().getCqListener();
assertNotNull(cqListener);
// the CqListener object should be an instance of...
// org.springframework.data.gemfire.listener.ContinuousQueryListenerContainer.EventDispatcherAdapter
// So, get the SDG "ContinuousQueryListener"...
ContinuousQueryListener listener = TestUtils.readField("delegate", cqListener);
// The CqListener should be an instance of o.s.d.g.listener.ContinuousQueryListenerContainer.EventDispatcherAdapter
// So, get the SDG "ContinuousQueryListener"
ContinuousQueryListener listener = TestUtils.readField("listener", cqListener);
assertTrue(listener instanceof ContinuousQueryListenerAdapter);
assertTrue(((ContinuousQueryListenerAdapter) listener).getDelegate() instanceof GemfireMDP);

View File

@@ -125,7 +125,7 @@ public class ContinuousQueryListenerContainerTests {
assertThat(cqListenerContainer.getQueryService()).isEqualTo(mockQueryService);
assertThat(cqListenerContainer.getTaskExecutor()).isInstanceOf(Executor.class);
verify(mockBeanFactory, times(1)).containsBean(eq(GemfireConstants.DEFAULT_GEMFIRE_POOL_NAME));
verify(mockBeanFactory, times(2)).containsBean(eq(GemfireConstants.DEFAULT_GEMFIRE_POOL_NAME));
verify(mockBeanFactory, times(1)).isTypeMatch(eq(GemfireConstants.DEFAULT_GEMFIRE_POOL_NAME), eq(Pool.class));
verify(mockBeanFactory, times(1)).getBean(eq(GemfireConstants.DEFAULT_GEMFIRE_POOL_NAME), eq(Pool.class));
verify(mockPool, times(2)).getName();
@@ -143,6 +143,7 @@ public class ContinuousQueryListenerContainerTests {
QueryService mockQueryService = mock(QueryService.class);
when(mockBeanFactory.containsBean(eq("TestPool"))).thenReturn(true);
when(mockBeanFactory.isTypeMatch(eq("TestPool"), eq(Pool.class))).thenReturn(true);
cqListenerContainer.setAutoStartup(false);
@@ -160,7 +161,7 @@ public class ContinuousQueryListenerContainerTests {
assertThat(cqListenerContainer.getQueryService()).isSameAs(mockQueryService);
assertThat(cqListenerContainer.getTaskExecutor()).isSameAs(mockExecutor);
verify(mockBeanFactory, never()).containsBean(eq("TestPool"));
verify(mockBeanFactory, times(1)).containsBean(eq("TestPool"));
verify(mockBeanFactory, times(1)).isTypeMatch(eq("TestPool"), eq(Pool.class));
verify(mockBeanFactory, times(1)).getBean(eq("TestPool"), eq(Pool.class));
verify(poolManagerSpy, never()).find(anyString());
@@ -170,6 +171,7 @@ public class ContinuousQueryListenerContainerTests {
@Test(expected = IllegalStateException.class)
public void afterPropertiesSetThrowsIllegalStateExceptionWhenQueryServiceIsUninitialized() {
when(mockBeanFactory.containsBean(eq("TestPool"))).thenReturn(true);
when(mockBeanFactory.isTypeMatch(eq("TestPool"), eq(Pool.class))).thenReturn(true);
try {
@@ -189,7 +191,7 @@ public class ContinuousQueryListenerContainerTests {
assertThat(cqListenerContainer.isAutoStartup()).isTrue();
assertThat(cqListenerContainer.isRunning()).isFalse();
verify(mockBeanFactory, never()).containsBean(eq("TestPool"));
verify(mockBeanFactory, times(1)).containsBean(eq("TestPool"));
verify(mockBeanFactory, times(1)).isTypeMatch(eq("TestPool"), eq(Pool.class));
verify(mockBeanFactory, times(1)).getBean(eq("TestPool"), eq(Pool.class));
}
@@ -240,12 +242,14 @@ public class ContinuousQueryListenerContainerTests {
@Test
public void eagerlyInitializePoolWithGivenPoolName() {
when(mockBeanFactory.containsBean(eq("TestPool"))).thenReturn(true);
when(mockBeanFactory.isTypeMatch(eq("TestPool"), eq(Pool.class))).thenReturn(true);
cqListenerContainer.setBeanFactory(mockBeanFactory);
assertThat(cqListenerContainer.eagerlyInitializePool("TestPool")).isEqualTo("TestPool");
verify(mockBeanFactory, times(1)).containsBean(eq("TestPool"));
verify(mockBeanFactory, times(1)).isTypeMatch(eq("TestPool"), eq(Pool.class));
verify(mockBeanFactory, times(1)).getBean(eq("TestPool"), eq(Pool.class));
}
@@ -255,7 +259,7 @@ public class ContinuousQueryListenerContainerTests {
Pool mockPool = mock(Pool.class);
when(mockBeanFactory.isTypeMatch(eq("TestPool"), eq(Pool.class))).thenReturn(false);
when(mockBeanFactory.containsBean(eq("TestPool"))).thenReturn(false);
when(mockPool.getName()).thenReturn("TestPool");
try {
@@ -268,7 +272,8 @@ public class ContinuousQueryListenerContainerTests {
finally {
assertThat(PoolManagerImpl.getPMI().unregister(mockPool)).isTrue();
verify(mockBeanFactory, times(1)).isTypeMatch(eq("TestPool"), eq(Pool.class));
verify(mockBeanFactory, times(1)).containsBean(eq("TestPool"));
verify(mockBeanFactory, never()).isTypeMatch(eq("TestPool"), eq(Pool.class));
verify(mockBeanFactory, never()).getBean(eq("TestPool"), eq(Pool.class));
verify(mockPool, atLeast(1)).getName();
}
@@ -279,6 +284,7 @@ public class ContinuousQueryListenerContainerTests {
Pool mockPool = mock(Pool.class);
when(mockBeanFactory.containsBean(eq("TestPool"))).thenReturn(true);
when(mockBeanFactory.isTypeMatch(eq("TestPool"), eq(Pool.class))).thenReturn(true);
when(mockBeanFactory.getBean(eq("TestPool"), eq(Pool.class)))
.thenThrow(new NoSuchBeanDefinitionException("TEST"));
@@ -294,6 +300,7 @@ public class ContinuousQueryListenerContainerTests {
finally {
assertThat(PoolManagerImpl.getPMI().unregister(mockPool)).isTrue();
verify(mockBeanFactory, times(1)).containsBean(eq("TestPool"));
verify(mockBeanFactory, times(1)).isTypeMatch(eq("TestPool"), eq(Pool.class));
verify(mockBeanFactory, times(1)).getBean(eq("TestPool"), eq(Pool.class));
verify(mockPool, atLeast(1)).getName();
@@ -303,7 +310,7 @@ public class ContinuousQueryListenerContainerTests {
@Test(expected = IllegalArgumentException.class)
public void eagerlyInitializePoolThrowsIllegalArgumentExceptionCausedByNoPoolWithGivenName() {
when(mockBeanFactory.isTypeMatch(eq("TestPool"), eq(Pool.class))).thenReturn(false);
when(mockBeanFactory.containsBean(eq("TestPool"))).thenReturn(false);
try {
cqListenerContainer.setBeanFactory(mockBeanFactory);
@@ -317,7 +324,8 @@ public class ContinuousQueryListenerContainerTests {
throw expected;
}
finally {
verify(mockBeanFactory, times(1)).isTypeMatch(eq("TestPool"), eq(Pool.class));
verify(mockBeanFactory, times(1)).containsBean(eq("TestPool"));
verify(mockBeanFactory, never()).isTypeMatch(anyString(), eq(Pool.class));
verify(mockBeanFactory, never()).getBean(eq("TestPool"), eq(Pool.class));
}
}