SGF-741 - Add support for enforcing Lucene Index creation before Region creation.
This commit is contained in:
@@ -5,12 +5,12 @@ https://pivotal.io/pivotal-gemfire[Pivotal GemFire] integrates with http://lucen
|
||||
to index and search on data stored in Pivotal GemFire using Lucene queries. Search-based queries also includes
|
||||
the capability to page through query results.
|
||||
|
||||
Additionally, _Spring Data GemFire_ adds support for query projections based on _Spring Data Commons_
|
||||
Additionally, _Spring Data for Pivotal GemFire_ adds support for query projections based on _Spring Data Commons_
|
||||
Projection infrastructure. This feature enables the query results to be projected into first-class,
|
||||
application domain types as needed or required by the application use case.
|
||||
|
||||
However, a Lucene `Index` must be created first before any Lucene search-based query can be ran. A `LuceneIndex`
|
||||
can be created in _Spring (Data GemFire)_ XML config like so...
|
||||
However, a Lucene `Index` must be created before any Lucene search-based query can be ran. A `LuceneIndex`
|
||||
can be created in _Spring (Data for Pivotal GemFire)_ XML config like so...
|
||||
|
||||
[source,xml]
|
||||
----
|
||||
@@ -38,16 +38,46 @@ and can be configured using...
|
||||
----
|
||||
|
||||
Of course, the `Map` can be specified as a top-level bean definition and referenced using the `ref` attribute
|
||||
on the nested `<gfe:field-analyzers>` element like this, `<gfe-field-analyzers ref="refToTopLevelMapBeanDefinition"/>`.
|
||||
in the nested `<gfe:field-analyzers>` element like this, `<gfe-field-analyzers ref="refToTopLevelMapBeanDefinition"/>`.
|
||||
|
||||
Alternatively, a `LuceneIndex` can be declared in _Spring_ Java config, inside a `@Configuration` class with...
|
||||
Spring Data for Pivotal GemFire's `LuceneIndexFactoryBean` API and SDG's XML namespace also allows the addition of a
|
||||
http://gemfire-95-javadocs.docs.pivotal.io/org/apache/geode/cache/lucene/LuceneSerializer.html[`org.apache.geode.cache.lucene.LuceneSerializer`]
|
||||
to be specified when creating the `LuceneIndex`. The `LuceneSerializer` is used to configure the way objects
|
||||
are converted to Lucene documents for the index when the object is indexed.
|
||||
|
||||
To add an `LuceneSerializer` to the `LuceneIndex`, you only need to...
|
||||
|
||||
[source,xml]
|
||||
----
|
||||
<bean id="MyLuceneSerializer" class="example.CustomLuceneSerializer"/>
|
||||
|
||||
<gfe:lucene-index id="IndexThree" lucene-service-ref="luceneService" region-path="/YetAnotherExample">
|
||||
<gfe:lucene-serializer ref="MyLuceneSerializer">
|
||||
</gfe:lucene-index>
|
||||
----
|
||||
|
||||
Of course, you may specify the `LuceneSerializer` as a anonymous, nested bean definition as well, like so...
|
||||
|
||||
[source,xml]
|
||||
----
|
||||
<gfe:lucene-index id="IndexThree" lucene-service-ref="luceneService" region-path="/YetAnotherExample">
|
||||
<gfe:lucene-serializer>
|
||||
<bean class="example.CustomLuceneSerializer"/>
|
||||
</gfe:lucene-serializer>
|
||||
</gfe:lucene-index>
|
||||
----
|
||||
|
||||
Alternatively, a developer may declare or define a `LuceneIndex` in Spring Java config,
|
||||
inside a `@Configuration` class with...
|
||||
|
||||
[source,java]
|
||||
----
|
||||
@Bean(name = "People")
|
||||
@DependsOn("personTitleIndex")
|
||||
PartitionedRegionFactoryBean<Long, Person> peopleRegion(GemFireCache gemfireCache) {
|
||||
PartitionedRegionFactoryBean<Long, Person> peopleRegion = new PartitionedRegionFactoryBean<>();
|
||||
@Bean(name = "Books")
|
||||
@DependsOn("bookTitleIndex")
|
||||
PartitionedRegionFactoryBean<Long, Book> booksRegion(GemFireCache gemfireCache) {
|
||||
|
||||
PartitionedRegionFactoryBean<Long, Book> peopleRegion =
|
||||
new PartitionedRegionFactoryBean<>();
|
||||
|
||||
peopleRegion.setCache(gemfireCache);
|
||||
peopleRegion.setClose(false);
|
||||
@@ -57,37 +87,60 @@ PartitionedRegionFactoryBean<Long, Person> peopleRegion(GemFireCache gemfireCach
|
||||
}
|
||||
|
||||
@Bean
|
||||
LuceneIndexFactoryBean personTitleIndex(GemFireCache gemFireCache) {
|
||||
LuceneIndexFactoryBean bookTitleIndex(GemFireCache gemFireCache,
|
||||
LuceneSerializer luceneSerializer) {
|
||||
|
||||
LuceneIndexFactoryBean luceneIndex = new LuceneIndexFactoryBean();
|
||||
|
||||
luceneIndex.setCache(gemFireCache);
|
||||
luceneIndex.setFields("title");
|
||||
luceneIndex.setRegionPath("/People");
|
||||
luceneIndex.setLuceneSerializer(luceneSerializer);
|
||||
luceneIndex.setRegionPath("/Books");
|
||||
|
||||
return luceneIndex;
|
||||
}
|
||||
|
||||
@Bean
|
||||
CustomLuceneSerializer myLuceneSerialier() {
|
||||
return new CustomeLuceneSerializer();
|
||||
}
|
||||
----
|
||||
|
||||
There are a few limitations of Pivotal GemFire's, Apache Lucene integration support. First, a `LuceneIndex` can only
|
||||
be created on a GemFire `PARTITION` Region. Second, all `LuceneIndexes` must be created before the the Region on which
|
||||
the `LuceneIndex` is applied.
|
||||
There are a few limitations of Pivotal GemFire's, Apache Lucene integration and support.
|
||||
|
||||
First, a `LuceneIndex` can only be created on an Pivotal GemFire `PARTITION` Region.
|
||||
|
||||
Second, all `LuceneIndexes` must be created before the Region to which the `LuceneIndex` applies.
|
||||
|
||||
NOTE: To help ensure that all declared `LuceneIndexes` defined in a Spring context are created before the Regions
|
||||
on which they apply, SDG includes the `org.springframework.data.gemfire.config.support.LuceneIndexRegionBeanFactoryPostProcessor`.
|
||||
You may register this Spring https://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/beans/factory/config/BeanFactoryPostProcessor.html[`BeanFactoryPostProcessor`]
|
||||
in XML config using `<bean class="org.springframework.data.gemfire.config.support.LuceneIndexRegionBeanFactoryPostProcessor"/>`
|
||||
The `o.s.d.g.config.support.LuceneIndexRegionBeanFactoryPostProcessor` may only be used when using SDG XML config.
|
||||
More details about Spring's `BeanFactoryPostProcessors` can be found https://docs.spring.io/spring/docs/current/spring-framework-reference/core.html#beans-factory-extension-factory-postprocessors[here].
|
||||
|
||||
It is possible that these Pivotal GemFire restrictions will not apply in a future release which is why
|
||||
the SDG `LuceneIndexFactoryBean` API takes a reference to the Region directly as well, rather than just the Region path.
|
||||
|
||||
This is more ideal if think about the case in which users may want to define a `LuceneIndex` on an existing Region
|
||||
with data at a later point during the application's lifecycle and as requirements demand. Where possible, SDG strives
|
||||
to stick to strongly-typed objects.
|
||||
to adhere to strongly-typed objects. However, for the time being, you must use the `regionPath` property
|
||||
to specify the Region to which the `LuceneIndex` will be applied.
|
||||
|
||||
NOTE: Additional, in the example above, you will notice the presence of Spring's `@DependsOn` annotation
|
||||
on the "Books" Region bean definition. This is used to create a dependency from the "Books" Region bean
|
||||
to the "bookTitleIndex" LuceneIndex bean definition ensuring that the `LuceneIndex` will be created before
|
||||
the Region on which it applies.
|
||||
|
||||
Now that we have a `LuceneIndex` we can perform Lucene based data access operations, such as queries.
|
||||
|
||||
== Lucene Template Data Accessors
|
||||
|
||||
_Spring Data GemFire_ provides 2 primary templates for Lucene data access operations, depending on how low a level
|
||||
your application is prepared to deal with.
|
||||
_Spring Data for Pivotal GemFire_ provides 2 primary templates for Lucene data access operations, depending on
|
||||
how low of a level your application is prepared to deal with.
|
||||
|
||||
The `LuceneOperations` interface defines query operations using Pivotal GemFire
|
||||
http://geode.apache.org/releases/latest/javadoc/org/apache/geode/cache/lucene/package-frame.html[Lucene types].
|
||||
http://gemfire-95-javadocs.docs.pivotal.io/org/apache/geode/cache/lucene/package-summary.html[Lucene types].
|
||||
|
||||
[source,java]
|
||||
----
|
||||
@@ -118,18 +171,18 @@ public interface LuceneOperations {
|
||||
NOTE: The `[, int resultLimit]` indicates that the `resultLimit` parameter is optional.
|
||||
|
||||
The operations in the `LuceneOperations` interface match the operations provided by the Pivotal GemFire's
|
||||
http://geode.apache.org/releases/latest/javadoc/org/apache/geode/cache/lucene/LuceneQuery.html[LuceneQuery] interface.
|
||||
However, SDG has the added value of translating proprietary GemFire or Lucene `Exceptions` into _Spring's_ highly
|
||||
consistent and expressive DAO
|
||||
http://gemfire-95-javadocs.docs.pivotal.io/org/apache/geode/cache/lucene/LuceneQuery.html[LuceneQuery] interface.
|
||||
However, SDG has the added value of translating proprietary Pivotal GemFire or Apache Lucene `Exceptions`
|
||||
into _Spring's_ highly consistent and expressive DAO
|
||||
http://docs.spring.io/spring/docs/current/spring-framework-reference/htmlsingle/#dao-exceptions[Exception Hierarchy],
|
||||
particularly as many modern data access operations involve more than single store or repository.
|
||||
|
||||
Additionally, SDG's `LuceneOperations` interface can shield your application from interface breaking changes
|
||||
introduced by the underlying Pivotal GemFire or Apache Lucene APIs when they do and will occur.
|
||||
|
||||
However, it would be remorse to only offer a Lucene Data Access Object that only uses Pivotal GemFire and Apache Lucene
|
||||
data types (e.g. GemFire's `LuceneResultStruct`), therefore SDG gives you the `ProjectingLuceneOperations` interface
|
||||
to remedy these important application concerns.
|
||||
However, it would be remorse to only offer a Lucene Data Access Object (DAO) that only uses Pivotal GemFire
|
||||
and Apache Lucene data types (e.g. Pivotal GemFire's `LuceneResultStruct`), therefore SDG gives you the
|
||||
`ProjectingLuceneOperations` interface to remedy these important application concerns.
|
||||
|
||||
[source,java]
|
||||
----
|
||||
@@ -145,14 +198,13 @@ public interface ProjectingLuceneOperations {
|
||||
}
|
||||
----
|
||||
|
||||
The `ProjectingLuceneOperations` interface primarily uses application domain object types to work with
|
||||
your application data. The `query` method variants accept a projection type and the template applies
|
||||
the query results to instances of the given projection type using the _Spring Data Commons_
|
||||
Projection infrastructure.
|
||||
The `ProjectingLuceneOperations` interface primarily uses application domain object types allowing you to work with
|
||||
your application data. The `query` method variants accept a projection type and the template applies the query results
|
||||
to instances of the given projection type using the _Spring Data Commons_ Projection infrastructure.
|
||||
|
||||
Additionally, the template wraps the paged Lucene query results in an instance of the _Spring Data Commons_
|
||||
abstraction representing a `Page`. The same projection logic can still be applied to the results in the page
|
||||
and are lazily projected as each page in the collection is accessed.
|
||||
`Page` abstraction. The same projection logic can still be applied to the results in the page and are lazily projected
|
||||
as each page in the collection is accessed.
|
||||
|
||||
By way of example, suppose I have a class representing a `Person` like so...
|
||||
|
||||
@@ -182,6 +234,7 @@ Additionally, I might have a single interface to represent people as `Customers`
|
||||
interface Customer {
|
||||
|
||||
String getName()
|
||||
|
||||
}
|
||||
----
|
||||
|
||||
@@ -191,7 +244,9 @@ If I define the following `LuceneIndex`...
|
||||
----
|
||||
@Bean
|
||||
LuceneIndexFactoryBean personLastNameIndex(GemFireCache gemfireCache) {
|
||||
LuceneIndexFactoryBean personLastNameIndex = new LuceneIndexFactoryBean();
|
||||
|
||||
LuceneIndexFactoryBean personLastNameIndex =
|
||||
new LuceneIndexFactoryBean();
|
||||
|
||||
personLastNameIndex.setCache(gemfireCache);
|
||||
personLastNameIndex.setFields("lastName");
|
||||
@@ -215,7 +270,7 @@ Or as a `Page` of type `Customer`...
|
||||
Page<Customer> customers = luceneTemplate.query("lastName: D*", "lastName", 100, 20, Customer.class);
|
||||
----
|
||||
|
||||
The `Page` can then be used to fetch individual pages of results...
|
||||
The `Page` can then be used to fetch individual pages of the results...
|
||||
|
||||
[source,java]
|
||||
----
|
||||
@@ -223,7 +278,7 @@ List<Customer> firstPage = customers.getContent();
|
||||
----
|
||||
|
||||
Conveniently, the _Spring Data Commons_ `Page` interface implements `java.lang.Iterable<T>` too making it very easy
|
||||
to iterate over the content as well.
|
||||
to iterate over the contents.
|
||||
|
||||
The only restriction to the _Spring Data Commons_ Projection infrastructure is that the projection type
|
||||
must be an interface. However, it is possible to extend the provided, out-of-the-box (OOTB)
|
||||
@@ -235,9 +290,9 @@ A custom `ProjectionFactory` can be set on a Lucene template using `setProjectio
|
||||
|
||||
== Annotation configuration support
|
||||
|
||||
Finally, _Spring Data GemFire_ provides Annotation configuration support for `LuceneIndexes`. Eventually, the SDG Lucene
|
||||
support will find its way into the _Repository_ infrastructure extension for Pivotal GemFire so that Lucene queries
|
||||
can be expressed as methods on an application `Repository` interface, much like the
|
||||
Finally, _Spring Data for Pivotal GemFire_ provides Annotation configuration support for `LuceneIndexes`.
|
||||
Eventually, the SDG Lucene support will find its way into the _Repository_ infrastructure extension for Pivotal GemFire
|
||||
so that Lucene queries can be expressed as methods on an application `Repository` interface, much like the
|
||||
http://docs.spring.io/spring-data-gemfire/docs/current/reference/html/#gemfire-repositories.executing-queries[OQL support]
|
||||
today.
|
||||
|
||||
@@ -263,8 +318,8 @@ class Person {
|
||||
}
|
||||
----
|
||||
|
||||
You must be using the SDG Annotation configuration support along with the `@EnableEntityDefineRegions`
|
||||
and `@EnableIndexing` Annotations to enable this feature...
|
||||
You must use SDG's Annotation configuration support along with the `@EnableEntityDefineRegions` and `@EnableIndexing`
|
||||
Annotations to enable this feature...
|
||||
|
||||
[source,java]
|
||||
----
|
||||
@@ -277,9 +332,12 @@ class ApplicationConfiguration {
|
||||
}
|
||||
----
|
||||
|
||||
NOTE: Keep in mind that `LuceneIndexes` can only be created on Apache Geode Servers since `LuceneIndexes` only apply
|
||||
to `PARTTION` Regions.
|
||||
|
||||
Given our definition of the `Person` class above, the SDG Annotation configuration support
|
||||
will find the `Person` entity class definition, determine that people will be stored in
|
||||
a `PARTITION` Region called "People" and that the Person will have an OQL `Index` on `birthDate`
|
||||
a `PARTITION` Region called "People" and that the `Person` will have an OQL `Index` on `birthDate`
|
||||
along with a `LuceneIndex` on `lastName`.
|
||||
|
||||
More will be described with this feature in subsequent releases.
|
||||
|
||||
@@ -102,9 +102,6 @@ public class IndexFactoryBean extends AbstractFactoryBeanSupport<Index> implemen
|
||||
private String indexName;
|
||||
private String name;
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
@Override
|
||||
public void afterPropertiesSet() throws Exception {
|
||||
|
||||
|
||||
@@ -17,36 +17,77 @@
|
||||
package org.springframework.data.gemfire.config.support;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
import org.apache.geode.cache.Region;
|
||||
import org.apache.geode.cache.lucene.LuceneIndex;
|
||||
import org.apache.geode.cache.query.Index;
|
||||
import org.springframework.beans.factory.BeanFactory;
|
||||
import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition;
|
||||
import org.springframework.beans.factory.config.BeanDefinition;
|
||||
import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
|
||||
import org.springframework.core.type.MethodMetadata;
|
||||
import org.springframework.data.gemfire.GenericRegionFactoryBean;
|
||||
import org.springframework.data.gemfire.LocalRegionFactoryBean;
|
||||
import org.springframework.data.gemfire.PartitionedRegionFactoryBean;
|
||||
import org.springframework.data.gemfire.ReplicatedRegionFactoryBean;
|
||||
import org.springframework.data.gemfire.client.ClientCacheFactoryBean;
|
||||
import org.springframework.data.gemfire.client.ClientRegionFactoryBean;
|
||||
import org.springframework.data.gemfire.client.PoolFactoryBean;
|
||||
import org.springframework.data.gemfire.util.SpringUtils;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* The AbstractDependencyStructuringBeanFactoryPostProcessor class...
|
||||
* The {@link AbstractDependencyStructuringBeanFactoryPostProcessor} class is a Spring {@link BeanFactoryPostProcessor}
|
||||
* post processing the Spring {@link BeanFactory} to help ensure that the dependencies between different Apache Geode
|
||||
* or Pivotal GemFire objects (e.g. {@link Region} and a {@link LuceneIndex} or an OQL {@link Index}) have been
|
||||
* properly declared in order to the lifecycle of those components are upheld according to Apache Geode
|
||||
* or Pivotal GemFire requirements/rules.
|
||||
*
|
||||
* @author John Blum
|
||||
* @since 1.0.0
|
||||
* @see org.springframework.beans.factory.BeanFactory
|
||||
* @see org.springframework.beans.factory.config.BeanDefinition
|
||||
* @see org.springframework.beans.factory.config.BeanFactoryPostProcessor
|
||||
* @since 2.1.0
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
public abstract class AbstractDependencyStructuringBeanFactoryPostProcessor implements BeanFactoryPostProcessor {
|
||||
|
||||
protected BeanDefinition addDependsOn(BeanDefinition beanDefinition, String... beanNames) {
|
||||
return SpringUtils.addDependsOn(beanDefinition, beanNames);
|
||||
}
|
||||
|
||||
protected Optional<Object> getPropertyValue(BeanDefinition beanDefinition, String propertyName) {
|
||||
return SpringUtils.getPropertyValue(beanDefinition, propertyName);
|
||||
}
|
||||
|
||||
protected boolean isBeanDefinitionOfType(BeanDefinition beanDefinition, Class<?> type) {
|
||||
|
||||
Assert.notNull(type, "Class type must not be null");
|
||||
|
||||
return isBeanDefinitionOfType(beanDefinition, typeName -> type.getName().equals(typeName));
|
||||
}
|
||||
|
||||
protected boolean isBeanDefinitionOfType(BeanDefinition beanDefinition, String typeName) {
|
||||
|
||||
return isBeanDefinitionOfType(beanDefinition,
|
||||
typeNameArgument -> String.valueOf(typeName).equals(typeNameArgument));
|
||||
}
|
||||
|
||||
protected boolean isBeanDefinitionOfType(BeanDefinition beanDefinition, Predicate<String> typeFilter) {
|
||||
|
||||
return Optional.of(beanDefinition)
|
||||
.map(it -> beanDefinition.getBeanClassName())
|
||||
.filter(StringUtils::hasText)
|
||||
.map(beanClassName -> type.getName().equals(beanClassName))
|
||||
.map(typeFilter::test)
|
||||
.orElseGet(() ->
|
||||
Optional.ofNullable(beanDefinition.getFactoryMethodName())
|
||||
.filter(StringUtils::hasText)
|
||||
.filter(it -> beanDefinition instanceof AnnotatedBeanDefinition)
|
||||
.map(it -> ((AnnotatedBeanDefinition) beanDefinition).getFactoryMethodMetadata())
|
||||
.map(methodMetadata -> type.getName().equals(methodMetadata.getReturnTypeName()))
|
||||
.map(MethodMetadata::getReturnTypeName)
|
||||
.map(typeFilter::test)
|
||||
.orElse(false)
|
||||
);
|
||||
}
|
||||
@@ -62,4 +103,15 @@ public abstract class AbstractDependencyStructuringBeanFactoryPostProcessor impl
|
||||
protected boolean isPoolBean(BeanDefinition beanDefinition) {
|
||||
return isBeanDefinitionOfType(beanDefinition, PoolFactoryBean.class);
|
||||
}
|
||||
|
||||
protected Predicate<String> isRegionBeanType() {
|
||||
|
||||
Predicate<String> genericRegionBeanType =
|
||||
typeName -> GenericRegionFactoryBean.class.getName().equals(typeName);
|
||||
|
||||
return genericRegionBeanType.or(typeName -> ClientRegionFactoryBean.class.getName().equals(typeName))
|
||||
.or(typeName -> LocalRegionFactoryBean.class.getName().equals(typeName))
|
||||
.or(typeName -> PartitionedRegionFactoryBean.class.getName().equals(typeName))
|
||||
.or(typeName -> ReplicatedRegionFactoryBean.class.getName().equals(typeName));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
/*
|
||||
* Copyright 2018 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.support;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
import org.apache.geode.cache.Region;
|
||||
import org.apache.geode.cache.lucene.LuceneIndex;
|
||||
import org.springframework.beans.BeansException;
|
||||
import org.springframework.beans.factory.config.BeanDefinition;
|
||||
import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
|
||||
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
|
||||
import org.springframework.data.gemfire.search.lucene.LuceneIndexFactoryBean;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* The {@link LuceneIndexRegionBeanFactoryPostProcessor} class is a Spring {@link BeanFactoryPostProcessor} ensuring
|
||||
* that a {@link LuceneIndex} is created before the {@link Region} on which the {@link LuceneIndex} is defined.
|
||||
*
|
||||
* @author John Blum
|
||||
* @see org.apache.geode.cache.Region
|
||||
* @see org.apache.geode.cache.lucene.LuceneIndex
|
||||
* @see org.springframework.beans.factory.config.BeanDefinition
|
||||
* @see org.springframework.beans.factory.config.BeanFactoryPostProcessor
|
||||
* @see org.springframework.beans.factory.config.ConfigurableListableBeanFactory
|
||||
* @see org.springframework.data.gemfire.search.lucene.LuceneIndexFactoryBean
|
||||
* @see org.springframework.data.gemfire.config.support.AbstractDependencyStructuringBeanFactoryPostProcessor
|
||||
* @since 2.1.0
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
public class LuceneIndexRegionBeanFactoryPostProcessor extends AbstractDependencyStructuringBeanFactoryPostProcessor {
|
||||
|
||||
@Override
|
||||
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
|
||||
|
||||
Map<String, String> regionNameToLuceneIndexBeanName = new HashMap<>();
|
||||
Map<String, String> regionNameToRegionBeanName = new HashMap<>();
|
||||
|
||||
Arrays.stream(beanFactory.getBeanDefinitionNames()).forEach(beanName -> {
|
||||
|
||||
BeanDefinition beanDefinition = beanFactory.getBeanDefinition(beanName);
|
||||
|
||||
if (isBeanDefinitionOfType(beanDefinition, LuceneIndexFactoryBean.class)) {
|
||||
resolveRegionNameFromLuceneIndex(beanDefinition)
|
||||
.ifPresent(regionName -> regionNameToLuceneIndexBeanName.put(regionName, beanName));
|
||||
}
|
||||
else if (isBeanDefinitionOfType(beanDefinition, isRegionBeanType())) {
|
||||
resolveRegionNameFromRegionBean(beanName, beanDefinition)
|
||||
.ifPresent(regionName -> regionNameToRegionBeanName.put(regionName, beanName));
|
||||
}
|
||||
});
|
||||
|
||||
regionNameToRegionBeanName.keySet().stream().forEach(regionName -> {
|
||||
|
||||
if (regionNameToLuceneIndexBeanName.containsKey(regionName)) {
|
||||
|
||||
String regionBeanName = regionNameToRegionBeanName.get(regionName);
|
||||
String luceneIndexBeanName = regionNameToLuceneIndexBeanName.get(regionName);
|
||||
|
||||
addDependsOn(beanFactory.getBeanDefinition(regionBeanName), luceneIndexBeanName);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private Optional<String> resolveRegionNameFromLuceneIndex(BeanDefinition beanDefinition) {
|
||||
|
||||
return getPropertyValue(beanDefinition, "regionPath")
|
||||
.map(this::asFullyQualifiedRegionName);
|
||||
}
|
||||
|
||||
private Optional<String> resolveRegionNameFromRegionBean(String beanName, BeanDefinition beanDefinition) {
|
||||
|
||||
String regionName = asString(getPropertyValue(beanDefinition, "regionName"));
|
||||
String name = asString(getPropertyValue(beanDefinition, "name"));
|
||||
|
||||
return Optional.ofNullable(StringUtils.hasText(regionName) ? regionName
|
||||
: (StringUtils.hasText(name) ? name : beanName)).filter(StringUtils::hasText);
|
||||
}
|
||||
|
||||
private String asFullyQualifiedRegionName(Object regionPath) {
|
||||
|
||||
return Optional.ofNullable(regionPath)
|
||||
.map(String::valueOf)
|
||||
.map(String::trim)
|
||||
.map(it -> {
|
||||
|
||||
int index = it.lastIndexOf(Region.SEPARATOR);
|
||||
|
||||
return index > 0 ? it : it.substring(1);
|
||||
})
|
||||
.filter(StringUtils::hasText)
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
@SuppressWarnings("all")
|
||||
private String asString(Optional<Object> value) {
|
||||
|
||||
return value.map(String::valueOf)
|
||||
.map(String::trim)
|
||||
.filter(StringUtils::hasText)
|
||||
.orElse(null);
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,7 @@ import org.apache.geode.cache.lucene.LuceneService;
|
||||
import org.apache.geode.cache.lucene.LuceneServiceProvider;
|
||||
import org.springframework.beans.factory.FactoryBean;
|
||||
import org.springframework.beans.factory.InitializingBean;
|
||||
import org.springframework.data.gemfire.support.AbstractFactoryBeanSupport;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
@@ -38,7 +39,7 @@ import org.springframework.util.Assert;
|
||||
* @since 1.1.0
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
public class LuceneServiceFactoryBean implements FactoryBean<LuceneService>, InitializingBean {
|
||||
public class LuceneServiceFactoryBean extends AbstractFactoryBeanSupport<LuceneService> implements InitializingBean {
|
||||
|
||||
private GemFireCache gemfireCache;
|
||||
|
||||
@@ -49,6 +50,7 @@ public class LuceneServiceFactoryBean implements FactoryBean<LuceneService>, Ini
|
||||
*/
|
||||
@Override
|
||||
public void afterPropertiesSet() throws Exception {
|
||||
|
||||
GemFireCache gemfireCache = getCache();
|
||||
|
||||
Assert.state(gemfireCache != null,
|
||||
@@ -83,15 +85,10 @@ public class LuceneServiceFactoryBean implements FactoryBean<LuceneService>, Ini
|
||||
*/
|
||||
@Override
|
||||
public Class<?> getObjectType() {
|
||||
return Optional.ofNullable(this.luceneService).<Class<?>>map(LuceneService::getClass).orElse(LuceneService.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
@Override
|
||||
public boolean isSingleton() {
|
||||
return true;
|
||||
return Optional.ofNullable(this.luceneService)
|
||||
.<Class<?>>map(LuceneService::getClass)
|
||||
.orElse(LuceneService.class);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -23,6 +23,7 @@ import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
@@ -51,6 +52,14 @@ public abstract class SpringUtils {
|
||||
return bean;
|
||||
}
|
||||
|
||||
public static Optional<Object> getPropertyValue(BeanDefinition beanDefinition, String propertyName) {
|
||||
|
||||
return Optional.ofNullable(beanDefinition)
|
||||
.map(it -> it.getPropertyValues())
|
||||
.map(propertyValues -> propertyValues.getPropertyValue(propertyName))
|
||||
.map(propertyValue -> propertyValue.getValue());
|
||||
}
|
||||
|
||||
public static BeanDefinition setPropertyReference(BeanDefinition beanDefinition,
|
||||
String propertyName, String beanName) {
|
||||
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
/*
|
||||
* Copyright 2018 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.support;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.springframework.data.gemfire.util.ArrayUtils.nullSafeArray;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
|
||||
import org.apache.geode.cache.Region;
|
||||
import org.apache.shiro.util.Assert;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.springframework.beans.BeansException;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.config.BeanDefinition;
|
||||
import org.springframework.beans.factory.config.BeanPostProcessor;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.support.AbstractApplicationContext;
|
||||
import org.springframework.data.gemfire.RegionFactoryBean;
|
||||
import org.springframework.data.gemfire.test.mock.context.GemFireMockObjectsApplicationContextInitializer;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.test.context.ContextConfiguration;
|
||||
import org.springframework.test.context.junit4.SpringRunner;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link LuceneIndexRegionBeanFactoryPostProcessor}.
|
||||
*
|
||||
* @author John Blum
|
||||
* @see org.junit.Test
|
||||
* @see org.springframework.data.gemfire.config.support.LuceneIndexRegionBeanFactoryPostProcessor
|
||||
* @since 2.1.0
|
||||
*/
|
||||
@RunWith(SpringRunner.class)
|
||||
@ContextConfiguration(initializers = GemFireMockObjectsApplicationContextInitializer.class)
|
||||
public class LuceneIndexRegionBeanFactoryPostProcessorIntegrationTests {
|
||||
|
||||
private static final List<String> beanNames = new CopyOnWriteArrayList<>();
|
||||
|
||||
@Autowired
|
||||
private ApplicationContext applicationContext;
|
||||
|
||||
private BeanDefinition getBeanDefinition(String beanName) {
|
||||
|
||||
return Optional.ofNullable(this.applicationContext)
|
||||
.filter(it -> it instanceof AbstractApplicationContext)
|
||||
.map(it -> (AbstractApplicationContext) it)
|
||||
.map(it -> it.getBeanFactory())
|
||||
.map(beanFactory -> beanFactory.getBeanDefinition(beanName))
|
||||
.orElse(null);
|
||||
}
|
||||
@Test
|
||||
public void regionLuceneIndexAndDiskStoreBeanDependenciesAreCorrect() {
|
||||
|
||||
BeanDefinition mockDiskStore = getBeanDefinition("MockDiskStore");
|
||||
|
||||
assertThat(mockDiskStore).isNotNull();
|
||||
assertThat(nullSafeArray(mockDiskStore.getDependsOn(), String.class))
|
||||
.doesNotContain("BookTitleLuceneIndex", "ContractDescriptionLuceneIndex");
|
||||
|
||||
BeanDefinition bookTitleLuceneIndex = getBeanDefinition("BookTitleLuceneIndex");
|
||||
|
||||
assertThat(bookTitleLuceneIndex).isNotNull();
|
||||
assertThat(nullSafeArray(bookTitleLuceneIndex.getDependsOn(), String.class))
|
||||
.doesNotContain("Books", "BookTitleLuceneIndex", "ContractDescriptionLuceneIndex");
|
||||
|
||||
BeanDefinition contractDescriptionLuceneIndex = getBeanDefinition("ContractDescriptionLuceneIndex");
|
||||
|
||||
assertThat(contractDescriptionLuceneIndex).isNotNull();
|
||||
assertThat(nullSafeArray(contractDescriptionLuceneIndex.getDependsOn(), String.class))
|
||||
.doesNotContain("Contracts", "BookTitleLuceneIndex", "ContractDescriptionLuceneIndex");
|
||||
|
||||
BeanDefinition booksRegion = getBeanDefinition("Books");
|
||||
|
||||
assertThat(booksRegion).isNotNull();
|
||||
assertThat(booksRegion.getDependsOn()).contains("BookTitleLuceneIndex");
|
||||
assertThat(booksRegion.getDependsOn()).doesNotContain("ContractDescriptionLuceneIndex");
|
||||
|
||||
BeanDefinition contractsRegion = getBeanDefinition("Contracts");
|
||||
|
||||
assertThat(contractsRegion).isNotNull();
|
||||
assertThat(contractsRegion.getDependsOn()).contains("ContractDescriptionLuceneIndex");
|
||||
assertThat(contractsRegion.getDependsOn()).doesNotContain("BookTitleLuceneIndex");
|
||||
|
||||
BeanDefinition peopleRegion = getBeanDefinition("People");
|
||||
|
||||
assertThat(peopleRegion).isNotNull();
|
||||
assertThat(nullSafeArray(peopleRegion.getDependsOn(), String.class))
|
||||
.doesNotContain("BookTitleLuceneIndex", "ContractDescriptionLucenenIndex");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void gemfireBeanProcessingOrderIsCorrect() {
|
||||
assertThat(beanNames.indexOf("BookTitleLuceneIndex")).isLessThan(beanNames.indexOf("Books"));
|
||||
assertThat(beanNames.indexOf("ContractDescriptionLuceneIndex")).isLessThan(beanNames.indexOf("Contracts"));
|
||||
}
|
||||
|
||||
static class BeanProcessingOrderRecordingBeanPostProcessor implements BeanPostProcessor {
|
||||
|
||||
@Nullable @Override
|
||||
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
|
||||
|
||||
if (isRegionBean(bean)) {
|
||||
|
||||
Assert.isTrue(!"Books".equals(beanName) || beanNames.contains("BookTitleLuceneIndex"),
|
||||
"Expected [BookTitleLuceneIndex] to already exist");
|
||||
|
||||
Assert.isTrue(!"Contracts".equals(beanName) || beanNames.contains("ContractDescriptionLuceneIndex"),
|
||||
"Expected [ContractDescriptionLuceneIndex] to already exist");
|
||||
}
|
||||
|
||||
if (!beanNames.contains(beanName)) {
|
||||
beanNames.add(beanName);
|
||||
}
|
||||
|
||||
return bean;
|
||||
}
|
||||
|
||||
private boolean isRegionBean(Object bean) {
|
||||
return bean instanceof Region || bean instanceof RegionFactoryBean;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -31,6 +31,7 @@ import static org.mockito.Mockito.spy;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.springframework.data.gemfire.util.ArrayUtils.nullSafeArray;
|
||||
import static org.springframework.data.gemfire.util.CollectionUtils.asSet;
|
||||
import static org.springframework.data.gemfire.util.CollectionUtils.nullSafeMap;
|
||||
import static org.springframework.data.gemfire.util.CollectionUtils.nullSafeSet;
|
||||
import static org.springframework.data.gemfire.util.RuntimeExceptionFactory.NOT_SUPPORTED;
|
||||
import static org.springframework.data.gemfire.util.RuntimeExceptionFactory.newIllegalArgumentException;
|
||||
@@ -55,6 +56,7 @@ import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
import java.util.concurrent.CopyOnWriteArraySet;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
@@ -97,6 +99,13 @@ import org.apache.geode.cache.client.Pool;
|
||||
import org.apache.geode.cache.client.PoolFactory;
|
||||
import org.apache.geode.cache.control.ResourceManager;
|
||||
import org.apache.geode.cache.execute.RegionFunctionContext;
|
||||
import org.apache.geode.cache.lucene.LuceneIndex;
|
||||
import org.apache.geode.cache.lucene.LuceneIndexFactory;
|
||||
import org.apache.geode.cache.lucene.LuceneQuery;
|
||||
import org.apache.geode.cache.lucene.LuceneQueryFactory;
|
||||
import org.apache.geode.cache.lucene.LuceneQueryProvider;
|
||||
import org.apache.geode.cache.lucene.LuceneSerializer;
|
||||
import org.apache.geode.cache.lucene.LuceneService;
|
||||
import org.apache.geode.cache.query.CqAttributes;
|
||||
import org.apache.geode.cache.query.CqQuery;
|
||||
import org.apache.geode.cache.query.Index;
|
||||
@@ -118,6 +127,7 @@ import org.apache.geode.distributed.DistributedMember;
|
||||
import org.apache.geode.distributed.DistributedSystem;
|
||||
import org.apache.geode.internal.concurrent.ConcurrentHashSet;
|
||||
import org.apache.geode.pdx.PdxSerializer;
|
||||
import org.apache.lucene.analysis.Analyzer;
|
||||
import org.mockito.ArgumentMatchers;
|
||||
import org.mockito.stubbing.Answer;
|
||||
import org.springframework.data.gemfire.IndexType;
|
||||
@@ -277,6 +287,20 @@ public abstract class GemFireMockObjectsSupport extends MockObjectsSupport {
|
||||
}
|
||||
}
|
||||
|
||||
private static <T> T doSafeOperation(ExceptionThrowingOperation<T> operation) {
|
||||
return doSafeOperation(operation, null);
|
||||
}
|
||||
|
||||
private static <T> T doSafeOperation(ExceptionThrowingOperation<T> operation, T defaultValue) {
|
||||
|
||||
try {
|
||||
return operation.doExceptionThrowingOperation();
|
||||
}
|
||||
catch (Exception ignore) {
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether the given {@link Region} is a root {@link Region}.
|
||||
*
|
||||
@@ -400,6 +424,19 @@ public abstract class GemFireMockObjectsSupport extends MockObjectsSupport {
|
||||
newIllegalStateException("RegionAttributes with ID [%s] cannot be found", regionAttributesId));
|
||||
}
|
||||
|
||||
private static <T> T rethrowAsRuntimeException(ExceptionThrowingOperation<T> operation) {
|
||||
|
||||
try {
|
||||
return operation.doExceptionThrowingOperation();
|
||||
}
|
||||
catch (RuntimeException cause) {
|
||||
throw cause;
|
||||
}
|
||||
catch (Exception cause) {
|
||||
throw new RuntimeException(cause);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the given {@link String Region name} into a proper {@link Region#getName() Region name}.
|
||||
*
|
||||
@@ -1772,6 +1809,204 @@ public abstract class GemFireMockObjectsSupport extends MockObjectsSupport {
|
||||
return mock(IndexStatistics.class, mockObjectIdentifier(name));
|
||||
}
|
||||
|
||||
public static LuceneIndexFactory mockLuceneIndexFactory() {
|
||||
return mockLuceneIndexFactory(null);
|
||||
}
|
||||
|
||||
private static LuceneIndexFactory mockLuceneIndexFactory(Map<LuceneIndexKey, LuceneIndex> luceneIndexes) {
|
||||
|
||||
LuceneIndexFactory mockLuceneIndexFactory = mock(LuceneIndexFactory.class);
|
||||
|
||||
AtomicReference<LuceneSerializer> luceneSerializerReference = new AtomicReference<>(null);
|
||||
|
||||
Map<String, Analyzer> fieldAnalyzers = new ConcurrentHashMap<>();
|
||||
|
||||
Set<String> fields = new CopyOnWriteArraySet<>();
|
||||
|
||||
when(mockLuceneIndexFactory.addField(anyString())).thenAnswer(invocation -> {
|
||||
|
||||
String fieldName = invocation.getArgument(0);
|
||||
|
||||
fields.add(fieldName);
|
||||
|
||||
return mockLuceneIndexFactory;
|
||||
});
|
||||
|
||||
when(mockLuceneIndexFactory.addField(anyString(), any(Analyzer.class))).thenAnswer(invocation -> {
|
||||
|
||||
String fieldName = invocation.getArgument(0);
|
||||
Analyzer analyzer = invocation.getArgument(1);
|
||||
|
||||
fieldAnalyzers.put(fieldName, analyzer);
|
||||
|
||||
return mockLuceneIndexFactory;
|
||||
});
|
||||
|
||||
when(mockLuceneIndexFactory.setFields(ArgumentMatchers.<String[]>any())).thenAnswer(invocation -> {
|
||||
|
||||
Object[] fieldsArgument = invocation.getArguments();
|
||||
|
||||
fields.clear();
|
||||
|
||||
Arrays.stream(nullSafeArray(fieldsArgument, Object.class))
|
||||
.filter(field -> field instanceof String)
|
||||
.map(String::valueOf)
|
||||
.forEach(fields::add);
|
||||
|
||||
return mockLuceneIndexFactory;
|
||||
});
|
||||
|
||||
when(mockLuceneIndexFactory.setFields(any(Map.class))).thenAnswer(invocation -> {
|
||||
|
||||
Map<String, Analyzer> fieldAnalyzersArgument = invocation.getArgument(0);
|
||||
|
||||
fieldAnalyzers.clear();
|
||||
fieldAnalyzers.putAll(nullSafeMap(fieldAnalyzers));
|
||||
|
||||
return mockLuceneIndexFactory;
|
||||
});
|
||||
|
||||
when(mockLuceneIndexFactory.setLuceneSerializer(any(LuceneSerializer.class))).thenAnswer(invocation -> {
|
||||
|
||||
Optional.ofNullable(invocation.<LuceneSerializer>getArgument(0))
|
||||
.map(luceneSerializer -> {
|
||||
luceneSerializerReference.set(luceneSerializer);
|
||||
return luceneSerializer;
|
||||
})
|
||||
.orElseGet(() -> {
|
||||
luceneSerializerReference.set(null);
|
||||
return null;
|
||||
});
|
||||
|
||||
return mockLuceneIndexFactory;
|
||||
});
|
||||
|
||||
doAnswer(invocation -> {
|
||||
|
||||
String indexName = invocation.getArgument(0);
|
||||
String regionPath = invocation.getArgument(1);
|
||||
|
||||
LuceneIndexKey key = LuceneIndexKey.of(indexName, regionPath);
|
||||
|
||||
LuceneIndex mockLuceneIndex = mock(LuceneIndex.class, key.toString());
|
||||
|
||||
when(mockLuceneIndex.getFieldAnalyzers()).thenReturn(Collections.unmodifiableMap(fieldAnalyzers));
|
||||
when(mockLuceneIndex.getFieldNames()).thenAnswer(in -> fields.toArray(new String[fields.size()]));
|
||||
when(mockLuceneIndex.getLuceneSerializer()).thenAnswer(in -> luceneSerializerReference.get());
|
||||
when(mockLuceneIndex.getName()).thenReturn(indexName);
|
||||
when(mockLuceneIndex.getRegionPath()).thenReturn(regionPath);
|
||||
|
||||
Optional.ofNullable(luceneIndexes).ifPresent(it -> it.put(key, mockLuceneIndex));
|
||||
|
||||
return null;
|
||||
|
||||
}).when(mockLuceneIndexFactory).create(anyString(), anyString());
|
||||
|
||||
return mockLuceneIndexFactory;
|
||||
}
|
||||
|
||||
public static LuceneQueryFactory mockLuceneQueryFactory() {
|
||||
|
||||
LuceneQueryFactory mockLuceneQueryFactory = mock(LuceneQueryFactory.class);
|
||||
|
||||
AtomicInteger limit = new AtomicInteger(LuceneQueryFactory.DEFAULT_LIMIT);
|
||||
AtomicInteger pageSize = new AtomicInteger(LuceneQueryFactory.DEFAULT_PAGESIZE);
|
||||
|
||||
when(mockLuceneQueryFactory.setLimit(anyInt())).thenAnswer(invocation -> {
|
||||
limit.set(invocation.getArgument(0));
|
||||
return mockLuceneQueryFactory;
|
||||
});
|
||||
|
||||
when(mockLuceneQueryFactory.setPageSize(anyInt())).thenAnswer(invocation -> {
|
||||
pageSize.set(invocation.getArgument(0));
|
||||
return mockLuceneQueryFactory;
|
||||
});
|
||||
|
||||
when(mockLuceneQueryFactory.create(anyString(), anyString(), any(LuceneQueryProvider.class)))
|
||||
.thenAnswer(invocation -> mockLuceneQuery(limit.get(), pageSize.get()));
|
||||
|
||||
when(mockLuceneQueryFactory.create(anyString(), anyString(), anyString(), anyString()))
|
||||
.thenAnswer(invocation -> mockLuceneQuery(limit.get(), pageSize.get()));
|
||||
|
||||
return mockLuceneQueryFactory;
|
||||
}
|
||||
|
||||
private static LuceneQuery mockLuceneQuery(int limit, int pageSize) {
|
||||
|
||||
LuceneQuery mockLuceneQuery = mock(LuceneQuery.class);
|
||||
|
||||
when(mockLuceneQuery.getLimit()).thenReturn(limit);
|
||||
when(mockLuceneQuery.getPageSize()).thenReturn(pageSize);
|
||||
|
||||
doSafeOperation(() -> when(mockLuceneQuery.findKeys())
|
||||
.thenReturn(Collections.emptySet()), Collections.emptySet());
|
||||
|
||||
rethrowAsRuntimeException(() -> when(mockLuceneQuery.findPages())
|
||||
.thenThrow(newUnsupportedOperationException("Operation Not Supported!")));
|
||||
|
||||
doSafeOperation(() -> when(mockLuceneQuery.findResults())
|
||||
.thenReturn(Collections.emptyList()), Collections.emptyList());
|
||||
|
||||
doSafeOperation(() -> when(mockLuceneQuery.findValues())
|
||||
.thenReturn(Collections.emptyList()), Collections.emptyList());
|
||||
|
||||
return mockLuceneQuery;
|
||||
}
|
||||
|
||||
public static LuceneService mockLuceneService(Cache mockCache) {
|
||||
|
||||
LuceneService mockLuceneService = mock(LuceneService.class);
|
||||
|
||||
Map<LuceneIndexKey, LuceneIndex> luceneIndexes = new ConcurrentHashMap<>();
|
||||
|
||||
when(mockLuceneService.createIndexFactory())
|
||||
.thenAnswer(invocation -> mockLuceneIndexFactory(luceneIndexes));
|
||||
|
||||
when(mockLuceneService.createLuceneQueryFactory())
|
||||
.thenAnswer(invocation -> mockLuceneQueryFactory());
|
||||
|
||||
doAnswer(invocation -> {
|
||||
|
||||
String indexName = invocation.getArgument(0);
|
||||
String regionName = invocation .getArgument(1);
|
||||
|
||||
luceneIndexes.remove(LuceneIndexKey.of(indexName, regionName));
|
||||
|
||||
return null;
|
||||
|
||||
}).when(mockLuceneService).destroyIndex(anyString(), anyString());
|
||||
|
||||
doAnswer(invocation -> {
|
||||
|
||||
String regionPath = invocation.getArgument(0);
|
||||
|
||||
luceneIndexes.keySet().stream().filter(key -> key.getRegionPath().equals(regionPath))
|
||||
.collect(Collectors.toSet()).forEach(key -> luceneIndexes.remove(key));
|
||||
|
||||
return null;
|
||||
|
||||
}).when(mockLuceneService).destroyIndexes(anyString());
|
||||
|
||||
when(mockLuceneService.getAllIndexes()).thenAnswer(invocation ->
|
||||
Collections.unmodifiableCollection(luceneIndexes.values()));
|
||||
|
||||
when(mockLuceneService.getCache()).thenReturn(mockCache);
|
||||
|
||||
when(mockLuceneService.getIndex(anyString(), anyString())).thenAnswer(invocation -> {
|
||||
|
||||
String indexName = invocation.getArgument(0);
|
||||
String regionPath = invocation.getArgument(1);
|
||||
|
||||
return luceneIndexes.get(LuceneIndexKey.of(indexName, regionPath));
|
||||
});
|
||||
|
||||
doSafeOperation(() ->
|
||||
when(mockLuceneService.waitUntilFlushed(anyString(), anyString(), anyLong(), any(TimeUnit.class)))
|
||||
.thenReturn(true));
|
||||
|
||||
return mockLuceneService;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public static <K, V> Region<K, V> mockRegion(RegionService regionService, String name,
|
||||
RegionAttributes<K, V> regionAttributes) {
|
||||
@@ -2550,7 +2785,78 @@ public abstract class GemFireMockObjectsSupport extends MockObjectsSupport {
|
||||
return clientCacheFactorySpy;
|
||||
}
|
||||
|
||||
protected interface ExceptionThrowingOperation<T> {
|
||||
T doExceptionThrowingOperation() throws Exception;
|
||||
}
|
||||
|
||||
protected interface IoExceptionThrowingOperation {
|
||||
void doIo() throws IOException;
|
||||
}
|
||||
|
||||
public static class LuceneIndexKey {
|
||||
|
||||
private final String indexName;
|
||||
private final String regionPath;
|
||||
|
||||
public static LuceneIndexKey of(String indexName, Region<?, ?> region) {
|
||||
|
||||
Assert.notNull(region, "Region is required");
|
||||
|
||||
return of(indexName, region.getFullPath());
|
||||
}
|
||||
|
||||
public static LuceneIndexKey of(String indexName, String regionPath) {
|
||||
return new LuceneIndexKey(indexName, regionPath);
|
||||
}
|
||||
|
||||
private LuceneIndexKey(String indexName, String regionPath) {
|
||||
|
||||
Assert.hasText(indexName, String.format("LuceneIndex name [%s] is required", indexName));
|
||||
Assert.hasText(regionPath, String.format("Region path [%s] is required", regionPath));
|
||||
|
||||
this.indexName = indexName;
|
||||
this.regionPath = regionPath;
|
||||
}
|
||||
|
||||
protected String getIndexName() {
|
||||
return this.indexName;
|
||||
}
|
||||
|
||||
protected String getRegionPath() {
|
||||
return this.regionPath;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
|
||||
if (obj == this) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!(obj instanceof LuceneIndexKey)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
LuceneIndexKey that = (LuceneIndexKey) obj;
|
||||
|
||||
return this.getIndexName().equals(that.getIndexName())
|
||||
&& this.getRegionPath().equals(that.getRegionPath());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
|
||||
int hashValue = 17;
|
||||
|
||||
hashValue = 37 * hashValue + getIndexName().hashCode();
|
||||
hashValue = 37 * hashValue + getRegionPath().hashCode();
|
||||
|
||||
return hashValue;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format("%1$s.%2$s", getRegionPath(), getIndexName());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,8 +18,12 @@
|
||||
package org.springframework.data.gemfire.util;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.doReturn;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.spy;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
@@ -27,6 +31,7 @@ import static org.springframework.data.gemfire.util.ArrayUtils.asArray;
|
||||
import static org.springframework.data.gemfire.util.RuntimeExceptionFactory.newIllegalStateException;
|
||||
import static org.springframework.data.gemfire.util.RuntimeExceptionFactory.newRuntimeException;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
@@ -35,6 +40,7 @@ import org.junit.runner.RunWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.MockitoJUnitRunner;
|
||||
import org.springframework.beans.MutablePropertyValues;
|
||||
import org.springframework.beans.PropertyValue;
|
||||
import org.springframework.beans.factory.config.BeanDefinition;
|
||||
import org.springframework.beans.factory.config.RuntimeBeanReference;
|
||||
|
||||
@@ -61,7 +67,8 @@ public class SpringUtilsUnitTests {
|
||||
|
||||
when(mockBeanDefinition.getDependsOn()).thenReturn(asArray("testBeanNameOne", "testBeanNameTwo"));
|
||||
|
||||
assertThat(SpringUtils.addDependsOn(mockBeanDefinition, "testBeanNameThree")).isSameAs(mockBeanDefinition);
|
||||
assertThat(SpringUtils.addDependsOn(mockBeanDefinition, "testBeanNameThree"))
|
||||
.isSameAs(mockBeanDefinition);
|
||||
|
||||
verify(mockBeanDefinition, times(1)).getDependsOn();
|
||||
verify(mockBeanDefinition, times(1))
|
||||
@@ -73,7 +80,8 @@ public class SpringUtilsUnitTests {
|
||||
|
||||
when(mockBeanDefinition.getDependsOn()).thenReturn(null);
|
||||
|
||||
assertThat(SpringUtils.addDependsOn(mockBeanDefinition, "testBeanName")).isSameAs(mockBeanDefinition);
|
||||
assertThat(SpringUtils.addDependsOn(mockBeanDefinition, "testBeanName"))
|
||||
.isSameAs(mockBeanDefinition);
|
||||
|
||||
verify(mockBeanDefinition, times(1)).getDependsOn();
|
||||
verify(mockBeanDefinition, times(1)).setDependsOn("testBeanName");
|
||||
@@ -92,6 +100,69 @@ public class SpringUtilsUnitTests {
|
||||
.setDependsOn("testBeanNameOne", "testBeanNameTwo", "testBeanNameThree", "testBeanNameFour");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getPropertyValueForExistingPropertyHavingValueReturnsValue() {
|
||||
|
||||
MutablePropertyValues propertyValues =
|
||||
new MutablePropertyValues(Collections.singletonMap("testProperty", "testValue"));
|
||||
|
||||
when(mockBeanDefinition.getPropertyValues()).thenReturn(propertyValues);
|
||||
|
||||
assertThat(SpringUtils.getPropertyValue(mockBeanDefinition, "testProperty").orElse(null))
|
||||
.isEqualTo("testValue");
|
||||
|
||||
verify(mockBeanDefinition, times(1)).getPropertyValues();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getPropertyValueForExistingPropertyHavingNullValueReturnsNull() {
|
||||
|
||||
MutablePropertyValues testPropertyValues = spy(new MutablePropertyValues());
|
||||
|
||||
PropertyValue testPropertyValue = spy(new PropertyValue("testProperty", null));
|
||||
|
||||
when(mockBeanDefinition.getPropertyValues()).thenReturn(testPropertyValues);
|
||||
doReturn(testPropertyValue).when(testPropertyValues).getPropertyValue(anyString());
|
||||
|
||||
assertThat(SpringUtils.getPropertyValue(mockBeanDefinition, "testProperty").orElse(null))
|
||||
.isNull();
|
||||
|
||||
verify(mockBeanDefinition, times(1)).getPropertyValues();
|
||||
verify(testPropertyValues, times(1)).getPropertyValue(eq("testProperty"));
|
||||
verify(testPropertyValue, times(1)).getValue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getPropertyValueForNonExistingPropertyReturnsNull() {
|
||||
|
||||
MutablePropertyValues testPropertyValues = spy(new MutablePropertyValues());
|
||||
|
||||
when(mockBeanDefinition.getPropertyValues()).thenReturn(testPropertyValues);
|
||||
|
||||
assertThat(SpringUtils.getPropertyValue(mockBeanDefinition, "testProperty").orElse(null))
|
||||
.isNull();
|
||||
|
||||
verify(mockBeanDefinition, times(1)).getPropertyValues();
|
||||
verify(testPropertyValues, times(1)).getPropertyValue(eq("testProperty"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getPropertyValueWithNullPropertyValuesReturnsNull() {
|
||||
|
||||
when(mockBeanDefinition.getPropertyValues()).thenReturn(null);
|
||||
|
||||
assertThat(SpringUtils.getPropertyValue(mockBeanDefinition, "testProperty").orElse(null))
|
||||
.isNull();
|
||||
|
||||
verify(mockBeanDefinition, times(1)).getPropertyValues();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getPropertyValueWithNullBeanDefinitionReturnsNull() {
|
||||
assertThat(SpringUtils.getPropertyValue(null, "testProperty").orElse(null))
|
||||
.isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("all")
|
||||
public void setBeanDefinitionPropertyReference() {
|
||||
@@ -136,6 +207,7 @@ public class SpringUtilsUnitTests {
|
||||
|
||||
@Test
|
||||
public void defaultIfEmptyReturnsValue() {
|
||||
|
||||
assertThat(SpringUtils.defaultIfEmpty("test", "DEFAULT")).isEqualTo("test");
|
||||
assertThat(SpringUtils.defaultIfEmpty("abc123", "DEFAULT")).isEqualTo("abc123");
|
||||
assertThat(SpringUtils.defaultIfEmpty("123", "DEFAULT")).isEqualTo("123");
|
||||
@@ -148,6 +220,7 @@ public class SpringUtilsUnitTests {
|
||||
|
||||
@Test
|
||||
public void defaultIfEmptyReturnsDefault() {
|
||||
|
||||
assertThat(SpringUtils.defaultIfEmpty(" ", "DEFAULT")).isEqualTo("DEFAULT");
|
||||
assertThat(SpringUtils.defaultIfEmpty("", "DEFAULT")).isEqualTo("DEFAULT");
|
||||
assertThat(SpringUtils.defaultIfEmpty(null, "DEFAULT")).isEqualTo("DEFAULT");
|
||||
@@ -155,6 +228,7 @@ public class SpringUtilsUnitTests {
|
||||
|
||||
@Test
|
||||
public void defaultIfNullReturnsValue() {
|
||||
|
||||
assertThat(SpringUtils.defaultIfNull(true, false)).isTrue();
|
||||
assertThat(SpringUtils.defaultIfNull('x', 'A')).isEqualTo('x');
|
||||
assertThat(SpringUtils.defaultIfNull(1, 2)).isEqualTo(1);
|
||||
@@ -164,6 +238,7 @@ public class SpringUtilsUnitTests {
|
||||
|
||||
@Test
|
||||
public void defaultIfNullReturnsDefault() {
|
||||
|
||||
assertThat(SpringUtils.defaultIfNull(null, false)).isFalse();
|
||||
assertThat(SpringUtils.defaultIfNull(null, 'A')).isEqualTo('A');
|
||||
assertThat(SpringUtils.defaultIfNull(null, 2)).isEqualTo(2);
|
||||
@@ -174,6 +249,7 @@ public class SpringUtilsUnitTests {
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
public void defaultIfNullWithSupplierReturnsValue() {
|
||||
|
||||
Supplier<String> mockSupplier = mock(Supplier.class);
|
||||
|
||||
assertThat(SpringUtils.defaultIfNull("value", mockSupplier)).isEqualTo("value");
|
||||
@@ -184,6 +260,7 @@ public class SpringUtilsUnitTests {
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
public void defaultIfNullWithSupplierReturnsSupplierValue() {
|
||||
|
||||
Supplier<String> mockSupplier = mock(Supplier.class);
|
||||
|
||||
when(mockSupplier.get()).thenReturn("supplier");
|
||||
@@ -200,6 +277,7 @@ public class SpringUtilsUnitTests {
|
||||
|
||||
@Test
|
||||
public void equalsIgnoreNullIsTrue() {
|
||||
|
||||
assertThat(SpringUtils.equalsIgnoreNull(null, null)).isTrue();
|
||||
assertThat(SpringUtils.equalsIgnoreNull(true, true)).isTrue();
|
||||
assertThat(SpringUtils.equalsIgnoreNull('x', 'x')).isTrue();
|
||||
@@ -211,6 +289,7 @@ public class SpringUtilsUnitTests {
|
||||
|
||||
@Test
|
||||
public void equalsIgnoreNullIsFalse() {
|
||||
|
||||
assertThat(SpringUtils.equalsIgnoreNull(null, "null")).isFalse();
|
||||
assertThat(SpringUtils.equalsIgnoreNull(true, false)).isFalse();
|
||||
assertThat(SpringUtils.equalsIgnoreNull('x', 'X')).isFalse();
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<beans xmlns="http://www.springframework.org/schema/beans"
|
||||
xmlns:gfe="http://www.springframework.org/schema/gemfire"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="
|
||||
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
|
||||
http://www.springframework.org/schema/gemfire http://www.springframework.org/schema/gemfire/spring-gemfire.xsd
|
||||
">
|
||||
|
||||
<gfe:cache/>
|
||||
|
||||
<bean class="org.springframework.data.gemfire.config.support.LuceneIndexRegionBeanFactoryPostProcessor"/>
|
||||
|
||||
<bean class="org.springframework.data.gemfire.config.support.LuceneIndexRegionBeanFactoryPostProcessorIntegrationTests.BeanProcessingOrderRecordingBeanPostProcessor"/>
|
||||
|
||||
<bean id="MockLuceneService" class="org.springframework.data.gemfire.test.mock.GemFireMockObjectsSupport"
|
||||
factory-method="mockLuceneService">
|
||||
<constructor-arg index="0" ref="gemfireCache"/>
|
||||
</bean>
|
||||
|
||||
<gfe:partitioned-region id="Books" persistent="false"/>
|
||||
<gfe:partitioned-region id="Contracts" persistent="false"/>
|
||||
<gfe:partitioned-region id="People" persistent="false"/>
|
||||
|
||||
<gfe:lucene-index id="BookTitleLuceneIndex" lucene-service-ref="MockLuceneService"
|
||||
fields="title" region-path="/Books"/>
|
||||
|
||||
<gfe:lucene-index id="ContractDescriptionLuceneIndex" lucene-service-ref="MockLuceneService"
|
||||
fields="description" region-path="/Contracts"/>
|
||||
|
||||
<gfe:disk-store id="MockDiskStore"/>
|
||||
|
||||
</beans>
|
||||
Reference in New Issue
Block a user