diff --git a/src/main/asciidoc/reference/lucene.adoc b/src/main/asciidoc/reference/lucene.adoc index 2967fce2..d6a2d152 100644 --- a/src/main/asciidoc/reference/lucene.adoc +++ b/src/main/asciidoc/reference/lucene.adoc @@ -5,12 +5,12 @@ http://geode.apache.org/[Apache Geode] integrates with http://lucene.apache.org/ to index and search on data stored in Apache Geode using Lucene queries. Search-based queries also includes the capability to page through query results. -Additionally, _Spring Data Geode_ adds support for query projections based on _Spring Data Commons_ +Additionally, _Spring Data for Apache Geode_ 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 Apache Geode)_ 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 `` element like this, ``. +in the nested `` element like this, ``. -Alternatively, a `LuceneIndex` can be declared in _Spring_ Java config, inside a `@Configuration` class with... +Spring Data for Apache Geode's `LuceneIndexFactoryBean` API and SDG's XML namespace also allows the addition of a +http://geode.apache.org/releases/latest/javadoc/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] +---- + + + + + +---- + +Of course, you may specify the `LuceneSerializer` as a anonymous, nested bean definition as well, like so... + +[source,xml] +---- + + + + + +---- + +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 peopleRegion(GemFireCache gemfireCache) { - PartitionedRegionFactoryBean peopleRegion = new PartitionedRegionFactoryBean<>(); +@Bean(name = "Books") +@DependsOn("bookTitleIndex") +PartitionedRegionFactoryBean booksRegion(GemFireCache gemfireCache) { + + PartitionedRegionFactoryBean peopleRegion = + new PartitionedRegionFactoryBean<>(); peopleRegion.setCache(gemfireCache); peopleRegion.setClose(false); @@ -57,34 +87,57 @@ PartitionedRegionFactoryBean 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 Apache Geode's, Apache Lucene integration support. First, a `LuceneIndex` can only -be created on a Geode `PARTITION` Region. Second, all `LuceneIndexes` must be created before the the Region on which -the `LuceneIndex` is applied. +There are a few limitations of Apache Geode's, Apache Lucene integration and support. + +First, a `LuceneIndex` can only be created on an Apache Geode `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 `` +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 Apache Geode 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 Geode_ 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 Apache Geode_ 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 Apache Geode http://geode.apache.org/releases/latest/javadoc/org/apache/geode/cache/lucene/package-frame.html[Lucene types]. @@ -119,17 +172,17 @@ NOTE: The `[, int resultLimit]` indicates that the `resultLimit` parameter is op The operations in the `LuceneOperations` interface match the operations provided by the Apache Geode'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 Geode or Lucene `Exceptions` into _Spring's_ highly -consistent and expressive DAO +However, SDG has the added value of translating proprietary Apache Geode 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 Apache Geode 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 Apache Geode and Apache Lucene -data types (e.g. Geode'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 Apache Geode +and Apache Lucene data types (e.g. Apache Geode'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 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 firstPage = customers.getContent(); ---- Conveniently, the _Spring Data Commons_ `Page` interface implements `java.lang.Iterable` 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 Geode_ provides Annotation configuration support for `LuceneIndexes`. Eventually, the SDG Lucene -support will find its way into the _Repository_ infrastructure extension for Apache Geode so that Lucene queries -can be expressed as methods on an application `Repository` interface, much like the +Finally, _Spring Data for Apache Geode_ provides Annotation configuration support for `LuceneIndexes`. +Eventually, the SDG Lucene support will find its way into the _Repository_ infrastructure extension for Apache Geode +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. diff --git a/src/main/java/org/springframework/data/gemfire/IndexFactoryBean.java b/src/main/java/org/springframework/data/gemfire/IndexFactoryBean.java index ca1942e2..5e2283db 100644 --- a/src/main/java/org/springframework/data/gemfire/IndexFactoryBean.java +++ b/src/main/java/org/springframework/data/gemfire/IndexFactoryBean.java @@ -102,9 +102,6 @@ public class IndexFactoryBean extends AbstractFactoryBeanSupport implemen private String indexName; private String name; - /** - * @inheritDoc - */ @Override public void afterPropertiesSet() throws Exception { diff --git a/src/main/java/org/springframework/data/gemfire/config/support/AbstractDependencyStructuringBeanFactoryPostProcessor.java b/src/main/java/org/springframework/data/gemfire/config/support/AbstractDependencyStructuringBeanFactoryPostProcessor.java index 7b11f0be..1f674f11 100644 --- a/src/main/java/org/springframework/data/gemfire/config/support/AbstractDependencyStructuringBeanFactoryPostProcessor.java +++ b/src/main/java/org/springframework/data/gemfire/config/support/AbstractDependencyStructuringBeanFactoryPostProcessor.java @@ -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 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 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 isRegionBeanType() { + + Predicate 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)); + } } diff --git a/src/main/java/org/springframework/data/gemfire/config/support/LuceneIndexRegionBeanFactoryPostProcessor.java b/src/main/java/org/springframework/data/gemfire/config/support/LuceneIndexRegionBeanFactoryPostProcessor.java new file mode 100644 index 00000000..5a034318 --- /dev/null +++ b/src/main/java/org/springframework/data/gemfire/config/support/LuceneIndexRegionBeanFactoryPostProcessor.java @@ -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 regionNameToLuceneIndexBeanName = new HashMap<>(); + Map 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 resolveRegionNameFromLuceneIndex(BeanDefinition beanDefinition) { + + return getPropertyValue(beanDefinition, "regionPath") + .map(this::asFullyQualifiedRegionName); + } + + private Optional 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 value) { + + return value.map(String::valueOf) + .map(String::trim) + .filter(StringUtils::hasText) + .orElse(null); + } +} diff --git a/src/main/java/org/springframework/data/gemfire/search/lucene/LuceneIndexFactoryBean.java b/src/main/java/org/springframework/data/gemfire/search/lucene/LuceneIndexFactoryBean.java index e8f67bc8..8b67f2d3 100644 --- a/src/main/java/org/springframework/data/gemfire/search/lucene/LuceneIndexFactoryBean.java +++ b/src/main/java/org/springframework/data/gemfire/search/lucene/LuceneIndexFactoryBean.java @@ -105,9 +105,6 @@ public class LuceneIndexFactoryBean extends AbstractFactoryBeanSupport, InitializingBean { +public class LuceneServiceFactoryBean extends AbstractFactoryBeanSupport implements InitializingBean { private GemFireCache gemfireCache; @@ -49,6 +50,7 @@ public class LuceneServiceFactoryBean implements FactoryBean, Ini */ @Override public void afterPropertiesSet() throws Exception { + GemFireCache gemfireCache = getCache(); Assert.state(gemfireCache != null, @@ -83,15 +85,10 @@ public class LuceneServiceFactoryBean implements FactoryBean, Ini */ @Override public Class getObjectType() { - return Optional.ofNullable(this.luceneService).>map(LuceneService::getClass).orElse(LuceneService.class); - } - /** - * @inheritDoc - */ - @Override - public boolean isSingleton() { - return true; + return Optional.ofNullable(this.luceneService) + .>map(LuceneService::getClass) + .orElse(LuceneService.class); } /** diff --git a/src/main/java/org/springframework/data/gemfire/util/SpringUtils.java b/src/main/java/org/springframework/data/gemfire/util/SpringUtils.java index 6c60bccf..c1d20788 100644 --- a/src/main/java/org/springframework/data/gemfire/util/SpringUtils.java +++ b/src/main/java/org/springframework/data/gemfire/util/SpringUtils.java @@ -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 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) { diff --git a/src/test/java/org/springframework/data/gemfire/config/support/LuceneIndexRegionBeanFactoryPostProcessorIntegrationTests.java b/src/test/java/org/springframework/data/gemfire/config/support/LuceneIndexRegionBeanFactoryPostProcessorIntegrationTests.java new file mode 100644 index 00000000..15fcd01e --- /dev/null +++ b/src/test/java/org/springframework/data/gemfire/config/support/LuceneIndexRegionBeanFactoryPostProcessorIntegrationTests.java @@ -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 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; + } + } +} diff --git a/src/test/java/org/springframework/data/gemfire/test/mock/GemFireMockObjectsSupport.java b/src/test/java/org/springframework/data/gemfire/test/mock/GemFireMockObjectsSupport.java index be220ba4..269beb4e 100644 --- a/src/test/java/org/springframework/data/gemfire/test/mock/GemFireMockObjectsSupport.java +++ b/src/test/java/org/springframework/data/gemfire/test/mock/GemFireMockObjectsSupport.java @@ -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 doSafeOperation(ExceptionThrowingOperation operation) { + return doSafeOperation(operation, null); + } + + private static T doSafeOperation(ExceptionThrowingOperation 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 rethrowAsRuntimeException(ExceptionThrowingOperation 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 luceneIndexes) { + + LuceneIndexFactory mockLuceneIndexFactory = mock(LuceneIndexFactory.class); + + AtomicReference luceneSerializerReference = new AtomicReference<>(null); + + Map fieldAnalyzers = new ConcurrentHashMap<>(); + + Set 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.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 fieldAnalyzersArgument = invocation.getArgument(0); + + fieldAnalyzers.clear(); + fieldAnalyzers.putAll(nullSafeMap(fieldAnalyzers)); + + return mockLuceneIndexFactory; + }); + + when(mockLuceneIndexFactory.setLuceneSerializer(any(LuceneSerializer.class))).thenAnswer(invocation -> { + + Optional.ofNullable(invocation.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 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 Region mockRegion(RegionService regionService, String name, RegionAttributes regionAttributes) { @@ -2550,7 +2785,78 @@ public abstract class GemFireMockObjectsSupport extends MockObjectsSupport { return clientCacheFactorySpy; } + protected interface ExceptionThrowingOperation { + 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()); + } + } } diff --git a/src/test/java/org/springframework/data/gemfire/util/SpringUtilsUnitTests.java b/src/test/java/org/springframework/data/gemfire/util/SpringUtilsUnitTests.java index 7937f6c6..0323d93f 100644 --- a/src/test/java/org/springframework/data/gemfire/util/SpringUtilsUnitTests.java +++ b/src/test/java/org/springframework/data/gemfire/util/SpringUtilsUnitTests.java @@ -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,10 +67,12 @@ 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)).setDependsOn("testBeanNameOne", "testBeanNameTwo", "testBeanNameThree"); + verify(mockBeanDefinition, times(1)) + .setDependsOn("testBeanNameOne", "testBeanNameTwo", "testBeanNameThree"); } @Test @@ -72,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"); @@ -91,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() { @@ -135,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"); @@ -147,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"); @@ -154,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); @@ -163,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); @@ -173,6 +249,7 @@ public class SpringUtilsUnitTests { @Test @SuppressWarnings("unchecked") public void defaultIfNullWithSupplierReturnsValue() { + Supplier mockSupplier = mock(Supplier.class); assertThat(SpringUtils.defaultIfNull("value", mockSupplier)).isEqualTo("value"); @@ -183,6 +260,7 @@ public class SpringUtilsUnitTests { @Test @SuppressWarnings("unchecked") public void defaultIfNullWithSupplierReturnsSupplierValue() { + Supplier mockSupplier = mock(Supplier.class); when(mockSupplier.get()).thenReturn("supplier"); @@ -199,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(); @@ -210,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(); diff --git a/src/test/resources/org/springframework/data/gemfire/config/support/LuceneIndexRegionBeanFactoryPostProcessorIntegrationTests-context.xml b/src/test/resources/org/springframework/data/gemfire/config/support/LuceneIndexRegionBeanFactoryPostProcessorIntegrationTests-context.xml new file mode 100644 index 00000000..e4c74f6d --- /dev/null +++ b/src/test/resources/org/springframework/data/gemfire/config/support/LuceneIndexRegionBeanFactoryPostProcessorIntegrationTests-context.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + +