From eccf60c737562987a2bbad691f56955b77506e01 Mon Sep 17 00:00:00 2001 From: John Blum Date: Fri, 11 Sep 2020 13:03:13 -0700 Subject: [PATCH] DATAGEODE-367 - Expose EntityInformation and the GemfireTemplate on SimpleGemfireRepository. --- .../gemfire/repository/GemfireRepository.java | 8 +- .../support/SimpleGemfireRepository.java | 219 ++++++--- .../gemfire/repository/sample/Animal.java | 17 +- .../repository/sample/Identifiable.java | 28 ++ ...mpleGemfireRepositoryIntegrationTests.java | 25 +- .../SimpleGemfireRepositoryUnitTests.java | 428 ++++++++++++++---- 6 files changed, 563 insertions(+), 162 deletions(-) create mode 100644 spring-data-geode/src/test/java/org/springframework/data/gemfire/repository/sample/Identifiable.java diff --git a/spring-data-geode/src/main/java/org/springframework/data/gemfire/repository/GemfireRepository.java b/spring-data-geode/src/main/java/org/springframework/data/gemfire/repository/GemfireRepository.java index 3ab3c26e8..b790bf9b1 100644 --- a/spring-data-geode/src/main/java/org/springframework/data/gemfire/repository/GemfireRepository.java +++ b/spring-data-geode/src/main/java/org/springframework/data/gemfire/repository/GemfireRepository.java @@ -19,7 +19,7 @@ import org.springframework.data.repository.CrudRepository; /** - * GemFire specific extension of the Spring Data {@link CrudRepository} interface. + * Apache Geode extension of the Spring Data {@link CrudRepository} interface. * * @author Oliver Gierke * @author John Blum @@ -28,10 +28,10 @@ public interface GemfireRepository extends CrudRepository { /** - * Returns all entities sorted by the given options. + * Returns all entities ordered by the given {@link Sort}. * - * @param sort the Spring Data Commons Sort type defining the ordering criteria. - * @return all entities sorted by the given options. + * @param sort {@link Sort} defining the ordering criteria. + * @return all entities ordered by the given {@link Sort}. * @see org.springframework.data.repository.PagingAndSortingRepository#findAll(org.springframework.data.domain.Sort) * @see org.springframework.data.domain.Sort * @see java.lang.Iterable diff --git a/spring-data-geode/src/main/java/org/springframework/data/gemfire/repository/support/SimpleGemfireRepository.java b/spring-data-geode/src/main/java/org/springframework/data/gemfire/repository/support/SimpleGemfireRepository.java index 1fd8011c2..ab3fe41eb 100644 --- a/spring-data-geode/src/main/java/org/springframework/data/gemfire/repository/support/SimpleGemfireRepository.java +++ b/spring-data-geode/src/main/java/org/springframework/data/gemfire/repository/support/SimpleGemfireRepository.java @@ -15,7 +15,7 @@ */ package org.springframework.data.gemfire.repository.support; -import java.util.Collection; +import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.List; @@ -37,22 +37,31 @@ import org.springframework.data.gemfire.repository.Wrapper; import org.springframework.data.gemfire.repository.query.QueryString; import org.springframework.data.gemfire.util.CollectionUtils; +import org.springframework.data.gemfire.util.SpringUtils; +import org.springframework.data.repository.CrudRepository; import org.springframework.data.repository.core.EntityInformation; import org.springframework.data.util.StreamUtils; import org.springframework.data.util.Streamable; +import org.springframework.lang.NonNull; +import org.springframework.lang.Nullable; import org.springframework.util.Assert; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + /** - * Basic Repository implementation for GemFire. + * Simple, basic {@link CrudRepository} implementation for Apache Geode. * * @author Oliver Gierke * @author David Turanski * @author John Blum - * @see java.io.Serializable - * @see org.springframework.data.gemfire.GemfireTemplate - * @see org.springframework.data.gemfire.repository.GemfireRepository * @see org.apache.geode.cache.Cache + * @see org.apache.geode.cache.CacheTransactionManager * @see org.apache.geode.cache.Region + * @see org.springframework.data.gemfire.GemfireTemplate + * @see org.springframework.data.gemfire.repository.GemfireRepository + * @see org.springframework.data.repository.CrudRepository + * @see org.springframework.data.repository.core.EntityInformation */ public class SimpleGemfireRepository implements GemfireRepository { @@ -60,18 +69,21 @@ public class SimpleGemfireRepository implements GemfireRepository private final GemfireTemplate template; + private final Logger logger = LoggerFactory.getLogger(getClass()); + /** * Constructs a new instance of {@link SimpleGemfireRepository} initialized with the {@link GemfireTemplate} * and {@link EntityInformation}. * * @param template {@link GemfireTemplate} used to perform basic data access operations and simple OQL queries; * must not be {@literal null}. - * @param entityInformation {@link EntityInformation} used to describe the entity; must not be {@literal null}. + * @param entityInformation {@link EntityInformation} that describes the entity; must not be {@literal null}. * @throws IllegalArgumentException if {@link GemfireTemplate} or {@link EntityInformation} is {@literal null}. * @see org.springframework.data.gemfire.GemfireTemplate * @see org.springframework.data.repository.core.EntityInformation */ - public SimpleGemfireRepository(GemfireTemplate template, EntityInformation entityInformation) { + public SimpleGemfireRepository(@NonNull GemfireTemplate template, + @NonNull EntityInformation entityInformation) { Assert.notNull(template, "GemfireTemplate must not be null"); Assert.notNull(entityInformation, "EntityInformation must not be null"); @@ -80,44 +92,110 @@ public SimpleGemfireRepository(GemfireTemplate template, EntityInformation getEntityInformation() { + return this.entityInformation; + } + + /** + * Returns a reference to the SLF4J {@link Logger} used to log the operations of this {@link GemfireRepository}. + * + * @return a reference to the SLF4J {@link Logger} used to log the operations of this {@link GemfireRepository}. + * @see org.slf4j.Logger + */ + public @NonNull Logger getLogger() { + return this.logger; + } + + /** + * Gets the {@link Region} to which this {@link GemfireRepository} performs all data access operations. + * + * @return a reference to the {@link Region} on which this {@link GemfireRepository} operates. + * @see org.apache.geode.cache.Region + * @see #getTemplate() + */ + public @NonNull Region getRegion() { + return getTemplate().getRegion(); + } + + /** + * Returns a reference to the {@link GemfireTemplate} used by this {@link GemfireRepository} to perform basic + * CRUD data access operations and simple OQL queries. + * + * @return a reference to the {@link GemfireTemplate} used by this {@link GemfireRepository}. + * @see org.springframework.data.gemfire.GemfireTemplate + */ + public @NonNull GemfireTemplate getTemplate() { + return this.template; + } + @Override - public U save(U entity) { + public U save(@NonNull U entity) { + + ID id = getEntityInformation().getRequiredId(entity); - ID id = this.entityInformation.getRequiredId(entity); + // CREATE/UPDATE entity in Region + T existingValue = getTemplate().put(id, entity); - this.template.put(id, entity); + if (getLogger().isDebugEnabled()) { + getLogger().debug("Overwrote existing value [{}] for ID [{}]", existingValue, id); + } return entity; } @Override - public T save(Wrapper wrapper) { + public T save(@NonNull Wrapper wrapper) { T entity = wrapper.getEntity(); - this.template.put(wrapper.getKey(), entity); + // CREATE/UPDATE entity in Region + T existingValue = getTemplate().put(wrapper.getKey(), entity); + + if (getLogger().isDebugEnabled()) { + getLogger().debug("Overwrote existing value [{}] for ID [{}]", existingValue, wrapper.getKey()); + } return entity; } @Override - public Iterable saveAll(Iterable entities) { + public Iterable saveAll(@NonNull Iterable entities) { + + EntityInformation entityInformation = getEntityInformation(); Map entitiesToSave = new HashMap<>(); - entities.forEach(entity -> entitiesToSave.put(this.entityInformation.getRequiredId(entity), entity)); + Streamable.of(CollectionUtils.nullSafeIterable(entities)).stream() + .filter(Objects::nonNull) + .forEach(entity -> entitiesToSave.put(entityInformation.getRequiredId(entity), entity)); - this.template.putAll(entitiesToSave); + if (!entitiesToSave.isEmpty()) { + getTemplate().putAll(entitiesToSave); + } return entitiesToSave.values(); } + /** + * Counts the number of entities stored in the {@link Region}. + * + * This method executes a {@literal SELECT count(*) FROM /Region} OQL query. + * + * @return a count of the number of entities stored in the {@link Region}. + */ @Override public long count() { - String countQuery = String.format("SELECT count(*) FROM %s", this.template.getRegion().getFullPath()); + String regionPath = getRegion().getFullPath(); + String countQuery = String.format("SELECT count(*) FROM %s", regionPath); - SelectResults results = this.template.find(countQuery); + SelectResults results = getTemplate().find(countQuery); return Optional.ofNullable(results) .map(SelectResults::iterator) @@ -127,64 +205,104 @@ public long count() { .orElse(0L); } + /** + * Determines whether an entity with the given ID is stored in the {@link Region}. + * + * @param id {@link Long} value identifying the entity. + * @return a boolean value indicating whether an entity with the given ID is stored in the {@link Region}. + * @see #findById(Object) + */ @Override public boolean existsById(ID id) { return findById(id).isPresent(); } @Override - public Optional findById(ID id) { - return Optional.ofNullable(this.template.get(id)); - } + public @NonNull Iterable findAll() { - @Override - public Collection findAll() { + String regionPath = getRegion().getFullPath(); + String query = String.format("SELECT * FROM %s", regionPath); - SelectResults results = - this.template.find(String.format("SELECT * FROM %s", this.template.getRegion().getFullPath())); + SelectResults selectResults = getTemplate().find(query); - return results.asList(); + return toList(selectResults); } @Override - public Iterable findAll(Sort sort) { + public @NonNull Iterable findAll(@NonNull Sort sort) { QueryString query = QueryString.of("SELECT * FROM /RegionPlaceholder") - .fromRegion(this.entityInformation.getJavaType(), this.template.getRegion()) + .fromRegion(getEntityInformation().getJavaType(), getRegion()) .orderBy(sort); - SelectResults selectResults = this.template.find(query.toString()); + SelectResults selectResults = getTemplate().find(query.toString()); - return selectResults.asList(); + return toList(selectResults); } @Override - public Collection findAllById(Iterable ids) { + public @NonNull Iterable findAllById(@NonNull Iterable ids) { + + List keys = Streamable.of(CollectionUtils.nullSafeIterable(ids)).stream() + .filter(Objects::nonNull) + .collect(StreamUtils.toUnmodifiableList()); - List keys = Streamable.of(ids).stream().collect(StreamUtils.toUnmodifiableList()); + Map keysValues = !keys.isEmpty() + ? getTemplate().getAll(keys) + : Collections.emptyMap(); - return CollectionUtils.nullSafeMap(this.template.getAll(keys)).values().stream() - .filter(Objects::nonNull).collect(Collectors.toList()); + List values = CollectionUtils.nullSafeMap(keysValues).values().stream() + .filter(Objects::nonNull) + .collect(Collectors.toList()); + + return values; } @Override - public void deleteById(ID id) { - this.template.remove(id); + public Optional findById(@NonNull ID id) { + + T value = id != null + ? getTemplate().get(id) + : null; + + return Optional.ofNullable(value); } @Override - public void delete(T entity) { - deleteById(this.entityInformation.getRequiredId(entity)); + public void delete(@NonNull T entity) { + deleteById(getEntityInformation().getRequiredId(entity)); } @Override - public void deleteAll(Iterable entities) { - entities.forEach(this::delete); + public void deleteAll() { + + getTemplate().execute((GemfireCallback) region -> { + + if (isPartitioned(region) || isTransactionPresent(region)) { + doRegionClear(region); + } + else { + SpringUtils.safeDoOperation(() -> region.clear(), () -> doRegionClear(region)); + } + + return null; + }); + } + + @Override + public void deleteAll(@NonNull Iterable entities) { + CollectionUtils.nullSafeIterable(entities).forEach(this::delete); + } + + @Override + public void deleteById(@NonNull ID id) { + getTemplate().remove(id); } boolean isPartitioned(Region region) { - return region != null && region.getAttributes() != null + return region != null + && region.getAttributes() != null && isPartitioned(region.getAttributes().getDataPolicy()); } @@ -206,23 +324,10 @@ void doRegionClear(Region region) { region.removeAll(region.keySet()); } - @Override - public void deleteAll() { - this.template.execute((GemfireCallback) region -> { + @NonNull List toList(@Nullable SelectResults selectResults) { - if (isPartitioned(region) || isTransactionPresent(region)) { - doRegionClear(region); - } - else { - try { - region.clear(); - } - catch (UnsupportedOperationException ignore) { - doRegionClear(region); - } - } - - return null; - }); + return selectResults != null + ? CollectionUtils.nullSafeList(selectResults.asList()) + : Collections.emptyList(); } } diff --git a/spring-data-geode/src/test/java/org/springframework/data/gemfire/repository/sample/Animal.java b/spring-data-geode/src/test/java/org/springframework/data/gemfire/repository/sample/Animal.java index 2c3320608..a6d9795e4 100644 --- a/spring-data-geode/src/test/java/org/springframework/data/gemfire/repository/sample/Animal.java +++ b/spring-data-geode/src/test/java/org/springframework/data/gemfire/repository/sample/Animal.java @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package org.springframework.data.gemfire.repository.sample; import org.springframework.data.annotation.Id; @@ -23,7 +22,7 @@ * @author Stuart Williams * @author John Blum */ -public class Animal { +public class Animal implements Identifiable { @Id private Long id; @@ -31,7 +30,7 @@ public class Animal { private String name; public Long getId() { - return id; + return this.id; } public void setId(Long id) { @@ -39,7 +38,7 @@ public void setId(Long id) { } public String getName() { - return name; + return this.name; } public void setName(String name) { @@ -48,7 +47,8 @@ public void setName(String name) { @Override public boolean equals(final Object obj) { - if (obj == this) { + + if (this == obj) { return true; } @@ -64,15 +64,18 @@ public boolean equals(final Object obj) { @Override public int hashCode() { + int hashValue = 17; + hashValue = 37 * hashValue + ObjectUtils.nullSafeHashCode(getId()); hashValue = 37 * hashValue + ObjectUtils.nullSafeHashCode(getName()); + return hashValue; } @Override public String toString() { - return String.format("{ @type = %1$s, id = %2$d, name = %3$s }", getClass().getSimpleName(), getId(), getName()); + return String.format("{ @type = %1$s, id = %2$d, name = %3$s }", + getClass().getSimpleName(), getId(), getName()); } - } diff --git a/spring-data-geode/src/test/java/org/springframework/data/gemfire/repository/sample/Identifiable.java b/spring-data-geode/src/test/java/org/springframework/data/gemfire/repository/sample/Identifiable.java new file mode 100644 index 000000000..73882e8ee --- /dev/null +++ b/spring-data-geode/src/test/java/org/springframework/data/gemfire/repository/sample/Identifiable.java @@ -0,0 +1,28 @@ +/* + * Copyright 2020 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 + * + * https://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.repository.sample; + +/** + * Defines a contract for Abstract Data Types (ADT) that can be identified. + * + * @author John Blum + * @since 2.4.0 + */ +public interface Identifiable { + + T getId(); + +} diff --git a/spring-data-geode/src/test/java/org/springframework/data/gemfire/repository/support/SimpleGemfireRepositoryIntegrationTests.java b/spring-data-geode/src/test/java/org/springframework/data/gemfire/repository/support/SimpleGemfireRepositoryIntegrationTests.java index 4dfdfa066..209268c80 100644 --- a/spring-data-geode/src/test/java/org/springframework/data/gemfire/repository/support/SimpleGemfireRepositoryIntegrationTests.java +++ b/spring-data-geode/src/test/java/org/springframework/data/gemfire/repository/support/SimpleGemfireRepositoryIntegrationTests.java @@ -13,27 +13,25 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package org.springframework.data.gemfire.repository.support; import static org.assertj.core.api.Assertions.assertThat; import java.util.Arrays; -import java.util.Collection; import java.util.Collections; import javax.annotation.Resource; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + import org.apache.geode.cache.GemFireCache; import org.apache.geode.cache.Region; import org.apache.geode.cache.RegionEvent; import org.apache.geode.cache.query.SelectResults; import org.apache.geode.cache.util.CacheListenerAdapter; -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.data.gemfire.GemfireTemplate; @@ -48,11 +46,16 @@ import org.springframework.test.context.junit4.SpringRunner; /** - * Integration tests for {@link SimpleGemfireRepository}. + * Integration Tests for {@link SimpleGemfireRepository}. * * @author Oliver Gierke * @author John Blum * @see org.junit.Test + * @see org.apache.geode.cache.GemFireCache + * @see org.apache.geode.cache.Region + * @see org.springframework.data.gemfire.GemfireTemplate + * @see org.springframework.data.gemfire.LocalRegionFactoryBean + * @see org.springframework.data.gemfire.config.annotation.PeerCacheApplication * @see org.springframework.data.gemfire.repository.support.SimpleGemfireRepository * @see org.springframework.test.context.ContextConfiguration * @see org.springframework.test.context.junit4.SpringRunner @@ -113,17 +116,17 @@ public void findAllWithIds() { this.template.put(carter.getId(), carter); this.template.put(leroi.getId(), leroi); - Collection result = this.repository.findAllById(Arrays.asList(carter.getId(), leroi.getId())); + Iterable result = this.repository.findAllById(Arrays.asList(carter.getId(), leroi.getId())); assertThat(result).isNotNull(); - assertThat(result.size()).isEqualTo(2); + assertThat(result).hasSize(2); assertThat(result).containsAll(Arrays.asList(carter, leroi)); } @Test public void findAllWithIdsReturnsNoMatches() { - Collection results = this.repository.findAllById(Arrays.asList(1L, 2L)); + Iterable results = this.repository.findAllById(Arrays.asList(1L, 2L)); assertThat(results).isNotNull(); assertThat(results).isEmpty(); @@ -139,7 +142,7 @@ public void findAllWithIdsReturnsPartialMatches() { this.template.put(kurt.getId(), kurt); this.template.put(eddie.getId(), eddie); - Collection results = this.repository.findAllById(Arrays.asList(0L, 1L, 2L, 4L)); + Iterable results = this.repository.findAllById(Arrays.asList(0L, 1L, 2L, 4L)); assertThat(results).isNotNull(); assertThat(results).hasSize(2); diff --git a/spring-data-geode/src/test/java/org/springframework/data/gemfire/repository/support/SimpleGemfireRepositoryUnitTests.java b/spring-data-geode/src/test/java/org/springframework/data/gemfire/repository/support/SimpleGemfireRepositoryUnitTests.java index a47566902..4ca083136 100644 --- a/spring-data-geode/src/test/java/org/springframework/data/gemfire/repository/support/SimpleGemfireRepositoryUnitTests.java +++ b/spring-data-geode/src/test/java/org/springframework/data/gemfire/repository/support/SimpleGemfireRepositoryUnitTests.java @@ -17,6 +17,8 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.doReturn; @@ -35,6 +37,7 @@ import java.util.Collections; import java.util.HashMap; import java.util.HashSet; +import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; @@ -54,11 +57,15 @@ import org.apache.geode.cache.RegionAttributes; import org.apache.geode.cache.query.SelectResults; +import org.springframework.data.domain.Sort; import org.springframework.data.gemfire.GemfireTemplate; import org.springframework.data.gemfire.repository.Wrapper; import org.springframework.data.gemfire.repository.sample.Animal; +import org.springframework.data.gemfire.repository.sample.Identifiable; import org.springframework.data.repository.core.EntityInformation; +import org.slf4j.Logger; + /** * Unit Tests for {@link SimpleGemfireRepository}. * @@ -75,10 +82,12 @@ * @see org.springframework.data.repository.core.EntityInformation * @since 1.4.5 */ -@SuppressWarnings({ "rawtypes", "unchecked" }) +@SuppressWarnings({ "rawtypes", "unchecked" }) public class SimpleGemfireRepositoryUnitTests { - protected Map asMap(Iterable animals) { + private final AtomicLong idSequence = new AtomicLong(0L); + + private Map asMap(Iterable animals) { Map animalMap = new HashMap<>(); @@ -89,16 +98,17 @@ protected Map asMap(Iterable animals) { return animalMap; } - protected Animal newAnimal(String name) { + private Animal newAnimal(String name) { Animal animal = new Animal(); + animal.setId(this.idSequence.incrementAndGet()); animal.setName(name); return animal; } - protected Animal newAnimal(Long id, String name) { + private Animal newAnimal(Long id, String name) { Animal animal = newAnimal(name); @@ -107,11 +117,15 @@ protected Animal newAnimal(Long id, String name) { return animal; } - protected GemfireTemplate newGemfireTemplate(Region region) { + private GemfireTemplate newGemfireTemplate(Region region) { return new GemfireTemplate(region); } - protected Cache mockCache(String name, boolean transactionExists) { + private > Wrapper newWrapper(T entity) { + return new Wrapper<>(entity, entity.getId()); + } + + private Cache mockCache(String name, boolean transactionExists) { Cache mockCache = mock(Cache.class, String.format("%s.MockCache", name)); @@ -124,7 +138,7 @@ protected Cache mockCache(String name, boolean transactionExists) { return mockCache; } - protected EntityInformation mockEntityInformation() { + private EntityInformation mockEntityInformation() { EntityInformation mockEntityInformation = mock(EntityInformation.class); @@ -149,11 +163,11 @@ private Long resolveId(Long id) { return mockEntityInformation; } - protected Region mockRegion() { + private Region mockRegion() { return mockRegion("MockRegion"); } - protected Region mockRegion(String name) { + private Region mockRegion(String name) { Region mockRegion = mock(Region.class, String.format("%s.MockRegion", name)); @@ -163,7 +177,7 @@ protected Region mockRegion(String name) { return mockRegion; } - protected Region mockRegion(String name, Cache mockCache, DataPolicy dataPolicy) { + private Region mockRegion(String name, Cache mockCache, DataPolicy dataPolicy) { Region mockRegion = mockRegion(name); @@ -179,17 +193,20 @@ protected Region mockRegion(String name, Cache mockCache, DataPolicy dataPolicy) } @Test - public void constructsSimpleGemfireRepositorySuccessfully() { + public void constructSimpleGemfireRepositorySuccessfully() { - Region mockRegion = mock(Region.class); + Region mockRegion = mockRegion(); - GemfireTemplate template = spy(new GemfireTemplate(mockRegion)); + GemfireTemplate template = spy(newGemfireTemplate(mockRegion)); - EntityInformation mockEntityInformation = mock(EntityInformation.class); + EntityInformation mockEntityInformation = mockEntityInformation(); SimpleGemfireRepository repository = new SimpleGemfireRepository(template, mockEntityInformation); assertThat(repository).isNotNull(); + assertThat(repository.getEntityInformation()).isEqualTo(mockEntityInformation); + assertThat(repository.getLogger()).isNotNull(); + assertThat(repository.getTemplate()).isEqualTo(template); verifyNoInteractions(template, mockRegion, mockEntityInformation); } @@ -225,29 +242,63 @@ public void constructSimpleGemfireRepositoryWithNullEntityInformationThrowsIlleg } @Test - public void saveEntityIsCorrect() { + public void getRegionFromTemplate() { Region mockRegion = mockRegion(); + GemfireTemplate template = spy(newGemfireTemplate(mockRegion)); + + EntityInformation mockEntityInformation = mockEntityInformation(); + SimpleGemfireRepository repository = - new SimpleGemfireRepository<>(newGemfireTemplate(mockRegion), mockEntityInformation()); + new SimpleGemfireRepository<>(template, mockEntityInformation); + + assertThat(repository).isNotNull(); + assertThat(repository.getTemplate()).isEqualTo(template); + assertThat(repository.getRegion()).isEqualTo(mockRegion); - Animal dog = repository.save(newAnimal("dog")); + verify(template, times(1)).getRegion(); + verifyNoInteractions(mockRegion, mockEntityInformation); + verifyNoMoreInteractions(template); + } + + @Test + public void saveEntitySuccessfully() { + + Animal cat = newAnimal(1L, "cat"); + + Logger mockLogger = mock(Logger.class); + + Region mockRegion = mockRegion(); + + SimpleGemfireRepository repository = + spy(new SimpleGemfireRepository<>(newGemfireTemplate(mockRegion), mockEntityInformation())); + + doReturn(mockLogger).when(repository).getLogger(); + doReturn(true).when(mockLogger).isDebugEnabled(); + doReturn(cat).when(mockRegion).put(anyLong(), any()); + + Animal dog = repository.save(newAnimal(1L, "dog")); assertThat(dog).isNotNull(); assertThat(dog.getId().longValue()).isEqualTo(1L); assertThat(dog.getName()).isEqualTo("dog"); + verify(mockLogger, times(1)).isDebugEnabled(); + verify(mockLogger, times(1)) + .debug(eq("Overwrote existing value [{}] for ID [{}]"), eq(cat), eq(1L)); verify(mockRegion, times(1)).put(eq(1L), eq(dog)); + verifyNoMoreInteractions(mockLogger, mockRegion); } @Test - public void saveEntitiesIsCorrect() { + public void saveEntitiesSuccessfully() { + List animals = new ArrayList<>(3); - animals.add(newAnimal("bird")); - animals.add(newAnimal("cat")); - animals.add(newAnimal("dog")); + animals.add(newAnimal(1L, "bird")); + animals.add(newAnimal(2L, "cat")); + animals.add(newAnimal(3L, "dog")); Region mockRegion = mockRegion(); @@ -257,24 +308,82 @@ public void saveEntitiesIsCorrect() { Iterable savedAnimals = repository.saveAll(animals); assertThat(savedAnimals).isNotNull(); + assertThat(savedAnimals).isNotSameAs(animals); + assertThat(savedAnimals).containsAll(animals); verify(mockRegion, times(1)).putAll(eq(asMap(savedAnimals))); + verifyNoMoreInteractions(mockRegion); } @Test - public void saveWrapperIsCorrect() { - Animal dog = newAnimal(1L, "dog"); + public void saveWrapperSuccessfully() { + + Animal dog = newAnimal(2L, "dog"); + + Logger mockLogger = mock(Logger.class); + + Region mockRegion = mockRegion(); + + SimpleGemfireRepository repository = + spy(new SimpleGemfireRepository<>(newGemfireTemplate(mockRegion), mockEntityInformation())); - Wrapper dogWrapper = new Wrapper(dog, dog.getId()); + doReturn(mockLogger).when(repository).getLogger(); + doReturn(true).when(mockLogger).isDebugEnabled(); + doReturn(dog).when(mockRegion).put(anyLong(), any()); + + Animal cat = repository.save(newWrapper(newAnimal(2L, "cat"))); + + assertThat(cat).isNotNull(); + assertThat(cat.getId()).isEqualTo(2L); + assertThat(cat.getName()).isEqualTo("cat"); + + verify(mockLogger, times(1)).isDebugEnabled(); + verify(mockLogger, times(1)) + .debug(eq("Overwrote existing value [{}] for ID [{}]"), eq(dog), eq(2L)); + verify(mockRegion, times(1)).put(eq(2L), eq(cat)); + verifyNoMoreInteractions(mockLogger, mockRegion); + } + + @Test + public void saveAllEntitiesWithIterableContainingNullEntitiesIsNullSafe() { + + List animals = new ArrayList<>(); + + animals.add(newAnimal(1L, "cat")); + animals.add(null); + animals.add(newAnimal(2L, "dog")); Region mockRegion = mockRegion(); SimpleGemfireRepository repository = new SimpleGemfireRepository<>(newGemfireTemplate(mockRegion), mockEntityInformation()); - assertThat(repository.save(dogWrapper)).isEqualTo(dog); + Iterable savedAnimals = repository.saveAll(animals); + + assertThat(savedAnimals).isNotNull(); + assertThat(savedAnimals).isNotSameAs(animals); + assertThat(savedAnimals).hasSize(2); + assertThat(savedAnimals).containsAll(Arrays.asList(animals.get(0), animals.get(2))); + + verify(mockRegion, times(1)) + .putAll(eq(asMap(Arrays.asList(animals.get(0), animals.get(2))))); + verifyNoMoreInteractions(mockRegion); + } + + @Test + public void saveAllEntitiesWithNullIterableIsNullSafe() { + + Region mockRegion = mock(Region.class); + + SimpleGemfireRepository repository = + new SimpleGemfireRepository<>(newGemfireTemplate(mockRegion), mockEntityInformation()); + + Iterable iterable = repository.saveAll(null); + + assertThat(iterable).isNotNull(); + assertThat(iterable).isEmpty(); - verify(mockRegion, times(1)).put(eq(dog.getId()), eq(dog)); + verifyNoInteractions(mockRegion); } @Test @@ -287,7 +396,7 @@ public void countReturnsNumberOfRegionEntries() { GemfireTemplate template = spy(newGemfireTemplate(mockRegion)); doReturn(mockSelectResults).when(template).find(eq("SELECT count(*) FROM /Example")); - when(mockSelectResults.iterator()).thenReturn(Collections.singletonList(21).iterator()); + doReturn(Collections.singletonList(21).iterator()).when(mockSelectResults).iterator(); SimpleGemfireRepository repository = new SimpleGemfireRepository<>(template, mockEntityInformation()); @@ -309,7 +418,7 @@ public void countWhenSelectResultsAreNullIsNullSafeAndReturnsZero() { GemfireTemplate template = spy(newGemfireTemplate(mockRegion)); - doReturn(null).when(template).find(eq("SELECT count(*) FROM /Example")); + doReturn(null).when(template).find(anyString()); SimpleGemfireRepository repository = new SimpleGemfireRepository<>(template, mockEntityInformation()); @@ -332,7 +441,7 @@ public void countWhenSelectResultsIteratorIsNullIsNullSafeAndReturnsZero() { GemfireTemplate template = spy(newGemfireTemplate(mockRegion)); - doReturn(mockSelectResults).when(template).find(eq("SELECT count(*) FROM /Example")); + doReturn(mockSelectResults).when(template).find(anyString()); doReturn(null).when(mockSelectResults).iterator(); SimpleGemfireRepository repository = @@ -357,7 +466,7 @@ public void countWhenSelectResultsIteratorIsEmptyReturnsZero() { GemfireTemplate template = spy(newGemfireTemplate(mockRegion)); - doReturn(mockSelectResults).when(template).find(eq("SELECT count(*) FROM /Example")); + doReturn(mockSelectResults).when(template).find(anyString()); doReturn(Collections.emptyIterator()).when(mockSelectResults).iterator(); SimpleGemfireRepository repository = @@ -374,123 +483,263 @@ public void countWhenSelectResultsIteratorIsEmptyReturnsZero() { } @Test - public void existsIsCorrect() { + public void countWhenSelectResultsIteratorContainsNullIsNullSafeReturnsZero() { + + SelectResults mockSelectResults = mock(SelectResults.class); + + Region mockRegion = mockRegion("Example"); + + Iterator mockIterator = mock(Iterator.class); + + GemfireTemplate template = spy(newGemfireTemplate(mockRegion)); + + doReturn(mockSelectResults).when(template).find(anyString()); + doReturn(mockIterator).when(mockSelectResults).iterator(); + doReturn(true).when(mockIterator).hasNext(); + doReturn(null).when(mockIterator).next(); + + SimpleGemfireRepository repository = + new SimpleGemfireRepository<>(template, mockEntityInformation()); + + assertThat(repository).isNotNull(); + assertThat(repository.count()).isEqualTo(0L); + + verify(template, times(1)).getRegion(); + verify(mockRegion, times(1)).getFullPath(); + verify(template, times(1)).find(eq("SELECT count(*) FROM /Example")); + verify(mockSelectResults, times(1)).iterator(); + verify(mockIterator, times(1)).hasNext(); + verify(mockIterator, times(1)).next(); + verifyNoMoreInteractions(mockIterator, mockRegion, mockSelectResults, template); + } + + @Test + public void existsByIdCallsFindById() { + Animal dog = newAnimal(1L, "dog"); Region mockRegion = mockRegion(); - when(mockRegion.get(any(Long.class))).then( - invocation -> (dog.getId().equals(invocation.getArguments()[0]) ? dog : null)); + doAnswer(invocation -> dog.getId().equals(invocation.getArgument(0)) ? dog : null) + .when(mockRegion).get(anyLong()); SimpleGemfireRepository repository = - new SimpleGemfireRepository<>(newGemfireTemplate(mockRegion), mockEntityInformation()); + spy(new SimpleGemfireRepository<>(newGemfireTemplate(mockRegion), mockEntityInformation())); assertThat(repository.existsById(1L)).isTrue(); + assertThat(repository.existsById(2L)).isFalse(); assertThat(repository.existsById(10L)).isFalse(); + + verify(repository, times(1)).findById(eq(1L)); + verify(mockRegion, times(1)).get(eq(1L)); + verify(repository, times(1)).findById(eq(2L)); + verify(mockRegion, times(1)).get(eq(2L)); + verify(repository, times(1)).findById(eq(10L)); + verify(mockRegion, times(1)).get(eq(10L)); + verifyNoMoreInteractions(mockRegion); } @Test - public void findOneIsCorrect() { - Animal dog = newAnimal(1L, "dog"); + public void findAllSuccessfully() { - Region mockRegion = mockRegion(); + GemfireTemplate mockTemplate = mock(GemfireTemplate.class); - when(mockRegion.get(any(Long.class))).then( - invocation -> (dog.getId().equals(invocation.getArguments()[0]) ? dog : null)); + List results = Arrays.asList("test", "mock", "fake"); - SimpleGemfireRepository repository = - new SimpleGemfireRepository<>(newGemfireTemplate(mockRegion), mockEntityInformation()); + Region mockRegion = mockRegion("Example"); - assertThat(repository.findById(1L).orElse(null)).isEqualTo(dog); - assertThat(repository.findById(10L).isPresent()).isFalse(); + SelectResults mockSelectResults = mock(SelectResults.class); + + doReturn(mockRegion).when(mockTemplate).getRegion(); + doReturn(mockSelectResults).when(mockTemplate).find(anyString()); + doReturn(results).when(mockSelectResults).asList(); + + SimpleGemfireRepository repository = new SimpleGemfireRepository(mockTemplate, mockEntityInformation()); + + Iterable returnValue = repository.findAll(); + + assertThat(returnValue).isNotNull(); + assertThat(returnValue).containsAll(results); + + verify(mockTemplate, times(1)).getRegion(); + verify(mockRegion, times(1)).getFullPath(); + verify(mockTemplate, times(1)).find(eq("SELECT * FROM /Example")); + verify(mockSelectResults, times(1)).asList(); + verifyNoMoreInteractions(mockRegion, mockSelectResults, mockTemplate); } @Test - public void findAllIsCorrect() { - Map animals = - Stream.of(newAnimal(1L, "bird"), newAnimal(2L, "cat"), newAnimal(3L, "dog")) - .collect(Collectors.toMap(Animal::getId, Function.identity())); + public void findAllOrderedBySortSuccessfully() { + + GemfireTemplate mockTemplate = mock(GemfireTemplate.class); + + List results = Arrays.asList("test", "mock", "fake"); + + Region mockRegion = mockRegion("Example"); + + SelectResults mockSelectResults = mock(SelectResults.class); + + doReturn(mockRegion).when(mockTemplate).getRegion(); + doReturn(mockSelectResults).when(mockTemplate).find(anyString()); + doReturn(results).when(mockSelectResults).asList(); + + SimpleGemfireRepository repository = new SimpleGemfireRepository(mockTemplate, mockEntityInformation()); + + Iterable returnValue = repository.findAll(Sort.by(Sort.Order.asc("name"), Sort.Order.desc("birthDate"))); + + assertThat(returnValue).isNotNull(); + assertThat(returnValue).containsAll(results); + + verify(mockTemplate, times(1)).getRegion(); + verify(mockRegion, times(1)).getFullPath(); + verify(mockTemplate, times(1)) + .find(eq("SELECT DISTINCT * FROM /Example ORDER BY name ASC, birthDate DESC")); + verify(mockSelectResults, times(1)).asList(); + verifyNoMoreInteractions(mockRegion, mockSelectResults, mockTemplate); + } + + @Test + public void findAllByIdSuccessfully() { + + Map animals = Stream.of( + newAnimal(1L, "bird"), + newAnimal(2L, "cat"), + newAnimal(3L, "dog") + ).collect(Collectors.toMap(Animal::getId, Function.identity())); Region mockRegion = mockRegion(); - when(mockRegion.getAll(any(Collection.class))).then(invocation -> { + doAnswer(invocation -> { + Collection keys = invocation.getArgument(0); - return animals.values().stream().filter((animal -> keys.contains(animal.getId()))) + return animals.values().stream() + .filter((animal -> keys.contains(animal.getId()))) .collect(Collectors.toMap(Animal::getId, Function.identity())); - }); - SimpleGemfireRepository repository = new SimpleGemfireRepository<>( - newGemfireTemplate(mockRegion), mockEntityInformation()); + }).when(mockRegion).getAll(any(Collection.class)); + + SimpleGemfireRepository repository = + new SimpleGemfireRepository<>(newGemfireTemplate(mockRegion), mockEntityInformation()); - Collection animalsFound = repository.findAllById(Arrays.asList(1L, 3L)); + Iterable animalsFound = repository.findAllById(Arrays.asList(1L, 3L)); assertThat(animalsFound).isNotNull(); assertThat(animalsFound).hasSize(2); assertThat(animalsFound).contains(animals.get(1L), animals.get(3L)); verify(mockRegion, times(1)).getAll(eq(Arrays.asList(1L, 3L))); + verifyNoMoreInteractions(mockRegion); } @Test - public void findAllWithIdsReturnsNoMatches() { - Region mockRegion = mockRegion(); + public void findAllByIdReturnsNoMatches() { - when(mockRegion.getAll(any(Collection.class))).then(invocation -> { - Collection keys = invocation.getArgument(0); - Map result = new HashMap<>(keys.size()); - - for (Long key : keys) { - result.put(key, null); - } + Region mockRegion = mockRegion(); - return result; - }); + doReturn(Collections.emptyMap()).when(mockRegion).getAll(any(Collection.class)); SimpleGemfireRepository repository = new SimpleGemfireRepository<>(newGemfireTemplate(mockRegion), mockEntityInformation()); - Collection animalsFound = repository.findAllById(Arrays.asList(1L, 2L, 3L)); + Iterable animalsFound = repository.findAllById(Arrays.asList(1L, null, 2L, null, 3L)); assertThat(animalsFound).isNotNull(); assertThat(animalsFound).isEmpty(); verify(mockRegion, times(1)).getAll(eq(Arrays.asList(1L, 2L, 3L))); + verifyNoMoreInteractions(mockRegion); } @Test - public void findAllWithIdsReturnsPartialMatches() { - Map animals = - Stream.of(newAnimal(1L, "bird"), newAnimal(2L, "cat"), newAnimal(3L, "dog")) - .collect(Collectors.toMap(Animal::getId, Function.identity())); + public void findAllByIdReturnsPartialMatches() { + + Map animals = Stream.of( + newAnimal(1L, "bird"), + newAnimal(2L, "cat"), + newAnimal(3L, "dog") + ).collect(Collectors.toMap(Animal::getId, Function.identity())); Region mockRegion = mockRegion(); - when(mockRegion.getAll(any(Collection.class))).then(invocation -> { + doAnswer(invocation -> { + Collection keys = invocation.getArgument(0); - Map result = new HashMap<>(keys.size()); - for (Long key : keys) { - result.put(key, animals.get(key)); - } + return keys.stream() + .filter(animals::containsKey) + .collect(Collectors.toMap(Function.identity(), animals::get)); - return result; - }); + }).when(mockRegion).getAll(any(Collection.class)); SimpleGemfireRepository repository = new SimpleGemfireRepository<>(newGemfireTemplate(mockRegion), mockEntityInformation()); - Collection animalsFound = repository.findAllById(Arrays.asList(0L, 1L, 2L, 4L)); + Iterable animalsFound = repository.findAllById(Arrays.asList(null, 0L, null, 1L, 2L, 4L, null)); assertThat(animalsFound).isNotNull(); assertThat(animalsFound).hasSize(2); assertThat(animalsFound).contains(animals.get(1L), animals.get(2L)); verify(mockRegion, times(1)).getAll(eq(Arrays.asList(0L, 1L, 2L, 4L))); + verifyNoMoreInteractions(mockRegion); } @Test - public void deleteByIdIsCorrect() { + public void findAllByIdWithNullIterableIsNullSafe() { + + Region mockRegion = mockRegion(); + + SimpleGemfireRepository repository = + new SimpleGemfireRepository(newGemfireTemplate(mockRegion), mockEntityInformation()); + + Iterable animals = repository.findAllById(null); + + assertThat(animals).isNotNull(); + assertThat(animals).isEmpty(); + + verifyNoInteractions(mockRegion); + } + + @Test + public void findByIdSuccessfully() { + + Animal dog = newAnimal(1L, "dog"); + + Region mockRegion = mockRegion(); + + doAnswer(invocation -> dog.getId().equals(invocation.getArgument(0)) ? dog : null) + .when(mockRegion).get(any(Long.class)); + + SimpleGemfireRepository repository = + new SimpleGemfireRepository<>(newGemfireTemplate(mockRegion), mockEntityInformation()); + + assertThat(repository.findById(1L).orElse(null)).isEqualTo(dog); + assertThat(repository.findById(2L).isPresent()).isFalse(); + assertThat(repository.findById(10L).isPresent()).isFalse(); + + verify(mockRegion, times(1)).get(eq(1L)); + verify(mockRegion, times(1)).get(eq(2L)); + verify(mockRegion, times(1)).get(eq(10L)); + verifyNoMoreInteractions(mockRegion); + } + + @Test + public void findByIdWithNullIdIsNullSafe() { + + Region mockRegion = mockRegion(); + + SimpleGemfireRepository repository = + new SimpleGemfireRepository(newGemfireTemplate(mockRegion), mockEntityInformation()); + + assertThat(repository.findById(null).isPresent()).isFalse(); + + verifyNoInteractions(mockRegion); + } + + @Test + public void deleteByIdSuccessfully() { + Region mockRegion = mockRegion(); SimpleGemfireRepository repository = @@ -499,37 +748,47 @@ public void deleteByIdIsCorrect() { repository.deleteById(1L); verify(mockRegion, times(1)).remove(eq(1L)); + verifyNoMoreInteractions(mockRegion); } @Test - public void deleteEntityIsCorrect() { + public void deleteEntitySuccessfully() { + Region mockRegion = mockRegion(); SimpleGemfireRepository repository = - new SimpleGemfireRepository<>(newGemfireTemplate(mockRegion), mockEntityInformation()); + spy(new SimpleGemfireRepository<>(newGemfireTemplate(mockRegion), mockEntityInformation())); repository.delete(newAnimal(1L, "dog")); + verify(repository, times(1)).deleteById(eq(1L)); verify(mockRegion, times(1)).remove(eq(1L)); + verifyNoMoreInteractions(mockRegion); } @Test - public void deleteEntitiesIsCorrect() { + public void deleteEntitiesSuccessfully() { + Region mockRegion = mockRegion(); SimpleGemfireRepository repository = new SimpleGemfireRepository<>(newGemfireTemplate(mockRegion), mockEntityInformation()); - repository.deleteAll(Arrays.asList(newAnimal(1L, "bird"), newAnimal(2L, "cat"), - newAnimal(3L, "dog"))); + repository.deleteAll(Arrays.asList( + newAnimal(1L, "bird"), + newAnimal(2L, "cat"), + newAnimal(3L, "dog") + )); verify(mockRegion, times(1)).remove(eq(1L)); verify(mockRegion, times(1)).remove(eq(2L)); verify(mockRegion, times(1)).remove(eq(3L)); + verifyNoMoreInteractions(mockRegion); } @Test public void deleteAllWithClear() { + Cache mockCache = mockCache("MockCache", false); Region mockRegion = mockRegion("MockRegion", mockCache, DataPolicy.REPLICATE); @@ -547,6 +806,7 @@ public void deleteAllWithClear() { @Test public void deleteAllWithKeysWhenClearThrowsException() { + Cache mockCache = mockCache("MockCache", false); Region mockRegion = mockRegion("MockRegion", mockCache, DataPolicy.PERSISTENT_REPLICATE); @@ -570,6 +830,7 @@ public void deleteAllWithKeysWhenClearThrowsException() { @Test public void deleteAllWithKeysWhenPartitionRegion() { + Cache mockCache = mockCache("MockCache", false); Region mockRegion = mockRegion("MockRegion", mockCache, DataPolicy.PERSISTENT_PARTITION); @@ -592,6 +853,7 @@ public void deleteAllWithKeysWhenPartitionRegion() { @Test public void deleteAllWithKeysWhenTransactionPresent() { + Cache mockCache = mockCache("MockCache", true); Region mockRegion = mockRegion("MockRegion", mockCache, DataPolicy.REPLICATE);