diff --git a/src/main/java/org/springframework/data/gemfire/search/lucene/LuceneOperations.java b/src/main/java/org/springframework/data/gemfire/search/lucene/LuceneOperations.java index ffa7849c..87eb8deb 100644 --- a/src/main/java/org/springframework/data/gemfire/search/lucene/LuceneOperations.java +++ b/src/main/java/org/springframework/data/gemfire/search/lucene/LuceneOperations.java @@ -106,6 +106,7 @@ public interface LuceneOperations { * to execute as well as de/serialize to distribute across the cluster. * @param projectionFields array of {@link String} values specifying the query projection. * @return a {@link List} of {@link LuceneResultStruct} containing the query results. + * @see org.apache.geode.cache.lucene.LuceneQueryProvider * @see org.apache.geode.cache.lucene.LuceneResultStruct * @see #query(LuceneQueryProvider, int, String...) * @see java.util.List @@ -126,6 +127,7 @@ public interface LuceneOperations { * @param resultLimit limit on the number of query results to return. * @param projectionFields array of {@link String} values specifying the query projection. * @return a {@link List} of {@link LuceneResultStruct} containing the query results. + * @see org.apache.geode.cache.lucene.LuceneQueryProvider * @see org.apache.geode.cache.lucene.LuceneResultStruct * @see java.util.List */ @@ -144,6 +146,7 @@ public interface LuceneOperations { * @param pageSize number of results per page. * @param projectionFields array of {@link String} values specifying the query projection. * @return a {@link PageableLuceneQueryResults} data structure containing the results of the Lucene query. + * @see org.apache.geode.cache.lucene.LuceneQueryProvider * @see org.apache.geode.cache.lucene.PageableLuceneQueryResults */ PageableLuceneQueryResults query(LuceneQueryProvider queryProvider, @@ -192,6 +195,7 @@ public interface LuceneOperations { * @param queryProvider {@link LuceneQueryProvider} is a provider implementation supplying the Lucene query * to execute as well as de/serialize to distribute across the cluster. * @return a {@link Collection} of keys matching the Lucene query clause (predicate). + * @see org.apache.geode.cache.lucene.LuceneQueryProvider * @see #queryForKeys(String, String, int) * @see java.util.Collection */ @@ -208,6 +212,7 @@ public interface LuceneOperations { * to execute as well as de/serialize to distribute across the cluster. * @param resultLimit limit on the number of keys returned. * @return a {@link Collection} of keys matching the Lucene query clause (predicate). + * @see org.apache.geode.cache.lucene.LuceneQueryProvider * @see #queryForKeys(String, String, int) * @see java.util.Collection */ @@ -256,6 +261,7 @@ public interface LuceneOperations { * @param queryProvider {@link LuceneQueryProvider} is a provider implementation supplying the Lucene query * to execute as well as de/serialize to distribute across the cluster. * @return a {@link Collection} of values matching Lucene query clause (predicate). + * @see org.apache.geode.cache.lucene.LuceneQueryProvider * @see #queryForValues(String, String, int) * @see java.util.Collection */ @@ -272,6 +278,7 @@ public interface LuceneOperations { * to execute as well as de/serialize to distribute across the cluster. * @param resultLimit limit on the number of values returned. * @return a {@link Collection} of values matching Lucene query clause (predicate). + * @see org.apache.geode.cache.lucene.LuceneQueryProvider * @see #queryForValues(String, String, int) * @see java.util.Collection */ diff --git a/src/main/java/org/springframework/data/gemfire/search/lucene/MappingLuceneOperations.java b/src/main/java/org/springframework/data/gemfire/search/lucene/MappingLuceneOperations.java deleted file mode 100644 index 999fcac2..00000000 --- a/src/main/java/org/springframework/data/gemfire/search/lucene/MappingLuceneOperations.java +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Copyright 2016 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ - -package org.springframework.data.gemfire.search.lucene; - -import java.util.List; - -import org.apache.geode.cache.lucene.LuceneQueryProvider; -import org.springframework.data.domain.Page; - -/** - * The {@link MappingLuceneOperations} interface defines a contract for implementing classes to execute - * Lucene data access operations and mapping the results to entity domain {@link Class types}. - * - * @author John Blum - * @see org.springframework.data.domain.Page - * @see org.springframework.data.gemfire.search.lucene.LuceneOperations - * @see org.apache.geode.cache.lucene.LuceneQueryProvider - * @since 1.1.0 - */ -@SuppressWarnings("unused") -public interface MappingLuceneOperations extends LuceneOperations { - - default List query(Class entityType, String query, String defaultField, - String... projectionFields) { - - return query(entityType, query, defaultField, DEFAULT_RESULT_LIMIT, projectionFields); - } - - List query(Class entityType, String query, String defaultField, - int resultLimit, String... projectionFields); - - Page query(Class entityType, String query, String defaultField, - int resultLimit, int pageSize, String... projectionFields); - - default List query(Class entityType, LuceneQueryProvider queryProvider, - String... projectionFields) { - - return query(entityType, queryProvider, DEFAULT_RESULT_LIMIT, projectionFields); - } - - List query(Class entityType, LuceneQueryProvider queryProvider, - int resultLimit, String... projectionFields); - - Page query(Class entityType, LuceneQueryProvider queryProvider, - int resultLimit, int pageSize, String... projectionFields); - -} diff --git a/src/main/java/org/springframework/data/gemfire/search/lucene/ProjectingLuceneAccessor.java b/src/main/java/org/springframework/data/gemfire/search/lucene/ProjectingLuceneAccessor.java new file mode 100644 index 00000000..db8fa4bf --- /dev/null +++ b/src/main/java/org/springframework/data/gemfire/search/lucene/ProjectingLuceneAccessor.java @@ -0,0 +1,197 @@ +/* + * Copyright 2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package org.springframework.data.gemfire.search.lucene; + +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.BeanClassLoaderAware; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanFactoryAware; +import org.springframework.data.gemfire.search.lucene.support.PdxInstanceMethodInterceptorFactory; +import org.springframework.data.projection.ProjectionFactory; +import org.springframework.data.projection.SpelAwareProxyProjectionFactory; + +/** + * {@link ProjectingLuceneAccessor} is an abstract class supporting implementations of + * the {@link ProjectingLuceneOperations} interface encapsulating common functionality + * necessary to execute Lucene queries and work with application domain object views. + * + * @author John Blum + * @see java.lang.ClassLoader + * @see org.springframework.beans.factory.BeanClassLoaderAware + * @see org.springframework.beans.factory.BeanFactory + * @see org.springframework.beans.factory.BeanFactoryAware + * @see org.springframework.data.gemfire.search.lucene.ProjectingLuceneOperations + * @see org.springframework.data.gemfire.search.lucene.support.PdxInstanceMethodInterceptorFactory + * @see org.springframework.data.projection.ProjectionFactory + * @see org.springframework.data.projection.SpelAwareProxyProjectionFactory + * @see org.apache.geode.cache.Region + * @see org.apache.geode.cache.lucene.LuceneIndex + * @see org.apache.geode.cache.lucene.LuceneQuery + * @see org.apache.geode.cache.lucene.LuceneQueryFactory + * @see org.apache.geode.cache.lucene.LuceneService + * @see org.apache.geode.cache.lucene.LuceneServiceProvider + * @since 1.1.0 + */ +public abstract class ProjectingLuceneAccessor extends LuceneTemplate + implements BeanClassLoaderAware, BeanFactoryAware, ProjectingLuceneOperations { + + private BeanFactory beanFactory; + + private ClassLoader beanClassLoader; + + private ProjectionFactory projectionFactory; + + /** + * Constructs a default, uninitialized instance of the {@link ProjectingLuceneAccessor}. + */ + public ProjectingLuceneAccessor() { + } + + /** + * Constructs an instance of the {@link ProjectingLuceneAccessor} initialized with the given {@link LuceneIndex} + * used to perform Lucene queries (searches). + * + * @param luceneIndex {@link LuceneIndex} used in Lucene queries. + * @see org.apache.geode.cache.lucene.LuceneIndex + */ + public ProjectingLuceneAccessor(LuceneIndex luceneIndex) { + super(luceneIndex); + } + + /** + * Constructs an instance of the {@link ProjectingLuceneAccessor} initialized with the given Lucene index name + * and {@link Region} reference upon which Lucene queries are executed. + * + * @param indexName {@link String} containing the name of the {@link LuceneIndex} used in Lucene queries. + * @param region {@link Region} on which Lucene queries are executed. + * @see org.apache.geode.cache.Region + */ + public ProjectingLuceneAccessor(String indexName, Region region) { + super(indexName, region); + } + + /** + * Constructs an instance of the {@link ProjectingLuceneAccessor} initialized with the given Lucene index name + * and {@link Region} reference upon which Lucene queries are executed. + * + * @param indexName {@link String} containing the name of the {@link LuceneIndex} used in Lucene queries. + * @param regionPath {@link String} containing the name of the {@link Region} on which Lucene queries are executed. + */ + public ProjectingLuceneAccessor(String indexName, String regionPath) { + super(indexName, regionPath); + } + + /** + * @inheritDoc + * @see #resolveProjectionFactory() + */ + @Override + public void afterPropertiesSet() throws Exception { + super.afterPropertiesSet(); + this.projectionFactory = resolveProjectionFactory(); + } + + /** + * Null-safe method to resolve the Spring Data {@link ProjectionFactory} used to create projections + * out of the Lucene query results. + * + * @return a resolved instance of the Spring Data {@link ProjectionFactory} used to create projections + * out of the Lucene query results. + * @see org.springframework.data.projection.ProjectionFactory + * @see org.springframework.data.projection.SpelAwareProxyProjectionFactory + * @see #afterPropertiesSet() + */ + protected ProjectionFactory resolveProjectionFactory() { + return Optional.ofNullable(getProjectionFactory()).orElseGet(() -> { + SpelAwareProxyProjectionFactory projectionFactory = new SpelAwareProxyProjectionFactory(); + projectionFactory.setBeanClassLoader(getBeanClassLoader()); + projectionFactory.setBeanFactory(getBeanFactory()); + projectionFactory.registerMethodInvokerFactory(PdxInstanceMethodInterceptorFactory.INSTANCE); + return setThenGetProjectionFactory(projectionFactory); + }); + } + + /** + * @inheritDoc + */ + @Override + public void setBeanClassLoader(ClassLoader beanClassLoader) { + this.beanClassLoader = beanClassLoader; + } + + /** + * Returns a reference to the {@link ClassLoader} used by the Spring {@link BeanFactory container} + * to load bean class definitions. + * + * @return a reference to the {@link ClassLoader} used by the Spring {@link BeanFactory container} + * to load bean class definitions. + * @see org.springframework.beans.factory.BeanClassLoaderAware#setBeanClassLoader(ClassLoader) + * @see java.lang.ClassLoader + */ + protected ClassLoader getBeanClassLoader() { + return this.beanClassLoader; + } + + /** + * @inheritDoc + */ + @Override + public void setBeanFactory(BeanFactory beanFactory) throws BeansException { + this.beanFactory = beanFactory; + } + + /** + * Returns a reference to the Spring {@link BeanFactory container}. + * + * @return a reference to the Spring {@link BeanFactory container}. + * @see org.springframework.beans.factory.BeanFactoryAware#setBeanFactory(BeanFactory) + * @see org.springframework.beans.factory.BeanFactory + */ + protected BeanFactory getBeanFactory() { + return this.beanFactory; + } + + protected ProjectionFactory setThenGetProjectionFactory(ProjectionFactory projectionFactory) { + setProjectionFactory(projectionFactory); + return getProjectionFactory(); + } + + /** + * Sets the Spring Data {@link ProjectionFactory} used to create projections out of query results. + * + * @param projectionFactory Spring Data {@link ProjectionFactory} used to created projects out of query results. + * @see org.springframework.data.projection.ProjectionFactory + */ + public void setProjectionFactory(ProjectionFactory projectionFactory) { + this.projectionFactory = projectionFactory; + } + + /** + * Returns the Spring Data {@link ProjectionFactory} used to create projections out of query results. + * + * @return the Spring Data {@link ProjectionFactory} used to created projects out of query results. + * @see org.springframework.data.projection.ProjectionFactory + */ + protected ProjectionFactory getProjectionFactory() { + return this.projectionFactory; + } +} diff --git a/src/main/java/org/springframework/data/gemfire/search/lucene/ProjectingLuceneOperations.java b/src/main/java/org/springframework/data/gemfire/search/lucene/ProjectingLuceneOperations.java new file mode 100644 index 00000000..350313b1 --- /dev/null +++ b/src/main/java/org/springframework/data/gemfire/search/lucene/ProjectingLuceneOperations.java @@ -0,0 +1,133 @@ +/* + * Copyright 2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package org.springframework.data.gemfire.search.lucene; + +import java.util.List; + +import org.apache.geode.cache.lucene.LuceneQueryProvider; +import org.springframework.data.domain.Page; + +/** + * The {@link ProjectingLuceneOperations} interface defines a contract for implementing classes to execute + * Lucene data access operations and mapping the results to entity domain {@link Class types}. + * + * @author John Blum + * @see java.util.List + * @see org.springframework.data.domain.Page + * @see org.springframework.data.gemfire.search.lucene.LuceneOperations + * @see org.apache.geode.cache.lucene.LuceneQueryProvider + * @since 1.1.0 + */ +@SuppressWarnings("unused") +public interface ProjectingLuceneOperations extends LuceneOperations { + + /** + * Executes the given {@link String query} with the results projected as instances of + * the {@link Class projectionType}. + * + * @param {@link Class} type of the projection. + * @param query Lucene {@link String query} to execute. + * @param defaultField {@link String} specifying the default field used in Lucene queries when a field + * is not explicitly defined in the Lucene query clause. + * @param projectionType {@link Class} type of the individual elements in the query results. + * @return a {@link List} of Lucene query results projected as instances of {@link Class projectionType}. + * @see #query(String, String, int, Class) + * @see java.util.List + */ + default List query(String query, String defaultField, Class projectionType) { + return query(query, defaultField, DEFAULT_RESULT_LIMIT, projectionType); + } + + /** + * Executes the given {@link String query} with the limited results projected as instances of + * the {@link Class projectionType}. + * + * @param {@link Class} type of the projection. + * @param query Lucene {@link String query} to execute. + * @param defaultField {@link String} specifying the default field used in Lucene queries when a field + * is not explicitly defined in the Lucene query clause. + * @param resultLimit limit on the number of query results to return. + * @param projectionType {@link Class} type of the individual elements in the query results. + * @return a {@link List} of Lucene query results projected as instances of {@link Class projectionType}. + * @see #query(String, String, int, Class) + * @see java.util.List + */ + List query(String query, String defaultField, int resultLimit, Class projectionType); + + /** + * Executes the given {@link String query} with the limited results projected as instances of + * the {@link Class projectionType}. + * + * @param {@link Class} type of the projection. + * @param query Lucene {@link String query} to execute. + * @param defaultField {@link String} specifying the default field used in Lucene queries when a field + * is not explicitly defined in the Lucene query clause. + * @param resultLimit limit on the number of query results to return. + * @param pageSize number of results on a {@link Page}. + * @param projectionType {@link Class} type of the individual elements in the query results. + * @return the first {@link Page} of results returned from the Lucene query. + * @see org.springframework.data.domain.Page + */ + Page query(String query, String defaultField, int resultLimit, int pageSize, Class projectionType); + + /** + * Executes the provided {@link String query} with the results projected as instances of + * the {@link Class projectionType}. + * + * @param {@link Class} type of the projection. + * @param queryProvider {@link LuceneQueryProvider} providing the Lucene {@link String query} to execute. + * @param projectionType {@link Class} type of the individual elements in the query results. + * @return a {@link List} of Lucene query results projected as instances of {@link Class projectionType}. + * @see org.apache.geode.cache.lucene.LuceneQueryProvider + * @see #query(LuceneQueryProvider, int, Class) + * @see java.util.List + */ + default List query(LuceneQueryProvider queryProvider, Class projectionType) { + return query(queryProvider, DEFAULT_RESULT_LIMIT, projectionType); + } + + /** + * Executes the provided {@link String query} with the limited results projected as instances of + * the {@link Class projectionType}. + * + * @param {@link Class} type of the projection. + * @param queryProvider {@link LuceneQueryProvider} providing the Lucene {@link String query} to execute. + * @param resultLimit limit on the number of query results to return. + * @param projectionType {@link Class} type of the individual elements in the query results. + * @return a {@link List} of Lucene query results projected as instances of {@link Class projectionType}. + * @see org.apache.geode.cache.lucene.LuceneQueryProvider + * @see java.util.List + */ + List query(LuceneQueryProvider queryProvider, int resultLimit, Class projectionType); + + /** + * Executes the provided {@link String query} with the limited results projected as instances of + * the {@link Class projectionType}. + * + * @param {@link Class} type of the projection. + * @param queryProvider {@link LuceneQueryProvider} providing the Lucene {@link String query} to execute. + * @param resultLimit limit on the number of query results to return. + * @param pageSize number of results on a {@link Page}. + * @param projectionType {@link Class} type of the individual elements in the query results. + * @return the first {@link Page} of results returned from the Lucene query. + * @see org.apache.geode.cache.lucene.LuceneQueryProvider + * @see org.springframework.data.domain.Page + */ + Page query(LuceneQueryProvider queryProvider, int resultLimit, int pageSize, Class projectionType); + +} diff --git a/src/main/java/org/springframework/data/gemfire/search/lucene/ProjectingLuceneTemplate.java b/src/main/java/org/springframework/data/gemfire/search/lucene/ProjectingLuceneTemplate.java new file mode 100644 index 00000000..bf09ae18 --- /dev/null +++ b/src/main/java/org/springframework/data/gemfire/search/lucene/ProjectingLuceneTemplate.java @@ -0,0 +1,129 @@ +/* + * Copyright 2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package org.springframework.data.gemfire.search.lucene; + +import static org.springframework.data.gemfire.util.RuntimeExceptionFactory.newUnsupportedOperationException; + +import java.util.List; +import java.util.stream.Collectors; + +import org.apache.geode.cache.Region; +import org.apache.geode.cache.lucene.LuceneIndex; +import org.apache.geode.cache.lucene.LuceneQueryProvider; +import org.springframework.data.domain.Page; + +/** + * {@link ProjectingLuceneTemplate} is a Lucene data access operations class encapsulating functionality + * for performing Lucene queries and other Lucene data access operations and returning the query results + * as application-specific domain object views. + * + * @author John Blum + * @see org.springframework.data.gemfire.search.lucene.ProjectingLuceneAccessor + * @see org.springframework.data.gemfire.search.lucene.ProjectingLuceneOperations + * @see org.apache.geode.cache.Region + * @see org.apache.geode.cache.lucene.LuceneIndex + * @see org.apache.geode.cache.lucene.LuceneQuery + * @see org.apache.geode.cache.lucene.LuceneQueryFactory + * @see org.apache.geode.cache.lucene.LuceneQueryProvider + * @see org.apache.geode.cache.lucene.LuceneResultStruct + * @see org.apache.geode.cache.lucene.PageableLuceneQueryResults + * @since 1.1.0 + */ +@SuppressWarnings("unused") +public class ProjectingLuceneTemplate extends ProjectingLuceneAccessor { + + /** + * Constructs a default, uninitialized instance of the {@link ProjectingLuceneTemplate}. + */ + public ProjectingLuceneTemplate() { + } + + /** + * Constructs an instance of the {@link ProjectingLuceneTemplate} initialized with the given {@link LuceneIndex} + * used to perform Lucene queries (searches). + * + * @param luceneIndex {@link LuceneIndex} used in Lucene queries. + * @see org.apache.geode.cache.lucene.LuceneIndex + */ + public ProjectingLuceneTemplate(LuceneIndex luceneIndex) { + super(luceneIndex); + } + + /** + * Constructs an instance of the {@link ProjectingLuceneTemplate} initialized with the given Lucene index name + * and {@link Region} reference upon which Lucene queries are executed. + * + * @param indexName {@link String} containing the name of the {@link LuceneIndex} used in Lucene queries. + * @param region {@link Region} on which Lucene queries are executed. + * @see org.apache.geode.cache.Region + */ + public ProjectingLuceneTemplate(String indexName, Region region) { + super(indexName, region); + } + + /** + * Constructs an instance of the {@link ProjectingLuceneTemplate} initialized with the given Lucene index name + * and {@link Region} reference upon which Lucene queries are executed. + * + * @param indexName {@link String} containing the name of the {@link LuceneIndex} used in Lucene queries. + * @param regionPath {@link String} containing the name of the {@link Region} on which Lucene queries are executed. + */ + public ProjectingLuceneTemplate(String indexName, String regionPath) { + super(indexName, regionPath); + } + + /** + * @inheritDoc + */ + @Override + public List query(String query, String defaultField, int resultLimit, Class projectionType) { + return query(query, defaultField, resultLimit).stream() + .map(luceneResultStruct -> getProjectionFactory().createProjection(projectionType, luceneResultStruct.getValue())) + .collect(Collectors.toList()); + } + + /** + * @inheritDoc + */ + @Override + public Page query(String query, String defaultField, int resultLimit, int pageSize, + Class projectionType) { + + throw newUnsupportedOperationException("Not Implemented"); + } + + /** + * @inheritDoc + */ + @Override + public List query(LuceneQueryProvider queryProvider, int resultLimit, Class projectionType) { + return query(queryProvider, resultLimit).stream() + .map(luceneResultStruct -> getProjectionFactory().createProjection(projectionType, luceneResultStruct.getValue())) + .collect(Collectors.toList()); + } + + /** + * @inheritDoc + */ + @Override + public Page query(LuceneQueryProvider queryProvider, int resultLimit, int pageSize, + Class projectionType) { + + throw newUnsupportedOperationException("Not Implemented"); + } +} diff --git a/src/main/java/org/springframework/data/gemfire/search/lucene/support/PdxInstanceMethodInterceptor.java b/src/main/java/org/springframework/data/gemfire/search/lucene/support/PdxInstanceMethodInterceptor.java new file mode 100644 index 00000000..e347a860 --- /dev/null +++ b/src/main/java/org/springframework/data/gemfire/search/lucene/support/PdxInstanceMethodInterceptor.java @@ -0,0 +1,144 @@ +/* + * Copyright 2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package org.springframework.data.gemfire.search.lucene.support; + +import java.lang.reflect.Method; +import java.util.Arrays; + +import org.aopalliance.intercept.MethodInterceptor; +import org.aopalliance.intercept.MethodInvocation; +import org.apache.geode.pdx.PdxInstance; +import org.apache.geode.pdx.WritablePdxInstance; +import org.springframework.data.projection.Accessor; +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; +import org.springframework.util.ReflectionUtils; + +/** + * The {@link PdxInstanceMethodInterceptor} class is a {@link MethodInterceptor} wrapping a {@link PdxInstance} + * to back a proxy during intercepted method invocations. + * + * @author John Blum + * @see org.aopalliance.intercept.MethodInterceptor + * @see org.apache.geode.pdx.PdxInstance + * @see org.apache.geode.pdx.WritablePdxInstance + * @since 1.1.0 + */ +public class PdxInstanceMethodInterceptor implements MethodInterceptor { + + private PdxInstance source; + + /** + * Factory method used to construct an instance of {@link PdxInstanceMethodInterceptor} initialized with + * the given {@link Object source}. + * + * @param source {@link Object} serving as the source to back the proxy in method invocations. + * Source must be an instance of {@link PdxInstance}. + * @return a new instance of {@link PdxInstanceMethodInterceptor} initialized with the given {@link Object source}. + * @throws IllegalArgumentException if {@link Object source} is not an instance of {@link PdxInstance}. + * @see #newPdxInstanceMethodInterceptor(PdxInstance) + */ + public static PdxInstanceMethodInterceptor newPdxInstanceMethodInterceptor(Object source) { + Assert.isInstanceOf(PdxInstance.class, source, () -> String.format("Source [%1$s] is not an instance of [%2$s]", + ObjectUtils.nullSafeClassName(source), PdxInstance.class.getName())); + + return newPdxInstanceMethodInterceptor((PdxInstance) source); + } + + /** + * Factory method used to construct an instance of {@link PdxInstanceMethodInterceptor} initialized + * with the given {@link PdxInstance}. + * + * @param source {@link PdxInstance} serving as the source to back the proxy in method invocations. + * Source must not be {@literal null}. + * @return a new instance of {@link PdxInstanceMethodInterceptor} initialized with + * the given {@link PdxInstance source}. + * @throws IllegalArgumentException if {@link PdxInstance source} is {@literal null}. + * @see #PdxInstanceMethodInterceptor(PdxInstance) + * @see org.apache.geode.pdx.PdxInstance + */ + public static PdxInstanceMethodInterceptor newPdxInstanceMethodInterceptor(PdxInstance source) { + return new PdxInstanceMethodInterceptor(source); + } + + /** + * Constructs an instance of {@link PdxInstanceMethodInterceptor} initialized with + * the given {@link PdxInstance source}. + * + * @param source {@link PdxInstance} used as the source to back the proxy in method invocations. + * @throws IllegalArgumentException if {@link PdxInstance source} is {@literal null}. + * @see org.apache.geode.pdx.PdxInstance + */ + public PdxInstanceMethodInterceptor(PdxInstance source) { + Assert.notNull(source, "Source must not be null"); + this.source = source; + } + + /** + * Returns the {@link PdxInstance source} backing the proxy for intercepted method invocations. + * + * @return the {@link PdxInstance source} backing the proxy for intercepted method invocations. + * @see org.apache.geode.pdx.PdxInstance + */ + protected PdxInstance getSource() { + return this.source; + } + + /** + * @inheritDoc + */ + @Override + public Object invoke(MethodInvocation invocation) throws Throwable { + Method method = invocation.getMethod(); + + if (ReflectionUtils.isObjectMethod(method)) { + return invocation.proceed(); + } + else { + Accessor methodAccessor = new Accessor(method); + PdxInstance pdxInstance = getSource(); + String propertyName = methodAccessor.getPropertyName(); + + Assert.state(pdxInstance.hasField(propertyName), () -> String.format( + "Source [%1$s] does not contain field with name [%2$s]", pdxInstance, propertyName)); + + if (methodAccessor.isGetter()) { + return pdxInstance.getField(propertyName); + } + else { // is setter + Assert.isTrue(invocation.getArguments().length == 1, () -> + String.format("Invoked setter method [%1$s] must expect exactly 1 argument; Arguments were [%2$s]", + method.getName(), Arrays.toString(invocation.getArguments()))); + + Object value = invocation.getArguments()[0]; + + WritablePdxInstance writablePdxInstance = pdxInstance.createWriter(); + + Assert.state(writablePdxInstance != null, () -> String.format( + "No writer for PdxInstance [%1$s] was found for setting field [%2$s] to value [%3$s]", + pdxInstance, propertyName, value)); + + writablePdxInstance.setField(propertyName, value); + + this.source = writablePdxInstance; + + return null; + } + } + } +} diff --git a/src/main/java/org/springframework/data/gemfire/search/lucene/support/PdxInstanceMethodInterceptorFactory.java b/src/main/java/org/springframework/data/gemfire/search/lucene/support/PdxInstanceMethodInterceptorFactory.java new file mode 100644 index 00000000..982a583c --- /dev/null +++ b/src/main/java/org/springframework/data/gemfire/search/lucene/support/PdxInstanceMethodInterceptorFactory.java @@ -0,0 +1,57 @@ +/* + * Copyright 2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package org.springframework.data.gemfire.search.lucene.support; + +import static org.springframework.data.gemfire.search.lucene.support.PdxInstanceMethodInterceptor.newPdxInstanceMethodInterceptor; + +import org.aopalliance.intercept.MethodInterceptor; +import org.apache.geode.pdx.PdxInstance; +import org.springframework.data.projection.MethodInterceptorFactory; + +/** + * The {@link PdxInstanceMethodInterceptorFactory} class is a Spring Data {@link MethodInterceptorFactory} used to + * identify {@link PdxInstance} types and instantiates an instance of the {@link PdxInstanceMethodInterceptor} + * in order to intercept and handle invocations on the {@link PdxInstance} for the proxied projection. + * + * @author John Blum + * @see org.aopalliance.intercept.MethodInterceptor + * @see org.apache.geode.pdx.PdxInstance + * @see org.springframework.data.gemfire.search.lucene.support.PdxInstanceMethodInterceptor + * @see org.springframework.data.projection.MethodInterceptorFactory + * @since 1.0.0 + */ +public enum PdxInstanceMethodInterceptorFactory implements MethodInterceptorFactory { + + INSTANCE; + + /** + * @inheritDoc + */ + @Override + public MethodInterceptor createMethodInterceptor(Object source, Class targetType) { + return newPdxInstanceMethodInterceptor(source); + } + + /** + * @inheritDoc + */ + @Override + public boolean supports(Object source, Class targetType) { + return PdxInstance.class.isInstance(source); + } +} diff --git a/src/main/java/org/springframework/data/gemfire/search/lucene/support/ProjectingLuceneAccessorSupport.java b/src/main/java/org/springframework/data/gemfire/search/lucene/support/ProjectingLuceneAccessorSupport.java new file mode 100644 index 00000000..16399ee3 --- /dev/null +++ b/src/main/java/org/springframework/data/gemfire/search/lucene/support/ProjectingLuceneAccessorSupport.java @@ -0,0 +1,75 @@ +/* + * Copyright 2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package org.springframework.data.gemfire.search.lucene.support; + +import static org.springframework.data.gemfire.util.RuntimeExceptionFactory.newUnsupportedOperationException; + +import java.util.List; + +import org.apache.geode.cache.lucene.LuceneQueryProvider; +import org.springframework.data.domain.Page; +import org.springframework.data.gemfire.search.lucene.ProjectingLuceneAccessor; +import org.springframework.data.gemfire.util.RuntimeExceptionFactory; + +/** + * {@link ProjectingLuceneAccessorSupport} is a {@link ProjectingLuceneAccessor} class implementation providing support + * for extending classes. + * + * @author John Blum + * @see org.springframework.data.gemfire.search.lucene.ProjectingLuceneAccessor + * @since 1.1.0 + */ +@SuppressWarnings("unused") +public abstract class ProjectingLuceneAccessorSupport extends ProjectingLuceneAccessor { + + /** + * @inheritDoc + */ + @Override + public List query(String query, String defaultField, int resultLimit, Class projectionType) { + throw newUnsupportedOperationException(RuntimeExceptionFactory.NOT_IMPLEMENTED); + } + + /** + * @inheritDoc + */ + @Override + public Page query(String query, String defaultField, int resultLimit, int pageSize, + Class projectionType) { + + throw newUnsupportedOperationException(RuntimeExceptionFactory.NOT_IMPLEMENTED); + } + + /** + * @inheritDoc + */ + @Override + public List query(LuceneQueryProvider queryProvider, int resultLimit, Class projectionType) { + throw newUnsupportedOperationException(RuntimeExceptionFactory.NOT_IMPLEMENTED); + } + + /** + * @inheritDoc + */ + @Override + public Page query(LuceneQueryProvider queryProvider, int resultLimit, int pageSize, + Class projectionType) { + + throw newUnsupportedOperationException(RuntimeExceptionFactory.NOT_IMPLEMENTED); + } +} diff --git a/src/main/java/org/springframework/data/gemfire/search/lucene/support/ProjectingLuceneOperationsSupport.java b/src/main/java/org/springframework/data/gemfire/search/lucene/support/ProjectingLuceneOperationsSupport.java new file mode 100644 index 00000000..51e0e749 --- /dev/null +++ b/src/main/java/org/springframework/data/gemfire/search/lucene/support/ProjectingLuceneOperationsSupport.java @@ -0,0 +1,76 @@ +/* + * Copyright 2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package org.springframework.data.gemfire.search.lucene.support; + +import static org.springframework.data.gemfire.util.RuntimeExceptionFactory.newUnsupportedOperationException; + +import java.util.List; + +import org.apache.geode.cache.lucene.LuceneQueryProvider; +import org.springframework.data.domain.Page; +import org.springframework.data.gemfire.search.lucene.ProjectingLuceneOperations; +import org.springframework.data.gemfire.util.RuntimeExceptionFactory; + +/** + * {@link ProjectingLuceneOperationsSupport} is a abstract supporting class for implementations + * of the {@link ProjectingLuceneOperations} interface. + * + * @author John Blum + * @see org.springframework.data.gemfire.search.lucene.ProjectingLuceneOperations + * @since 1.1.0 + */ +@SuppressWarnings("unused") +public abstract class ProjectingLuceneOperationsSupport extends LuceneOperationsSupport + implements ProjectingLuceneOperations { + + /** + * @inheritDoc + */ + @Override + public List query(String query, String defaultField, int resultLimit, Class projectionType) { + throw newUnsupportedOperationException(RuntimeExceptionFactory.NOT_IMPLEMENTED); + } + + /** + * @inheritDoc + */ + @Override + public Page query(String query, String defaultField, int resultLimit, int pageSize, + Class projectionType) { + + throw newUnsupportedOperationException(RuntimeExceptionFactory.NOT_IMPLEMENTED); + } + + /** + * @inheritDoc + */ + @Override + public List query(LuceneQueryProvider queryProvider, int resultLimit, Class projectionType) { + throw newUnsupportedOperationException(RuntimeExceptionFactory.NOT_IMPLEMENTED); + } + + /** + * @inheritDoc + */ + @Override + public Page query(LuceneQueryProvider queryProvider, int resultLimit, int pageSize, + Class projectionType) { + + throw newUnsupportedOperationException(RuntimeExceptionFactory.NOT_IMPLEMENTED); + } +} diff --git a/src/test/java/org/springframework/data/gemfire/search/lucene/LuceneTemplateUnitTests.java b/src/test/java/org/springframework/data/gemfire/search/lucene/LuceneTemplateUnitTests.java index e183e888..9af531f5 100644 --- a/src/test/java/org/springframework/data/gemfire/search/lucene/LuceneTemplateUnitTests.java +++ b/src/test/java/org/springframework/data/gemfire/search/lucene/LuceneTemplateUnitTests.java @@ -59,6 +59,11 @@ import org.mockito.runners.MockitoJUnitRunner; * @see org.mockito.Spy * @see org.mockito.runners.MockitoJUnitRunner * @see org.springframework.data.gemfire.search.lucene.LuceneTemplate + * @see org.apache.geode.cache.lucene.LuceneQuery + * @see org.apache.geode.cache.lucene.LuceneQueryFactory + * @see org.apache.geode.cache.lucene.LuceneQueryProvider + * @see org.apache.geode.cache.lucene.LuceneResultStruct + * @see org.apache.geode.cache.lucene.PageableLuceneQueryResults * @since 1.0.0 */ @RunWith(MockitoJUnitRunner.class) diff --git a/src/test/java/org/springframework/data/gemfire/search/lucene/ProjectingLuceneAccessorUnitTests.java b/src/test/java/org/springframework/data/gemfire/search/lucene/ProjectingLuceneAccessorUnitTests.java new file mode 100644 index 00000000..da5b2859 --- /dev/null +++ b/src/test/java/org/springframework/data/gemfire/search/lucene/ProjectingLuceneAccessorUnitTests.java @@ -0,0 +1,163 @@ +/* + * Copyright 2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package org.springframework.data.gemfire.search.lucene; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyZeroInteractions; + +import org.apache.geode.cache.GemFireCache; +import org.apache.geode.cache.Region; +import org.apache.geode.cache.lucene.LuceneIndex; +import org.apache.geode.cache.lucene.LuceneService; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.data.gemfire.search.lucene.support.ProjectingLuceneAccessorSupport; +import org.springframework.data.projection.ProjectionFactory; +import org.springframework.data.projection.SpelAwareProxyProjectionFactory; + +/** + * Unit tests for {@link ProjectingLuceneAccessor}. + * + * @author John Blum + * @see org.junit.Test + * @see org.junit.runner.RunWith + * @see org.mockito.Mock + * @see org.mockito.Mockito + * @see org.mockito.Spy + * @see org.mockito.runners.MockitoJUnitRunner + * @see org.springframework.data.gemfire.search.lucene.ProjectingLuceneAccessor + * @since 1.1.0 + */ +@RunWith(MockitoJUnitRunner.class) +public class ProjectingLuceneAccessorUnitTests { + + @Mock + private BeanFactory mockBeanFactory; + + @Mock + private GemFireCache mockCache; + + @Mock + private ClassLoader mockClassLoader; + + @Mock + private LuceneIndex mockLuceneIndex; + + @Mock + private LuceneService mockLuceneService; + + // SUT + private ProjectingLuceneAccessor projectingLuceneAccessor; + + @Mock + private ProjectionFactory mockProjectionFactory; + + @Mock + private Region mockRegion; + + @Before + public void setup() { + projectingLuceneAccessor = spy(new ProjectingLuceneAccessorSupport() {}); + + doReturn(mockCache).when(projectingLuceneAccessor).resolveCache(); + doReturn(mockLuceneService).when(projectingLuceneAccessor).resolveLuceneService(); + doReturn("MockLuceneIndex").when(projectingLuceneAccessor).resolveIndexName(); + doReturn("/Example").when(projectingLuceneAccessor).resolveRegionPath(); + } + + @Test + public void afterPropertiesSetInitializesTheProjectingLuceneAccessorCorrectly() throws Exception { + doReturn(mockProjectionFactory).when(projectingLuceneAccessor).resolveProjectionFactory(); + + projectingLuceneAccessor.afterPropertiesSet(); + + assertThat(projectingLuceneAccessor.getProjectionFactory()).isSameAs(mockProjectionFactory); + + verify(projectingLuceneAccessor, times(1)).resolveProjectionFactory(); + verifyZeroInteractions(mockProjectionFactory); + } + + @Test + public void projectingLuceneAccessorIsInitializedCorrectly() { + assertThat(projectingLuceneAccessor.getBeanClassLoader()).isNull(); + assertThat(projectingLuceneAccessor.getBeanFactory()).isNull(); + assertThat(projectingLuceneAccessor.getCache()).isNull(); + assertThat(projectingLuceneAccessor.getIndexName()).isNull(); + assertThat(projectingLuceneAccessor.getLuceneIndex()).isNull(); + assertThat(projectingLuceneAccessor.getLuceneService()).isNull(); + assertThat(projectingLuceneAccessor.getProjectionFactory()).isNull(); + assertThat(projectingLuceneAccessor.getRegion()).isNull(); + assertThat(projectingLuceneAccessor.getRegionPath()).isNull(); + + projectingLuceneAccessor.setBeanClassLoader(mockClassLoader); + projectingLuceneAccessor.setBeanFactory(mockBeanFactory); + projectingLuceneAccessor.setCache(mockCache); + projectingLuceneAccessor.setIndexName("TestLuceneIdx"); + projectingLuceneAccessor.setLuceneIndex(mockLuceneIndex); + projectingLuceneAccessor.setLuceneService(mockLuceneService); + projectingLuceneAccessor.setProjectionFactory(mockProjectionFactory); + projectingLuceneAccessor.setRegion(mockRegion); + projectingLuceneAccessor.setRegionPath("/Example"); + + assertThat(projectingLuceneAccessor.getBeanClassLoader()).isEqualTo(mockClassLoader); + assertThat(projectingLuceneAccessor.getBeanFactory()).isEqualTo(mockBeanFactory); + assertThat(projectingLuceneAccessor.getCache()).isEqualTo(mockCache); + assertThat(projectingLuceneAccessor.getIndexName()).isEqualTo("TestLuceneIdx"); + assertThat(projectingLuceneAccessor.getLuceneIndex()).isEqualTo(mockLuceneIndex); + assertThat(projectingLuceneAccessor.getLuceneService()).isEqualTo(mockLuceneService); + assertThat(projectingLuceneAccessor.getProjectionFactory()).isEqualTo(mockProjectionFactory); + assertThat(projectingLuceneAccessor.getRegion()).isEqualTo(mockRegion); + assertThat(projectingLuceneAccessor.getRegionPath()).isEqualTo("/Example"); + } + + @Test + public void setThenGetProjectionFactoryIsCorrect() { + assertThat(projectingLuceneAccessor.getProjectionFactory()).isNull(); + assertThat(projectingLuceneAccessor.setThenGetProjectionFactory(mockProjectionFactory)) + .isSameAs(mockProjectionFactory); + assertThat(projectingLuceneAccessor.getProjectionFactory()).isSameAs(mockProjectionFactory); + } + + @Test + public void resolveProjectFactoryReturnsProvidedProjectFactory() { + assertThat(projectingLuceneAccessor.getProjectionFactory()).isNull(); + + projectingLuceneAccessor.setProjectionFactory(mockProjectionFactory); + + assertThat(projectingLuceneAccessor.getProjectionFactory()).isSameAs(mockProjectionFactory); + assertThat(projectingLuceneAccessor.resolveProjectionFactory()).isSameAs(mockProjectionFactory); + } + + @Test + public void resolveProjectionFactoryCreatesNewProjectionFactory() { + assertThat(projectingLuceneAccessor.getProjectionFactory()).isNull(); + + ProjectionFactory projectionFactory = projectingLuceneAccessor.resolveProjectionFactory(); + + assertThat(projectionFactory).isInstanceOf(SpelAwareProxyProjectionFactory.class); + assertThat(projectingLuceneAccessor.getProjectionFactory()).isSameAs(projectionFactory); + } +} diff --git a/src/test/java/org/springframework/data/gemfire/search/lucene/ProjectingLuceneOperationsUnitTests.java b/src/test/java/org/springframework/data/gemfire/search/lucene/ProjectingLuceneOperationsUnitTests.java new file mode 100644 index 00000000..fcf2d254 --- /dev/null +++ b/src/test/java/org/springframework/data/gemfire/search/lucene/ProjectingLuceneOperationsUnitTests.java @@ -0,0 +1,83 @@ +/* + * Copyright 2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package org.springframework.data.gemfire.search.lucene; + +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyString; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import org.apache.geode.cache.lucene.LuceneQueryProvider; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; + +/** + * Unit tests for {@link ProjectingLuceneOperations}. + * + * @author John Blum + * @see org.junit.Test + * @see org.junit.runner.RunWith + * @see org.mockito.Mock + * @see org.mockito.Mockito + * @see org.mockito.runners.MockitoJUnitRunner + * @see org.springframework.data.gemfire.search.lucene.ProjectingLuceneOperations + * @since 1.1.0 + */ +@RunWith(MockitoJUnitRunner.class) +public class ProjectingLuceneOperationsUnitTests { + + @Mock + private LuceneQueryProvider mockQueryProvider; + + @Mock + private TestProjectingLuceneOperations projectingLuceneOperations; + + @Test + @SuppressWarnings("unchecked") + public void stringQueryCallsQueryWithResultLimit() { + when(projectingLuceneOperations.query(anyString(), anyString(), any(Class.class))).thenCallRealMethod(); + + projectingLuceneOperations.query("title : Up Shit Creek Without A Paddle", + "title", Book.class); + + verify(projectingLuceneOperations, times(1)) + .query(eq("title : Up Shit Creek Without A Paddle"), eq("title"), + eq(ProjectingLuceneOperations.DEFAULT_RESULT_LIMIT), eq(Book.class)); + } + + @Test + @SuppressWarnings("unchecked") + public void queryProviderQueryCallsQueryWithResultLimit() { + when(projectingLuceneOperations.query(any(LuceneQueryProvider.class), any(Class.class))) + .thenCallRealMethod(); + + projectingLuceneOperations.query(mockQueryProvider, Book.class); + + verify(projectingLuceneOperations, times(1)) + .query(eq(mockQueryProvider), eq(ProjectingLuceneOperations.DEFAULT_RESULT_LIMIT), eq(Book.class)); + } + + static class Book {} + + abstract class TestProjectingLuceneOperations implements ProjectingLuceneOperations { + } +} diff --git a/src/test/java/org/springframework/data/gemfire/search/lucene/ProjectingLuceneTemplateUnitTests.java b/src/test/java/org/springframework/data/gemfire/search/lucene/ProjectingLuceneTemplateUnitTests.java new file mode 100644 index 00000000..0bd3b08f --- /dev/null +++ b/src/test/java/org/springframework/data/gemfire/search/lucene/ProjectingLuceneTemplateUnitTests.java @@ -0,0 +1,172 @@ +/* + * Copyright 2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package org.springframework.data.gemfire.search.lucene; + +import static java.util.Arrays.asList; +import static org.assertj.core.api.Java6Assertions.assertThat; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyInt; +import static org.mockito.Matchers.anyString; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.List; + +import org.apache.geode.cache.lucene.LuceneQueryProvider; +import org.apache.geode.cache.lucene.LuceneResultStruct; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; +import org.springframework.data.annotation.Id; +import org.springframework.data.gemfire.mapping.annotation.Region; +import org.springframework.data.projection.ProjectionFactory; + +import lombok.Data; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; + +/** + * Unit tests for {@link ProjectingLuceneTemplate}. + * + * @author John Blum + * @see org.junit.Test + * @see org.junit.runner.RunWith + * @see org.mockito.Mock + * @see org.mockito.Mockito + * @see org.mockito.Spy + * @see org.mockito.runners.MockitoJUnitRunner + * @since 1.1.0 + */ +@RunWith(MockitoJUnitRunner.class) +public class ProjectingLuceneTemplateUnitTests { + + @Mock + private LuceneQueryProvider mockQueryProvider; + + // SUT + private ProjectingLuceneTemplate luceneTemplate; + + @Mock + private ProjectionFactory mockProjectionFactory; + + @Before + public void setup() { + luceneTemplate = spy(new ProjectingLuceneTemplate()); + luceneTemplate.setProjectionFactory(mockProjectionFactory); + } + + protected LuceneResultStruct mockLuceneResultStruct(Book book) { + return mockLuceneResultStruct(book.getIsbn(), book.getTitle()); + } + + @SuppressWarnings("unchecked") + protected LuceneResultStruct mockLuceneResultStruct(K key, V value) { + LuceneResultStruct mockLuceneResultStruct = + mock(LuceneResultStruct.class, String.format("MockLuceneResultStruct%s", key)); + + when(mockLuceneResultStruct.getKey()).thenReturn(key); + when(mockLuceneResultStruct.getValue()).thenReturn(value); + + return mockLuceneResultStruct; + } + + @Test + public void queryWithString() { + List books = asList( + Book.newBook(4L, "Star Wars - Episode IV New Hope"), + Book.newBook(5L, "Star Wars - Episode V Empire Strikes Back"), + Book.newBook(6L, "Star Wars - Episode VI Return of the Jedi") + ); + + List> queryResults = asList( + mockLuceneResultStruct(books.get(0)), + mockLuceneResultStruct(books.get(1)), + mockLuceneResultStruct(books.get(2)) + ); + + doReturn(queryResults).when(luceneTemplate).query(anyString(), anyString(), anyInt()); + + when(mockProjectionFactory.createProjection(eq(Book.class), anyString())).thenAnswer( invocationOnMock -> + books.stream().filter(book -> + book.getTitle().equals(invocationOnMock.getArgumentAt(1, String.class))) + .findFirst().orElse(null) + ); + + assertThat(luceneTemplate.query("title : Star Wars Episode *V*", "title", Book.class)) + .containsAll(books); + + verify(luceneTemplate, times(1)) + .query(eq("title : Star Wars Episode *V*"), eq("title"), + eq(ProjectingLuceneTemplate.DEFAULT_RESULT_LIMIT), eq(Book.class)); + verify(mockProjectionFactory, times(1)) + .createProjection(eq(Book.class), eq(books.get(0).getTitle())); + verify(mockProjectionFactory, times(1)) + .createProjection(eq(Book.class), eq(books.get(1).getTitle())); + verify(mockProjectionFactory, times(1)) + .createProjection(eq(Book.class), eq(books.get(2).getTitle())); + } + + @Test + public void queryWithQueryProvider() { + List books = asList( + Book.newBook(1L, "Star Wars - Episode I Phantom Menace"), + Book.newBook(2L, "Star Wars - Episode II Attack of the Clones"), + Book.newBook(3L, "Star Wars - Episode III Revenge of the Sith") + ); + + List> queryResults = asList( + mockLuceneResultStruct(books.get(0)), + mockLuceneResultStruct(books.get(1)), + mockLuceneResultStruct(books.get(2)) + ); + + doReturn(queryResults).when(luceneTemplate).query(any(LuceneQueryProvider.class), anyInt()); + + when(mockProjectionFactory.createProjection(eq(Book.class), anyString())).thenAnswer( invocationOnMock -> + books.stream().filter(book -> + book.getTitle().equals(invocationOnMock.getArgumentAt(1, String.class))) + .findFirst().orElse(null) + ); + + assertThat(luceneTemplate.query(mockQueryProvider, Book.class)).containsAll(books); + + verify(luceneTemplate, times(1)) + .query(eq(mockQueryProvider), eq(ProjectingLuceneTemplate.DEFAULT_RESULT_LIMIT), eq(Book.class)); + verify(mockProjectionFactory, times(1)) + .createProjection(eq(Book.class), eq(books.get(0).getTitle())); + verify(mockProjectionFactory, times(1)) + .createProjection(eq(Book.class), eq(books.get(1).getTitle())); + verify(mockProjectionFactory, times(1)) + .createProjection(eq(Book.class), eq(books.get(2).getTitle())); + } + + @Data + @Region("Books") + @RequiredArgsConstructor(staticName = "newBook") + static class Book { + @NonNull @Id Long isbn; + @NonNull String title; + } +} diff --git a/src/test/java/org/springframework/data/gemfire/search/lucene/support/PdxInstanceMethodInterceptorFactoryUnitTests.java b/src/test/java/org/springframework/data/gemfire/search/lucene/support/PdxInstanceMethodInterceptorFactoryUnitTests.java new file mode 100644 index 00000000..d057690f --- /dev/null +++ b/src/test/java/org/springframework/data/gemfire/search/lucene/support/PdxInstanceMethodInterceptorFactoryUnitTests.java @@ -0,0 +1,72 @@ +/* + * Copyright 2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package org.springframework.data.gemfire.search.lucene.support; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +import org.aopalliance.intercept.MethodInterceptor; +import org.apache.geode.pdx.PdxInstance; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; + +/** + * Unit tests for {@link PdxInstanceMethodInterceptorFactory}. + * + * @author John Blum + * @see org.junit.Test + * @see org.junit.runner.RunWith + * @see org.mockito.Mock + * @see org.mockito.Mockito + * @see org.mockito.runners.MockitoJUnitRunner + * @see org.aopalliance.intercept.MethodInterceptor + * @see org.apache.geode.pdx.PdxInstance + * @see org.springframework.data.gemfire.search.lucene.support.PdxInstanceMethodInterceptorFactory + * @since 1.1.0 + */ +@RunWith(MockitoJUnitRunner.class) +public class PdxInstanceMethodInterceptorFactoryUnitTests { + + @Mock + private PdxInstance mockSource; + + @Test + public void createMethodInterceptorIsSuccessful() { + MethodInterceptor methodInterceptor = + PdxInstanceMethodInterceptorFactory.INSTANCE.createMethodInterceptor(mockSource, Object.class); + + assertThat(methodInterceptor).isInstanceOf(PdxInstanceMethodInterceptor.class); + assertThat(((PdxInstanceMethodInterceptor) methodInterceptor).getSource()).isSameAs(mockSource); + } + + @Test + public void supportsPdxInstances() { + assertThat(PdxInstanceMethodInterceptorFactory.INSTANCE.supports(mockSource, Object.class)).isTrue(); + } + + @Test + public void doesNotSupportNonPdxInstances() { + assertThat(PdxInstanceMethodInterceptorFactory.INSTANCE.supports(new Object(), Object.class)).isFalse(); + assertThat(PdxInstanceMethodInterceptorFactory.INSTANCE.supports(Boolean.TRUE, Object.class)).isFalse(); + assertThat(PdxInstanceMethodInterceptorFactory.INSTANCE.supports('X', Object.class)).isFalse(); + assertThat(PdxInstanceMethodInterceptorFactory.INSTANCE.supports(2, Object.class)).isFalse(); + assertThat(PdxInstanceMethodInterceptorFactory.INSTANCE.supports(Math.PI, Object.class)).isFalse(); + assertThat(PdxInstanceMethodInterceptorFactory.INSTANCE.supports("test", Object.class)).isFalse(); + } +} diff --git a/src/test/java/org/springframework/data/gemfire/search/lucene/support/PdxInstanceMethodInterceptorUnitTests.java b/src/test/java/org/springframework/data/gemfire/search/lucene/support/PdxInstanceMethodInterceptorUnitTests.java new file mode 100644 index 00000000..aea9fc7b --- /dev/null +++ b/src/test/java/org/springframework/data/gemfire/search/lucene/support/PdxInstanceMethodInterceptorUnitTests.java @@ -0,0 +1,271 @@ +/* + * Copyright 2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package org.springframework.data.gemfire.search.lucene.support; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.mockito.Matchers.anyString; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.verifyZeroInteractions; +import static org.mockito.Mockito.when; +import static org.springframework.data.gemfire.search.lucene.support.PdxInstanceMethodInterceptor.newPdxInstanceMethodInterceptor; + +import java.lang.reflect.Method; + +import org.aopalliance.intercept.MethodInvocation; +import org.apache.geode.pdx.PdxInstance; +import org.apache.geode.pdx.WritablePdxInstance; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; + +import lombok.Data; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; + +/** + * Unit tests for {@link PdxInstanceMethodInterceptor}. + * + * @author John Blum + * @see org.junit.Test + * @see org.junit.runner.RunWith + * @see org.mockito.Mock + * @see org.mockito.Mockito + * @see org.mockito.runners.MockitoJUnitRunner + * @see org.springframework.data.gemfire.search.lucene.support.PdxInstanceMethodInterceptor + * @since 1.1.0 + */ +@RunWith(MockitoJUnitRunner.class) +public class PdxInstanceMethodInterceptorUnitTests { + + @Mock + private MethodInvocation mockMethodInvocation; + + @Mock + private PdxInstance mockSource; + + @Mock + private WritablePdxInstance mockNewSource; + + @SafeVarargs + protected static T[] asArray(T... array) { + return array; + } + + @Test + public void newPdxInstanceMethodInterceptorWithValidObjectSourceIsSuccessful() { + PdxInstanceMethodInterceptor methodInterceptor = newPdxInstanceMethodInterceptor((Object) mockSource); + + assertThat(methodInterceptor).isNotNull(); + assertThat(methodInterceptor.getSource()).isSameAs(mockSource); + } + + @Test(expected = IllegalArgumentException.class) + public void newPdxInstanceMethodInterceptorWithInvalidObjectSourceThrowsIllegalArgumentException() { + try { + newPdxInstanceMethodInterceptor(new Object()); + } + catch (IllegalArgumentException expected) { + assertThat(expected).hasMessageStartingWith(String.format( + "Source [java.lang.Object] is not an instance of [%s]", PdxInstance.class.getName())); + assertThat(expected).hasNoCause(); + + throw expected; + } + } + + @Test + public void newPdxInstanceMethodInterceptorWithPdxInstanceIsSuccessful() { + PdxInstanceMethodInterceptor methodInterceptor = newPdxInstanceMethodInterceptor(mockSource); + + assertThat(methodInterceptor).isNotNull(); + assertThat(methodInterceptor.getSource()).isSameAs(mockSource); + } + + @Test(expected = IllegalArgumentException.class) + public void constructPdxInstanceMethodInterceptorWithNullThrowsIllegalArgumentException() { + try { + new PdxInstanceMethodInterceptor(null); + } + catch (IllegalArgumentException expected) { + assertThat(expected).hasMessage("Source must not be null"); + assertThat(expected).hasNoCause(); + + throw expected; + } + } + + @Test + public void invokeObjectMethodIsHandled() throws Throwable { + Person jonDoe = Person.newPerson("Jon", "Doe"); + Method toString = jonDoe.getClass().getMethod("toString"); + + when(mockMethodInvocation.getMethod()).thenReturn(toString); + when(mockMethodInvocation.proceed()).thenAnswer(invocationOnMock -> toString.invoke(jonDoe)); + + assertThat(newPdxInstanceMethodInterceptor(mockSource).invoke(mockMethodInvocation)).isEqualTo("Jon Doe"); + + verify(mockMethodInvocation, times(1)).getMethod(); + verify(mockMethodInvocation, times(1)).proceed(); + verifyZeroInteractions(mockSource); + } + + @Test + public void invokeGetterOnSourceIsHandled() throws Throwable { + Person jonDoe = Person.newPerson("Jon", "Doe"); + Method getFirstName = jonDoe.getClass().getMethod("getFirstName"); + + when(mockMethodInvocation.getMethod()).thenReturn(getFirstName); + when(mockSource.hasField(eq("firstName"))).thenReturn(true); + when(mockSource.getField(eq("firstName"))).thenReturn(jonDoe.getFirstName()); + + assertThat(newPdxInstanceMethodInterceptor(mockSource).invoke(mockMethodInvocation)).isEqualTo("Jon"); + + verify(mockMethodInvocation, never()).proceed(); + verify(mockSource, times(1)).hasField(eq("firstName")); + verify(mockSource, times(1)).getField(eq("firstName")); + verifyNoMoreInteractions(mockSource); + } + + @Test + public void invokeSetterOnSourceIsHandled() throws Throwable { + Person jonDoe = Person.newPerson("Jon", "Doe"); + Method setLastName = jonDoe.getClass().getMethod("setLastName", String.class); + + when(mockMethodInvocation.getMethod()).thenReturn(setLastName); + when(mockMethodInvocation.getArguments()).thenReturn(asArray("Smith")); + when(mockSource.hasField(eq("lastName"))).thenReturn(true); + when(mockSource.createWriter()).thenReturn(mockNewSource); + + PdxInstanceMethodInterceptor methodInterceptor = newPdxInstanceMethodInterceptor(mockSource); + + assertThat(methodInterceptor.getSource()).isSameAs(mockSource); + assertThat(methodInterceptor.invoke(mockMethodInvocation)).isNull(); + assertThat(methodInterceptor.getSource()).isEqualTo(mockNewSource); + + verify(mockMethodInvocation, times(1)).getMethod(); + verify(mockMethodInvocation, times(2)).getArguments(); + verify(mockSource, times(1)).hasField(eq("lastName")); + verify(mockNewSource, times(1)).setField(eq("lastName"), eq("Smith")); + } + + @Test(expected = IllegalStateException.class) + public void invokeThrowsIllegalStateExceptionWhenPdxInstanceDoesNotHaveField() throws Throwable { + Method getGender = Person.class.getMethod("getGender"); + + when(mockMethodInvocation.getMethod()).thenReturn(getGender); + when(mockSource.hasField(anyString())).thenReturn(false); + + try { + newPdxInstanceMethodInterceptor(mockSource).invoke(mockMethodInvocation); + } + catch (IllegalStateException expected) { + assertThat(expected).hasMessage("Source [%s] does not contain field with name [gender]", mockSource); + assertThat(expected).hasNoCause(); + + throw expected; + } + finally { + verify(mockMethodInvocation, times(1)).getMethod(); + verify(mockSource, times(1)).hasField(eq("gender")); + verifyNoMoreInteractions(mockSource); + } + } + + @Test(expected = IllegalArgumentException.class) + public void invokeThrowsIllegalArgumentExceptionWhenMethodInvocationHasIncorrectNumberOfArguments() + throws Throwable { + + Method setGender = Person.class.getMethod("setGender", Gender.class); + + when(mockMethodInvocation.getMethod()).thenReturn(setGender); + when(mockSource.hasField(eq("gender"))).thenReturn(true); + when(mockMethodInvocation.getArguments()).thenReturn(asArray(Gender.FEMALE, Gender.MALE)); + + try { + newPdxInstanceMethodInterceptor(mockSource).invoke(mockMethodInvocation); + } + catch (IllegalArgumentException expected) { + assertThat(expected).hasMessage( + "Invoked setter method [setGender] must expect exactly 1 argument; Arguments were [[FEMALE, MALE]]"); + assertThat(expected).hasNoCause(); + + throw expected; + } + finally { + verify(mockMethodInvocation, times(1)).getMethod(); + verify(mockMethodInvocation, times(2)).getArguments(); + verify(mockSource, times(1)).hasField(eq("gender")); + } + } + + @Test(expected = IllegalStateException.class) + public void invokeThrowsIllegalArgumentExceptionWhenPdxInstanceHasNoWriter() throws Throwable { + Method setGender = Person.class.getMethod("setGender", Gender.class); + + when(mockMethodInvocation.getMethod()).thenReturn(setGender); + when(mockMethodInvocation.getArguments()).thenReturn(asArray(Gender.FEMALE)); + when(mockSource.hasField(eq("gender"))).thenReturn(true); + when(mockSource.createWriter()).thenReturn(null); + + try { + newPdxInstanceMethodInterceptor(mockSource).invoke(mockMethodInvocation); + } + catch (IllegalStateException expected) { + assertThat(expected).hasMessage( + "No writer for PdxInstance [%s] was found for setting field [gender] to value [FEMALE]", mockSource); + assertThat(expected).hasNoCause(); + + throw expected; + } + finally { + verify(mockMethodInvocation, times(1)).getMethod(); + verify(mockMethodInvocation, times(2)).getArguments(); + verify(mockSource, times(1)).hasField(eq("gender")); + verify(mockSource, times(1)).createWriter(); + } + } + + enum Gender { + FEMALE, + MALE + } + + @Data + @RequiredArgsConstructor(staticName = "newPerson") + @SuppressWarnings("unused") + static class Person { + + Gender gender; + + @NonNull String firstName; + @NonNull String lastName; + + /** + * @inheritDoc + */ + @Override + public String toString() { + return String.format("%1$s %2$s", getFirstName(), getLastName()); + } + } +}