From 681fd81ad2def9afedc3fc6e157dc1db7ba998e8 Mon Sep 17 00:00:00 2001 From: John Blum Date: Fri, 10 Jul 2020 16:05:25 -0700 Subject: [PATCH] Define new, abstract ResourceCapableCacheDataImporterExporter capable of handling dynamic Resources. The primary concerns involve resolving Resources for import/export along with reading from and writing to any Resource using the Spring Resource abstraction. Resolves gh-92. --- ...ourceCapableCacheDataImporterExporter.java | 550 +++++++++++++ ...bleCacheDataImporterExporterUnitTests.java | 770 ++++++++++++++++++ 2 files changed, 1320 insertions(+) create mode 100644 spring-geode/src/main/java/org/springframework/geode/data/support/ResourceCapableCacheDataImporterExporter.java create mode 100644 spring-geode/src/test/java/org/springframework/geode/data/support/ResourceCapableCacheDataImporterExporterUnitTests.java diff --git a/spring-geode/src/main/java/org/springframework/geode/data/support/ResourceCapableCacheDataImporterExporter.java b/spring-geode/src/main/java/org/springframework/geode/data/support/ResourceCapableCacheDataImporterExporter.java new file mode 100644 index 00000000..35c2d84a --- /dev/null +++ b/spring-geode/src/main/java/org/springframework/geode/data/support/ResourceCapableCacheDataImporterExporter.java @@ -0,0 +1,550 @@ +/* + * 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.data.support; + +import static org.springframework.geode.core.util.ObjectUtils.initialize; + +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; + +import org.apache.geode.cache.GemFireCache; +import org.apache.geode.cache.Region; + +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.ResourceLoaderAware; +import org.springframework.context.expression.BeanFactoryAccessor; +import org.springframework.core.convert.ConversionService; +import org.springframework.core.io.Resource; +import org.springframework.core.io.ResourceLoader; +import org.springframework.expression.EvaluationContext; +import org.springframework.expression.Expression; +import org.springframework.expression.ExpressionParser; +import org.springframework.expression.ParserContext; +import org.springframework.expression.PropertyAccessor; +import org.springframework.expression.spel.SpelCompilerMode; +import org.springframework.expression.spel.SpelParserConfiguration; +import org.springframework.expression.spel.standard.SpelExpressionParser; +import org.springframework.expression.spel.support.DataBindingPropertyAccessor; +import org.springframework.expression.spel.support.SimpleEvaluationContext; +import org.springframework.geode.core.env.EnvironmentMapAdapter; +import org.springframework.geode.core.io.ResourceReader; +import org.springframework.geode.core.io.ResourceResolver; +import org.springframework.geode.core.io.ResourceWriter; +import org.springframework.geode.core.io.support.ByteArrayResourceReader; +import org.springframework.geode.core.io.support.FileResourceWriter; +import org.springframework.geode.core.io.support.ResourceLoaderResourceResolver; +import org.springframework.geode.core.io.support.ResourcePrefix; +import org.springframework.geode.core.io.support.ResourceUtils; +import org.springframework.geode.data.AbstractCacheDataImporterExporter; +import org.springframework.geode.data.CacheDataImporterExporter; +import org.springframework.geode.expression.SmartEnvironmentAccessor; +import org.springframework.lang.NonNull; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import org.springframework.util.StringUtils; + +/** + * An {@link AbstractCacheDataImporterExporter} extension and implementation capable of handling and managing import + * and export {@link Resource Resources}. + * + * @author John Blum + * @see org.apache.geode.cache.GemFireCache + * @see org.apache.geode.cache.Region + * @see org.springframework.beans.factory.InitializingBean + * @see org.springframework.context.ResourceLoaderAware + * @see org.springframework.expression.Expression + * @see org.springframework.expression.ExpressionParser + * @see org.springframework.core.io.Resource + * @see org.springframework.core.io.ResourceLoader + * @see org.springframework.geode.core.io.ResourceReader + * @see org.springframework.geode.core.io.ResourceResolver + * @see org.springframework.geode.core.io.ResourceWriter + * @see org.springframework.geode.data.AbstractCacheDataImporterExporter + * @since 1.3.1 + */ +@SuppressWarnings("unused") +public abstract class ResourceCapableCacheDataImporterExporter extends AbstractCacheDataImporterExporter + implements InitializingBean, ResourceLoaderAware { + + protected static final String CACHE_DATA_EXPORT_RESOURCE_LOCATION_PROPERTY_NAME = + "spring.boot.data.gemfire.cache.data.export.resource.location"; + + protected static final String CACHE_DATA_IMPORT_RESOURCE_LOCATION_PROPERTY_NAME = + "spring.boot.data.gemfire.cache.data.import.resource.location"; + + protected static final String RESOURCE_NAME_PATTERN = "data-%s.json"; + + @Autowired(required = false) + private ExportResourceResolver exportResourceResolver; + + @Autowired(required = false) + private ImportResourceResolver importResourceResolver; + + private ResourceLoader resourceLoader; + + @Autowired(required = false) + private ResourceReader resourceReader; + + @Autowired(required = false) + private ResourceWriter resourceWriter; + + /** + * Initializes the Export & Import {@link ResourceResolver ResourceResolvers} as needed along with + * the reader and writer for the {@link Resource} used on import and export. + */ + @Override + public void afterPropertiesSet() { + + this.exportResourceResolver = initialize(this.exportResourceResolver, FileSystemExportResourceResolver::new); + this.importResourceResolver = initialize(this.importResourceResolver, ClassPathImportResourceResolver::new); + this.resourceReader = initialize(this.resourceReader, ByteArrayResourceReader::new); + this.resourceWriter = initialize(this.resourceWriter, FileResourceWriter::new); + + getResourceLoader().ifPresent(resourceLoader -> { + + if (this.exportResourceResolver instanceof ResourceLoaderAware) { + ((ResourceLoaderAware) this.exportResourceResolver).setResourceLoader(resourceLoader); + } + + if (this.importResourceResolver instanceof ResourceLoaderAware) { + ((ResourceLoaderAware) this.importResourceResolver).setResourceLoader(resourceLoader); + } + }); + } + + /** + * Gets the configured reference to the {@link ExportResourceResolver}. + * + * @return the configured reference to the {@link ExportResourceResolver}. + * @see ExportResourceResolver + */ + protected @NonNull ExportResourceResolver getExportResourceResolver() { + return this.exportResourceResolver; + } + + /** + * Gets the configured reference to the {@link ImportResourceResolver}. + * + * @return the configured reference to the {@link ImportResourceResolver}. + * @see ImportResourceResolver + */ + protected @NonNull ImportResourceResolver getImportResourceResolver() { + return this.importResourceResolver; + } + + /** + * Configures the {@link ResourceLoader} used by this {@link CacheDataImporterExporter} to resolve + * and load {@link Resource Resources}. + * + * @param resourceLoader {@link ResourceLoader} used to resolve and load {@link Resource Resources}. + * @see org.springframework.core.io.ResourceLoader + */ + @Override + public void setResourceLoader(@Nullable ResourceLoader resourceLoader) { + this.resourceLoader = resourceLoader; + } + + /** + * Returns an {@link Optional} reference to the configured {@link ResourceLoader} + * used to load {@link Resource Resources}. + * + * @return an {@link Optional} reference to the configured {@link ResourceLoader}. + * @see org.springframework.core.io.ResourceLoader + * @see java.util.Optional + */ + protected Optional getResourceLoader() { + return Optional.ofNullable(this.resourceLoader); + } + + /** + * Gets the configured {@link ResourceReader} used to read data from the {@link Resource} on {@literal import}. + * + * @return the configured {@link ResourceReader}. + * @see org.springframework.geode.core.io.ResourceReader + */ + protected @NonNull ResourceReader getResourceReader() { + return this.resourceReader; + } + + /** + * Gets the configured {@link ResourceWriter} used to write data to the {@link Resource} on {@literal export}. + * + * @return the configured {@link ResourceWriter}. + * @see org.springframework.geode.core.io.ResourceWriter + */ + protected @NonNull ResourceWriter getResourceWriter() { + return this.resourceWriter; + } + + /** + * {@link ResourceResolver} interface extension used to resolve {@link GemFireCache cache} + * {@link Resource Resources}. + * + * @see org.springframework.geode.core.io.ResourceResolver + * @see org.apache.geode.cache.GemFireCache + * @see org.apache.geode.cache.Region + */ + protected interface CacheResourceResolver extends ResourceResolver { + + /** + * Tries to resolve a {@link Resource} to a {@link String location} containing data for the given {@link Region}. + * The {@link Region} is used to determine the {@link String location} of the {@link Resource} to load. + * + * @param region {@link Region} used to resolve the {@link Resource}. + * @return an {@link Optional} {@link Resource} handle to a {@link String location} containing data + * for the given {@link Region}. + * @see org.springframework.core.io.Resource + * @see org.apache.geode.cache.Region + * @see java.util.Optional + */ + Optional resolve(@NonNull Region region); + + } + + /** + * Abstract base class containing functionality common to all {@link GemFireCache cache} based + * {@link ResourceResolver ResourceResolvers}, whether for import or export. + * + * @see org.springframework.geode.core.io.support.ResourceLoaderResourceResolver + * @see CacheResourceResolver + */ + protected abstract class AbstractCacheResourceResolver extends ResourceLoaderResourceResolver + implements CacheResourceResolver { + + private final ExpressionParser expressionParser; + + private final Map compiledExpressions; + + private final SimpleEvaluationContext.Builder evaluationContextBuilder; + + /** + * Constructs a new instance of {@link AbstractCacheResourceResolver}. + * + * This constructor initializes the SpEL objects used to parse and evaluate SpEL expressions in order to + * fully qualify and resolve {@link Resource} {@link String locations} defined as properties + * in Spring Boot {@literal application.properties} for Import & Export {@link Resource Resources}. + * + * @see #newExpressionParser() + * @see #newEvaluationContextBuilder() + */ + public AbstractCacheResourceResolver() { + + this.expressionParser = newExpressionParser(); + this.evaluationContextBuilder = newEvaluationContextBuilder(); + this.compiledExpressions = new ConcurrentHashMap<>(); + } + + private ExpressionParser newExpressionParser() { + + ClassLoader classLoader = getApplicationContext() + .map(ApplicationContext::getClassLoader) + .orElseGet(ClassUtils::getDefaultClassLoader); + + return new SpelExpressionParser(new SpelParserConfiguration(SpelCompilerMode.MIXED, classLoader)); + } + + private SimpleEvaluationContext.Builder newEvaluationContextBuilder() { + + PropertyAccessor[] propertyAccessors = { + new BeanFactoryAccessor(), + DataBindingPropertyAccessor.forReadOnlyAccess(), + SmartEnvironmentAccessor.create() + }; + + SimpleEvaluationContext.Builder builder = SimpleEvaluationContext.forPropertyAccessors(propertyAccessors) + .withInstanceMethods(); + + String conversionServiceBeanName = ConfigurableApplicationContext.CONVERSION_SERVICE_BEAN_NAME; + + return getApplicationContext() + .filter(applicationContext -> applicationContext.containsBean(conversionServiceBeanName)) + .map(applicationContext -> applicationContext.getBean(conversionServiceBeanName, ConversionService.class)) + .map(builder::withConversionService) + .orElse(builder); + } + + /** + * Constructs a new {@link EvaluationContext} used during the evaluation of SpEL {@link String expressions}. + * + * @return a new {@link EvaluationContext}; never {@literal null}. + * @see org.springframework.expression.EvaluationContext + */ + protected @NonNull EvaluationContext newEvaluationContext() { + return this.evaluationContextBuilder.build(); + } + + /** + * Gets the configured {@link ExpressionParser} used to parse SpEL {@link String expressions}. + * + * @return the configured {@link ExpressionParser}; never {@literal null}. + * @see org.springframework.expression.ExpressionParser + */ + protected @NonNull ExpressionParser getExpressionParser() { + return this.expressionParser; + } + + /** + * Gets the configured {@link ParserContext} used by the {@link ExpressionParser} to identify SpEL expressions. + * + * @return the configured {@link ParserContext}. + * @see org.springframework.expression.ParserContext + */ + protected @NonNull ParserContext getParserContext() { + return ParserContext.TEMPLATE_EXPRESSION; + } + + /** + * @inheritDoc + */ + @Override + protected boolean isQualified(@Nullable Resource resource) { + return super.isQualified(resource) && resource.exists(); + } + + /** + * Determines a fully-qualified {@link String resource location} for the given {@link Region}. + * + * @param region {@link Region} to evaluate; must not be {@literal null}. + * @return a fully-qualified {@link String resource location} for the given {@link Region}. + * @see Region + * @see #getResourceName(Region) + * @see #getResourcePath() + */ + protected @NonNull String getFullyQualifiedResourceLocation(@NonNull Region region) { + return String.format("%1$s%2$s", getResourcePath(), getResourceName(region)); + } + + /** + * Determines the {@link String location} of a {@link Resource} for the given {@link Region}. + * + * @param region {@link Region} used to locate the desired {@link Resource}; must not be {@literal null}. + * @return a {@link Resource} {@link String location} for the given {@link Region}. + * @throws IllegalArgumentException if {@link Region} is {@literal null}. + * @see org.apache.geode.cache.Region + */ + protected @NonNull String getResourceLocation(@NonNull Region region, @NonNull String propertyName) { + + Assert.notNull(region, "Region must not be null"); + Assert.hasText(propertyName, () -> String.format("Property name [%s] must be specified", propertyName)); + + return getEnvironment() + .filter(environment -> environment.containsProperty(propertyName)) + .map(environment -> environment.getProperty(propertyName)) + .filter(StringUtils::hasText) + .map(resourceLocation -> evaluate(resourceLocation, region)) + .orElseGet(() -> getFullyQualifiedResourceLocation(region)); + } + + /** + * Evaluates the given SpEL {@link String expression}. + * + * @param expressionString {@link String} containing the SpEL expression to evaluate; must not be {@literal null}. + * @param region {@link Region} used to resolve {@literal regionName} variable references + * in the {@link String expression}; must not be {@literal null}. + * @return the value of the evaluated {@link String expression}. + * @see org.springframework.expression.Expression#getValue(EvaluationContext, Object) + * @see org.apache.geode.cache.Region + * @see #parse(String) + */ + protected @Nullable String evaluate(@NonNull String expressionString, @NonNull Region region) { + + EvaluationContext evaluationContext = newEvaluationContext(); + + evaluationContext.setVariable("regionName", region.getName().toLowerCase()); + + getEnvironment().ifPresent(environment -> + evaluationContext.setVariable("env", EnvironmentMapAdapter.from(environment))); + + Expression expression = parse(expressionString); + + Object value = getApplicationContext() + .map(applicationContext -> expression.getValue(evaluationContext, applicationContext)) + .orElseGet(() -> expression.getValue(evaluationContext)); + + return value != null ? value.toString() : null; + } + + /** + * Parses the given {@link String expressionString}. + * + * This method will cache parsed {@link Expression Expressions} to speed up the evaluation process. + * + * @param expressionString {@link String} containing the SpEL expression to parse. + * @return an {@link Expression} object parsed from the given {@link String expression}. + * @see org.springframework.expression.ExpressionParser#parseExpression(String, ParserContext) + * @see org.springframework.expression.Expression + * @see #getExpressionParser() + * @see #getParserContext() + */ + protected Expression parse(String expressionString) { + return this.compiledExpressions.computeIfAbsent(expressionString, + it -> getExpressionParser().parseExpression(it, getParserContext())); + } + + /** + * Determines a {@link String resource name} for the given {@link Region}. + * + * The default implementation bases the {@link String resource name} on + * the {@link Region#getName() Region's lowercase name}. + * + * @param region {@link Region} to evaluate; must not be {@literal null}. + * @return a {@link String resource name} for the given {@link Region}. + * @see Region + * @see #getResourceName(String) + */ + protected @NonNull String getResourceName(@NonNull Region region) { + return getResourceName(region.getName().toLowerCase()); + } + + /** + * Determines a {@link String resource name} for the given {@link String name}. + * + * @param name {@link String} containing the name to evaluate; must not be {@literal null}. + * @return a {@link String resource name} from the given {@link String name}. + */ + protected @NonNull String getResourceName(@NonNull String name) { + return String.format(RESOURCE_NAME_PATTERN, name); + } + + /** + * Get the {@link String base path} for the targeted {@link Resource}. + * + * @return the {@link String base path} for the targeted {@link Resource}. + */ + protected abstract @NonNull String getResourcePath(); + + } + + /** + * Marker interface extending {@link CacheResourceResolver} for cache data exports. + * + * @see org.springframework.geode.core.io.ResourceResolver + * @see CacheResourceResolver + */ + public interface ExportResourceResolver extends CacheResourceResolver { } + + /** + * Abstract base class extended by export {@link CacheResourceResolver} implementations, providing a template + * to resolve the {@link Resource} used on export. + * + * @see AbstractCacheResourceResolver + * @see ExportResourceResolver + */ + protected abstract class AbstractExportResourceResolver extends AbstractCacheResourceResolver + implements ExportResourceResolver { + + /** + * @inheritDoc + */ + @Override + public Optional resolve(@NonNull Region region) { + + Assert.notNull(region, "Region must not be null"); + + String resourceLocation = getResourceLocation(region, CACHE_DATA_EXPORT_RESOURCE_LOCATION_PROPERTY_NAME); + + Optional resource = resolve(resourceLocation); + + if (!resource.filter(ResourceUtils::isWritable).isPresent()) { + getLogger().warn("WARNING! Resource [{}] for Region [{}] is not writable", + resourceLocation, region.getFullPath()); + } + + return resource; + } + + /** + * @inheritDoc + */ + @Override + protected @Nullable Resource onMissingResource(Resource resource, String location) { + + getLogger().warn("WARNING! Resource at location [{}] does not exist; will try to create it", location); + + return resource; + } + } + + /** + * Resolves the {@link Resource} used for {@literal export} from the {@literal filesystem}. + */ + public class FileSystemExportResourceResolver extends AbstractExportResourceResolver { + + @Override + protected @NonNull String getResourcePath() { + return String.format("%1$s%2$s", ResourcePrefix.FILESYSTEM_URL_PREFIX.toUrlPrefix(), + System.getProperty("user.dir")); + } + } + + /** + * Marker interface extending {@link CacheResourceResolver} for cache data imports. + * + * @see org.springframework.geode.core.io.ResourceResolver + * @see CacheResourceResolver + */ + public interface ImportResourceResolver extends CacheResourceResolver { } + + /** + * Abstract base class extended by import {@link ResourceResolver} implementations, providing a template + * to resolve the {@link Resource} to import. + * + * @see AbstractCacheResourceResolver + * @see ImportResourceResolver + */ + protected abstract class AbstractImportResourceResolver extends AbstractCacheResourceResolver + implements ImportResourceResolver { + + /** + * @inheritDoc + */ + @Override + public Optional resolve(@NonNull Region region) { + + Assert.notNull(region, "Region must not be null"); + + String resourceLocation = getResourceLocation(region, CACHE_DATA_IMPORT_RESOURCE_LOCATION_PROPERTY_NAME); + + Optional resource = resolve(resourceLocation); + + Assert.state(resource.isPresent(), () -> String.format("Resource [%1$s] for Region [%2$s] does not exist", + resourceLocation, region.getFullPath())); + + boolean readable = resource.filter(Resource::isReadable).isPresent(); + + Assert.state(readable, () -> String.format("Resource [%1$s] for Region [%2$s] is not readable", + resourceLocation, region.getFullPath())); + + return resource; + } + } + + /** + * Resolves the {@link Resource} to {@literal import} from the {@literal classpath}. + */ + public class ClassPathImportResourceResolver extends AbstractImportResourceResolver { + + @Override + protected @NonNull String getResourcePath() { + return ResourcePrefix.CLASSPATH_URL_PREFIX.toUrlPrefix(); + } + } +} diff --git a/spring-geode/src/test/java/org/springframework/geode/data/support/ResourceCapableCacheDataImporterExporterUnitTests.java b/spring-geode/src/test/java/org/springframework/geode/data/support/ResourceCapableCacheDataImporterExporterUnitTests.java new file mode 100644 index 00000000..d8164636 --- /dev/null +++ b/spring-geode/src/test/java/org/springframework/geode/data/support/ResourceCapableCacheDataImporterExporterUnitTests.java @@ -0,0 +1,770 @@ +/* + * 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.data.support; + +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.Mockito.doAnswer; +import static org.mockito.Mockito.doReturn; +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.verifyNoInteractions; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.springframework.data.gemfire.util.RuntimeExceptionFactory.newRuntimeException; + +import java.util.Map; +import java.util.Optional; + +import org.junit.Test; + +import org.apache.geode.cache.Region; + +import org.springframework.context.ApplicationContext; +import org.springframework.context.ResourceLoaderAware; +import org.springframework.core.env.Environment; +import org.springframework.core.io.Resource; +import org.springframework.core.io.ResourceLoader; +import org.springframework.core.io.WritableResource; +import org.springframework.data.gemfire.tests.support.MapBuilder; +import org.springframework.data.gemfire.tests.util.ReflectionUtils; +import org.springframework.expression.ParserContext; +import org.springframework.expression.spel.standard.SpelExpressionParser; +import org.springframework.geode.core.io.ResourceReader; +import org.springframework.geode.core.io.ResourceWriter; +import org.springframework.geode.core.io.support.ByteArrayResourceReader; +import org.springframework.geode.core.io.support.FileResourceWriter; +import org.springframework.geode.core.io.support.ResourcePrefix; +import org.springframework.geode.data.support.ResourceCapableCacheDataImporterExporter.AbstractCacheResourceResolver; +import org.springframework.geode.data.support.ResourceCapableCacheDataImporterExporter.AbstractExportResourceResolver; +import org.springframework.geode.data.support.ResourceCapableCacheDataImporterExporter.AbstractImportResourceResolver; +import org.springframework.geode.data.support.ResourceCapableCacheDataImporterExporter.ClassPathImportResourceResolver; +import org.springframework.geode.data.support.ResourceCapableCacheDataImporterExporter.ExportResourceResolver; +import org.springframework.geode.data.support.ResourceCapableCacheDataImporterExporter.FileSystemExportResourceResolver; +import org.springframework.geode.data.support.ResourceCapableCacheDataImporterExporter.ImportResourceResolver; +import org.springframework.lang.NonNull; +import org.springframework.lang.Nullable; +import org.springframework.util.ObjectUtils; + +import org.slf4j.Logger; + +/** + * Unit Tests for {@link ResourceCapableCacheDataImporterExporter}. + * + * @author John Blum + * @see org.junit.Test + * @see org.mockito.Mockito + * @see org.apache.geode.cache.Region + * @see org.springframework.context.ApplicationContext + * @see org.springframework.core.env.Environment + * @see org.springframework.core.io.Resource + * @see org.springframework.core.io.ResourceLoader + * @see org.springframework.core.io.WritableResource + * @see org.springframework.geode.core.io.ResourceReader + * @see org.springframework.geode.core.io.ResourceWriter + * @see org.springframework.geode.data.support.ResourceCapableCacheDataImporterExporter + * @since 1.3.1 + */ +public class ResourceCapableCacheDataImporterExporterUnitTests { + + @SuppressWarnings("unchecked") + private T inject(@NonNull T target, @NonNull String fieldName, @Nullable Object value) { + + try { + return (T) ReflectionUtils.setField(target, fieldName, value); + } + catch (NoSuchFieldException cause) { + throw newRuntimeException(cause, "Failed to set field [%s] on object of type [%s] to value [%s]", + fieldName, ObjectUtils.nullSafeClassName(target), value); + } + } + + @Test + public void afterPropertiesSetUsesImportExportReaderWriterResourceDefaults() { + + ResourceCapableCacheDataImporterExporter importerExporter = new TestResourceCapableCacheDataImporterExporter(); + + importerExporter.afterPropertiesSet(); + + assertThat(importerExporter.getExportResourceResolver()).isInstanceOf(FileSystemExportResourceResolver.class); + assertThat(importerExporter.getImportResourceResolver()).isInstanceOf(ClassPathImportResourceResolver.class); + assertThat(importerExporter.getResourceLoader().orElse(null)).isNull(); + assertThat(importerExporter.getResourceReader()).isInstanceOf(ByteArrayResourceReader.class); + assertThat(importerExporter.getResourceWriter()).isInstanceOf(FileResourceWriter.class); + } + + @Test + public void afterPropertiesSetWithInjectedImportExportResourceResolversAndResourceReaderWriter() { + + ResourceLoaderAwareExportResourceResolver mockExportResourceResolver = + mock(ResourceLoaderAwareExportResourceResolver.class); + + ResourceLoaderAwareImportResourceResolver mockImportResourceResolver = + mock(ResourceLoaderAwareImportResourceResolver.class); + + ResourceLoader mockResourceLoader = mock(ResourceLoader.class); + + ResourceReader mockResourceReader = mock(ResourceReader.class); + + ResourceWriter mockResourceWriter = mock(ResourceWriter.class); + + ResourceCapableCacheDataImporterExporter importerExporter = new TestResourceCapableCacheDataImporterExporter(); + + inject(importerExporter, "exportResourceResolver", mockExportResourceResolver); + inject(importerExporter, "importResourceResolver", mockImportResourceResolver); + inject(importerExporter, "resourceReader", mockResourceReader); + inject(importerExporter, "resourceWriter", mockResourceWriter); + + importerExporter.setResourceLoader(mockResourceLoader); + importerExporter.afterPropertiesSet(); + + assertThat(importerExporter.getExportResourceResolver()).isEqualTo(mockExportResourceResolver); + assertThat(importerExporter.getImportResourceResolver()).isEqualTo(mockImportResourceResolver); + assertThat(importerExporter.getResourceLoader().orElse(null)).isEqualTo(mockResourceLoader); + assertThat(importerExporter.getResourceReader()).isEqualTo(mockResourceReader); + assertThat(importerExporter.getResourceWriter()).isEqualTo(mockResourceWriter); + + verify(mockExportResourceResolver, times(1)).setResourceLoader(eq(mockResourceLoader)); + verify(mockImportResourceResolver, times(1)).setResourceLoader(eq(mockResourceLoader)); + verifyNoInteractions(mockResourceLoader, mockResourceReader, mockResourceWriter); + } + + @Test + public void setAndGetResourceLoader() { + + ResourceLoader mockResourceLoader = mock(ResourceLoader.class); + + ResourceCapableCacheDataImporterExporter importerExporter = new TestResourceCapableCacheDataImporterExporter(); + + assertThat(importerExporter.getResourceLoader().orElse(null)).isNull(); + + importerExporter.setResourceLoader(mockResourceLoader); + + assertThat(importerExporter.getResourceLoader().orElse(null)).isEqualTo(mockResourceLoader); + + importerExporter.setResourceLoader(null); + + assertThat(importerExporter.getResourceLoader().orElse(null)).isNull(); + } + + // Tests for CacheResourceResolver and friends (sub-types) + + @Test + public void expressionParserIsASpelExpressionParser() { + + AbstractCacheResourceResolver resourceResolver = + new TestResourceCapableCacheDataImporterExporter().new TestCacheResourceResolver(); + + assertThat(resourceResolver.getExpressionParser()).isInstanceOf(SpelExpressionParser.class); + } + + @Test + public void parserContextIsTemplateBased() { + + AbstractCacheResourceResolver resourceResolver = + new TestResourceCapableCacheDataImporterExporter().new TestCacheResourceResolver(); + + assertThat(resourceResolver.getParserContext()).isEqualTo(ParserContext.TEMPLATE_EXPRESSION); + } + + @Test + public void isQualifiedWithExistingResource() { + + AbstractCacheResourceResolver resourceResolver = + new TestResourceCapableCacheDataImporterExporter().new TestCacheResourceResolver(); + + Resource mockResource = mock(Resource.class); + + doReturn(true).when(mockResource).exists(); + + assertThat(resourceResolver.isQualified(mockResource)).isTrue(); + + verify(mockResource, times(1)).exists(); + verifyNoMoreInteractions(mockResource); + } + + @Test + public void isQualifiedWithNonExistingResource() { + + AbstractCacheResourceResolver resourceResolver = + new TestResourceCapableCacheDataImporterExporter().new TestCacheResourceResolver(); + + Resource mockResource = mock(Resource.class); + + doReturn(false).when(mockResource).exists(); + + assertThat(resourceResolver.isQualified(mockResource)).isFalse(); + + verify(mockResource, times(1)).exists(); + verifyNoMoreInteractions(mockResource); + } + + @Test + public void isQualifiedWithNullResource() { + + AbstractCacheResourceResolver resourceResolver = + new TestResourceCapableCacheDataImporterExporter().new TestCacheResourceResolver(); + + assertThat(resourceResolver.isQualified(null)).isFalse(); + } + + @Test + public void getFullyQualifiedResourceLocationCallsGetResourcePathAndGetResourceNameWithRegion() { + + Region mockRegion = mock(Region.class); + + AbstractCacheResourceResolver resourceResolver = + spy(new TestResourceCapableCacheDataImporterExporter().new TestCacheResourceResolver()); + + doReturn("/path/to/").when(resourceResolver).getResourcePath(); + doReturn("resource.xml").when(resourceResolver).getResourceName(eq(mockRegion)); + + assertThat(resourceResolver.getFullyQualifiedResourceLocation(mockRegion)).isEqualTo("/path/to/resource.xml"); + + verify(resourceResolver, times(1)).getResourcePath(); + verify(resourceResolver, times(1)).getResourceName(eq(mockRegion)); + } + + @Test + public void getResourceLocationIsEvaluated() { + + String testPropertyName = "test.property.name"; + String rawPropertyValue = "https://skullbox:8181/nurv/cache/%s/data/resource.json"; + String evaluatedPropertyValue = "https://skullbox:8181/nurv/cache/example/data/resource.json"; + + Region mockRegion = mock(Region.class); + + doReturn("Example").when(mockRegion).getName(); + + Environment mockEnvironment = mock(Environment.class); + + doReturn(true).when(mockEnvironment).containsProperty(eq(testPropertyName)); + doReturn(rawPropertyValue).when(mockEnvironment).getProperty(eq(testPropertyName)); + + TestResourceCapableCacheDataImporterExporter importerExporter = + spy(new TestResourceCapableCacheDataImporterExporter()); + + AbstractCacheResourceResolver resourceResolver = spy(importerExporter.new TestCacheResourceResolver()); + + doReturn(Optional.of(mockEnvironment)).when(importerExporter).getEnvironment(); + + doAnswer(invocation -> { + + String expression = invocation.getArgument(0); + Region region = invocation.getArgument(1); + + return String.format(expression, region.getName().toLowerCase()); + + }).when(resourceResolver).evaluate(anyString(), any(Region.class)); + + assertThat(resourceResolver.getResourceLocation(mockRegion, testPropertyName)) + .isEqualTo(evaluatedPropertyValue); + + verify(mockEnvironment, times(1)).containsProperty(eq(testPropertyName)); + verify(mockEnvironment, times(1)).getProperty(eq(testPropertyName)); + verify(mockRegion, times(1)).getName(); + verify(importerExporter, times(1)).getEnvironment(); + verify(resourceResolver, times(1)).evaluate(eq(rawPropertyValue), eq(mockRegion)); + verifyNoMoreInteractions(mockEnvironment, mockRegion); + } + + @Test + public void getResourceLocationCallsGetFullyQualifiedResourceLocationWhenEnvironmentIsNull() { + + Region mockRegion = mock(Region.class); + + TestResourceCapableCacheDataImporterExporter importerExporter = + spy(new TestResourceCapableCacheDataImporterExporter()); + + AbstractCacheResourceResolver resourceResolver = spy(importerExporter.new TestCacheResourceResolver()); + + doReturn(Optional.empty()).when(importerExporter).getEnvironment(); + doReturn("/path/to/resource.xml").when(resourceResolver) + .getFullyQualifiedResourceLocation(eq(mockRegion)); + + assertThat(resourceResolver.getResourceLocation(mockRegion, "test.property.name")) + .isEqualTo("/path/to/resource.xml"); + + verify(importerExporter, times(1)).getEnvironment(); + verify(resourceResolver, never()).evaluate(anyString(), any(Region.class)); + verifyNoInteractions(mockRegion); + } + + @Test + public void getResourceLocationCallsGetFullyQualifiedResourceLocationWhenEnvironmentDoesNotContainProperty() { + + Region mockRegion = mock(Region.class); + + Environment mockEnvironment = mock(Environment.class); + + doReturn(false).when(mockEnvironment).containsProperty(anyString()); + + TestResourceCapableCacheDataImporterExporter importerExporter = + spy(new TestResourceCapableCacheDataImporterExporter()); + + AbstractCacheResourceResolver resourceResolver = spy(importerExporter.new TestCacheResourceResolver()); + + doReturn(Optional.of(mockEnvironment)).when(importerExporter).getEnvironment(); + doReturn("/path/to/resource.bin").when(resourceResolver) + .getFullyQualifiedResourceLocation(eq(mockRegion)); + + assertThat(resourceResolver.getResourceLocation(mockRegion, "test.property.name")) + .isEqualTo("/path/to/resource.bin"); + + verify(importerExporter, times(1)).getEnvironment(); + verify(resourceResolver, never()).evaluate(anyString(), any(Region.class)); + verify(mockEnvironment, times(1)).containsProperty(eq("test.property.name")); + verifyNoMoreInteractions(mockEnvironment); + verifyNoInteractions(mockRegion); + } + + @Test + public void getResourceLocationCallsGetFullyQualifiedResourceLocationWhenEnvironmentPropertyIsNotSet() { + + Region mockRegion = mock(Region.class); + + Environment mockEnvironment = mock(Environment.class); + + doReturn(true).when(mockEnvironment).containsProperty(eq("test.property.name")); + doReturn(null).when(mockEnvironment).getProperty(anyString()); + + TestResourceCapableCacheDataImporterExporter importerExporter = + spy(new TestResourceCapableCacheDataImporterExporter()); + + AbstractCacheResourceResolver resourceResolver = spy(importerExporter.new TestCacheResourceResolver()); + + doReturn(Optional.of(mockEnvironment)).when(importerExporter).getEnvironment(); + doReturn("/path/to/resource.dat").when(resourceResolver) + .getFullyQualifiedResourceLocation(eq(mockRegion)); + + assertThat(resourceResolver.getResourceLocation(mockRegion, "test.property.name")) + .isEqualTo("/path/to/resource.dat"); + + verify(importerExporter, times(1)).getEnvironment(); + verify(resourceResolver, never()).evaluate(anyString(), any(Region.class)); + verify(mockEnvironment, times(1)).containsProperty(eq("test.property.name")); + verify(mockEnvironment, times(1)).getProperty(eq("test.property.name")); + verifyNoMoreInteractions(mockEnvironment); + verifyNoInteractions(mockRegion); + } + + @Test(expected = IllegalArgumentException.class) + public void getResourceLocationWithNullRegion() { + + AbstractCacheResourceResolver resourceResolver = + new TestResourceCapableCacheDataImporterExporter().new TestCacheResourceResolver(); + + try { + resourceResolver.getResourceLocation(null, "test.property.name"); + } + catch (IllegalArgumentException expected) { + + assertThat(expected).hasMessage("Region must not be null"); + assertThat(expected).hasNoCause(); + + throw expected; + } + } + + @Test(expected = IllegalArgumentException.class) + public void getResourceLocationWithNoPropertyName() { + + Region mockRegion = mock(Region.class); + + AbstractCacheResourceResolver resourceResolver = + new TestResourceCapableCacheDataImporterExporter().new TestCacheResourceResolver(); + + try { + resourceResolver.getResourceLocation(mockRegion, " "); + } + catch (IllegalArgumentException expected) { + + assertThat(expected).hasMessage("Property name [ ] must be specified"); + assertThat(expected).hasNoCause(); + + throw expected; + } + finally { + verifyNoInteractions(mockRegion); + } + } + + @Test + public void getResourceNameFromRegion() { + + Region mockRegion = mock(Region.class); + + doReturn("Example").when(mockRegion).getName(); + + AbstractCacheResourceResolver resourceResolver = + new TestResourceCapableCacheDataImporterExporter().new TestCacheResourceResolver(); + + assertThat(resourceResolver.getResourceName(mockRegion)) + .isEqualTo(String.format(ResourceCapableCacheDataImporterExporter.RESOURCE_NAME_PATTERN, "example")); + + verify(mockRegion, times(1)).getName(); + verifyNoMoreInteractions(mockRegion); + } + + @Test + public void getResourceNameFromString() { + + AbstractCacheResourceResolver resourceResolver = + new TestResourceCapableCacheDataImporterExporter().new TestCacheResourceResolver(); + + assertThat(resourceResolver.getResourceName("EXAMPLE")) + .isEqualTo(String.format(ResourceCapableCacheDataImporterExporter.RESOURCE_NAME_PATTERN, "EXAMPLE")); + } + + @Test + @SuppressWarnings("all") + public void evaluatesExpressionContainingBeanAndPropertyAndRegionReferences() { + + ApplicationContext mockApplicationContext = mock(ApplicationContext.class); + + Environment mockEnvironment = mock(Environment.class); + + Map environmentProperties = MapBuilder.newMapBuilder() + .put("port", "8181") + .put("user.password", "s3c3rt") + .put("user.name", System.getProperty("user.name", "jonDoe")) + .build(); + + Region mockRegion = mock(Region.class); + + doReturn(true).when(mockApplicationContext).containsBean("utility"); + doReturn(UtilityBean.INSTANCE).when(mockApplicationContext).getBean("utility"); + doReturn(mockEnvironment).when(mockApplicationContext).getEnvironment(); + doAnswer(invocation -> environmentProperties.containsKey(invocation.getArgument(0))) + .when(mockEnvironment).containsProperty(any()); + doAnswer(invocation -> environmentProperties.get(invocation.getArgument(0))) + .when(mockEnvironment).getProperty(any()); + doReturn("Example").when(mockRegion).getName(); + + TestResourceCapableCacheDataImporterExporter importerExporter = + mock(TestResourceCapableCacheDataImporterExporter.class); + + doReturn(Optional.ofNullable(mockApplicationContext)).when(importerExporter).getApplicationContext(); + doReturn(Optional.ofNullable(mockEnvironment)).when(importerExporter).getEnvironment(); + + AbstractCacheResourceResolver resourceResolver = importerExporter.new TestCacheResourceResolver(); + + String expression = "https://#{#env['user.name']}:#{#env['user.password']}@skullbox:#{port}/nurv/cache/#{utility.toUpperCase(#regionName)}/data/import"; + String parsedExpression = String.format("https://%s:s3c3rt@skullbox:8181/nurv/cache/EXAMPLE/data/import", + environmentProperties.get("user.name")); + + assertThat(resourceResolver.evaluate(expression, mockRegion)).isEqualTo(parsedExpression); + + verify(mockApplicationContext, times(1)).containsBean(eq("utility")); + verify(mockApplicationContext, times(1)).getBean(eq("utility")); + verify(mockApplicationContext, times(2)).getEnvironment(); + verify(mockEnvironment, times(1)).containsProperty(eq("port")); + verify(mockEnvironment, times(1)).getProperty(eq("port")); + verify(mockEnvironment, times(1)).getProperty(eq("user.password")); + verify(mockEnvironment, times(1)).getProperty(eq("user.name")); + verify(mockRegion, times(1)).getName(); + verifyNoMoreInteractions(mockEnvironment, mockRegion); + } + + @Test + public void resolveExportResource() { + + Logger mockLogger = mock(Logger.class); + + Region mockRegion = mock(Region.class); + + doReturn("/Example").when(mockRegion).getFullPath(); + + ResourceLoader mockResourceLoader = mock(ResourceLoader.class); + + WritableResource mockResource = mock(WritableResource.class); + + doReturn(mockResource).when(mockResourceLoader).getResource(eq("/path/to/resource.xml")); + doReturn(false).when(mockResource).exists(); + doReturn(false).when(mockResource).isWritable(); + + TestResourceCapableCacheDataImporterExporter importerExporter = + spy(new TestResourceCapableCacheDataImporterExporter()); + + AbstractExportResourceResolver exportResourceResolver = spy(importerExporter.new TestExportResourceResolver()); + + exportResourceResolver.setResourceLoader(mockResourceLoader); + + doReturn(mockLogger).when(importerExporter).getLogger(); + + doReturn("/path/to/resource.xml") + .when(exportResourceResolver).getResourceLocation(eq(mockRegion), + eq(ResourceCapableCacheDataImporterExporter.CACHE_DATA_EXPORT_RESOURCE_LOCATION_PROPERTY_NAME)); + + //doReturn(Optional.of(mockResource)).when(exportResourceResolver).resolve(eq("/path/to/resource.xml")); + + assertThat(exportResourceResolver.resolve(mockRegion)).isEqualTo(Optional.of(mockResource)); + + verify(importerExporter, times(2)).getLogger(); + verify(exportResourceResolver, times(1)).getResourceLocation(eq(mockRegion), + eq(ResourceCapableCacheDataImporterExporter.CACHE_DATA_EXPORT_RESOURCE_LOCATION_PROPERTY_NAME)); + verify(exportResourceResolver, times(1)).isQualified(eq(mockResource)); + verify(exportResourceResolver, times(1)) + .onMissingResource(eq(mockResource), eq("/path/to/resource.xml")); + verify(exportResourceResolver, times(1)).resolve(eq("/path/to/resource.xml")); + verify(mockResourceLoader, times(1)).getResource(eq("/path/to/resource.xml")); + verify(mockResource, times(1)).exists(); + verify(mockResource, times(1)).isWritable(); + verify(mockLogger, times(1)) + .warn(eq("WARNING! Resource at location [{}] does not exist; will try to create it"), + eq("/path/to/resource.xml")); + verify(mockLogger, times(1)) + .warn(eq("WARNING! Resource [{}] for Region [{}] is not writable"), eq("/path/to/resource.xml"), + eq("/Example")); + verify(mockRegion, times(1)).getFullPath(); + verifyNoMoreInteractions(mockLogger, mockRegion, mockResource, mockResourceLoader); + } + + @Test(expected = IllegalArgumentException.class) + public void resolveExportResourceWithNullRegionThrowsIllegalArgumentException() { + + AbstractExportResourceResolver exportResourceResolver = + spy(new TestResourceCapableCacheDataImporterExporter().new TestExportResourceResolver()); + + try { + exportResourceResolver.resolve((Region) null); + } + catch (IllegalArgumentException expected) { + + assertThat(expected).hasMessage("Region must not be null"); + assertThat(expected).hasNoCause(); + + throw expected; + } + } + + @Test + public void fileSystemExportResourceResolverResolvesToFileSystemPath() { + + AbstractExportResourceResolver exportResourceResolver = + spy(new TestResourceCapableCacheDataImporterExporter().new FileSystemExportResourceResolver()); + + assertThat(exportResourceResolver.getResourcePath()) + .isEqualTo(String.format("file://%s", System.getProperty("user.dir"))); + } + + @Test + public void resolveImportResource() { + + Region mockRegion = mock(Region.class); + + Resource mockResource = mock(Resource.class); + + doReturn(true).when(mockResource).isReadable(); + + AbstractImportResourceResolver importResourceResolver = + spy(new TestResourceCapableCacheDataImporterExporter().new TestImportResourceResolver()); + + doReturn("/path/to/resource.json") + .when(importResourceResolver).getResourceLocation(eq(mockRegion), + eq(ResourceCapableCacheDataImporterExporter.CACHE_DATA_IMPORT_RESOURCE_LOCATION_PROPERTY_NAME)); + + doReturn(Optional.of(mockResource)).when(importResourceResolver).resolve(eq("/path/to/resource.json")); + + assertThat(importResourceResolver.resolve(mockRegion)).isEqualTo(Optional.of(mockResource)); + + verify(importResourceResolver, times(1)).getResourceLocation(eq(mockRegion), + eq(ResourceCapableCacheDataImporterExporter.CACHE_DATA_IMPORT_RESOURCE_LOCATION_PROPERTY_NAME)); + verify(importResourceResolver, times(1)).resolve(eq("/path/to/resource.json")); + verify(mockResource, times(1)).isReadable(); + verifyNoMoreInteractions(mockRegion); + verifyNoInteractions(mockRegion); + } + + @Test(expected = IllegalStateException.class) + public void resolveImportResourceWhenResourceIsNotPresentThrowsIllegalStateException() { + + Region mockRegion = mock(Region.class); + + doReturn("/Example").when(mockRegion).getFullPath(); + + AbstractImportResourceResolver importResourceResolver = + spy(new TestResourceCapableCacheDataImporterExporter().new TestImportResourceResolver()); + + doReturn("/path/to/resource.json") + .when(importResourceResolver).getResourceLocation(eq(mockRegion), + eq(ResourceCapableCacheDataImporterExporter.CACHE_DATA_IMPORT_RESOURCE_LOCATION_PROPERTY_NAME)); + + doReturn(Optional.empty()).when(importResourceResolver).resolve(eq("/path/to/resource.json")); + + try { + importResourceResolver.resolve(mockRegion); + } + catch (IllegalStateException expected) { + + assertThat(expected).hasMessage("Resource [/path/to/resource.json] for Region [/Example] does not exist"); + assertThat(expected).hasNoCause(); + + throw expected; + } + finally { + verify(importResourceResolver, times(1)).getResourceLocation(eq(mockRegion), + eq(ResourceCapableCacheDataImporterExporter.CACHE_DATA_IMPORT_RESOURCE_LOCATION_PROPERTY_NAME)); + verify(importResourceResolver, times(1)).resolve(eq("/path/to/resource.json")); + verify(mockRegion, times(1)).getFullPath(); + verifyNoMoreInteractions(mockRegion); + } + } + + @Test(expected = IllegalStateException.class) + public void resolveImportResourceWhenResourceIsNotReadableThrowsIllegalStateException() { + + Region mockRegion = mock(Region.class); + + doReturn("/Example").when(mockRegion).getFullPath(); + + Resource mockResource = mock(Resource.class); + + doReturn(false).when(mockResource).isReadable(); + + AbstractImportResourceResolver importResourceResolver = + spy(new TestResourceCapableCacheDataImporterExporter().new TestImportResourceResolver()); + + doReturn("/path/to/resource.json") + .when(importResourceResolver).getResourceLocation(eq(mockRegion), + eq(ResourceCapableCacheDataImporterExporter.CACHE_DATA_IMPORT_RESOURCE_LOCATION_PROPERTY_NAME)); + + doReturn(Optional.of(mockResource)).when(importResourceResolver).resolve(eq("/path/to/resource.json")); + + try { + importResourceResolver.resolve(mockRegion); + } + catch (IllegalStateException expected) { + + assertThat(expected).hasMessage("Resource [/path/to/resource.json] for Region [/Example] is not readable"); + assertThat(expected).hasNoCause(); + + throw expected; + } + finally { + verify(importResourceResolver, times(1)).getResourceLocation(eq(mockRegion), + eq(ResourceCapableCacheDataImporterExporter.CACHE_DATA_IMPORT_RESOURCE_LOCATION_PROPERTY_NAME)); + verify(importResourceResolver, times(1)).resolve(eq("/path/to/resource.json")); + verify(mockResource, times(1)).isReadable(); + verify(mockRegion, times(1)).getFullPath(); + verifyNoMoreInteractions(mockRegion, mockResource); + } + } + + @Test(expected = IllegalArgumentException.class) + public void resolveImportResourceWithNullRegionThrowsIllegalArgumentException() { + + AbstractImportResourceResolver importResourceResolver = + spy(new TestResourceCapableCacheDataImporterExporter().new TestImportResourceResolver()); + + try { + importResourceResolver.resolve((Region) null); + } + catch (IllegalArgumentException expected) { + + assertThat(expected).hasMessage("Region must not be null"); + assertThat(expected).hasNoCause(); + + throw expected; + } + } + + @Test + public void classPathImportResourceResolverResolvesToClassPath() { + + AbstractImportResourceResolver importResourceResolver = + spy(new TestResourceCapableCacheDataImporterExporter().new ClassPathImportResourceResolver()); + + assertThat(importResourceResolver.getResourcePath()) + .isEqualTo(ResourcePrefix.CLASSPATH_URL_PREFIX.toUrlPrefix()); + } + + interface ResourceLoaderAwareExportResourceResolver extends ResourceLoaderAware, ExportResourceResolver { } + + interface ResourceLoaderAwareImportResourceResolver extends ResourceLoaderAware, ImportResourceResolver { } + + @SuppressWarnings("rawtypes") + static class TestResourceCapableCacheDataImporterExporter extends ResourceCapableCacheDataImporterExporter { + + @Override + protected Optional getApplicationContext() { + return super.getApplicationContext(); + } + + @Override + protected Optional getEnvironment() { + return super.getEnvironment(); + } + + @Override + protected Logger getLogger() { + return super.getLogger(); + } + + @Override + protected @NonNull Region doExportFrom(@NonNull Region region) { + return region; + } + + @Override + protected @NonNull Region doImportInto(@NonNull Region region) { + return region; + } + + class TestCacheResourceResolver extends AbstractCacheResourceResolver { + + @Override + public Optional resolve(@NonNull Region region) { + return Optional.empty(); + } + + @NonNull @Override + protected String getResourcePath() { + return null; + } + } + + class TestExportResourceResolver extends AbstractExportResourceResolver { + + @NonNull @Override + protected String getResourcePath() { + return null; + } + } + + class TestImportResourceResolver extends AbstractImportResourceResolver { + + @NonNull @Override + protected String getResourcePath() { + return null; + } + } + } + + @SuppressWarnings("unused") + public static class UtilityBean { + + public static UtilityBean INSTANCE = new UtilityBean(); + + public String toUpperCase(String value) { + return value.toUpperCase(); + } + } +}