Complete base implementation of the JSON CacheDataImporterExporter.

Resolves gh-67.
This commit is contained in:
John Blum
2020-05-05 02:27:11 -07:00
parent ec20642cc1
commit c3b697e759
3 changed files with 999 additions and 22 deletions

View File

@@ -116,7 +116,7 @@ public abstract class AbstractCacheDataImporterExporter
* @see org.springframework.context.ApplicationContext
* @see java.util.Optional
*/
protected Optional<ApplicationContext> getApplicationContext() {
public Optional<ApplicationContext> getApplicationContext() {
return Optional.ofNullable(this.applicationContext);
}

View File

@@ -17,24 +17,34 @@ package org.springframework.geode.data.json;
import static org.springframework.data.gemfire.util.RuntimeExceptionFactory.newIllegalStateException;
import java.io.BufferedWriter;
import java.io.ByteArrayOutputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.Writer;
import java.util.Optional;
import org.apache.geode.cache.Region;
import org.apache.geode.pdx.JSONFormatter;
import org.apache.geode.pdx.PdxInstance;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import org.springframework.dao.DataAccessResourceFailureException;
import org.springframework.data.gemfire.util.CollectionUtils;
import org.springframework.geode.data.AbstractCacheDataImporterExporter;
import org.springframework.geode.data.CacheDataExporter;
import org.springframework.geode.data.CacheDataImporter;
import org.springframework.geode.data.json.converter.JsonToPdxConverter;
import org.springframework.geode.data.json.converter.ObjectToJsonConverter;
import org.springframework.geode.data.json.support.JSONFormatterJsonToPdxConverter;
import org.springframework.geode.data.json.support.JSONFormatterPdxToJsonConverter;
import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;
import org.springframework.stereotype.Component;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/**
* The {@link JsonCacheDataImporterExporter} class is a {@link CacheDataImporter} and {@link CacheDataExporter}
@@ -42,10 +52,15 @@ import org.springframework.util.Assert;
*
* @author John Blum
* @see org.apache.geode.cache.Region
* @see org.apache.geode.pdx.PdxInstance
* @see org.springframework.core.io.ClassPathResource
* @see org.springframework.core.io.Resource
* @see org.springframework.core.io.ResourceLoader
* @see org.springframework.geode.data.AbstractCacheDataImporterExporter
* @see org.springframework.geode.data.CacheDataExporter
* @see org.springframework.geode.data.CacheDataImporter
* @see org.springframework.geode.data.json.converter.JsonToPdxConverter
* @see org.springframework.geode.data.json.converter.ObjectToJsonConverter
* @see org.springframework.stereotype.Component
* @since 1.3.0
*/
@@ -53,46 +68,154 @@ import org.springframework.util.Assert;
@SuppressWarnings("rawtypes")
public class JsonCacheDataImporterExporter extends AbstractCacheDataImporterExporter {
private static final int CONTENT_PREVIEW_LENGTH = 50;
private static final int DEFAULT_BUFFER_SIZE = 32768;
protected static final String CLASSPATH_RESOURCE_PREFIX = ResourceLoader.CLASSPATH_URL_PREFIX;
protected static final String FILESYSTEM_RESOURCE_PREFIX = "file://";
protected static final String ID_FIELD_NAME = "id";
protected static final String RESOURCE_NAME_PATTERN = "data-%s.json";
private JsonToPdxConverter toPdxConverter = newJsonToPdxConverter();
private ObjectToJsonConverter toJsonConverter = newObjectToJsonConverter();
// TODO configure via an SPI
private @NonNull JsonToPdxConverter newJsonToPdxConverter() {
return new JSONFormatterJsonToPdxConverter();
}
// TODO configure via an SPI
private @NonNull ObjectToJsonConverter newObjectToJsonConverter() {
return new JSONFormatterPdxToJsonConverter();
}
/**
* Gets a reference to the configured {@link JsonToPdxConverter}.
*
* @return a reference to the configured {@link JsonToPdxConverter}.
* @see org.springframework.geode.data.json.converter.JsonToPdxConverter
*/
protected @NonNull JsonToPdxConverter getJsonToPdxConverter() {
return this.toPdxConverter;
}
/**
* Gets a reference to the configured {@link ObjectToJsonConverter}.
*
* @return a reference to the configured {@link ObjectToJsonConverter}.
* @see org.springframework.geode.data.json.converter.ObjectToJsonConverter
*/
protected @NonNull ObjectToJsonConverter getObjectToJsonConverter() {
return this.toJsonConverter;
}
/**
* @inheritDoc
*/
@NonNull @Override
public Region doExportFrom(@NonNull Region region) {
Assert.notNull(region, "Region must not be null");
String regionName = region.getName();
String json = toJson(region);
getLogger().debug("Saving JSON [{}] from Region [{}]", json, region.getName());
getResource(region, getResourceLocation())
.ifPresent(resource -> save(json, resource));
return region;
}
/**
* Saves the given {@link String content} (e.g. {@literal JSON}) to the specified {@link Resource}.
*
* @param content {@link String} containing the content to save.
* @param resource {@link Resource} to save the {@link String content} to; must not be {@literal null}.
* @throws IllegalArgumentException if {@link Resource} is {@literal null}.
* @see org.springframework.core.io.Resource
*/
protected void save(@Nullable String content, @NonNull Resource resource) {
Assert.notNull(resource, "Resource must not be null");
if (StringUtils.hasText(content)) {
try (Writer writer = newWriter(resource)) {
writer.write(content, 0, content.length());
writer.flush();
}
catch (IOException cause) {
String message = String.format("Failed to save content '%s' to Resource [%s]",
formatContentForPreview(content), resource.getDescription());
throw new DataAccessResourceFailureException(message, cause);
}
}
}
@NonNull Writer newWriter(@NonNull Resource resource) throws IOException {
return new BufferedWriter(new FileWriter(resource.getFile(), false));
}
@Nullable String formatContentForPreview(@Nullable String content) {
if (StringUtils.hasText(content)) {
int length = Math.min(content.length(), CONTENT_PREVIEW_LENGTH);
content = content.substring(0, length).concat(length < content.length() ? "..." : "");
}
return content;
}
/**
* @inheritDoc
*/
@NonNull @Override @SuppressWarnings("unchecked")
public Region doImportInto(@NonNull Region region) {
Assert.notNull(region, "Region must not be null");
// TODO: handle a JSON array of objects and PdxInstances!!!
getResource(region)
getResource(region, CLASSPATH_RESOURCE_PREFIX)
.filter(Resource::exists)
.map(this::getContent)
.map(this::toPdxInstance)
.map(this::toPdx)
.ifPresent(pdxInstance -> region.put(getIdentifier(pdxInstance), pdxInstance));
return region;
}
private @NonNull byte[] getContent(@NonNull Resource resource) {
/**
* Gets the content of the given {@link Resource} as an array of {@literal Byte#TYPE bytes}.
*
* @param resource {@link Resource} from which to load the content; must not be {@literal null}.
* @return an array of {@link Byte#TYPE bytes} containing the contents of the given {@link Resource}.
* @throws IllegalArgumentException if {@link Resource} is {@literal null}.
* @throws DataAccessResourceFailureException if an {@link IOException} occurs while reading from
* and loading the contents of the given {@link Resource}.
* @see org.springframework.core.io.Resource
*/
protected @NonNull byte[] getContent(@NonNull Resource resource) {
Assert.notNull(resource, "Resource must not be null");
try (InputStream in = resource.getInputStream()){
ByteArrayOutputStream out = new ByteArrayOutputStream(in.available());
byte[] buffer = new byte[32768];
byte[] buffer = new byte[DEFAULT_BUFFER_SIZE];
for (int bytesRead = in.read(buffer); bytesRead != -1; bytesRead = in.read(buffer)) {
out.write(buffer, 0, bytesRead);
out.flush();
}
out.flush();
out.close();
return out.toByteArray();
}
catch (IOException cause) {
@@ -101,27 +224,139 @@ public class JsonCacheDataImporterExporter extends AbstractCacheDataImporterExpo
}
}
private Optional<Resource> getResource(@NonNull Region region) {
/**
* Determines the {@link Object identifier} for the given {@link PdxInstance}.
*
* @param pdxInstance {@link PdxInstance} to evaluate; must not be {@literal null}.
* @return the {@link Object identifier} for the given {@link PdxInstance}.
* @throws IllegalArgumentException if {@link PdxInstance} is {@literal null}.
* @throws IllegalStateException if the {@link Object identifier} for the {@link PdxInstance}
* cannot be determined.
* @see org.apache.geode.pdx.PdxInstance
* @see #getIdField(PdxInstance)
*/
protected @NonNull Object getIdentifier(@NonNull PdxInstance pdxInstance) {
Assert.notNull(pdxInstance, "PdxInstance must not be null");
Optional<String> idFieldName = CollectionUtils.nullSafeList(pdxInstance.getFieldNames()).stream()
.filter(StringUtils::hasText)
.filter(pdxInstance::isIdentityField)
.findFirst();
return idFieldName
.map(pdxInstance::getField)
.orElseGet(() -> getIdField(pdxInstance));
}
/**
* Searches for a PDX {@link String field name} on the {@link PdxInstance} named {@literal id} and returns
* its value as the {@link Object identifier} for the {@link PdxInstance}.
*
* @param pdxInstance {@link PdxInstance} to evaluate; must not be {@literal null}.
* @return the value of the {@literal id} {@link String field name} of the {@link PdxInstance} if present.
* @throws IllegalStateException if the {@link PdxInstance} does not have an {@literal id}
* {@link String field name}.
* @see org.apache.geode.pdx.PdxInstance
* @see #getIdentifier(PdxInstance)
*/
protected @NonNull Object getIdField(@NonNull PdxInstance pdxInstance) {
Assert.notNull(pdxInstance, "PdxInstance must not be null");
return Optional.of(pdxInstance)
.filter(it -> it.hasField(ID_FIELD_NAME))
.map(it -> it.getField(ID_FIELD_NAME))
.orElseThrow(() -> newIllegalStateException("PdxInstance for type [%1$s] has no %2$s",
pdxInstance.getClassName(), pdxInstance.hasField(ID_FIELD_NAME) ? "id" : "declared identity field"));
}
/**
* Returns a computed {@literal JSON} {@link Resource} containing {@literal JSON} data that will be loaded into
* the given a {@link Region}.
*
* @param region target {@link Region} used to compute the {@literal JSON} {@link Resource} to load.
* @return an {@link Optional} {@literal JSON} {@link Resource} containing {@literal JSON} data
* to load into the given {@link Region}.
* @see org.springframework.core.io.Resource
* @see org.apache.geode.cache.Region
* @see java.util.Optional
*/
protected Optional<Resource> getResource(@NonNull Region region, @Nullable String resourcePrefix) {
Assert.notNull(region, "Region must not be null");
String regionName = region.getName().toLowerCase();
String resourceName = String.format(RESOURCE_NAME_PATTERN, regionName);
String resolvedResourcePrefix = StringUtils.hasText(resourcePrefix)
? resourcePrefix
: CLASSPATH_RESOURCE_PREFIX;
return getApplicationContext()
.map(it -> it.getResource(String.format("classpath:%s", resourceName)));
Resource resource = getApplicationContext()
.map(it -> it.getResource(String.format("%1$s%2$s", resolvedResourcePrefix, resourceName)))
.orElseGet((() -> new ClassPathResource(resourceName)));
return Optional.of(resource);
}
protected @NonNull Object getIdentifier(@NonNull PdxInstance pdxInstance) {
String idField = CollectionUtils.nullSafeList(pdxInstance.getFieldNames()).stream()
.filter(pdxInstance::isIdentityField)
.findFirst()
.orElseThrow(() -> newIllegalStateException("PdxInstance for type [%s] has no declared identify field",
pdxInstance.getClassName()));
return pdxInstance.getField(idField);
/**
* Returns the {@link String file system path} specifying the location for where to export the {@link Resource}.
*
* @return the {@link String file system path} specifying the location for where to export the {@link Resource}.
*/
protected @NonNull String getResourceLocation() {
return String.format("%1$s%2$s", FILESYSTEM_RESOURCE_PREFIX, System.getProperty("user.dir"));
}
protected PdxInstance toPdxInstance(byte[] json) {
return JSONFormatter.fromJSON(json);
/**
* Convert {@link Object values} contained in the {@link Region} to {@literal JSON}.
*
* @param region {@link Region} to process; must not be {@literal null}.
* @return a {@literal JSON} {@link String} containing the {@link Object values}
* from the given {@link Region}.
* @see org.apache.geode.cache.Region
* @see #toJson(Object)
*/
@SuppressWarnings("unchecked")
protected @NonNull String toJson(@NonNull Region region) {
Assert.notNull(region, "Region must not be null");
StringBuilder json = new StringBuilder("[");
boolean addComma = false;
for (Object value : CollectionUtils.nullSafeCollection(region.values())) {
json.append(addComma ? ", " : "");
json.append(toJson(value));
addComma = true;
}
json.append("]");
return json.toString();
}
/**
* Converts the given {@link Object} into a {@literal JSON} {@link String}.
*
* @param source {@link Object} to convert into {@literal JSON}.
* @return a {@link String} containing the {@literal JSON} generated from the given {@link Object}.
* @see #getObjectToJsonConverter()
*/
protected @NonNull String toJson(@NonNull Object source) {
return getObjectToJsonConverter().convert(source);
}
/**
* Converts the array of {@literal JSON} {@link Byte#TYPE bytes} into a {@link PdxInstance}.
*
* @param json array of {@link Byte#TYPE} containing {@literal JSON} data.
* @return a {@link PdxInstance} converted from the {@literal JSON} {@link Byte#TYPE bytes}.
* @see org.apache.geode.pdx.PdxInstance
* @see #getJsonToPdxConverter()
*/
protected @NonNull PdxInstance toPdx(@NonNull byte[] json) {
return getJsonToPdxConverter().convert(json);
}
}