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.
This commit is contained in:
John Blum
2020-07-10 16:05:25 -07:00
parent 9a77049a76
commit 681fd81ad2
2 changed files with 1320 additions and 0 deletions

View File

@@ -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<ResourceLoader> 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<Resource> 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<String, Expression> 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<Resource> 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> 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<Resource> 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> 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();
}
}
}

View File

@@ -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> 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<String, String> environmentProperties = MapBuilder.<String, String>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<ApplicationContext> getApplicationContext() {
return super.getApplicationContext();
}
@Override
protected Optional<Environment> 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<Resource> 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();
}
}
}