Refactor JSONFormatterPdxToJsonConverter to handle missing type metadata in both PdxInstances and in JSON, which is required to reconstitute the Object.

Resolves gh-67.
This commit is contained in:
John Blum
2020-05-10 13:23:47 -07:00
parent 30a84eefdd
commit f40685fb4e
2 changed files with 648 additions and 2 deletions

View File

@@ -17,22 +17,37 @@ package org.springframework.geode.data.json.converter.support;
import java.util.Optional;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.apache.geode.pdx.JSONFormatter;
import org.apache.geode.pdx.PdxInstance;
import org.apache.geode.pdx.WritablePdxInstance;
import org.springframework.dao.DataRetrievalFailureException;
import org.springframework.geode.data.json.converter.ObjectToJsonConverter;
import org.springframework.geode.pdx.PdxInstanceBuilder;
import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/**
* An {@link ObjectToJsonConverter} implementation using the Apache Geode {@link JSONFormatter} to convert
* from a {@link PdxInstance} to a {@literal JSON} {@link String}.
*
* @author John Blum
* @see com.fasterxml.jackson.databind.JsonNode
* @see com.fasterxml.jackson.databind.ObjectMapper
* @see com.fasterxml.jackson.databind.node.ObjectNode
* @see org.apache.geode.pdx.JSONFormatter
* @see org.apache.geode.pdx.PdxInstance
* @see org.apache.geode.pdx.WritablePdxInstance
* @see org.springframework.geode.data.json.converter.ObjectToJsonConverter
* @see org.springframework.geode.data.json.converter.support.JacksonObjectToJsonConverter
* @see org.springframework.geode.pdx.PdxInstanceBuilder
* @since 1.3.0
*/
public class JSONFormatterPdxToJsonConverter extends JacksonObjectToJsonConverter {
@@ -70,6 +85,165 @@ public class JSONFormatterPdxToJsonConverter extends JacksonObjectToJsonConverte
* @see org.apache.geode.pdx.PdxInstance
*/
protected @NonNull String convertPdxToJson(@NonNull PdxInstance pdxInstance) {
//return JSONFormatter.toJSON(pdxInstance);
//return JSONFormatter.toJSON(decorate(pdxInstance));
return decorate(pdxInstance, jsonFormatterToJson(pdxInstance));
}
@NonNull String jsonFormatterToJson(@NonNull PdxInstance pdxInstance) {
return JSONFormatter.toJSON(pdxInstance);
}
/**
* WARNING!!!
*
* First, this method might be less than optimal and could lead to PDX type explosion!
*
* Second, this {@code pdxInstance.createWriter().setField(AT_TYPE_METADATA_PROPERTY_NAME, className);} ...
*
* Throws:
* org.apache.geode.pdx.PdxFieldDoesNotExistException: A field named @type does not exist on ...
* PdxType[dsid=0,typenum=7232261,name=example.app.crm.model.Customer,fields=[id:long:identity:0:idx0(relativeOffset)=0:idx1(vlfOffsetIndex)=0, name:String:1:idx0(relativeOffset)=8:idx1(vlfOffsetIndex)=-1,]]
* at org.apache.geode.pdx.internal.WritablePdxInstanceImpl.setField(WritablePdxInstanceImpl.java:119)
* ...
*
* This code needs to create a {@literal new} {@link PdxInstance} from an existing {@link PdxInstance}
* or add the new (PDX) field to the PDX type metadata using {@code PdxType.addField(:PdxField)} before
* setting the new field on the {@link PdxInstance} using the {@link WritablePdxInstance}. Unfortunately,
* {@code PdxType} is part of the internal API and updating and ditributing a {@code PdxType} is complicated,
* requiring a Distributed Lock, among other responsibilities.
*/
@SuppressWarnings("unused")
protected @NonNull PdxInstance decorate(@NonNull PdxInstance pdxInstance) {
if (isMissingObjectTypeMetadata(pdxInstance)) {
String pdxInstanceClassName = pdxInstance.getClassName();
Assert.isTrue(hasValidClassName(pdxInstance), () ->
String.format("Class name [%s] is required and cannot be equal to [%s]",
pdxInstanceClassName, JSONFormatter.JSON_CLASSNAME));
pdxInstance = newPdxInstanceBuilder()
.copy(pdxInstance)
.writeString(AT_TYPE_METADATA_PROPERTY_NAME, pdxInstanceClassName)
.create();
}
return pdxInstance;
}
/**
* Constructs a new instance of {@link PdxInstanceBuilder}.
*
* @return a new instance of {@link PdxInstanceBuilder}; never {@literal null}.
* @see org.springframework.geode.pdx.PdxInstanceBuilder
*/
@NonNull PdxInstanceBuilder newPdxInstanceBuilder() {
return PdxInstanceBuilder.create();
}
/**
* Decorates the given {@link String JSON} to include the {@literal @type} metadata property in order to
* indicate the type of the {@literal JSON} object, which is required for deserialization back to PDX.
*
* If an {@link JsonProcessingException} is thrown during this operation and if the {@link PdxInstance}
* has a {@link #hasValidClassName(PdxInstance) valid class name}, then an attempt is made to serialize
* the {@link PdxInstance#getObject() object instance} of the {@link PdxInstance} to {@link String JSON}
* using Jackson's {@link ObjectMapper}.
*
* @param pdxInstance required {@link PdxInstance} from which the {@link String JSON} was serialized;
* must not be {@literal null}.
* @param json {@link String JSON} generated from the serialization of the {@link PdxInstance};
* must not be {@literal null}.
* @return the decorated {@link String JSON} including the {@literal @type} metadata property.
* @throws DataRetrievalFailureException if {@link String JSON} cannot be decorated with type metadata
* and the {@link PdxInstance} is not based on a valid {@link Class} type.
* @see org.apache.geode.pdx.PdxInstance
* @see #newObjectMapper(Object)
* @see super#convert(Object)
*/
@SuppressWarnings("unused")
protected @NonNull String decorate(@NonNull PdxInstance pdxInstance, @NonNull String json) {
if (isDecorationRequired(pdxInstance, json)) {
try {
ObjectMapper objectMapper = newObjectMapper(json);
JsonNode jsonNode = objectMapper.readTree(json);
if (isMissingObjectTypeMetadata(jsonNode)) {
((ObjectNode) jsonNode).put(AT_TYPE_METADATA_PROPERTY_NAME, pdxInstance.getClassName());
json = objectMapper.writeValueAsString(jsonNode);
}
return json;
}
catch (JsonProcessingException cause) {
if (hasValidClassName(pdxInstance)) {
return convertObjectToJson(pdxInstance.getObject());
}
String message = String.format("Failed to parse JSON [%s]", json);
throw new DataRetrievalFailureException(message, cause);
}
}
return json;
}
/**
* Null-safe method to determine whether the given {@link PdxInstance}
* has a valid {@link Class#getName() Class Name}.
*
* @param pdxInstance {@link PdxInstance} to evaluate;
* @return a boolean value indicating whether the {@link PdxInstance}
* has a valid {@link Class#getName() Class Name}.
* @see org.apache.geode.pdx.PdxInstance
*/
boolean hasValidClassName(@Nullable PdxInstance pdxInstance) {
return Optional.ofNullable(pdxInstance)
.map(PdxInstance::getClassName)
.filter(StringUtils::hasText)
.filter(className -> !JSONFormatter.JSON_CLASSNAME.equals(className))
.isPresent();
}
private boolean isDecorationRequired(@Nullable PdxInstance pdxInstance, @Nullable String json) {
return isMissingObjectTypeMetadata(pdxInstance) && isValidJson(json);
}
private boolean isMissingObjectTypeMetadata(@Nullable JsonNode node) {
return isObjectNode(node) && !node.has(AT_TYPE_METADATA_PROPERTY_NAME);
}
private boolean isMissingObjectTypeMetadata(@Nullable PdxInstance pdxInstance) {
return pdxInstance != null && !pdxInstance.hasField(AT_TYPE_METADATA_PROPERTY_NAME);
}
/**
* Null-safe method to determine if the given {@link JsonNode} represents a valid {@link String JSON} object.
*
* @param node {@link JsonNode} to evaluate.
* @return a boolean valued indicating whether the given {@link JsonNode} is a valid {@link ObjectNode}.
* @see com.fasterxml.jackson.databind.node.ObjectNode
* @see com.fasterxml.jackson.databind.JsonNode
*/
boolean isObjectNode(@Nullable JsonNode node) {
return node != null && (node.isObject() || node instanceof ObjectNode);
}
/**
* Null-safe method to determine whether the given {@link String JSON} is valid.
*
* @param json {@link String} containing JSON to evaluate.
* @return a boolean value indicating whether the given {@link String JSON} is valid.
*/
boolean isValidJson(@Nullable String json) {
return StringUtils.hasText(json);
}
}