From f96767542378d12c1ca421a1d7564cba4b2de467 Mon Sep 17 00:00:00 2001 From: John Blum Date: Tue, 12 May 2020 19:48:35 -0700 Subject: [PATCH] Add Spring Boot auto-configuration for cache data imports/exports. Resolves gh-67. --- .../DataImportExportAutoConfiguration.java | 118 ++++++++++ .../support/PdxInstanceWrapperAspect.java | 170 +++++++++++++++ .../main/resources/META-INF/spring.factories | 1 + ...portAutoConfigurationIntegrationTests.java | 206 ++++++++++++++++++ .../src/test/resources/application.properties | 1 + .../src/test/resources/data-books.json | 24 ++ 6 files changed, 520 insertions(+) create mode 100644 spring-geode-autoconfigure/src/main/java/org/springframework/geode/boot/autoconfigure/DataImportExportAutoConfiguration.java create mode 100644 spring-geode-autoconfigure/src/main/java/org/springframework/geode/boot/autoconfigure/support/PdxInstanceWrapperAspect.java create mode 100644 spring-geode-autoconfigure/src/test/java/org/springframework/geode/boot/autoconfigure/data/ClientCacheDataImportExportAutoConfigurationIntegrationTests.java create mode 100644 spring-geode-autoconfigure/src/test/resources/data-books.json diff --git a/spring-geode-autoconfigure/src/main/java/org/springframework/geode/boot/autoconfigure/DataImportExportAutoConfiguration.java b/spring-geode-autoconfigure/src/main/java/org/springframework/geode/boot/autoconfigure/DataImportExportAutoConfiguration.java new file mode 100644 index 00000000..dcc41188 --- /dev/null +++ b/spring-geode-autoconfigure/src/main/java/org/springframework/geode/boot/autoconfigure/DataImportExportAutoConfiguration.java @@ -0,0 +1,118 @@ +/* + * 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.geode.boot.autoconfigure; + +import java.util.Optional; + +import org.apache.geode.cache.GemFireCache; + +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.condition.AnyNestedCondition; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Condition; +import org.springframework.context.annotation.ConditionContext; +import org.springframework.context.annotation.Conditional; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.env.Environment; +import org.springframework.core.type.AnnotatedTypeMetadata; +import org.springframework.data.gemfire.CacheFactoryBean; +import org.springframework.geode.boot.autoconfigure.support.PdxInstanceWrapperAspect; +import org.springframework.geode.cache.SimpleCacheResolver; +import org.springframework.geode.data.json.JsonCacheDataImporterExporter; +import org.springframework.lang.NonNull; + +/** + * Spring Boot {@link EnableAutoConfiguration auto-configuration} for cache data import/export. + * + * @author John Blum + * @see org.apache.geode.cache.GemFireCache + * @see org.springframework.boot.autoconfigure.EnableAutoConfiguration + * @see org.springframework.boot.autoconfigure.condition.AnyNestedCondition + * @see org.springframework.boot.autoconfigure.condition.ConditionalOnBean + * @see org.springframework.boot.autoconfigure.condition.ConditionalOnClass + * @see org.springframework.boot.autoconfigure.condition.ConditionalOnProperty + * @see org.springframework.context.annotation.Bean + * @see org.springframework.context.annotation.Condition + * @see org.springframework.context.annotation.Conditional + * @see org.springframework.context.annotation.Configuration + * @see org.springframework.core.env.Environment + * @see org.springframework.core.type.AnnotatedTypeMetadata + * @see org.springframework.data.gemfire.CacheFactoryBean + * @see org.springframework.geode.boot.autoconfigure.support.PdxInstanceWrapperAspect + * @see org.springframework.geode.cache.SimpleCacheResolver + * @see org.springframework.geode.data.json.JsonCacheDataImporterExporter + * @since 1.3.0 + */ +@Configuration +@ConditionalOnBean(GemFireCache.class) +@ConditionalOnClass({ CacheFactoryBean.class, GemFireCache.class }) +@SuppressWarnings("unused") +public class DataImportExportAutoConfiguration { + + protected static final String PDX_READ_SERIALIZED_PROPERTY = "spring.data.gemfire.pdx.read-serialized"; + protected static final String REGION_ADVICE_ENABLED_PROPERTY = + "spring.boot.data.gemfire.cache.region.advice.enabled"; + + @Bean + JsonCacheDataImporterExporter jsonCacheDataImporterExporter() { + return new JsonCacheDataImporterExporter(); + } + + @Bean + @Conditional(RegionAdviceConditions.class) + PdxInstanceWrapperAspect pdxInstanceWrapperAspect() { + return new PdxInstanceWrapperAspect(); + } + + static class RegionAdviceConditions extends AnyNestedCondition { + + RegionAdviceConditions() { + super(ConfigurationPhase.REGISTER_BEAN); + } + + @ConditionalOnProperty(name = REGION_ADVICE_ENABLED_PROPERTY, havingValue = "true") + static class AdviseRegionOnRegionAdviceEnabledProperty { } + + @Conditional(PdxReadSerializedCondition.class) + static class AdviceRegionOnPdxReadSerializedCondition { } + + } + + static class PdxReadSerializedCondition implements Condition { + + @Override + public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) { + return isPdxReadSerializedTrue(context.getEnvironment()) || isCachePdxReadSerializedTrue(); + } + + private boolean isCachePdxReadSerializedTrue() { + + return SimpleCacheResolver.getInstance().resolve() + .filter(GemFireCache::getPdxReadSerialized) + .isPresent(); + } + + private boolean isPdxReadSerializedTrue(@NonNull Environment environment) { + + return Optional.ofNullable(environment) + .filter(env -> env.getProperty(PDX_READ_SERIALIZED_PROPERTY, Boolean.class, false)) + .isPresent(); + } + } +} diff --git a/spring-geode-autoconfigure/src/main/java/org/springframework/geode/boot/autoconfigure/support/PdxInstanceWrapperAspect.java b/spring-geode-autoconfigure/src/main/java/org/springframework/geode/boot/autoconfigure/support/PdxInstanceWrapperAspect.java new file mode 100644 index 00000000..bce886c7 --- /dev/null +++ b/spring-geode-autoconfigure/src/main/java/org/springframework/geode/boot/autoconfigure/support/PdxInstanceWrapperAspect.java @@ -0,0 +1,170 @@ +/* + * 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.geode.boot.autoconfigure.support; + +import java.util.Collection; +import java.util.Collections; +import java.util.Map; +import java.util.stream.Collectors; + +import org.apache.geode.cache.CacheStatistics; +import org.apache.geode.cache.Region; + +import org.apache.shiro.util.Assert; + +import org.springframework.geode.pdx.PdxInstanceWrapper; + +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Pointcut; + +/** + * The PdxInstanceWrapperAspect class... + * + * @author John Blum + * @since 1.0.0 + */ +@Aspect +@SuppressWarnings("unused") +public class PdxInstanceWrapperAspect { + + private Collection asCollection(Object value) { + return value instanceof Collection ? (Collection) value : Collections.emptyList(); + } + + private Map asMap(Object value) { + return value instanceof Map ? (Map) value : Collections.emptyMap(); + } + + @Pointcut("target(org.apache.geode.cache.Region)") + private void regionPointcut() { } + + @Pointcut("execution(* org.apache.geode.cache.Region.get(..))") + private void regionGetPointcut() { } + + @Pointcut("execution(* org.apache.geode.cache.Region.getAll(..))") + private void regionGetAllPointcut() { } + + @Pointcut("execution(* org.apache.geode.cache.Region.getEntry(..))") + private void regionGetEntryPointcut() { } + + @Pointcut("execution(* org.apache.geode.cache.Region.selectValue(..))") + private void regionSelectValuePointcut() { + } + + @Pointcut("execution(* org.apache.geode.cache.Region.values())") + private void regionValuesPointcut() { } + + @Around("regionPointcut() && regionGetPointcut()") + public Object regionGetAdvice(ProceedingJoinPoint joinPoint) throws Throwable { + return PdxInstanceWrapper.from(joinPoint.proceed()); + } + + @Around("regionPointcut() && regionGetAllPointcut()") + public Object regionGetAllAdvice(ProceedingJoinPoint joinPoint) throws Throwable { + return asMap(joinPoint.proceed()).entrySet().stream() + .collect(Collectors.toMap(Map.Entry::getKey, mapEntry -> PdxInstanceWrapper.from(mapEntry.getValue()))); + } + + @Around("regionPointcut() && regionGetEntryPointcut()") + public Object regionGetEntryAdvice(ProceedingJoinPoint joinPoint) throws Throwable { + return RegionEntryWrapper.from(joinPoint.proceed()); + } + + @Around("regionPointcut() && regionSelectValuePointcut()") + public Object regionSelectValueAdvice(ProceedingJoinPoint joinPoint) throws Throwable { + return PdxInstanceWrapper.from(joinPoint.proceed()); + } + + @Around("regionPointcut() && regionValuesPointcut()") + public Object regionValuesAdvice(ProceedingJoinPoint joinPoint) throws Throwable { + + return asCollection(joinPoint.proceed()).stream() + .map(PdxInstanceWrapper::from) + .collect(Collectors.toList()); + } + + protected static class RegionEntryWrapper implements Region.Entry { + + @SuppressWarnings("unchecked") + protected static T from(T value) { + + return value instanceof Region.Entry + ? (T) new RegionEntryWrapper((Region.Entry) value) + : value; + } + + private final Region.Entry delegate; + + protected RegionEntryWrapper(Region.Entry regionEntry) { + + Assert.notNull(regionEntry, "Region.Entry must not be null"); + + this.delegate = regionEntry; + } + + protected Region.Entry getDelegate() { + return this.delegate; + } + + @Override + public boolean isDestroyed() { + return getDelegate().isDestroyed(); + } + + @Override + public boolean isLocal() { + return getDelegate().isLocal(); + } + + @Override + public K getKey() { + return getDelegate().getKey(); + } + + @Override + public Region getRegion() { + return getDelegate().getRegion(); + } + + @Override + public CacheStatistics getStatistics() { + return getDelegate().getStatistics(); + } + + @Override + public Object setUserAttribute(Object userAttribute) { + return getDelegate().setUserAttribute(userAttribute); + } + + @Override + public Object getUserAttribute() { + return getDelegate().getUserAttribute(); + } + + @Override + public V setValue(V value) { + return getDelegate().setValue(value); + } + + @Override + @SuppressWarnings("unchecked") + public V getValue() { + return (V) PdxInstanceWrapper.from(getDelegate().getValue()); + } + } +} diff --git a/spring-geode-autoconfigure/src/main/resources/META-INF/spring.factories b/spring-geode-autoconfigure/src/main/resources/META-INF/spring.factories index 6509983c..20a16639 100644 --- a/spring-geode-autoconfigure/src/main/resources/META-INF/spring.factories +++ b/spring-geode-autoconfigure/src/main/resources/META-INF/spring.factories @@ -5,6 +5,7 @@ org.springframework.geode.boot.autoconfigure.ClientCacheAutoConfiguration,\ org.springframework.geode.boot.autoconfigure.CachingProviderAutoConfiguration,\ org.springframework.geode.boot.autoconfigure.ClientSecurityAutoConfiguration,\ org.springframework.geode.boot.autoconfigure.ContinuousQueryAutoConfiguration,\ +org.springframework.geode.boot.autoconfigure.DataImportExportAutoConfiguration,\ org.springframework.geode.boot.autoconfigure.EnvironmentSourcedGemFirePropertiesAutoConfiguration,\ org.springframework.geode.boot.autoconfigure.FunctionExecutionAutoConfiguration,\ org.springframework.geode.boot.autoconfigure.GemFirePropertiesAutoConfiguration,\ diff --git a/spring-geode-autoconfigure/src/test/java/org/springframework/geode/boot/autoconfigure/data/ClientCacheDataImportExportAutoConfigurationIntegrationTests.java b/spring-geode-autoconfigure/src/test/java/org/springframework/geode/boot/autoconfigure/data/ClientCacheDataImportExportAutoConfigurationIntegrationTests.java new file mode 100644 index 00000000..d58748bd --- /dev/null +++ b/spring-geode-autoconfigure/src/test/java/org/springframework/geode/boot/autoconfigure/data/ClientCacheDataImportExportAutoConfigurationIntegrationTests.java @@ -0,0 +1,206 @@ +/* + * 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.geode.boot.autoconfigure.data; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.IOException; +import java.time.LocalDate; +import java.time.Month; +import java.util.Collection; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; + +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.apache.geode.cache.DataPolicy; +import org.apache.geode.cache.GemFireCache; +import org.apache.geode.cache.client.ClientRegionShortcut; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Profile; +import org.springframework.data.gemfire.GemfireTemplate; +import org.springframework.data.gemfire.LocalRegionFactoryBean; +import org.springframework.data.gemfire.client.ClientRegionFactoryBean; +import org.springframework.data.gemfire.config.annotation.CacheServerApplication; +import org.springframework.data.gemfire.tests.integration.ForkingClientServerIntegrationTestsSupport; +import org.springframework.geode.boot.autoconfigure.DataImportExportAutoConfiguration; +import org.springframework.geode.core.util.ObjectUtils; +import org.springframework.geode.util.CacheUtils; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.junit4.SpringRunner; + +import example.app.books.model.Author; +import example.app.books.model.Book; +import example.app.books.model.ISBN; + +/** + * Integration Tests for {@link DataImportExportAutoConfiguration}. + * + * @author John Blum + * @see org.junit.Test + * @see org.apache.geode.cache.GemFireCache + * @see org.springframework.boot.autoconfigure.SpringBootApplication + * @see org.springframework.boot.test.context.SpringBootTest + * @see org.springframework.context.annotation.AnnotationConfigApplicationContext + * @see org.springframework.context.annotation.Bean + * @see org.springframework.context.annotation.Profile + * @see org.springframework.data.gemfire.GemfireTemplate + * @see org.springframework.data.gemfire.LocalRegionFactoryBean + * @see org.springframework.data.gemfire.client.ClientRegionFactoryBean + * @see org.springframework.data.gemfire.config.annotation.CacheServerApplication + * @see org.springframework.data.gemfire.tests.integration.ForkingClientServerIntegrationTestsSupport + * @see org.springframework.geode.boot.autoconfigure.DataImportExportAutoConfiguration + * @see org.springframework.test.context.ActiveProfiles + * @see org.springframework.test.context.junit4.SpringRunner + * @since 1.3.0 + */ +@ActiveProfiles("IMPORT") +@RunWith(SpringRunner.class) +@SpringBootTest( + classes = ClientCacheDataImportExportAutoConfigurationIntegrationTests.TestGeodeClientConfiguration.class, + properties = { + "spring.boot.data.gemfire.cache.data.import.active-profiles=IMPORT", + "spring.boot.data.gemfire.cache.region.advice.enabled=true" + } +) +@SuppressWarnings("unused") +public class ClientCacheDataImportExportAutoConfigurationIntegrationTests + extends ForkingClientServerIntegrationTestsSupport { + + @BeforeClass + public static void startGeodeServer() throws IOException { + startGemFireServer(TestGeodeServerConfiguration.class,"-Dspring.profiles.active=IMPORT-SERVER"); + } + + @Autowired + private GemfireTemplate booksTemplate; + + @Before + public void assertBooksTemplate() { + + assertThat(this.booksTemplate).isNotNull(); + assertThat(this.booksTemplate.getRegion()).isNotNull(); + assertThat(this.booksTemplate.getRegion().getName()).isEqualTo("Books"); + } + + private void assertBook(Book book, String title, LocalDate publishedDate, ISBN isbn, Author author) { + + assertThat(book).isNotNull(); + assertThat(book.getTitle()).isEqualTo(title); + assertThat(book.getPublishedDate()).isEqualTo(publishedDate); + assertThat(book.getIsbn()).isEqualTo(isbn); + assertThat(book.getAuthor()).isEqualTo(author); + } + + private Book findBy(Iterable books, String title) { + + return StreamSupport.stream(books.spliterator(), false) + .filter(book -> book.getTitle().equals(title)) + .findFirst() + .orElse(null); + } + + private Collection getRegionValues(GemfireTemplate template) { + + return Optional.ofNullable(template) + .map(GemfireTemplate::getRegion) + .filter(region -> DataPolicy.EMPTY.equals(region.getAttributes().getDataPolicy())) + .map(CacheUtils::collectValues) + .orElseGet(() -> template.getRegion().values()); + } + + private Object log(Object value) { + + System.err.printf("%s%n", value); + System.err.flush(); + + return value; + } + + @Test + public void booksWasLoaded() { + + Collection bookValues = getRegionValues(this.booksTemplate); + + assertThat(bookValues).isNotNull(); + assertThat(bookValues).hasSize(2); + + Set books = bookValues.stream() + //.peek(this::log) + .map(value -> ObjectUtils.asType(value, Book.class)) + .collect(Collectors.toSet()); + + Book cloudNativeJava = findBy(books, "Cloud Native Java"); + + assertBook(cloudNativeJava, "Cloud Native Java", LocalDate.of(2017, Month.AUGUST, 1), + ISBN.of("978-1-449-374640-8"), Author.newAuthor("Josh Long").identifiedBy(1L)); + + Book databaseInternals = findBy(books, "Database Internals"); + + assertBook(databaseInternals, "Database Internals", LocalDate.of(2019, Month.OCTOBER, 1), + ISBN.of("978-1-492-04034-7"), Author.newAuthor("Alex Petrov").identifiedBy(2L)); + } + + @Profile("IMPORT") + @SpringBootApplication + static class TestGeodeClientConfiguration { + + @Bean("Books") + ClientRegionFactoryBean clientRegion(GemFireCache cache) { + + ClientRegionFactoryBean clientRegion = new ClientRegionFactoryBean<>(); + + clientRegion.setCache(cache); + clientRegion.setShortcut(ClientRegionShortcut.PROXY); + + return clientRegion; + } + } + + @Profile("IMPORT-SERVER") + @CacheServerApplication(name = "ClientCacheDataImportExportAutoConfigurationIntegrationTestsServer") + static class TestGeodeServerConfiguration { + + public static void main(String[] args) { + + AnnotationConfigApplicationContext applicationContext = + new AnnotationConfigApplicationContext(TestGeodeServerConfiguration.class); + + applicationContext.registerShutdownHook(); + } + + @Bean("Books") + LocalRegionFactoryBean peerRegion(GemFireCache cache) { + + LocalRegionFactoryBean peerRegion = new LocalRegionFactoryBean<>(); + + peerRegion.setCache(cache); + peerRegion.setPersistent(false); + + return peerRegion; + } + } +} diff --git a/spring-geode-autoconfigure/src/test/resources/application.properties b/spring-geode-autoconfigure/src/test/resources/application.properties index b0cef9a9..601e0a49 100644 --- a/spring-geode-autoconfigure/src/test/resources/application.properties +++ b/spring-geode-autoconfigure/src/test/resources/application.properties @@ -1,4 +1,5 @@ # Spring Boot application.properties for Unit and Integration Tests +spring.boot.data.gemfire.cache.data.import.active-profiles=IMPORT spring.boot.data.gemfire.security.ssl.keystore.name=non-existing-trusted.keystore spring.main.allow-bean-definition-overriding=true diff --git a/spring-geode-autoconfigure/src/test/resources/data-books.json b/spring-geode-autoconfigure/src/test/resources/data-books.json new file mode 100644 index 00000000..2a189b6d --- /dev/null +++ b/spring-geode-autoconfigure/src/test/resources/data-books.json @@ -0,0 +1,24 @@ +[ + { + "@type": "example.app.books.model.Book", + "@identifier": "isbn", + "author": { + "id": 1, + "name": "Josh Long" + }, + "isbn": "978-1-449-374640-8", + "publishedDate": "2017-08-01", + "title": "Cloud Native Java" + }, + { + "@type": "example.app.books.model.Book", + "@identifier": "isbn", + "author": { + "id": 2, + "name": "Alex Petrov" + }, + "isbn": "978-1-492-04034-7", + "publishedDate": "2019-10-01", + "title": "Database Internals" + } +]