diff --git a/src/main/java/org/springframework/data/gemfire/domain/EmptyPage.java b/src/main/java/org/springframework/data/gemfire/domain/EmptyPage.java new file mode 100644 index 00000000..af0fc184 --- /dev/null +++ b/src/main/java/org/springframework/data/gemfire/domain/EmptyPage.java @@ -0,0 +1,62 @@ +/* + * 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.domain; + +import org.springframework.core.convert.converter.Converter; +import org.springframework.data.domain.Page; + +/** + * The {@link EmptyPage} class is an implementation of an empty Spring Data {@link Page}. + * + * @author John Blum + * @param {@link Class} type of the elements in this {@link Page}. + * @see org.springframework.core.convert.converter.Converter + * @see org.springframework.data.domain.Page + * @since 1.1.0 + */ +@SuppressWarnings("unused") +public final class EmptyPage extends EmptySlice implements Page { + + @SuppressWarnings("all") + public static final EmptyPage EMPTY_PAGE = new EmptyPage<>(); + + /** + * @inheritDoc + */ + @Override + public int getTotalPages() { + return 1; + } + + /** + * @inheritDoc + */ + @Override + public long getTotalElements() { + return 0; + } + + /** + * @inheritDoc + */ + @Override + @SuppressWarnings("unchecked") + public Page map(Converter converter) { + return (Page) EMPTY_PAGE; + } +} diff --git a/src/main/java/org/springframework/data/gemfire/domain/EmptySlice.java b/src/main/java/org/springframework/data/gemfire/domain/EmptySlice.java new file mode 100644 index 00000000..9af1bf24 --- /dev/null +++ b/src/main/java/org/springframework/data/gemfire/domain/EmptySlice.java @@ -0,0 +1,110 @@ +/* + * 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.domain; + +import java.util.Collections; +import java.util.List; + +import org.springframework.core.convert.converter.Converter; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; +import org.springframework.data.domain.Sort; +import org.springframework.data.gemfire.domain.support.AbstractSliceSupport; + +/** + * The {@link EmptySlice} class is an implementation of an empty Spring Data {@link Slice}. + * + * @author John Blum + * @param {@link Class} type of the elements in this {@link Slice}. + * @see org.springframework.data.domain.Pageable + * @see org.springframework.data.domain.Slice + * @see org.springframework.data.domain.Sort + * @see org.springframework.data.gemfire.domain.support.AbstractSliceSupport + * @since 1.1.0 + */ +@SuppressWarnings("unused") +public abstract class EmptySlice extends AbstractSliceSupport { + + @SuppressWarnings("all") + public static final EmptySlice EMPTY_SLICE = new EmptySlice() { }; + + /** + * @inheritDoc + */ + @Override + public boolean hasNext() { + return false; + } + + /** + * @inheritDoc + */ + @Override + public boolean hasPrevious() { + return false; + } + + /** + * @inheritDoc + */ + @Override + public List getContent() { + return Collections.emptyList(); + } + + /** + * @inheritDoc + */ + @Override + public int getNumber() { + return 1; + } + + /** + * @inheritDoc + */ + @Override + public Sort getSort() { + return null; + } + + /** + * @inheritDoc + */ + @Override + public Pageable nextPageable() { + return null; + } + + /** + * @inheritDoc + */ + @Override + public Pageable previousPageable() { + return null; + } + + /** + * @inheritDoc + */ + @Override + @SuppressWarnings("unchecked") + public Slice map(Converter converter) { + return (Slice) EMPTY_SLICE; + } +} diff --git a/src/main/java/org/springframework/data/gemfire/domain/ListablePage.java b/src/main/java/org/springframework/data/gemfire/domain/ListablePage.java new file mode 100644 index 00000000..d379da11 --- /dev/null +++ b/src/main/java/org/springframework/data/gemfire/domain/ListablePage.java @@ -0,0 +1,169 @@ +/* + * 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.domain; + +import java.util.Arrays; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +import org.springframework.core.convert.converter.Converter; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Sort; +import org.springframework.data.gemfire.domain.support.AbstractPageSupport; + +/** + * The {@link ListablePage} class is a Spring Data {@link Page} implementation wrapping a {@link List} as the content + * for this {@link ListablePage page}. + * + * @author John Blum + * @see java.util.Iterator + * @see java.util.List + * @see org.springframework.data.domain.Page + * @see org.springframework.data.gemfire.domain.support.AbstractPageSupport + * @since 1.0.0 + */ +@SuppressWarnings("unused") +public class ListablePage extends AbstractPageSupport { + + /** + * Factory method used to construct a new instance of {@link ListablePage} initialized with the given array, + * serving as the content for this {@link Page page}. + * + * @param {@link Class} type of the elements in the array. + * @param content array of elements serving as the content for this {@link ListablePage page}. + * @return a new {@link ListablePage} initialized with the given array for content. + * @see java.util.Arrays#asList(Object[]) + * @see #ListablePage(List) + */ + @SafeVarargs + public static ListablePage newListablePage(T... content) { + return new ListablePage<>(Arrays.asList(content)); + } + + /** + * Factory method used to construct a new instance of {@link ListablePage} initialized with the given {@link List}, + * serving as the content for this {@link Page page}. + * + * @param {@link Class} type of the elements in the {@link List}. + * @param content {@link List} of elements serving as the content for this {@link ListablePage page}. + * @return a new {@link ListablePage} initialized with the given {@link List} for content. + * @see #ListablePage(List) + */ + public static ListablePage newListablePage(List content) { + return new ListablePage<>(content); + } + + private final List content; + + /** + * Constructs an new instance of {@link ListablePage} initialized with the given {@link List} used as the content + * for this {@link ListablePage page}. + * + * @param content {@link List} of elements serving as the content for this {@link ListablePage page}. + * Guards against {@literal null} by initializing the {@code content} to an empty {@link List} + * if the given {@link List} is {@literal null}. + * @see java.util.Collections#emptyList() + * @see java.util.List + */ + public ListablePage(List content) { + this.content = Optional.ofNullable(content).orElse(Collections.emptyList()); + } + + /** + * @inheritDoc + */ + @Override + public boolean hasContent() { + return !getContent().isEmpty(); + } + + /** + * @inheritDoc + */ + @Override + public boolean hasNext() { + return false; + } + + /** + * @inheritDoc + */ + @Override + public boolean hasPrevious() { + return false; + } + + /** + * @inheritDoc + */ + @Override + public List getContent() { + return Collections.unmodifiableList(this.content); + } + + /** + * @inheritDoc + */ + @Override + public int getNumber() { + return 1; + } + + /** + * @inheritDoc + */ + @Override + public Sort getSort() { + return null; + } + + /** + * @inheritDoc + */ + @Override + public long getTotalElements() { + return getSize(); + } + + /** + * @inheritDoc + */ + @Override + public int getTotalPages() { + return 1; + } + + /** + * @inheritDoc + */ + @Override + public Iterator iterator() { + return getContent().iterator(); + } + + /** + * @inheritDoc + */ + @Override + public Page map(Converter converter) { + return newListablePage(getContent().stream().map(converter::convert).collect(Collectors.toList())); + } +} diff --git a/src/main/java/org/springframework/data/gemfire/domain/support/AbstractPageSupport.java b/src/main/java/org/springframework/data/gemfire/domain/support/AbstractPageSupport.java new file mode 100644 index 00000000..93c6426b --- /dev/null +++ b/src/main/java/org/springframework/data/gemfire/domain/support/AbstractPageSupport.java @@ -0,0 +1,64 @@ +/* + * 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.domain.support; + +import static org.springframework.data.gemfire.util.RuntimeExceptionFactory.newUnsupportedOperationException; + +import org.springframework.core.convert.converter.Converter; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Slice; +import org.springframework.data.gemfire.util.RuntimeExceptionFactory; + +/** + * The {@link AbstractPageSupport} class is an abstract Spring Data {@link Page} type supporting the implementation of + * application specific {@link Page} implementations. + * + * @author John Blum + * @param {@link Class} type of the individual elements on this {@link Slice}. + * @see org.springframework.core.convert.converter.Converter + * @see org.springframework.data.domain.Page + * @see org.springframework.data.domain.Slice + * @since 1.1.0 + */ +@SuppressWarnings("unused") +public abstract class AbstractPageSupport extends AbstractSliceSupport implements Page { + + /** + * @inheritDoc + */ + @Override + public long getTotalElements() { + throw newUnsupportedOperationException(RuntimeExceptionFactory.NOT_IMPLEMENTED); + } + + /** + * @inheritDoc + */ + @Override + public int getTotalPages() { + throw newUnsupportedOperationException(RuntimeExceptionFactory.NOT_IMPLEMENTED); + } + + /** + * @inheritDoc + */ + @Override + public Page map(Converter converter) { + throw newUnsupportedOperationException(RuntimeExceptionFactory.NOT_IMPLEMENTED); + } +} diff --git a/src/main/java/org/springframework/data/gemfire/domain/support/AbstractSliceSupport.java b/src/main/java/org/springframework/data/gemfire/domain/support/AbstractSliceSupport.java new file mode 100644 index 00000000..efabeeed --- /dev/null +++ b/src/main/java/org/springframework/data/gemfire/domain/support/AbstractSliceSupport.java @@ -0,0 +1,175 @@ +/* + * 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.domain.support; + +import static org.springframework.data.gemfire.util.RuntimeExceptionFactory.newUnsupportedOperationException; + +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicInteger; + +import org.springframework.core.convert.converter.Converter; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; +import org.springframework.data.domain.Sort; +import org.springframework.data.gemfire.util.RuntimeExceptionFactory; + +/** + * The {@link AbstractSliceSupport} class is an abstract Spring Data {@link Slice} type + * supporting the implementation of application specific {@link Slice} implementations. + * + * @author John Blum + * @param {@link Class} type of the individual elements on this {@link Slice}. + * @see org.springframework.core.convert.converter.Converter + * @see org.springframework.data.domain.Page + * @see org.springframework.data.domain.Pageable + * @see org.springframework.data.domain.Slice + * @see org.springframework.data.domain.Sort + * @since 1.1.0 + */ +@SuppressWarnings("unused") +public abstract class AbstractSliceSupport implements Slice { + + /** + * @inheritDoc + */ + @Override + public boolean hasContent() { + return (getNumberOfElements() > 0); + } + + /** + * @inheritDoc + */ + @Override + public boolean hasNext() { + throw newUnsupportedOperationException(RuntimeExceptionFactory.NOT_IMPLEMENTED); + } + + /** + * @inheritDoc + */ + @Override + public boolean hasPrevious() { + throw newUnsupportedOperationException(RuntimeExceptionFactory.NOT_IMPLEMENTED); + } + + /** + * @inheritDoc + */ + @Override + public boolean isFirst() { + return !hasPrevious(); + } + + /** + * @inheritDoc + */ + @Override + public boolean isLast() { + return !hasNext(); + } + + /** + * @inheritDoc + */ + @Override + public List getContent() { + throw newUnsupportedOperationException(RuntimeExceptionFactory.NOT_IMPLEMENTED); + } + + /** + * @inheritDoc + */ + @Override + public int getNumber() { + AtomicInteger number = new AtomicInteger(1); + + Optional.ofNullable(previousPageable()).ifPresent(previousPageable -> { + Pageable currentPageable; + + do { + number.incrementAndGet(); + currentPageable = previousPageable; + previousPageable = previousPageable.previousOrFirst(); + } + while (currentPageable != previousPageable); + }); + + return number.get(); + } + + /** + * @inheritDoc + */ + @Override + public int getNumberOfElements() { + return getContent().size(); + } + + /** + * @inheritDoc + */ + @Override + public int getSize() { + return getNumberOfElements(); + } + + /** + * @inheritDoc + */ + @Override + public Sort getSort() { + throw newUnsupportedOperationException(RuntimeExceptionFactory.NOT_IMPLEMENTED); + } + + /** + * @inheritDoc + */ + @Override + public Iterator iterator() { + return Collections.unmodifiableList(Optional.ofNullable(getContent()) + .orElseGet(Collections::emptyList)).iterator(); + } + + /** + * @inheritDoc + */ + @Override + public Slice map(Converter converter) { + throw newUnsupportedOperationException(RuntimeExceptionFactory.NOT_IMPLEMENTED); + } + + /** + * @inheritDoc + */ + @Override + public Pageable nextPageable() { + throw newUnsupportedOperationException(RuntimeExceptionFactory.NOT_IMPLEMENTED); + } + + /** + * @inheritDoc + */ + @Override + public Pageable previousPageable() { + throw newUnsupportedOperationException(RuntimeExceptionFactory.NOT_IMPLEMENTED); + } +} 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 index db8fa4bf..7ca143af 100644 --- a/src/main/java/org/springframework/data/gemfire/search/lucene/ProjectingLuceneAccessor.java +++ b/src/main/java/org/springframework/data/gemfire/search/lucene/ProjectingLuceneAccessor.java @@ -17,10 +17,13 @@ package org.springframework.data.gemfire.search.lucene; +import java.util.List; import java.util.Optional; +import java.util.stream.Collectors; import org.apache.geode.cache.Region; import org.apache.geode.cache.lucene.LuceneIndex; +import org.apache.geode.cache.lucene.LuceneResultStruct; import org.springframework.beans.BeansException; import org.springframework.beans.factory.BeanClassLoaderAware; import org.springframework.beans.factory.BeanFactory; @@ -194,4 +197,17 @@ public abstract class ProjectingLuceneAccessor extends LuceneTemplate protected ProjectionFactory getProjectionFactory() { return this.projectionFactory; } + + public List project(List> source, Class projectionType) { + return source.stream().map(luceneResultStruct -> project(luceneResultStruct, projectionType)) + .collect(Collectors.toList()); + } + + public T project(LuceneResultStruct source, Class projectionType) { + return project(source.getValue(), projectionType); + } + + public T project(Object source, Class projectionType) { + return getProjectionFactory().createProjection(projectionType, source); + } } 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 index bf09ae18..05cb51ff 100644 --- a/src/main/java/org/springframework/data/gemfire/search/lucene/ProjectingLuceneTemplate.java +++ b/src/main/java/org/springframework/data/gemfire/search/lucene/ProjectingLuceneTemplate.java @@ -17,10 +17,9 @@ package org.springframework.data.gemfire.search.lucene; -import static org.springframework.data.gemfire.util.RuntimeExceptionFactory.newUnsupportedOperationException; +import static org.springframework.data.gemfire.search.lucene.support.LucenePage.newLucenePage; import java.util.List; -import java.util.stream.Collectors; import org.apache.geode.cache.Region; import org.apache.geode.cache.lucene.LuceneIndex; @@ -92,9 +91,7 @@ public class ProjectingLuceneTemplate extends ProjectingLuceneAccessor { */ @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()); + return project(query(query, defaultField, resultLimit), projectionType); } /** @@ -104,7 +101,7 @@ public class ProjectingLuceneTemplate extends ProjectingLuceneAccessor { public Page query(String query, String defaultField, int resultLimit, int pageSize, Class projectionType) { - throw newUnsupportedOperationException("Not Implemented"); + return newLucenePage(this, query(query, defaultField, resultLimit, pageSize), pageSize, projectionType); } /** @@ -112,9 +109,7 @@ public class ProjectingLuceneTemplate extends ProjectingLuceneAccessor { */ @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()); + return project(query(queryProvider, resultLimit), projectionType); } /** @@ -124,6 +119,6 @@ public class ProjectingLuceneTemplate extends ProjectingLuceneAccessor { public Page query(LuceneQueryProvider queryProvider, int resultLimit, int pageSize, Class projectionType) { - throw newUnsupportedOperationException("Not Implemented"); + return newLucenePage(this, query(queryProvider, resultLimit, pageSize), pageSize, projectionType); } } diff --git a/src/main/java/org/springframework/data/gemfire/search/lucene/support/LucenePage.java b/src/main/java/org/springframework/data/gemfire/search/lucene/support/LucenePage.java new file mode 100644 index 00000000..2c3084dd --- /dev/null +++ b/src/main/java/org/springframework/data/gemfire/search/lucene/support/LucenePage.java @@ -0,0 +1,333 @@ +/* + * 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.domain.ListablePage.newListablePage; + +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Collectors; + +import org.apache.geode.cache.lucene.LuceneResultStruct; +import org.apache.geode.cache.lucene.PageableLuceneQueryResults; +import org.springframework.core.convert.converter.Converter; +import org.springframework.data.domain.Page; +import org.springframework.data.gemfire.domain.support.AbstractPageSupport; +import org.springframework.data.gemfire.search.lucene.ProjectingLuceneAccessor; +import org.springframework.util.Assert; + +/** + * The {@link LucenePage} class is a Spring Data {@link Page} implementation supporting Spring Data style paging + * of {@link PageableLuceneQueryResults} complete with Spring Data projections. + * + * @author John Blum + * @see java.util.List + * @see org.apache.geode.cache.lucene.LuceneResultStruct + * @see org.apache.geode.cache.lucene.PageableLuceneQueryResults + * @see org.springframework.data.domain.Page + * @see org.springframework.data.gemfire.domain.support.AbstractPageSupport + * @see org.springframework.data.gemfire.search.lucene.ProjectingLuceneAccessor + * @since 1.1.0 + */ +public class LucenePage extends AbstractPageSupport { + + /** + * Factory method used to construct a new instance of {@link LucenePage} initialized with + * the given {@link PageableLuceneQueryResults Lucene query results}, {@link Integer page size}, + * and {@link Class projection type}. + * + * The {@link LucenePage previous page} is set to {@literal null}. + * + * @param template {@link ProjectingLuceneAccessor} used to perform Lucene queries and data access operations + * along with projections. + * @param queryResults {@link PageableLuceneQueryResults} wrapped by this {@link LucenePage}. + * @param pageSize number of elements on a {@link LucenePage}. + * @param projectionType {@link Class} type of the projection used to view an individual {@link LuceneResultStruct} + * in the {@link PageableLuceneQueryResults Lucene query results}. + * @throws IllegalArgumentException if {@link ProjectingLuceneAccessor} or the {@link PageableLuceneQueryResults} + * are {@literal null}, or the {@link PageableLuceneQueryResults} do not have + * a {@link PageableLuceneQueryResults#hasNext() next page}. + * @see #LucenePage(ProjectingLuceneAccessor, PageableLuceneQueryResults, int, Class) + */ + public static LucenePage newLucenePage(ProjectingLuceneAccessor template, + PageableLuceneQueryResults queryResults, int pageSize, Class projectionType) { + + return new LucenePage<>(template, queryResults, pageSize, projectionType); + } + + /** + * Factory method used to construct a new instance of {@link LucenePage} initialized with + * the given {@link PageableLuceneQueryResults Lucene query results}, {@link Integer page size}, + * {@link Class projection type} and {@link LucenePage previous page}, if one exists. + * + * @param template {@link ProjectingLuceneAccessor} used to perform Lucene queries and data access operations + * along with projections. + * @param queryResults {@link PageableLuceneQueryResults} wrapped by this {@link LucenePage}. + * @param pageSize number of elements on a {@link LucenePage}. + * @param projectionType {@link Class} type of the projection used to view an individual {@link LuceneResultStruct} + * in the {@link PageableLuceneQueryResults Lucene query results}. + * @param previousPage {@link LucenePage previous page} in the chain of {@link LucenePage pages}, + * if this {@link LucenePage} is not the first {@link LucenePage}. Can be {@literal null}. + * @throws IllegalArgumentException if {@link ProjectingLuceneAccessor} or the {@link PageableLuceneQueryResults} + * are {@literal null}, or the {@link PageableLuceneQueryResults} do not have + * a {@link PageableLuceneQueryResults#hasNext() next page}. + * @see #LucenePage(ProjectingLuceneAccessor, PageableLuceneQueryResults, int, Class, LucenePage) + */ + public static LucenePage newLucenePage(ProjectingLuceneAccessor template, + PageableLuceneQueryResults queryResults, int pageSize, Class projectionType, + LucenePage previousPage) { + + return new LucenePage<>(template, queryResults, pageSize, projectionType, previousPage); + } + + private LucenePage next; + private LucenePage previous; + + private final int pageSize; + + private final Class projectionType; + + private final List content; + + private final PageableLuceneQueryResults queryResults; + + private final ProjectingLuceneAccessor template; + + /** + * Constructs a new instance of {@link LucenePage} initialized with + * the given {@link PageableLuceneQueryResults Lucene query results}, {@link Integer page size} + * and {@link Class projection type}. + * + * The {@link LucenePage previous page} is set to {@literal null}. + * + * @param template {@link ProjectingLuceneAccessor} used to perform Lucene queries and data access operations + * along with projections. + * @param queryResults {@link PageableLuceneQueryResults} wrapped by this {@link LucenePage}. + * @param pageSize number of elements on a {@link LucenePage}. + * @param projectionType {@link Class} type of the projection used to view an individual {@link LuceneResultStruct} + * in the {@link PageableLuceneQueryResults Lucene query results}. + * @throws IllegalArgumentException if {@link ProjectingLuceneAccessor} or the {@link PageableLuceneQueryResults} + * are {@literal null}, or the {@link PageableLuceneQueryResults} do not have + * a {@link PageableLuceneQueryResults#hasNext() next page}. + * @see #LucenePage(ProjectingLuceneAccessor, PageableLuceneQueryResults, int, Class, LucenePage) + */ + public LucenePage(ProjectingLuceneAccessor template, PageableLuceneQueryResults queryResults, + int pageSize, Class projectionType) { + + this(template, queryResults, pageSize, projectionType, null); + } + + /** + * Constructs a new instance of {@link LucenePage} initialized with + * the given {@link PageableLuceneQueryResults Lucene query results}, {@link Integer page size}, + * {@link Class projection type} and {@link LucenePage previous page}, if one exists. + * + * @param template {@link ProjectingLuceneAccessor} used to perform Lucene queries and data access operations + * along with projections. + * @param queryResults {@link PageableLuceneQueryResults} wrapped by this {@link LucenePage}. + * @param pageSize number of elements on a {@link LucenePage}. + * @param projectionType {@link Class} type of the projection used to view an individual {@link LuceneResultStruct} + * in the {@link PageableLuceneQueryResults Lucene query results}. + * @param previous {@link LucenePage previous page} in the chain of {@link LucenePage pages}, + * if this {@link LucenePage} is not the first {@link LucenePage}. Can be {@literal null}. + * @throws IllegalArgumentException if {@link ProjectingLuceneAccessor} or the {@link PageableLuceneQueryResults} + * are {@literal null}, or the {@link PageableLuceneQueryResults} do not have + * a {@link PageableLuceneQueryResults#hasNext() next page}. + * @see #materialize(ProjectingLuceneAccessor, List, Class) + */ + public LucenePage(ProjectingLuceneAccessor template, PageableLuceneQueryResults queryResults, + int pageSize, Class projectionType, LucenePage previous) { + + Assert.notNull(template, "ProjectingLuceneAccessor must not be null"); + Assert.notNull(queryResults, "PageableLuceneQueryResults must not be null"); + Assert.isTrue(queryResults.hasNext(), "PageableLuceneQueryResults must have content"); + + this.template = template; + this.queryResults = queryResults; + this.pageSize = pageSize; + this.projectionType = projectionType; + this.previous = previous; + this.content = materialize(template, queryResults.next(), projectionType); + } + + /** + * Renders the {@link List} of {@link LuceneResultStruct} objects into projected values based on + * the {@link Class projection type}. + * + * @param template {@link ProjectingLuceneAccessor} used to project the desired values + * from the {@link List} of {@link LuceneResultStruct} objects. + * @param pageOfQueryResults Lucene query results captured in the {@link List} + * of {@link LuceneResultStruct} objects. + * @param projectionType {@link Class} type to project the {@link LuceneResultStruct} objects as. + * @return a {@link List} of projected values of the given {@link Class projection type}. + * @see org.apache.geode.cache.lucene.LuceneResultStruct + */ + protected List materialize(ProjectingLuceneAccessor template, List> pageOfQueryResults, + Class projectionType) { + + return template.project(pageOfQueryResults, projectionType); + } + + /** + * Returns the number of elements per {@link LucenePage page}. + * + * @return an integer value indicating the number of elements on a {@link LucenePage page}. + */ + protected int getPageSize() { + return this.pageSize; + } + + /** + * Returns the {@link Class} type of the projection. + * + * @return a {@link Class} specifying the projection type. + */ + protected Class getProjectionType() { + return this.projectionType; + } + + /** + * Returns the {@link PageableLuceneQueryResults Lucene query results} backing this {@link LucenePage}. + * + * @return a reference to the {@link PageableLuceneQueryResults} backing this {@link LucenePage}. + * @see org.springframework.data.gemfire.search.lucene.support.LucenePage + * @see org.apache.geode.cache.lucene.PageableLuceneQueryResults + */ + protected PageableLuceneQueryResults getQueryResults() { + return this.queryResults; + } + + /** + * Returns the {@link ProjectingLuceneAccessor} used by this {@link LucenePage} to perform + * Lucene data access operations and projections. + * + * @return the {@link ProjectingLuceneAccessor} used by this {@link LucenePage} to perform + * Lucene data access operations and projections. + * @see org.springframework.data.gemfire.search.lucene.ProjectingLuceneAccessor + */ + protected ProjectingLuceneAccessor getTemplate() { + return this.template; + } + + /** + * @inheritDoc + */ + @Override + public boolean hasNext() { + return getQueryResults().hasNext(); + } + + /** + * @inheritDoc + */ + @Override + public boolean hasPrevious() { + return (getPrevious() != null); + } + + /** + * @inheritDoc + */ + @Override + public List getContent() { + return Collections.unmodifiableList(this.content); + } + + /** + * Null-safe method to return the next {@link LucenePage page} in the collection of {@link Page pages}. + * + * @return the next {@link LucenePage page} in the collection of {@link Page pages}. + * @throws IllegalStateException if no more {@link Page pages} exist beyond this {@link LucenePage page}. + * @see org.springframework.data.gemfire.search.lucene.support.LucenePage + * @see #getPrevious() + */ + public LucenePage getNext() { + return Optional.ofNullable(this.next).orElseGet(() -> { + Assert.state(hasNext(), "No more pages"); + this.next = newLucenePage(getTemplate(), getQueryResults(), getPageSize(), getProjectionType(), this); + return this.next; + }); + } + + /** + * @inheritDoc + */ + @Override + public int getNumber() { + AtomicInteger number = new AtomicInteger(1); + + Optional.ofNullable(getPrevious()).ifPresent(previous -> { + while (previous != null) { + previous = previous.getPrevious(); + number.incrementAndGet(); + } + }); + + return number.get(); + } + + /** + * Returns the previous {@link LucenePage page} in the collection of {@link Page pages}. + * + * @return the previous {@link LucenePage page} in the collection of {@link Page pages} + * or {@literal null} if no {@link LucenePage} proceeds this {@link LucenePage page}. + * @see org.springframework.data.gemfire.search.lucene.support.LucenePage + * @see #getNext() + */ + public LucenePage getPrevious() { + return this.previous; + } + + /** + * @inheritDoc + */ + @Override + public int getSize() { + return getPageSize(); + } + + /** + * @inheritDoc + */ + @Override + public long getTotalElements() { + return getQueryResults().size(); + } + + /** + * @inheritDoc + */ + @Override + public int getTotalPages() { + long totalElements = getTotalElements(); + int pageSize = getPageSize(); + int totalPages = Double.valueOf(Math.floor(totalElements / pageSize)).intValue(); + totalPages += (totalElements % pageSize != 0 ? 1 : 0); + return totalPages; + } + + /** + * @inheritDoc + */ + @Override + public Page map(Converter converter) { + return newListablePage(getContent().stream().map(converter::convert).collect(Collectors.toList())); + } +} diff --git a/src/main/java/org/springframework/data/gemfire/util/RuntimeExceptionFactory.java b/src/main/java/org/springframework/data/gemfire/util/RuntimeExceptionFactory.java index 1dc58603..42a3e4fa 100644 --- a/src/main/java/org/springframework/data/gemfire/util/RuntimeExceptionFactory.java +++ b/src/main/java/org/springframework/data/gemfire/util/RuntimeExceptionFactory.java @@ -31,6 +31,7 @@ import java.text.MessageFormat; public abstract class RuntimeExceptionFactory { public static final String NOT_IMPLEMENTED = "Not Implemented"; + public static final String NOT_SUPPORTED = "Operation Not Supported"; /** * Constructs and initializes an {@link IllegalArgumentException} with the given {@link String message} diff --git a/src/test/java/org/springframework/data/gemfire/domain/ListablePageUnitTests.java b/src/test/java/org/springframework/data/gemfire/domain/ListablePageUnitTests.java new file mode 100644 index 00000000..e9e9302f --- /dev/null +++ b/src/test/java/org/springframework/data/gemfire/domain/ListablePageUnitTests.java @@ -0,0 +1,135 @@ +/* + * 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.domain; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.data.gemfire.domain.ListablePage.newListablePage; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import org.junit.Test; +import org.springframework.data.domain.Page; + +/** + * Unit tests for {@link ListablePage}. + * + * @author John Blum + * @see org.junit.Test + * @since 1.0.0 + */ +public class ListablePageUnitTests { + + @Test + public void newListablePageWithArray() { + ListablePage page = newListablePage("one", "two", "three"); + + assertThat(page).isNotNull(); + assertThat(page).contains("one", "two", "three"); + } + + @Test + public void newListablePageWithList() { + ListablePage page = newListablePage(Arrays.asList(1, 2, 3)); + + assertThat(page).isNotNull(); + assertThat(page).contains(1, 2, 3); + } + + @Test + public void newListablePageWithNull() { + ListablePage page = newListablePage((List) null); + + assertThat(page).isNotNull(); + assertThat(page).isEmpty(); + } + + @Test + public void listablePageWithContentHasCorrectState() { + List content = Arrays.asList(1, 2, 3); + ListablePage page = newListablePage(content); + + assertThat(page).isNotNull(); + assertThat(page).isNotEmpty(); + assertThat(page).containsAll(content); + assertThat(page.hasContent()).isTrue(); + assertThat(page.hasNext()).isFalse(); + assertThat(page.hasPrevious()).isFalse(); + assertThat(page.isFirst()).isTrue(); + assertThat(page.isLast()).isTrue(); + assertThat(page.getContent()).isEqualTo(content); + assertThat(page.getNumber()).isEqualTo(1); + assertThat(page.getNumberOfElements()).isEqualTo(content.size()); + assertThat(page.getSize()).isEqualTo(content.size()); + assertThat(page.getSort()).isNull(); + assertThat(page.getTotalElements()).isEqualTo(content.size()); + assertThat(page.getTotalPages()).isEqualTo(1); + } + + @Test + public void listablePageWithNoContentHasCorrectState() { + ListablePage page = newListablePage(); + + assertThat(page).isNotNull(); + assertThat(page).isEmpty(); + assertThat(page.hasContent()).isFalse(); + assertThat(page.hasNext()).isFalse(); + assertThat(page.hasPrevious()).isFalse(); + assertThat(page.isFirst()).isTrue(); + assertThat(page.isLast()).isTrue(); + assertThat(page.getContent()).isEqualTo(Collections.emptyList()); + assertThat(page.getNumber()).isEqualTo(1); + assertThat(page.getNumberOfElements()).isEqualTo(0); + assertThat(page.getSize()).isEqualTo(0); + assertThat(page.getSort()).isNull(); + assertThat(page.getTotalElements()).isEqualTo(0); + assertThat(page.getTotalPages()).isEqualTo(1); + } + + @Test + public void iterationIsCorrect() { + ListablePage page = newListablePage(1, 2, 3); + List elements = new ArrayList<>(page.getSize()); + + for (Object element : page) { + elements.add(element); + } + + assertThat(elements).isEqualTo(page.getContent()); + } + + @Test + public void mapWithConvertersIsCorrect() { + ListablePage page = newListablePage("1", "2", "3"); + + assertThat(page).isNotNull(); + assertThat(page).hasSize(3); + + Page integersPage = page.map(value -> Integer.parseInt(String.valueOf(value))); + + assertThat(integersPage).isNotNull(); + assertThat(integersPage).contains(1, 2, 3); + + Page doublesPage = integersPage.map(Integer::doubleValue); + + assertThat(doublesPage).isNotNull(); + assertThat(doublesPage).contains(1.0d, 2.0d, 3.0d); + } +} diff --git a/src/test/java/org/springframework/data/gemfire/domain/support/AbstractSliceSupportTests.java b/src/test/java/org/springframework/data/gemfire/domain/support/AbstractSliceSupportTests.java new file mode 100644 index 00000000..bb10c717 --- /dev/null +++ b/src/test/java/org/springframework/data/gemfire/domain/support/AbstractSliceSupportTests.java @@ -0,0 +1,161 @@ +/* + * 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.domain.support; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.Arrays; +import java.util.List; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Spy; +import org.mockito.runners.MockitoJUnitRunner; +import org.springframework.data.domain.Pageable; + +/** + * Unit tests for {@link AbstractSliceSupport}. + * + * @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 + * @since 1.0.0 + */ +@RunWith(MockitoJUnitRunner.class) +public class AbstractSliceSupportTests { + + @Spy + private AbstractSliceSupport mockSlice; + + @Test + public void hasContentIsTrue() { + doReturn(1).when(mockSlice).getNumberOfElements(); + assertThat(mockSlice.hasContent()).isTrue(); + verify(mockSlice, times(1)).getNumberOfElements(); + } + + @Test + public void hasContentIsFalse() { + doReturn(0).when(mockSlice).getNumberOfElements(); + assertThat(mockSlice.hasContent()).isFalse(); + verify(mockSlice, times(1)).getNumberOfElements(); + } + + @Test + public void isFirstReturnsTrueWhenHasPreviousIsFalse() { + doReturn(false).when(mockSlice).hasPrevious(); + assertThat(mockSlice.isFirst()).isTrue(); + verify(mockSlice, times(1)).hasPrevious(); + } + + @Test + public void isFirstReturnsFalseWhenHasPreviousIsTrue() { + doReturn(true).when(mockSlice).hasPrevious(); + assertThat(mockSlice.isFirst()).isFalse(); + verify(mockSlice, times(1)).hasPrevious(); + } + + @Test + public void isLastReturnsTrueWhenHasNextIsFalse() { + doReturn(false).when(mockSlice).hasNext(); + assertThat(mockSlice.isLast()).isTrue(); + verify(mockSlice, times(1)).hasNext(); + } + + @Test + public void isLastReturnFalseWhenHasNextIsTrue() { + doReturn(true).when(mockSlice).hasNext(); + assertThat(mockSlice.isLast()).isFalse(); + verify(mockSlice, times(1)).hasNext(); + } + + @Test + public void getNumberReturnsOne() { + doReturn(null).when(mockSlice).previousPageable(); + assertThat(mockSlice.getNumber()).isEqualTo(1); + verify(mockSlice, times(1)).previousPageable(); + } + + @Test + public void getNumberReturnsTwo() { + Pageable mockPageable = mock(Pageable.class); + + when(mockPageable.previousOrFirst()).thenReturn(mockPageable); + when(mockPageable.hasPrevious()).thenReturn(false); + doReturn(mockPageable).when(mockSlice).previousPageable(); + + assertThat(mockSlice.getNumber()).isEqualTo(2); + + verify(mockSlice, times(1)).previousPageable(); + verify(mockPageable, times(1)).previousOrFirst(); + } + + @Test + public void getNumberReturnsThree() { + Pageable mockPageableOne = mock(Pageable.class, "Page One"); + Pageable mockPageableTwo = mock(Pageable.class, "Page Two"); + + when(mockPageableOne.previousOrFirst()).thenReturn(mockPageableOne); + when(mockPageableOne.hasPrevious()).thenReturn(false); + when(mockPageableTwo.previousOrFirst()).thenReturn(mockPageableOne); + when(mockPageableTwo.hasPrevious()).thenReturn(true); + doReturn(mockPageableTwo).when(mockSlice).previousPageable(); + + assertThat(mockSlice.getNumber()).isEqualTo(3); + + verify(mockSlice, times(1)).previousPageable(); + verify(mockPageableOne, times(1)).previousOrFirst(); + verify(mockPageableTwo, times(1)).previousOrFirst(); + } + + @Test + public void getNumberOfElementsReturnsTwenty() { + List mockContent = mock(List.class); + + when(mockContent.size()).thenReturn(20); + doReturn(mockContent).when(mockSlice).getContent(); + + assertThat(mockSlice.getNumberOfElements()).isEqualTo(20); + + verify(mockSlice, times(1)).getContent(); + verify(mockContent, times(1)).size(); + } + + @Test + public void getSizeCallsGetNumberOfElements() { + doReturn(18).when(mockSlice).getNumberOfElements(); + assertThat(mockSlice.getSize()).isEqualTo(18); + verify(mockSlice, times(1)).getNumberOfElements(); + } + + @Test + @SuppressWarnings("unchecked") + public void iteratorWithContent() { + doReturn(Arrays.asList(1, 2, 3)).when(mockSlice).getContent(); + assertThat(mockSlice.iterator()).contains(1, 2, 3); + verify(mockSlice, times(1)).getContent(); + } +} diff --git a/src/test/java/org/springframework/data/gemfire/search/lucene/LuceneOperationsIntegrationTests.java b/src/test/java/org/springframework/data/gemfire/search/lucene/LuceneOperationsIntegrationTests.java new file mode 100644 index 00000000..c0494e85 --- /dev/null +++ b/src/test/java/org/springframework/data/gemfire/search/lucene/LuceneOperationsIntegrationTests.java @@ -0,0 +1,201 @@ +/* + * 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.Assertions.assertThat; + +import java.io.Serializable; +import java.time.LocalDate; +import java.time.Month; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.concurrent.atomic.AtomicLong; +import java.util.stream.Collectors; + +import javax.annotation.Resource; + +import org.apache.geode.cache.GemFireCache; +import org.apache.geode.cache.Region; +import org.apache.geode.cache.lucene.LuceneIndex; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.DependsOn; +import org.springframework.data.annotation.Id; +import org.springframework.data.gemfire.PartitionedRegionFactoryBean; +import org.springframework.data.gemfire.config.annotation.PeerCacheApplication; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringRunner; + +import lombok.Data; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; + +/** + * Integration tests for the Spring Data Geode, Apache Geode and Apache Lucene Integration. + * + * @author John Blum + * @see org.junit.Test + * @see lombok + * @see org.apache.geode.cache.GemFireCache + * @see org.apache.geode.cache.Region + * @see org.apache.geode.cache.lucene.LuceneIndex + * @see org.springframework.data.gemfire.config.annotation.PeerCacheApplication + * @see org.springframework.test.context.ContextConfiguration + * @see org.springframework.test.context.junit4.SpringRunner + * @since 1.1.0 + */ +@RunWith(SpringRunner.class) +@ContextConfiguration +@SuppressWarnings("unused") +public class LuceneOperationsIntegrationTests { + + private static final AtomicLong IDENTIFIER = new AtomicLong(0L); + + protected static final String LOG_LEVEL = "none"; + + private Person jonDoe; + private Person janeDoe; + private Person cookieDoe; + private Person froDoe; + private Person hoDoe; + private Person pieDoe; + private Person sourDoe; + + @Autowired + private ProjectingLuceneOperations template; + + @Resource(name = "People") + private Region people; + + protected Person save(Person person) { + person.setId(IDENTIFIER.incrementAndGet()); + people.put(person.getId(), person); + return person; + } + + @Before + public void setup() { + jonDoe = save(Person.newPerson(LocalDate.of(1969, Month.JULY, 4), "Jon", "Doe").with("Master of Science")); + janeDoe = save(Person.newPerson(LocalDate.of(1969, Month.AUGUST, 16), "Jane", "Doe").with("Doctor of Astrophysics")); + cookieDoe = save(Person.newPerson(LocalDate.of(1991, Month.APRIL, 2), "Cookie", "Doe").with("Bachelor of Physics")); + froDoe = save(Person.newPerson(LocalDate.of(1988, Month.MAY, 25), "Fro", "Doe").with("Doctor of Computer Science")); + hoDoe = save(Person.newPerson(LocalDate.of(1984, Month.NOVEMBER, 11), "Ho", "Doe").with("Doctor of Math")); + pieDoe = save(Person.newPerson(LocalDate.of(1996, Month.JUNE, 4), "Pie", "Doe").with("Master of Astronomy")); + sourDoe = save(Person.newPerson(LocalDate.of(1999, Month.DECEMBER, 1), "Sour", "Doe").with("Bachelor of Art")); + } + + protected List asNames(List nameables) { + return nameables.stream().map(Nameable::getName).collect(Collectors.toList()); + } + + protected List asUsers(Person... people) { + return Arrays.stream(people).map(User::from).collect(Collectors.toList()); + } + + @Test + public void findsDoctorDoesAsTypePersonSuccessfully() { + Collection doctorDoes = template.queryForValues("title: Doctor*", "title"); + + assertThat(doctorDoes).isNotNull(); + assertThat(doctorDoes).hasSize(3); + assertThat(doctorDoes).contains(janeDoe, froDoe, hoDoe); + + } + + @Test + @SuppressWarnings("all") + public void findsMasterDoesAsTypeUserSuccessfully() { + List masterDoes = template.query("title: Master*", "title", User.class); + + assertThat(masterDoes).isNotNull(); + assertThat(masterDoes).hasSize(2); + assertThat(masterDoes.stream().allMatch(user -> user instanceof User)).isTrue(); + assertThat(asNames(masterDoes)).containsAll(asNames(asUsers(jonDoe, pieDoe))); + } + + @SuppressWarnings("unused") + @PeerCacheApplication(name = "LuceneOperationsIntegrationTests", logLevel = LOG_LEVEL) + static class LuceneOperationsIntegrationTestConfiguration { + + @Bean(name = "People") + @DependsOn("personTitleIndex") + PartitionedRegionFactoryBean peopleRegion(GemFireCache gemfireCache) { + PartitionedRegionFactoryBean peopleRegion = new PartitionedRegionFactoryBean<>(); + + peopleRegion.setCache(gemfireCache); + peopleRegion.setClose(false); + peopleRegion.setPersistent(false); + + return peopleRegion; + } + + @Bean + LuceneIndexFactoryBean personTitleIndex(GemFireCache gemFireCache) { + LuceneIndexFactoryBean luceneIndex = new LuceneIndexFactoryBean(); + + luceneIndex.setCache(gemFireCache); + luceneIndex.setFields("title"); + luceneIndex.setRegionPath("/People"); + + return luceneIndex; + } + + @Bean + ProjectingLuceneOperations luceneTemplate(LuceneIndex luceneIndex) { + return new ProjectingLuceneTemplate("personTitleIndex", "/People"); + } + } + + interface Nameable { + String getName(); + } + + @Data + @RequiredArgsConstructor(staticName = "newPerson") + static class Person implements Nameable, Serializable { + + @Id + Long id; + + @NonNull LocalDate birthDate; + + @NonNull String firstName; + @NonNull String lastName; + + String title; + + public String getName() { + return String.format("%1$s %2$s", getFirstName(), getLastName()); + } + + Person with(String title) { + setTitle(title); + return this; + } + } + + interface User extends Nameable { + static User from(Person person) { + return person::getName; + } + } +} diff --git a/src/test/java/org/springframework/data/gemfire/search/lucene/support/LucenePageUnitTests.java b/src/test/java/org/springframework/data/gemfire/search/lucene/support/LucenePageUnitTests.java new file mode 100644 index 00000000..7b15b666 --- /dev/null +++ b/src/test/java/org/springframework/data/gemfire/search/lucene/support/LucenePageUnitTests.java @@ -0,0 +1,549 @@ +/* + * 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.Assertions.assertThat; +import static org.mockito.Matchers.eq; +import static org.mockito.Matchers.isA; +import static org.mockito.Mockito.mock; +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.LucenePage.newLucenePage; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.concurrent.atomic.AtomicLong; +import java.util.stream.Collectors; + +import org.apache.geode.cache.lucene.LuceneResultStruct; +import org.apache.geode.cache.lucene.PageableLuceneQueryResults; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; +import org.springframework.data.domain.Page; +import org.springframework.data.gemfire.search.lucene.ProjectingLuceneAccessor; + +import lombok.Data; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; + +/** + * Unit tests for {@link LucenePage}. + * + * @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.ProjectingLuceneAccessor + * @see org.springframework.data.gemfire.search.lucene.support.LucenePage + * @see org.apache.geode.cache.lucene.LuceneResultStruct + * @see org.apache.geode.cache.lucene.PageableLuceneQueryResults + * @since 1.0.0 + */ +@RunWith(MockitoJUnitRunner.class) +public class LucenePageUnitTests { + + @Mock + private PageableLuceneQueryResults mockQueryResults; + + @Mock + private ProjectingLuceneAccessor mockTemplate; + + @SuppressWarnings("unchecked") + protected LuceneResultStruct mockLuceneResultStruct(K key, V value) { + LuceneResultStruct mockLuceneResultStruct = mock(LuceneResultStruct.class); + + when(mockLuceneResultStruct.getKey()).thenReturn(key); + when(mockLuceneResultStruct.getValue()).thenReturn(value); + + return mockLuceneResultStruct; + } + + protected List> mockLuceneResultStructList(List people) { + AtomicLong id = new AtomicLong(0L); + + return people.stream().map(person -> mockLuceneResultStruct(id.incrementAndGet(), person.getName())) + .collect(Collectors.toList()); + } + + protected List> prepare(PageableLuceneQueryResults mockQueryResults, + Person... results) { + + return prepare(mockQueryResults, Arrays.asList(results), results.length).get(0); + } + + protected List> prepare(PageableLuceneQueryResults mockQueryResults, + Iterable results) { + + return prepare(mockQueryResults, results, size(results)).get(0); + } + + protected List>> prepare( + PageableLuceneQueryResults mockQueryResults, Iterable results, int pageSize) { + + List> pages = paginate(results, pageSize); + + List>> resultStructs = + pages.stream().map(this::mockLuceneResultStructList).collect(Collectors.toList()); + + Iterator iterator = resultStructs.iterator(); + + when(mockQueryResults.hasNext()).thenAnswer(invocation -> iterator.hasNext()); + when(mockQueryResults.next()).thenAnswer(invocation -> iterator.next()); + + return resultStructs; + } + + private List> paginate(Iterable people, int pageSize) { + List> pages = new ArrayList<>(); + List page = new ArrayList<>(pageSize); + + for (Person person : people) { + if (page.size() == pageSize) { + pages.add(page); + page = new ArrayList<>(pageSize); + } + + page.add(person); + } + + pages.add(page); + + return pages; + } + + private int size(Iterable iterable) { + int size = 0; + + for (Object element : iterable) { + size++; + } + + return size; + } + + @SuppressWarnings("unchecked") + protected ProjectingLuceneAccessor prepare(ProjectingLuceneAccessor mockTemplate) { + when(mockTemplate.project(isA(List.class), eq(Person.class))).thenAnswer(invocation -> { + List> results = invocation.getArgumentAt(0, List.class); + assertThat(invocation.getArgumentAt(1, Class.class)).isEqualTo(Person.class); + return results.stream().map(result -> Person.parse(result.getValue())).collect(Collectors.toList()); + }); + + return mockTemplate; + } + + @Test + @SuppressWarnings("unchecked") + public void newLucenePageWithNoPreviousPageIsMaterialized() { + List people = Collections.singletonList(Person.newPerson("Jon", "Doe")); + + List> mockResultStructList = prepare(mockQueryResults, people); + + LucenePage page = + newLucenePage(prepare(mockTemplate), mockQueryResults, 20, Person.class); + + assertThat(page).isNotNull(); + assertThat(page.getContent()).isEqualTo(people); + assertThat(page.getPageSize()).isEqualTo(20); + assertThat(page.getPrevious()).isNull(); + assertThat(page.getProjectionType()).isEqualTo(Person.class); + assertThat(page.getQueryResults()).isSameAs(mockQueryResults); + assertThat(page.getTemplate()).isSameAs(mockTemplate); + assertThat(page.hasNext()).isFalse(); + assertThat(page.hasPrevious()).isFalse(); + + verify(mockQueryResults, times(2)).hasNext(); + verify(mockQueryResults, times(1)).next(); + verifyNoMoreInteractions(mockQueryResults); + verify(mockTemplate, times(1)).project(eq(mockResultStructList), eq(Person.class)); + verifyNoMoreInteractions(mockTemplate); + } + + @Test + public void newLucenePageWithPreviousPageIsMaterialized() { + List expectedContent = Arrays.asList(Person.newPerson("Jon", "Doe"), Person.newPerson("Jane", "Doe")); + + List>> mockResults = + prepare(mockQueryResults, expectedContent, 1); + + LucenePage firstPage = + newLucenePage(prepare(mockTemplate), mockQueryResults, 1, Person.class); + + LucenePage secondPage = + newLucenePage(mockTemplate, mockQueryResults, 1, Person.class, firstPage); + + assertThat(secondPage).isNotNull(); + assertThat(secondPage.getContent()).isEqualTo(Collections.singletonList(expectedContent.get(1))); + assertThat(secondPage.getPageSize()).isEqualTo(1); + assertThat(secondPage.getPrevious()).isEqualTo(firstPage); + assertThat(secondPage.getProjectionType()).isEqualTo(Person.class); + assertThat(secondPage.getQueryResults()).isSameAs(mockQueryResults); + assertThat(secondPage.getTemplate()).isSameAs(mockTemplate); + assertThat(secondPage.hasNext()).isFalse(); + assertThat(secondPage.hasPrevious()).isTrue(); + + verify(mockQueryResults, times(3)).hasNext(); + verify(mockQueryResults, times(2)).next(); + verifyNoMoreInteractions(mockQueryResults); + verify(mockTemplate, times(1)) + .project(eq(mockResults.get(0)), eq(Person.class)); + verify(mockTemplate, times(1)) + .project(eq(mockResults.get(1)), eq(Person.class)); + verifyNoMoreInteractions(mockTemplate); + } + + @Test(expected = IllegalArgumentException.class) + public void newLucenePageWithNullTemplateThrowsIllegalArgumentException() { + try { + newLucenePage(null, mockQueryResults, 10, Person.class); + } + catch (IllegalArgumentException expected) { + assertThat(expected).hasMessage("ProjectingLuceneAccessor must not be null"); + assertThat(expected).hasNoCause(); + + throw expected; + } + finally { + verifyZeroInteractions(mockQueryResults); + } + } + + @Test(expected = IllegalArgumentException.class) + public void newLucenePageWithNullQueryResultsThrowsIllegalArgumentException() { + try { + newLucenePage(mockTemplate, null, 10, Person.class); + } + catch (IllegalArgumentException expected) { + assertThat(expected).hasMessage("PageableLuceneQueryResults must not be null"); + assertThat(expected).hasNoCause(); + + throw expected; + } + finally { + verifyZeroInteractions(mockTemplate); + } + } + + @Test(expected = IllegalArgumentException.class) + public void newLucenePageWithNoContentThrowsIllegalArgumentException() { + try { + when(mockQueryResults.hasNext()).thenReturn(false); + newLucenePage(mockTemplate, mockQueryResults, 10, Person.class); + } + catch (IllegalArgumentException expected) { + assertThat(expected).hasMessage("PageableLuceneQueryResults must have content"); + assertThat(expected).hasNoCause(); + + throw expected; + } + finally { + verify(mockQueryResults, times(1)).hasNext(); + verifyZeroInteractions(mockTemplate); + } + } + + @Test + public void getNextReturnsNextPage() { + List expectedContent = Arrays.asList(Person.newPerson("Jon", "Doe"), Person.newPerson("Jane", "Doe")); + + List>> mockResults = + prepare(mockQueryResults, expectedContent, 1); + + LucenePage firstPage = + newLucenePage(prepare(mockTemplate), mockQueryResults, 20, Person.class); + + assertThat(firstPage).isNotNull(); + assertThat(firstPage.getContent()).isEqualTo(Collections.singletonList(expectedContent.get(0))); + + LucenePage nextPage = firstPage.getNext(); + + assertThat(nextPage).isNotNull(); + assertThat(nextPage.getContent()).isEqualTo(Collections.singletonList(expectedContent.get(1))); + + verify(mockQueryResults, times(3)).hasNext(); + verify(mockQueryResults, times(2)).next(); + verifyNoMoreInteractions(mockQueryResults); + verify(mockTemplate, times(1)).project(eq(mockResults.get(0)), eq(Person.class)); + verify(mockTemplate, times(1)).project(eq(mockResults.get(1)), eq(Person.class)); + verifyNoMoreInteractions(mockQueryResults); + } + + @SuppressWarnings("unchecked") + @Test(expected = IllegalStateException.class) + public void getNextThrowsIllegalStateExceptionWhenNoMorePages() { + try { + prepare(mockQueryResults, Person.newPerson("Jon", "Doe")); + + LucenePage page = newLucenePage(mockTemplate, mockQueryResults, 20, Person.class); + + assertThat(page).isNotNull(); + assertThat(page.getPrevious()).isNull(); + + page.getNext(); + } + catch (IllegalStateException expected) { + assertThat(expected).hasMessage("No more pages"); + assertThat(expected).hasNoCause(); + + throw expected; + } + finally { + verify(mockQueryResults, times(2)).hasNext(); + verify(mockQueryResults, times(1)).next(); + verifyNoMoreInteractions(mockQueryResults); + verify(mockTemplate, times(1)).project(isA(List.class), eq(Person.class)); + verifyNoMoreInteractions(mockTemplate); + } + } + + @Test + @SuppressWarnings("unchecked") + public void getNumberReturnsOne() { + prepare(mockQueryResults, Person.newPerson("Jon", "Doe")); + + LucenePage page = newLucenePage(mockTemplate, mockQueryResults, 20, Person.class); + + assertThat(page).isNotNull(); + assertThat(page.getPrevious()).isNull(); + assertThat(page.getNumber()).isEqualTo(1); + } + + @Test + @SuppressWarnings("unchecked") + public void getNumberReturnsTwo() { + prepare(mockQueryResults, Arrays.asList(Person.newPerson("Jon", "Doe"), Person.newPerson("Jane", "Doe")), + 1); + + LucenePage previousPage = + newLucenePage(mockTemplate, mockQueryResults, 20, Person.class); + + LucenePage page = + newLucenePage(mockTemplate, mockQueryResults, 20, Person.class, previousPage); + + assertThat(page).isNotNull(); + assertThat(page.getPrevious()).isEqualTo(previousPage); + assertThat(page.getNumber()).isEqualTo(2); + } + + @Test + @SuppressWarnings("unchecked") + public void getNumberReturnsThree() { + prepare(mockQueryResults, Arrays.asList(Person.newPerson("Jon", "Doe"), Person.newPerson("Jane", "Doe"), + Person.newPerson("Pie", "Doe")), 1); + + LucenePage firstPage = newLucenePage(mockTemplate, mockQueryResults, 20, Person.class); + + LucenePage secondPage = + newLucenePage(mockTemplate, mockQueryResults, 20, Person.class, firstPage); + + LucenePage thirdPage = + newLucenePage(mockTemplate, mockQueryResults, 20, Person.class, secondPage); + + assertThat(thirdPage).isNotNull(); + assertThat(thirdPage.getPrevious()).isEqualTo(secondPage); + assertThat(secondPage.getPrevious()).isEqualTo(firstPage); + assertThat(firstPage.getPrevious()).isNull(); + assertThat(thirdPage.getNumber()).isEqualTo(3); + } + + @Test + public void getSizeWithNoContentEqualsPageSize() { + when(mockQueryResults.hasNext()).thenReturn(true); + when(mockQueryResults.next()).thenReturn(Collections.emptyList()); + + LucenePage page = newLucenePage(mockTemplate, mockQueryResults, 20, Person.class); + + assertThat(page).isNotNull(); + assertThat(page.getNumberOfElements()).isEqualTo(0); + assertThat(page.getPageSize()).isEqualTo(20); + assertThat(page.getSize()).isEqualTo(20); + } + + @Test + public void getSizeWithSingleElementEqualsPageSize() { + prepare(mockQueryResults, Person.newPerson("Jon", "Doe")); + + LucenePage page = + newLucenePage(prepare(mockTemplate), mockQueryResults, 20, Person.class); + + assertThat(page).isNotNull(); + assertThat(page.getNumberOfElements()).isEqualTo(1); + assertThat(page.getPageSize()).isEqualTo(20); + assertThat(page.getSize()).isEqualTo(20); + } + + @Test + @SuppressWarnings("unchecked") + public void getSizeWithMultipleElementsEqualsPageSize() { + List mockList = mock(List.class); + + when(mockList.size()).thenReturn(101); + when(mockQueryResults.hasNext()).thenReturn(true); + when(mockQueryResults.next()).thenReturn(Collections.emptyList()); + when(mockTemplate.project(isA(List.class), eq(Person.class))).thenReturn(mockList); + + LucenePage page = newLucenePage(mockTemplate, mockQueryResults, 20, Person.class); + + assertThat(page).isNotNull(); + assertThat(page.getNumberOfElements()).isEqualTo(101); + assertThat(page.getPageSize()).isEqualTo(20); + assertThat(page.getSize()).isEqualTo(20); + + verify(mockList, times(1)).size(); + } + + @Test + public void totalElementsEqualsLuceneQueryResultsSize() { + when(mockQueryResults.hasNext()).thenReturn(true); + when(mockQueryResults.next()).thenReturn(Collections.emptyList()); + when(mockQueryResults.size()).thenReturn(409); + + LucenePage page = newLucenePage(mockTemplate, mockQueryResults, 20, Person.class); + + assertThat(page).isNotNull(); + assertThat(page.getTotalElements()).isEqualTo(409); + + verify(mockQueryResults, times(1)).size(); + } + + @Test + public void totalPagesIsOneWhenTotalElementsIsLessThanPageSize() { + when(mockQueryResults.hasNext()).thenReturn(true); + when(mockQueryResults.next()).thenReturn(Collections.emptyList()); + when(mockQueryResults.size()).thenReturn(9); + + LucenePage page = newLucenePage(mockTemplate, mockQueryResults, 20, Person.class); + + assertThat(page).isNotNull(); + assertThat(page.getTotalPages()).isEqualTo(1); + + verify(mockQueryResults, times(1)).size(); + } + + @Test + public void totalPagesIsOneWhenTotalElementsEqualsPageSize() { + when(mockQueryResults.hasNext()).thenReturn(true); + when(mockQueryResults.next()).thenReturn(Collections.emptyList()); + when(mockQueryResults.size()).thenReturn(20); + + LucenePage page = newLucenePage(mockTemplate, mockQueryResults, 20, Person.class); + + assertThat(page).isNotNull(); + assertThat(page.getTotalPages()).isEqualTo(1); + + verify(mockQueryResults, times(1)).size(); + } + + @Test + public void totalPagesIsTwoWhenTotalElementsIsGreaterThanPageSize() { + when(mockQueryResults.hasNext()).thenReturn(true); + when(mockQueryResults.next()).thenReturn(Collections.emptyList()); + when(mockQueryResults.size()).thenReturn(31); + + LucenePage page = newLucenePage(mockTemplate, mockQueryResults, 20, Person.class); + + assertThat(page).isNotNull(); + assertThat(page.getTotalPages()).isEqualTo(2); + + verify(mockQueryResults, times(1)).size(); + } + + @Test + public void totalPagesIsFiveWhenTotalElementsIsGreaterThanEqualToPageSize() { + when(mockQueryResults.hasNext()).thenReturn(true); + when(mockQueryResults.next()).thenReturn(Collections.emptyList()); + when(mockQueryResults.size()).thenReturn(100); + + LucenePage page = newLucenePage(mockTemplate, mockQueryResults, 20, Person.class); + + assertThat(page).isNotNull(); + assertThat(page.getTotalPages()).isEqualTo(5); + + verify(mockQueryResults, times(1)).size(); + } + + @Test + public void totalPagesIsSixWhenTotalElementsIsGreaterThanPageSize() { + when(mockQueryResults.hasNext()).thenReturn(true); + when(mockQueryResults.next()).thenReturn(Collections.emptyList()); + when(mockQueryResults.size()).thenReturn(101); + + LucenePage page = newLucenePage(mockTemplate, mockQueryResults, 20, Person.class); + + assertThat(page).isNotNull(); + assertThat(page.getTotalPages()).isEqualTo(6); + + verify(mockQueryResults, times(1)).size(); + } + + @Test + public void mapIsSuccessful() { + List expectedContent = Arrays.asList(Person.newPerson("Jon", "Doe"), Person.newPerson("Jane", "Doe")); + + prepare(mockQueryResults, expectedContent); + + LucenePage page = + newLucenePage(prepare(mockTemplate), mockQueryResults, 20, Person.class); + + assertThat(page).isNotNull(); + assertThat(page.getNumberOfElements()).isEqualTo(expectedContent.size()); + assertThat(page).containsAll(expectedContent); + + Page users = page.map(User::from); + + assertThat(users).isNotNull(); + assertThat(users.getNumberOfElements()).isEqualTo(expectedContent.size()); + assertThat(users).contains(User.newUser("jonDoe"), User.newUser("janeDoe")); + } + + @Data + @RequiredArgsConstructor(staticName = "newPerson") + static class Person { + @NonNull String firstName; + @NonNull String lastName; + + static Person parse(String name) { + String[] firstNameLastName = name.split(" "); + return newPerson(firstNameLastName[0], firstNameLastName[1]); + } + + String getName() { + return String.format("%1$s %2$s", getFirstName(), getLastName()); + } + } + + @Data + @RequiredArgsConstructor(staticName = "newUser") + static class User { + @NonNull String name; + + static User from(Person person) { + return newUser(String.format("%1$s%2$s", person.getFirstName().toLowerCase(), person.getLastName())); + } + } +}