From a1300c5fc50d1305b34a340e9ac2f3b5138c8221 Mon Sep 17 00:00:00 2001 From: John Blum Date: Thu, 9 May 2019 19:49:29 -0700 Subject: [PATCH] Add abstract base class supporting the development of Spring Data Repository, Apache Geode CacheLoader/CacheWriter implementations. --- .../RepositoryCacheLoaderWriterSupport.java | 114 +++++++++ ...toryCacheLoaderWriterSupportUnitTests.java | 231 ++++++++++++++++++ 2 files changed, 345 insertions(+) create mode 100644 spring-geode/src/main/java/org/springframework/geode/cache/support/RepositoryCacheLoaderWriterSupport.java create mode 100644 spring-geode/src/test/java/org/springframework/geode/cache/support/RepositoryCacheLoaderWriterSupportUnitTests.java diff --git a/spring-geode/src/main/java/org/springframework/geode/cache/support/RepositoryCacheLoaderWriterSupport.java b/spring-geode/src/main/java/org/springframework/geode/cache/support/RepositoryCacheLoaderWriterSupport.java new file mode 100644 index 00000000..48be1015 --- /dev/null +++ b/spring-geode/src/main/java/org/springframework/geode/cache/support/RepositoryCacheLoaderWriterSupport.java @@ -0,0 +1,114 @@ +/* + * 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.support; + +import java.util.Optional; +import java.util.function.Function; +import java.util.function.Supplier; + +import org.apache.geode.cache.CacheLoader; +import org.apache.geode.cache.CacheLoaderException; +import org.apache.geode.cache.CacheRuntimeException; +import org.apache.geode.cache.CacheWriter; +import org.apache.geode.cache.LoaderHelper; +import org.springframework.context.EnvironmentAware; +import org.springframework.core.env.Environment; +import org.springframework.data.repository.CrudRepository; +import org.springframework.data.repository.Repository; +import org.springframework.lang.NonNull; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * Abstract base class supporting the implementation of Apache Geode / Pivotal GemFire {@link CacheLoader CacheLoaders} + * and {@link CacheWriter CacheWriters} backed by Spring Data {@link Repository Repositories}. + * + * @author John Blum + * @see org.apache.geode.cache.CacheLoader + * @see org.apache.geode.cache.CacheWriter + * @see org.springframework.context.EnvironmentAware + * @see org.springframework.core.env.Environment + * @see org.springframework.data.repository.CrudRepository + * @see org.springframework.data.repository.Repository + * @see org.springframework.geode.cache.support.CacheLoaderSupport + * @since 1.1.0 + */ +public abstract class RepositoryCacheLoaderWriterSupport + implements CacheLoaderSupport, CacheWriterSupport, EnvironmentAware { + + public static final String NUKE_AND_PAVE_PROPERTY = "spring.boot.data.gemfire.data.source.nuke-and-pave"; + + protected static final String DATA_ACCESS_ERROR = + "Exception occurred while accessing entity [%s] in external data source"; + + private final CrudRepository repository; + + private Environment environment; + + protected RepositoryCacheLoaderWriterSupport(CrudRepository repository) { + + Assert.notNull(repository, "Repository is required"); + + this.repository = repository; + } + + protected boolean isNukeAndPaveEnabled() { + + return getEnvironment() + .map(env -> env.getProperty(NUKE_AND_PAVE_PROPERTY, Boolean.class)) + .orElse(Boolean.getBoolean(NUKE_AND_PAVE_PROPERTY)); + } + + @Override @SuppressWarnings("all") + public void setEnvironment(@Nullable Environment environment) { + this.environment = environment; + } + + protected Optional getEnvironment() { + return Optional.ofNullable(this.environment); + } + + protected @NonNull CrudRepository getRepository() { + return this.repository; + } + + @SuppressWarnings("all") + protected R doRepositoryOp(S entity, Function repositoryOperation) { + + try { + return repositoryOperation.apply(entity); + } + catch (Throwable cause) { + throw newCacheRuntimeException(() -> String.format(DATA_ACCESS_ERROR, entity), cause); + } + } + + @Override + public T load(LoaderHelper helper) throws CacheLoaderException { + return null; + } + + protected abstract CacheRuntimeException newCacheRuntimeException( + Supplier messageSupplier, Throwable cause); + + @SuppressWarnings("unchecked") + public > U with(Environment environment) { + + setEnvironment(environment); + + return (U) this; + } +} diff --git a/spring-geode/src/test/java/org/springframework/geode/cache/support/RepositoryCacheLoaderWriterSupportUnitTests.java b/spring-geode/src/test/java/org/springframework/geode/cache/support/RepositoryCacheLoaderWriterSupportUnitTests.java new file mode 100644 index 00000000..095fcd5f --- /dev/null +++ b/spring-geode/src/test/java/org/springframework/geode/cache/support/RepositoryCacheLoaderWriterSupportUnitTests.java @@ -0,0 +1,231 @@ +/* + * 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.support; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +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 java.util.function.Function; +import java.util.function.Supplier; + +import org.apache.geode.cache.CacheRuntimeException; +import org.apache.geode.cache.LoaderHelper; +import org.junit.After; +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; + +/** + * Unit Tests for {@link RepositoryCacheLoaderWriterSupport}. + * + * @author John Blum + * @see java.util.function.Function + * @see org.junit.Test + * @see org.mockito.Mockito + * @see org.mockito.junit.MockitoJUnitRunner + * @see org.springframework.core.env.Environment + * @see org.springframework.data.repository.CrudRepository + * @see org.springframework.geode.cache.support.RepositoryCacheLoaderWriterSupport + * @since 1.1.0 + */ +@RunWith(MockitoJUnitRunner.class) +@SuppressWarnings("unchecked") +public class RepositoryCacheLoaderWriterSupportUnitTests { + + @Mock + private CrudRepository mockCrudRepository; + + @After + public void tearDown() { + System.clearProperty(RepositoryCacheLoaderWriterSupport.NUKE_AND_PAVE_PROPERTY); + } + + @Test + public void constructsRepositoryCacheLoaderWriterSupportSuccessfully() { + + RepositoryCacheLoaderWriterSupport cacheLoaderWriter = + new TestRepositoryCacheLoaderWriterSupport(this.mockCrudRepository); + + assertThat(cacheLoaderWriter).isNotNull(); + assertThat(cacheLoaderWriter.getEnvironment().orElse(null)).isNull(); + assertThat(cacheLoaderWriter.getRepository()).isEqualTo(mockCrudRepository); + } + + @Test(expected = IllegalArgumentException.class) + public void constructsRepositoryCacheLoaderWriterSupportWithNull() { + + try { + new TestRepositoryCacheLoaderWriterSupport<>(null); + } + catch (IllegalArgumentException expected) { + + assertThat(expected).hasMessage("Repository is required"); + assertThat(expected).hasNoCause(); + + throw expected; + } + } + + @Test + public void setAndGetEnvironment() { + + Environment mockEnvironment = mock(Environment.class); + + RepositoryCacheLoaderWriterSupport cacheLoaderWriter = + new TestRepositoryCacheLoaderWriterSupport(this.mockCrudRepository); + + cacheLoaderWriter.setEnvironment(mockEnvironment); + + assertThat(cacheLoaderWriter.getEnvironment().orElse(null)).isEqualTo(mockEnvironment); + + cacheLoaderWriter.setEnvironment(null); + + assertThat(cacheLoaderWriter.getEnvironment().orElse(null)).isNull(); + } + + @Test + public void isNukeAndPaveEnabledReturnsFalse() { + assertThat(new TestRepositoryCacheLoaderWriterSupport<>(this.mockCrudRepository) + .isNukeAndPaveEnabled()).isFalse(); + } + + @Test + public void isNukeAndPaveEnabledWhenEnvironmentPropertyIsSetReturnsTrue() { + + System.setProperty(RepositoryCacheLoaderWriterSupport.NUKE_AND_PAVE_PROPERTY, String.valueOf(false)); + + assertThat(Boolean.parseBoolean(System.getProperty(RepositoryCacheLoaderWriterSupport.NUKE_AND_PAVE_PROPERTY))) + .isFalse(); + + Environment mockEnvironment = mock(Environment.class); + + when(mockEnvironment.getProperty(eq(RepositoryCacheLoaderWriterSupport.NUKE_AND_PAVE_PROPERTY), + eq(Boolean.class))).thenReturn(true); + + assertThat(new TestRepositoryCacheLoaderWriterSupport<>(this.mockCrudRepository) + .with(mockEnvironment).isNukeAndPaveEnabled()).isTrue(); + + verify(mockEnvironment, times(1)) + .getProperty(eq(RepositoryCacheLoaderWriterSupport.NUKE_AND_PAVE_PROPERTY), eq(Boolean.class)); + } + + @Test + public void isNukeAndPaveEnabledWhenSystemPropertyIsSetReturnsTrue() { + + System.setProperty(RepositoryCacheLoaderWriterSupport.NUKE_AND_PAVE_PROPERTY, String.valueOf(true)); + + assertThat(Boolean.parseBoolean(System.getProperty(RepositoryCacheLoaderWriterSupport.NUKE_AND_PAVE_PROPERTY))) + .isTrue(); + + assertThat(new TestRepositoryCacheLoaderWriterSupport<>(this.mockCrudRepository).isNukeAndPaveEnabled()) + .isTrue(); + } + + @Test + public void doRepositoryOperationSuccessfully() { + + Function mockRepositoryOperationFunction = mock(Function.class); + + when(mockRepositoryOperationFunction.apply(any())).thenReturn("TEST"); + + Object testEntity = new Object(); + + RepositoryCacheLoaderWriterSupport cacheLoaderWriter = + new TestRepositoryCacheLoaderWriterSupport<>(this.mockCrudRepository); + + assertThat(cacheLoaderWriter.doRepositoryOp(testEntity, mockRepositoryOperationFunction)).isEqualTo("TEST"); + + verify(mockRepositoryOperationFunction, times(1)).apply(eq(testEntity)); + } + + @Test + public void loadReturnsNull() { + + LoaderHelper mockLoadHelper = mock(LoaderHelper.class); + + assertThat(new TestRepositoryCacheLoaderWriterSupport<>(this.mockCrudRepository).load(mockLoadHelper)).isNull(); + + verifyZeroInteractions(mockLoadHelper); + } + + @Test(expected = CacheRuntimeException.class) + public void doRepositoryOperationThrowsException() { + + Function mockRepositoryOperationFunction = mock(Function.class); + + when(mockRepositoryOperationFunction.apply(any())).thenThrow(new RuntimeException("TEST")); + + Object testEntity = new Object(); + + RepositoryCacheLoaderWriterSupport cacheLoaderWriter = + new TestRepositoryCacheLoaderWriterSupport<>(this.mockCrudRepository); + + try { + cacheLoaderWriter.doRepositoryOp(testEntity, mockRepositoryOperationFunction); + } + catch (CacheRuntimeException expected) { + + assertThat(expected).hasMessage(RepositoryCacheLoaderWriterSupport.DATA_ACCESS_ERROR, testEntity); + assertThat(expected).hasCauseInstanceOf(RuntimeException.class); + assertThat(expected.getCause()).hasMessage("TEST"); + assertThat(expected.getCause()).hasNoCause(); + + throw expected; + } + finally { + verify(mockRepositoryOperationFunction, times(1)).apply(eq(testEntity)); + } + } + + static class TestRepositoryCacheLoaderWriterSupport extends RepositoryCacheLoaderWriterSupport { + + TestRepositoryCacheLoaderWriterSupport(CrudRepository crudRepository) { + super(crudRepository); + } + + @Override + protected CacheRuntimeException newCacheRuntimeException(Supplier messageSupplier, Throwable cause) { + return new TestCacheRuntimeException(messageSupplier.get(), cause); + } + } + + @SuppressWarnings("unused") + static final class TestCacheRuntimeException extends CacheRuntimeException { + + public TestCacheRuntimeException() { } + + public TestCacheRuntimeException(String message) { + super(message); + } + + public TestCacheRuntimeException(Throwable cause) { + super(cause); + } + + public TestCacheRuntimeException(String message, Throwable cause) { + super(message, cause); + } + } +}