From c33d75db27c6c7002e12006fc620e3fb4a25c3ba Mon Sep 17 00:00:00 2001 From: John Blum Date: Thu, 9 May 2019 19:51:11 -0700 Subject: [PATCH] Add a concrete o.s.g.cache.RepositoryCacheWriter class, which implements org.apache.geode.cache.CacheWriter and uses (delegates to) a Spring Data CrudRepository. --- .../geode/cache/RepositoryCacheWriter.java | 81 +++++++++ .../cache/RepositoryCacheWriterUnitTests.java | 168 ++++++++++++++++++ 2 files changed, 249 insertions(+) create mode 100644 spring-geode/src/main/java/org/springframework/geode/cache/RepositoryCacheWriter.java create mode 100644 spring-geode/src/test/java/org/springframework/geode/cache/RepositoryCacheWriterUnitTests.java diff --git a/spring-geode/src/main/java/org/springframework/geode/cache/RepositoryCacheWriter.java b/spring-geode/src/main/java/org/springframework/geode/cache/RepositoryCacheWriter.java new file mode 100644 index 00000000..2979791e --- /dev/null +++ b/spring-geode/src/main/java/org/springframework/geode/cache/RepositoryCacheWriter.java @@ -0,0 +1,81 @@ +/* + * Copyright 2019 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.geode.cache; + +import java.util.function.Supplier; + +import org.apache.geode.cache.CacheRuntimeException; +import org.apache.geode.cache.CacheWriter; +import org.apache.geode.cache.CacheWriterException; +import org.apache.geode.cache.EntryEvent; +import org.apache.geode.cache.RegionEvent; +import org.springframework.data.repository.CrudRepository; +import org.springframework.geode.cache.support.RepositoryCacheLoaderWriterSupport; +import org.springframework.geode.core.util.function.FunctionUtils; + +/** + * A {@link CacheWriter} implementation backed by a Spring Data {@link CrudRepository} used to persist a cache entry + * (i.e. entity) to a backend, external data source. + * + * @author John Blum + * @see org.apache.geode.cache.CacheWriter + * @see org.springframework.data.repository.CrudRepository + * @see org.springframework.geode.cache.support.RepositoryCacheLoaderWriterSupport + * @since 1.1.0 + */ +@SuppressWarnings("unused") +public class RepositoryCacheWriter extends RepositoryCacheLoaderWriterSupport { + + public RepositoryCacheWriter(CrudRepository repository) { + super(repository); + } + + @Override + public void beforeCreate(EntryEvent event) throws CacheWriterException { + doRepositoryOp(event.getNewValue(), getRepository()::save); + } + + @Override + public void beforeUpdate(EntryEvent event) throws CacheWriterException { + doRepositoryOp(event.getNewValue(), getRepository()::save); + } + + @Override + public void beforeDestroy(EntryEvent event) throws CacheWriterException { + + //doRepositoryOp(event.getOldValue(), FunctionUtils.toNullReturningFunction(getRepository()::delete)); + doRepositoryOp(event.getKey(), FunctionUtils.toNullReturningFunction(getRepository()::deleteById)); + } + + @Override + public void beforeRegionClear(RegionEvent event) throws CacheWriterException { + + if (isNukeAndPaveEnabled()) { + doRepositoryOp(null, FunctionUtils.toNullReturningFunction(it -> getRepository().deleteAll())); + } + } + + @Override + public void beforeRegionDestroy(RegionEvent event) throws CacheWriterException { + // TODO: perhaps implement by releasing external data source resources + // (i.e. destroy database object(s), e.g. DROP TABLE) + } + + @Override + protected CacheRuntimeException newCacheRuntimeException(Supplier messageSupplier, Throwable cause) { + return new CacheWriterException(messageSupplier.get(), cause); + } +} diff --git a/spring-geode/src/test/java/org/springframework/geode/cache/RepositoryCacheWriterUnitTests.java b/spring-geode/src/test/java/org/springframework/geode/cache/RepositoryCacheWriterUnitTests.java new file mode 100644 index 00000000..86f5ba68 --- /dev/null +++ b/spring-geode/src/test/java/org/springframework/geode/cache/RepositoryCacheWriterUnitTests.java @@ -0,0 +1,168 @@ +/* + * Copyright 2019 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.geode.cache; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyZeroInteractions; +import static org.mockito.Mockito.when; + +import org.apache.geode.cache.CacheRuntimeException; +import org.apache.geode.cache.CacheWriterException; +import org.apache.geode.cache.EntryEvent; +import org.apache.geode.cache.RegionEvent; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import org.springframework.core.env.Environment; +import org.springframework.data.repository.CrudRepository; +import org.springframework.geode.cache.support.RepositoryCacheLoaderWriterSupport; + +/** + * Unit Tests for {@link RepositoryCacheWriter}. + * + * @author John Blum + * @see org.junit.Test + * @see org.mockito.Mockito + * @see org.mockito.junit.MockitoJUnitRunner + * @see org.springframework.data.repository.CrudRepository + * @see org.springframework.geode.cache.RepositoryCacheWriter + * @see org.springframework.geode.cache.support.RepositoryCacheLoaderWriterSupport + * @since 1.1.0 + */ +@RunWith(MockitoJUnitRunner.class) +@SuppressWarnings("unchecked") +public class RepositoryCacheWriterUnitTests { + + @Mock + private CrudRepository mockCrudRepository; + + @Mock + private EntryEvent mockEntryEvent; + + @Mock + private Environment mockEnvironment; + + private final Object testEntity = new Object(); + + private RepositoryCacheWriter cacheWriter; + + @Before + public void setup() { + + this.cacheWriter = new RepositoryCacheWriter<>(this.mockCrudRepository) + .with(this.mockEnvironment); + } + @Test + public void beforeCreateSavesEntityWithRepository() { + + when(this.mockEntryEvent.getNewValue()).thenReturn(this.testEntity); + + this.cacheWriter.beforeCreate(this.mockEntryEvent); + + verify(this.mockEntryEvent, times(1)).getNewValue(); + verify(this.mockCrudRepository, times(1)).save(eq(this.testEntity)); + } + + @Test + public void beforeUpdateSavesEntityWithRepository() { + + when(this.mockEntryEvent.getNewValue()).thenReturn(this.testEntity); + + this.cacheWriter.beforeUpdate(this.mockEntryEvent); + + verify(this.mockEntryEvent, times(1)).getNewValue(); + verify(this.mockCrudRepository, times(1)).save(eq(this.testEntity)); + } + + @Test + public void beforeDestroyDeletesByIdWithRepository() { + + when(this.mockEntryEvent.getKey()).thenReturn("TestKey"); + + this.cacheWriter.beforeDestroy(this.mockEntryEvent); + + verify(this.mockEntryEvent, times(1)).getKey(); + verify(this.mockCrudRepository, times(1)).deleteById(eq("TestKey")); + } + + @Test + public void beforeRegionClearDeletesAllWithRepositoryWhenNukeAndPaveIsEnabled() { + + RegionEvent mockRegionEvent = mock(RegionEvent.class); + + when(this.mockEnvironment.getProperty(eq(RepositoryCacheLoaderWriterSupport.NUKE_AND_PAVE_PROPERTY), + eq(Boolean.class))).thenReturn(true); + + this.cacheWriter.beforeRegionClear(mockRegionEvent); + + verify(this.mockEnvironment, times(1)) + .getProperty(eq(RepositoryCacheLoaderWriterSupport.NUKE_AND_PAVE_PROPERTY), eq(Boolean.class)); + + verify(this.mockCrudRepository, times(1)).deleteAll(); + + verifyZeroInteractions(mockRegionEvent); + } + + @Test + public void beforeRegionClearWillNotDeleteAllWithRepositoryWhenNukeAndPaveIsDisabled() { + + RegionEvent mockRegionEvent = mock(RegionEvent.class); + + when(this.mockEnvironment.getProperty(eq(RepositoryCacheLoaderWriterSupport.NUKE_AND_PAVE_PROPERTY), + eq(Boolean.class))).thenReturn(false); + + this.cacheWriter.beforeRegionClear(mockRegionEvent); + + verify(this.mockEnvironment, times(1)) + .getProperty(eq(RepositoryCacheLoaderWriterSupport.NUKE_AND_PAVE_PROPERTY), eq(Boolean.class)); + + verify(this.mockCrudRepository, never()).deleteAll(); + + verifyZeroInteractions(mockRegionEvent); + } + + @Test + public void beforeRegionDestroyDoesNothing() { + + RegionEvent mockRegionEvent = mock(RegionEvent.class); + + this.cacheWriter.beforeRegionDestroy(mockRegionEvent); + + verifyZeroInteractions(this.mockEnvironment); + verifyZeroInteractions(this.mockCrudRepository); + verifyZeroInteractions(mockRegionEvent); + } + + @Test + @SuppressWarnings("all") + public void newCacheRuntimeExceptionIsCorrect() { + + RuntimeException cause = new RuntimeException("TEST"); + + CacheRuntimeException cacheRuntimeException = this.cacheWriter.newCacheRuntimeException(() -> "TEST", cause); + + assertThat(cacheRuntimeException).isInstanceOf(CacheWriterException.class); + assertThat(cacheRuntimeException.getMessage()).isEqualTo("TEST"); + assertThat(cacheRuntimeException.getCause()).isEqualTo(cause); + } +}