From dfeb09c13eff35603744613e31a5a28923856f43 Mon Sep 17 00:00:00 2001 From: John Blum Date: Mon, 30 Apr 2018 15:45:16 -0700 Subject: [PATCH] Add Mock Object support for Apache Geode Lucene integration. --- .../tests/mock/GemFireMockObjectsSupport.java | 324 ++++++++++++++++-- .../data/gemfire/tests/util/IOUtils.java | 24 ++ .../data/gemfire/tests/util/ObjectUtils.java | 43 +++ 3 files changed, 364 insertions(+), 27 deletions(-) create mode 100644 src/main/java/org/springframework/data/gemfire/tests/util/ObjectUtils.java diff --git a/src/main/java/org/springframework/data/gemfire/tests/mock/GemFireMockObjectsSupport.java b/src/main/java/org/springframework/data/gemfire/tests/mock/GemFireMockObjectsSupport.java index 271c644..c2a5782 100644 --- a/src/main/java/org/springframework/data/gemfire/tests/mock/GemFireMockObjectsSupport.java +++ b/src/main/java/org/springframework/data/gemfire/tests/mock/GemFireMockObjectsSupport.java @@ -29,8 +29,12 @@ import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.when; +import static org.springframework.data.gemfire.tests.util.IOUtils.doSafeIo; +import static org.springframework.data.gemfire.tests.util.ObjectUtils.doSafeOperation; +import static org.springframework.data.gemfire.tests.util.ObjectUtils.rethrowAsRuntimeException; import static org.springframework.data.gemfire.util.ArrayUtils.nullSafeArray; import static org.springframework.data.gemfire.util.CollectionUtils.asSet; +import static org.springframework.data.gemfire.util.CollectionUtils.nullSafeMap; import static org.springframework.data.gemfire.util.CollectionUtils.nullSafeSet; import static org.springframework.data.gemfire.util.RuntimeExceptionFactory.NOT_SUPPORTED; import static org.springframework.data.gemfire.util.RuntimeExceptionFactory.newIllegalArgumentException; @@ -38,7 +42,6 @@ import static org.springframework.data.gemfire.util.RuntimeExceptionFactory.newI import static org.springframework.data.gemfire.util.RuntimeExceptionFactory.newUnsupportedOperationException; import java.io.File; -import java.io.IOException; import java.io.InputStream; import java.net.InetSocketAddress; import java.util.ArrayList; @@ -53,8 +56,10 @@ import java.util.Properties; import java.util.Set; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentSkipListSet; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.CopyOnWriteArraySet; +import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; @@ -97,6 +102,13 @@ import org.apache.geode.cache.client.Pool; import org.apache.geode.cache.client.PoolFactory; import org.apache.geode.cache.control.ResourceManager; import org.apache.geode.cache.execute.RegionFunctionContext; +import org.apache.geode.cache.lucene.LuceneIndex; +import org.apache.geode.cache.lucene.LuceneIndexFactory; +import org.apache.geode.cache.lucene.LuceneQuery; +import org.apache.geode.cache.lucene.LuceneQueryFactory; +import org.apache.geode.cache.lucene.LuceneQueryProvider; +import org.apache.geode.cache.lucene.LuceneSerializer; +import org.apache.geode.cache.lucene.LuceneService; import org.apache.geode.cache.query.CqAttributes; import org.apache.geode.cache.query.CqQuery; import org.apache.geode.cache.query.Index; @@ -116,8 +128,8 @@ import org.apache.geode.cache.wan.GatewayTransportFilter; import org.apache.geode.compression.Compressor; import org.apache.geode.distributed.DistributedMember; import org.apache.geode.distributed.DistributedSystem; -import org.apache.geode.internal.concurrent.ConcurrentHashSet; import org.apache.geode.pdx.PdxSerializer; +import org.apache.lucene.analysis.Analyzer; import org.mockito.ArgumentMatchers; import org.mockito.stubbing.Answer; import org.springframework.data.gemfire.IndexType; @@ -131,24 +143,44 @@ import org.springframework.util.Assert; * Mock GemFire Objects (e.g. {@link Cache}, {@link ClientCache}, {@link Region}, etc). * * @author John Blum + * @see java.util.Properties + * @see org.apache.geode.cache.AttributesMutator * @see org.apache.geode.cache.Cache * @see org.apache.geode.cache.CacheFactory + * @see org.apache.geode.cache.CustomExpiry * @see org.apache.geode.cache.DiskStore * @see org.apache.geode.cache.DiskStoreFactory * @see org.apache.geode.cache.GemFireCache + * @see org.apache.geode.cache.EvictionAttributes + * @see org.apache.geode.cache.EvictionAttributesMutator + * @see org.apache.geode.cache.ExpirationAttributes + * @see org.apache.geode.cache.PartitionAttributes * @see org.apache.geode.cache.Region + * @see org.apache.geode.cache.RegionAttributes * @see org.apache.geode.cache.RegionFactory * @see org.apache.geode.cache.RegionService * @see org.apache.geode.cache.asyncqueue.AsyncEventQueue * @see org.apache.geode.cache.asyncqueue.AsyncEventQueueFactory * @see org.apache.geode.cache.client.ClientCache * @see org.apache.geode.cache.client.ClientCacheFactory + * @see org.apache.geode.cache.client.ClientRegionFactory * @see org.apache.geode.cache.client.Pool * @see org.apache.geode.cache.client.PoolFactory * @see org.apache.geode.cache.control.ResourceManager + * @see org.apache.geode.cache.lucene.LuceneIndex + * @see org.apache.geode.cache.lucene.LuceneIndexFactory + * @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.LuceneSerializer + * @see org.apache.geode.cache.lucene.LuceneService + * @see org.apache.geode.cache.query.CqAttributes * @see org.apache.geode.cache.query.CqQuery + * @see org.apache.geode.cache.query.Index + * @see org.apache.geode.cache.query.IndexStatistics * @see org.apache.geode.cache.query.Query * @see org.apache.geode.cache.query.QueryService + * @see org.apache.geode.cache.query.QueryStatistics * @see org.apache.geode.cache.server.CacheServer * @see org.apache.geode.cache.server.ClientSubscriptionConfig * @see org.apache.geode.cache.wan.GatewayReceiver @@ -157,6 +189,7 @@ import org.springframework.util.Assert; * @see org.apache.geode.cache.wan.GatewaySenderFactory * @see org.apache.geode.distributed.DistributedMember * @see org.apache.geode.distributed.DistributedSystem + * @see org.apache.geode.pdx.PdxSerializer * @see org.mockito.Mockito * @see org.springframework.data.gemfire.tests.mock.MockObjectsSupport * @since 0.0.1 @@ -272,26 +305,6 @@ public abstract class GemFireMockObjectsSupport extends MockObjectsSupport { }).orElse(DataPolicy.DEFAULT); } - /** - * Executes the given {@link IoExceptionThrowingOperation}, handling any {@link IOException IOExceptions} thrown - * during normal IO processing. - * - * @param operation {@link IoExceptionThrowingOperation} to execute. - * @return a boolean indicating whether the IO operation was successful, or {@literal false} if the IO operation - * threw an {@link IOException}. - * @see IOException - */ - private static boolean doSafeIo(IoExceptionThrowingOperation operation) { - - try { - operation.doIo(); - return true; - } - catch (IOException cause) { - return false; - } - } - /** * Determines whether the given {@link Region} is a root {@link Region}. * @@ -1548,13 +1561,13 @@ public abstract class GemFireMockObjectsSupport extends MockObjectsSupport { return regionService; } - // TODO write more mocking logic for the QueryService interface + // TODO write additional mocking logic for the QueryService interface public static QueryService mockQueryService() { QueryService mockQueryService = mock(QueryService.class); - Set cqQueries = new ConcurrentHashSet<>(); - Set indexes = new ConcurrentHashSet<>(); + Set cqQueries = new ConcurrentSkipListSet<>(); + Set indexes = new ConcurrentSkipListSet<>(); try { when(mockQueryService.getCqs()).thenAnswer(invocation -> cqQueries.toArray(new CqQuery[cqQueries.size()])); @@ -1787,6 +1800,200 @@ public abstract class GemFireMockObjectsSupport extends MockObjectsSupport { return mock(IndexStatistics.class, mockObjectIdentifier(name)); } + public static LuceneIndexFactory mockLuceneIndexFactory() { + return mockLuceneIndexFactory(null); + } + + private static LuceneIndexFactory mockLuceneIndexFactory(Map luceneIndexes) { + + LuceneIndexFactory mockLuceneIndexFactory = mock(LuceneIndexFactory.class); + + AtomicReference luceneSerializerReference = new AtomicReference<>(null); + + Map fieldAnalyzers = new ConcurrentHashMap<>(); + + Set fields = new CopyOnWriteArraySet<>(); + + when(mockLuceneIndexFactory.addField(anyString())).thenAnswer(invocation -> { + + String fieldName = invocation.getArgument(0); + + fields.add(fieldName); + + return mockLuceneIndexFactory; + }); + + when(mockLuceneIndexFactory.addField(anyString(), any(Analyzer.class))).thenAnswer(invocation -> { + + String fieldName = invocation.getArgument(0); + Analyzer analyzer = invocation.getArgument(1); + + fieldAnalyzers.put(fieldName, analyzer); + + return mockLuceneIndexFactory; + }); + + when(mockLuceneIndexFactory.setFields(ArgumentMatchers.any())).thenAnswer(invocation -> { + + String[] fieldsArgument = invocation.getArgument(0); + + fields.clear(); + fields.addAll(Arrays.asList(nullSafeArray(fieldsArgument, String.class))); + + return mockLuceneIndexFactory; + }); + + when(mockLuceneIndexFactory.setFields(any(Map.class))).thenAnswer(invocation -> { + + Map fieldAnalyzersArgument = invocation.getArgument(0); + + fieldAnalyzers.clear(); + fieldAnalyzers.putAll(nullSafeMap(fieldAnalyzers)); + + return mockLuceneIndexFactory; + }); + + when(mockLuceneIndexFactory.setLuceneSerializer(any(LuceneSerializer.class))).thenAnswer(invocation -> { + + Optional.ofNullable(invocation.getArgument(0)) + .map(luceneSerializer -> { + luceneSerializerReference.set(luceneSerializer); + return luceneSerializer; + }) + .orElseGet(() -> { + luceneSerializerReference.set(null); + return null; + }); + + return mockLuceneIndexFactory; + }); + + doAnswer(invocation -> { + + String indexName = invocation.getArgument(0); + String regionPath = invocation.getArgument(1); + + LuceneIndexKey key = LuceneIndexKey.of(indexName, regionPath); + + LuceneIndex mockLuceneIndex = mock(LuceneIndex.class, key.toString()); + + when(mockLuceneIndex.getFieldAnalyzers()).thenReturn(Collections.unmodifiableMap(fieldAnalyzers)); + when(mockLuceneIndex.getFieldNames()).thenAnswer(in -> fields.toArray(new String[fields.size()])); + when(mockLuceneIndex.getLuceneSerializer()).thenAnswer(in -> luceneSerializerReference.get()); + when(mockLuceneIndex.getName()).thenReturn(indexName); + when(mockLuceneIndex.getRegionPath()).thenReturn(regionPath); + + Optional.ofNullable(luceneIndexes).ifPresent(it -> it.put(key, mockLuceneIndex)); + + return null; + + }).when(mockLuceneIndexFactory).create(anyString(), anyString()); + + return mockLuceneIndexFactory; + } + + public static LuceneQueryFactory mockLuceneQueryFactory() { + + LuceneQueryFactory mockLuceneQueryFactory = mock(LuceneQueryFactory.class); + + AtomicInteger limit = new AtomicInteger(LuceneQueryFactory.DEFAULT_LIMIT); + AtomicInteger pageSize = new AtomicInteger(LuceneQueryFactory.DEFAULT_PAGESIZE); + + when(mockLuceneQueryFactory.setLimit(anyInt())).thenAnswer(invocation -> { + limit.set(invocation.getArgument(0)); + return mockLuceneQueryFactory; + }); + + when(mockLuceneQueryFactory.setPageSize(anyInt())).thenAnswer(invocation -> { + pageSize.set(invocation.getArgument(0)); + return mockLuceneQueryFactory; + }); + + when(mockLuceneQueryFactory.create(anyString(), anyString(), any(LuceneQueryProvider.class))) + .thenAnswer(invocation -> mockLuceneQuery(limit.get(), pageSize.get())); + + when(mockLuceneQueryFactory.create(anyString(), anyString(), anyString(), anyString())) + .thenAnswer(invocation -> mockLuceneQuery(limit.get(), pageSize.get())); + + return mockLuceneQueryFactory; + } + + private static LuceneQuery mockLuceneQuery(int limit, int pageSize) { + + LuceneQuery mockLuceneQuery = mock(LuceneQuery.class); + + when(mockLuceneQuery.getLimit()).thenReturn(limit); + when(mockLuceneQuery.getPageSize()).thenReturn(pageSize); + + doSafeOperation(() -> when(mockLuceneQuery.findKeys()) + .thenReturn(Collections.emptySet()), Collections.emptySet()); + + rethrowAsRuntimeException(() -> when(mockLuceneQuery.findPages()) + .thenThrow(newUnsupportedOperationException("Operation Not Supported!"))); + + doSafeOperation(() -> when(mockLuceneQuery.findResults()) + .thenReturn(Collections.emptyList()), Collections.emptyList()); + + doSafeOperation(() -> when(mockLuceneQuery.findValues()) + .thenReturn(Collections.emptyList()), Collections.emptyList()); + + return mockLuceneQuery; + } + + public static LuceneService mockLuceneService(Cache mockCache) { + + LuceneService mockLuceneService = mock(LuceneService.class); + + Map luceneIndexes = new ConcurrentHashMap<>(); + + when(mockLuceneService.createIndexFactory()) + .thenAnswer(invocation -> mockLuceneIndexFactory(luceneIndexes)); + + when(mockLuceneService.createLuceneQueryFactory()) + .thenAnswer(invocation -> mockLuceneQueryFactory()); + + doAnswer(invocation -> { + + String indexName = invocation.getArgument(0); + String regionName = invocation .getArgument(1); + + luceneIndexes.remove(LuceneIndexKey.of(indexName, regionName)); + + return null; + + }).when(mockLuceneService).destroyIndex(anyString(), anyString()); + + doAnswer(invocation -> { + + String regionPath = invocation.getArgument(0); + + luceneIndexes.keySet().stream().filter(key -> key.getRegionPath().equals(regionPath)) + .collect(Collectors.toSet()).forEach(key -> luceneIndexes.remove(key)); + + return null; + + }).when(mockLuceneService).destroyIndexes(anyString()); + + when(mockLuceneService.getAllIndexes()).thenAnswer(invocation -> + Collections.unmodifiableCollection(luceneIndexes.values())); + + when(mockLuceneService.getCache()).thenReturn(mockCache); + + when(mockLuceneService.getIndex(anyString(), anyString())).thenAnswer(invocation -> { + + String indexName = invocation.getArgument(0); + String regionPath = invocation.getArgument(1); + + return luceneIndexes.get(LuceneIndexKey.of(indexName, regionPath)); + }); + + doSafeOperation(() -> + when(mockLuceneService.waitUntilFlushed(anyString(), anyString(), anyLong(), any(TimeUnit.class))) + .thenReturn(true)); + + return mockLuceneService; + } + @SuppressWarnings("unchecked") public static Region mockRegion(RegionService regionService, String name, RegionAttributes regionAttributes) { @@ -2565,7 +2772,70 @@ public abstract class GemFireMockObjectsSupport extends MockObjectsSupport { return clientCacheFactorySpy; } - protected interface IoExceptionThrowingOperation { - void doIo() throws IOException; + public static class LuceneIndexKey { + + private final String indexName; + private final String regionPath; + + public static LuceneIndexKey of(String indexName, Region region) { + + Assert.notNull(region, "Region is required"); + + return of(indexName, region.getFullPath()); + } + + public static LuceneIndexKey of(String indexName, String regionPath) { + return new LuceneIndexKey(indexName, regionPath); + } + + private LuceneIndexKey(String indexName, String regionPath) { + + Assert.hasText(indexName, String.format("LuceneIndex name [%s] is required", indexName)); + Assert.hasText(regionPath, String.format("Region path [%s] is required", regionPath)); + + this.indexName = indexName; + this.regionPath = regionPath; + } + + protected String getIndexName() { + return this.indexName; + } + + protected String getRegionPath() { + return this.regionPath; + } + + @Override + public boolean equals(Object obj) { + + if (obj == this) { + return true; + } + + if (!(obj instanceof LuceneIndexKey)) { + return false; + } + + LuceneIndexKey that = (LuceneIndexKey) obj; + + return this.getIndexName().equals(that.getIndexName()) + && this.getRegionPath().equals(that.getRegionPath()); + } + + @Override + public int hashCode() { + + int hashValue = 17; + + hashValue = 37 * hashValue + getIndexName().hashCode(); + hashValue = 37 * hashValue + getRegionPath().hashCode(); + + return hashValue; + } + + @Override + public String toString() { + return String.format("%1$s.%2$s", getRegionPath(), getIndexName()); + } } } diff --git a/src/main/java/org/springframework/data/gemfire/tests/util/IOUtils.java b/src/main/java/org/springframework/data/gemfire/tests/util/IOUtils.java index aa10334..24d5100 100644 --- a/src/main/java/org/springframework/data/gemfire/tests/util/IOUtils.java +++ b/src/main/java/org/springframework/data/gemfire/tests/util/IOUtils.java @@ -58,6 +58,26 @@ public abstract class IOUtils { return false; } + /** + * Executes the given {@link IoExceptionThrowingOperation}, handling any {@link IOException IOExceptions} thrown + * during normal IO processing. + * + * @param operation {@link IoExceptionThrowingOperation} to execute. + * @return a boolean indicating whether the IO operation was successful, or {@literal false} if the IO operation + * threw an {@link IOException}. + * @see IOException + */ + public static boolean doSafeIo(IoExceptionThrowingOperation operation) { + + try { + operation.doIo(); + return true; + } + catch (IOException cause) { + return false; + } + } + @SuppressWarnings("unchecked") public static T deserializeObject(byte[] objectBytes) throws IOException, ClassNotFoundException { @@ -92,4 +112,8 @@ public abstract class IOUtils { IOUtils.close(objectOutputStream); } } + + public interface IoExceptionThrowingOperation { + void doIo() throws IOException; + } } diff --git a/src/main/java/org/springframework/data/gemfire/tests/util/ObjectUtils.java b/src/main/java/org/springframework/data/gemfire/tests/util/ObjectUtils.java new file mode 100644 index 0000000..ded598f --- /dev/null +++ b/src/main/java/org/springframework/data/gemfire/tests/util/ObjectUtils.java @@ -0,0 +1,43 @@ +package org.springframework.data.gemfire.tests.util; + +/** + * {@link ObjectUtils} is a utility class for working with {@link Object objects}. + * + * @author John Blum + * @see java.lang.Object + * @since 1.0.0 + */ +@SuppressWarnings("all") +public abstract class ObjectUtils { + + public static T doSafeOperation(ExceptionThrowingOperation operation) { + return doSafeOperation(operation, null); + } + + public static T doSafeOperation(ExceptionThrowingOperation operation, T defaultValue) { + + try { + return operation.doExceptionThrowingOperation(); + } + catch (Exception ignore) { + return defaultValue; + } + } + + public static T rethrowAsRuntimeException(ExceptionThrowingOperation operation) { + + try { + return operation.doExceptionThrowingOperation(); + } + catch (RuntimeException cause) { + throw cause; + } + catch (Exception cause) { + throw new RuntimeException(cause); + } + } + + public interface ExceptionThrowingOperation { + T doExceptionThrowingOperation() throws Exception; + } +}