From f82bf7380a07b0b3a58b926ebab5a1538da75d64 Mon Sep 17 00:00:00 2001 From: John Blum Date: Wed, 12 Jun 2019 10:59:23 -0700 Subject: [PATCH] Add RegionConfigurer to register a SD CrudRepository as a Region CacheLoader to handle cache misses in Inline Caching. --- ...RepositoryCacheLoaderRegionConfigurer.java | 175 ++++++++++++++++ ...yCacheLoaderRegionConfigurerUnitTests.java | 188 ++++++++++++++++++ 2 files changed, 363 insertions(+) create mode 100644 spring-geode/src/main/java/org/springframework/geode/cache/RepositoryCacheLoaderRegionConfigurer.java create mode 100644 spring-geode/src/test/java/org/springframework/geode/cache/RepositoryCacheLoaderRegionConfigurerUnitTests.java diff --git a/spring-geode/src/main/java/org/springframework/geode/cache/RepositoryCacheLoaderRegionConfigurer.java b/spring-geode/src/main/java/org/springframework/geode/cache/RepositoryCacheLoaderRegionConfigurer.java new file mode 100644 index 00000000..d8acceed --- /dev/null +++ b/spring-geode/src/main/java/org/springframework/geode/cache/RepositoryCacheLoaderRegionConfigurer.java @@ -0,0 +1,175 @@ +/* + * 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.Predicate; + +import org.apache.geode.cache.CacheLoader; +import org.apache.geode.cache.Region; + +import org.springframework.data.gemfire.PeerRegionFactoryBean; +import org.springframework.data.gemfire.client.ClientRegionFactoryBean; +import org.springframework.data.gemfire.config.annotation.RegionConfigurer; +import org.springframework.data.repository.CrudRepository; +import org.springframework.util.Assert; + +/** + * Spring Data {@link RegionConfigurer} implementation used to adapt and register a Spring Data {@link CrudRepository} + * as a {@link CacheLoader} for a targeted {@link Region}. + * + * @author John Blum + * @param {@link Class type} of the persistent entity. + * @param {@link Class type} of the persistent entity identifier (ID). + * @see java.util.function.Predicate + * @see org.apache.geode.cache.CacheLoader + * @see org.apache.geode.cache.Region + * @see org.springframework.data.gemfire.PeerRegionFactoryBean + * @see org.springframework.data.gemfire.client.ClientRegionFactoryBean + * @see org.springframework.data.gemfire.config.annotation.RegionConfigurer + * @see org.springframework.data.repository.CrudRepository + * @since 1.1.0 + */ +public class RepositoryCacheLoaderRegionConfigurer implements RegionConfigurer { + + /** + * Factory method used to construct a new instance of {@link RepositoryCacheLoaderRegionConfigurer} initialized with + * the given Spring Data {@link CrudRepository} used to load {@link Region} values on cache misses as well as + * the given {@link Predicate} used to identify/qualify the {@link Region} on which the {@link CrudRepository} + * will be registered and used as a {@link CacheLoader}. + * + * @param {@link Class type} of the persistent entity. + * @param {@link Class type} of the persistent entity identifier (ID). + * @param repository {@link CrudRepository} used to load {@link Region} values on cache misses. + * @param regionBeanName {@link Predicate} used to identify/qualify the {@link Region} on which + * the {@link CrudRepository} will be registered and used as a {@link CacheLoader}. + * @return a new instance of {@link RepositoryCacheLoaderRegionConfigurer}. + * @throws IllegalArgumentException if {@link CrudRepository} is {@literal null}. + * @see org.springframework.data.repository.CrudRepository + * @see java.util.function.Predicate + * @see #RepositoryCacheLoaderRegionConfigurer(CrudRepository, Predicate) + */ + public static RepositoryCacheLoaderRegionConfigurer create(CrudRepository repository, + Predicate regionBeanName) { + + return new RepositoryCacheLoaderRegionConfigurer<>(repository, regionBeanName); + } + + /** + * Factory method used to construct a new instance of {@link RepositoryCacheLoaderRegionConfigurer} initialized with + * the given Spring Data {@link CrudRepository} used to load {@link Region} values on cache misses as well as + * the given {@link String} identifying/qualifying the {@link Region} on which the {@link CrudRepository} + * will be registered and used as a {@link CacheLoader}. + * + * @param {@link Class type} of the persistent entity. + * @param {@link Class type} of the persistent entity identifier (ID). + * @param repository {@link CrudRepository} used to load {@link Region} values on cache misses. + * @param regionBeanName {@link String} containing the bean name identifying/qualifying the {@link Region} + * on which the {@link CrudRepository} will be registered and used as a {@link CacheLoader}. + * @return a new instance of {@link RepositoryCacheLoaderRegionConfigurer}. + * @throws IllegalArgumentException if {@link CrudRepository} is {@literal null}. + * @see org.springframework.data.repository.CrudRepository + * @see java.lang.String + * @see #create(CrudRepository, Predicate) + */ + public static RepositoryCacheLoaderRegionConfigurer create(CrudRepository repository, + String regionBeanName) { + + return create(repository, Predicate.isEqual(regionBeanName)); + } + + private final CrudRepository repository; + + private final Predicate regionBeanName; + + /** + * Constructs a new instance of {@link RepositoryCacheLoaderRegionConfigurer} initialized with the given Spring Data + * {@link CrudRepository} used to load {@link Region} values on cache misses as well as the given {@link Predicate} + * used to identify/qualify the {@link Region} on which the {@link CrudRepository} will be registered + * and used as a {@link CacheLoader}. + * + * @param repository {@link CrudRepository} used to load {@link Region} values on cache misses. + * @param regionBeanName {@link Predicate} used to identify/qualify the {@link Region} on which + * the {@link CrudRepository} will be registered and used as a {@link CacheLoader}. + * @throws IllegalArgumentException if {@link CrudRepository} is {@literal null}. + * @see org.springframework.data.repository.CrudRepository + * @see java.util.function.Predicate + */ + public RepositoryCacheLoaderRegionConfigurer(CrudRepository repository, Predicate regionBeanName) { + + Assert.notNull(repository, "CrudRepository is required"); + + this.repository = repository; + + this.regionBeanName = regionBeanName != null + ? regionBeanName + : beanName -> false; + } + + /** + * Returns the configured {@link Predicate} used to identify/qualify the {@link Region} + * on which the {@link CrudRepository} will be registered as a {@link CacheLoader} for cache misses. + * + * @return the configured {@link Predicate} used to identify/qualify the {@link Region} + * targeted for the {@link CacheLoader} registration. + * @see java.util.function.Predicate + */ + protected Predicate getRegionBeanName() { + return this.regionBeanName; + } + + /** + * Returns the configured Spring Data {@link CrudRepository} adapted/wrapped as a {@link CacheLoader} + * and used to load {@link Region} values on cache misses. + * + * @return the configured {@link CrudRepository} used to load {@link Region} values on cache misses. + * @see org.springframework.data.repository.CrudRepository + */ + protected CrudRepository getRepository() { + return this.repository; + } + + @Override + @SuppressWarnings("unchecked") + public void configure(String beanName, ClientRegionFactoryBean bean) { + + if (getRegionBeanName().test(beanName)) { + bean.setCacheLoader(newRepositoryCacheLoader()); + } + } + + @Override + @SuppressWarnings("unchecked") + public void configure(String beanName, PeerRegionFactoryBean bean) { + + if (getRegionBeanName().test(beanName)) { + bean.setCacheLoader(newRepositoryCacheLoader()); + } + } + + /** + * Constructs a new instance of {@link RepositoryCacheLoader} adapting the {@link CrudRepository} + * as an instance of a {@link CacheLoader}. + * + * @return a new {@link RepositoryCacheLoader}. + * @see org.springframework.geode.cache.RepositoryCacheLoader + * @see org.springframework.data.repository.CrudRepository + * @see org.apache.geode.cache.CacheLoader + * @see #getRepository() + */ + protected RepositoryCacheLoader newRepositoryCacheLoader() { + return new RepositoryCacheLoader<>(getRepository()); + } +} diff --git a/spring-geode/src/test/java/org/springframework/geode/cache/RepositoryCacheLoaderRegionConfigurerUnitTests.java b/spring-geode/src/test/java/org/springframework/geode/cache/RepositoryCacheLoaderRegionConfigurerUnitTests.java new file mode 100644 index 00000000..3b64356f --- /dev/null +++ b/spring-geode/src/test/java/org/springframework/geode/cache/RepositoryCacheLoaderRegionConfigurerUnitTests.java @@ -0,0 +1,188 @@ +/* + * 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.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isA; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.function.Predicate; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import org.apache.geode.cache.CacheLoader; + +import org.springframework.data.gemfire.PeerRegionFactoryBean; +import org.springframework.data.gemfire.client.ClientRegionFactoryBean; +import org.springframework.data.repository.CrudRepository; + +/** + * Unit Tests for {@link RepositoryCacheLoaderRegionConfigurer}. + * + * @author John Blum + * @see org.junit.Test + * @see org.mockito.Mock + * @see org.mockito.Mockito + * @see org.mockito.junit.MockitoJUnitRunner + * @see org.apache.geode.cache.CacheLoader + * @see org.springframework.data.gemfire.PeerRegionFactoryBean + * @see org.springframework.data.gemfire.client.ClientRegionFactoryBean + * @see org.springframework.data.repository.CrudRepository + * @see org.springframework.geode.cache.RepositoryCacheLoader + * @see org.springframework.geode.cache.RepositoryCacheLoaderRegionConfigurer + * @since 1.1.0 + */ +@RunWith(MockitoJUnitRunner.class) +public class RepositoryCacheLoaderRegionConfigurerUnitTests { + + @Mock + private CrudRepository mockRepository; + + @Mock + private Predicate mockPredicate; + + @Test + public void constructRepositoryCacheLoaderRegionConfigurerIsSuccessful() { + + RepositoryCacheLoaderRegionConfigurer regionConfigurer = + new RepositoryCacheLoaderRegionConfigurer<>(this.mockRepository, this.mockPredicate); + + assertThat(regionConfigurer).isNotNull(); + assertThat(regionConfigurer.getRegionBeanName()).isEqualTo(this.mockPredicate); + assertThat(regionConfigurer.getRepository()).isEqualTo(this.mockRepository); + } + + @Test(expected = IllegalArgumentException.class) + public void constructRepositoryCacheLoaderRegionConfigurerWithNoRepositoryThrowsException() { + + try { + new RepositoryCacheLoaderRegionConfigurer<>(null, this.mockPredicate); + } + catch (IllegalArgumentException expected) { + + assertThat(expected).hasMessage("CrudRepository is required"); + assertThat(expected).hasNoCause(); + + throw expected; + } + } + + @Test + @SuppressWarnings("unchecked") + public void configuresClientRegionFactoryBeanWithRepositoryCacheLoaderWhenPredicateReturnsTrue() { + + ClientRegionFactoryBean clientRegionFactoryBean = spy(new ClientRegionFactoryBean()); + + doAnswer(answer -> { + + CacheLoader cacheLoader = answer.getArgument(0); + + assertThat(cacheLoader).isInstanceOf(RepositoryCacheLoader.class); + assertThat(((RepositoryCacheLoader) cacheLoader).getRepository()).isEqualTo(this.mockRepository); + + return null; + + }).when(clientRegionFactoryBean).setCacheLoader(isA(CacheLoader.class)); + + when(this.mockPredicate.test(anyString())).thenReturn(true); + + RepositoryCacheLoaderRegionConfigurer regionConfigurer = + new RepositoryCacheLoaderRegionConfigurer(this.mockRepository, this.mockPredicate); + + regionConfigurer.configure("Example", clientRegionFactoryBean); + + verify(clientRegionFactoryBean, times(1)) + .setCacheLoader(isA(RepositoryCacheLoader.class)); + + verify(this.mockPredicate, times(1)).test(eq("Example")); + } + + @Test + @SuppressWarnings("unchecked") + public void doesNotConfigureClientRegionFactoryBeanWithRepositoryCacheLoaderWhenPredicateReturnsFalse() { + + when(this.mockPredicate.test(anyString())).thenReturn(false); + + ClientRegionFactoryBean clientRegionFactoryBean = spy(new ClientRegionFactoryBean()); + + RepositoryCacheLoaderRegionConfigurer regionConfigurer = + new RepositoryCacheLoaderRegionConfigurer(this.mockRepository, this.mockPredicate); + + regionConfigurer.configure("Example", clientRegionFactoryBean); + + verify(clientRegionFactoryBean, never()).setCacheLoader(any(CacheLoader.class)); + verify(this.mockPredicate, times(1)).test(eq("Example")); + } + + @Test + @SuppressWarnings("unchecked") + public void configuresPeerRegionFactoryBeanWithRepositoryCacheLoaderWhenPredicateReturnsTrue() { + + PeerRegionFactoryBean peerRegionFactoryBean = mock(PeerRegionFactoryBean.class); + + doAnswer(answer -> { + + CacheLoader cacheLoader = answer.getArgument(0); + + assertThat(cacheLoader).isInstanceOf(RepositoryCacheLoader.class); + assertThat(((RepositoryCacheLoader) cacheLoader).getRepository()).isEqualTo(this.mockRepository); + + return null; + + }).when(peerRegionFactoryBean).setCacheLoader(isA(CacheLoader.class)); + + when(this.mockPredicate.test(anyString())).thenReturn(true); + + RepositoryCacheLoaderRegionConfigurer regionConfigurer = + new RepositoryCacheLoaderRegionConfigurer(this.mockRepository, this.mockPredicate); + + regionConfigurer.configure("Example", peerRegionFactoryBean); + + verify(peerRegionFactoryBean, times(1)) + .setCacheLoader(isA(RepositoryCacheLoader.class)); + + verify(this.mockPredicate, times(1)).test(eq("Example")); + } + + @Test + @SuppressWarnings("unchecked") + public void doesNotConfigurePeerRegionFactoryBeanWithRepositoryCacheLoaderWhenPredicateReturnsFalse() { + + when(this.mockPredicate.test(anyString())).thenReturn(false); + + PeerRegionFactoryBean peerRegionFactoryBean = mock(PeerRegionFactoryBean.class); + + RepositoryCacheLoaderRegionConfigurer regionConfigurer = + new RepositoryCacheLoaderRegionConfigurer(this.mockRepository, this.mockPredicate); + + regionConfigurer.configure("Example", peerRegionFactoryBean); + + verify(peerRegionFactoryBean, never()).setCacheLoader(any(CacheLoader.class)); + verify(this.mockPredicate, times(1)).test(eq("Example")); + } +}