diff --git a/spring-geode/src/main/java/org/springframework/geode/data/json/converter/support/JSONFormatterPdxToJsonConverter.java b/spring-geode/src/main/java/org/springframework/geode/data/json/converter/support/JSONFormatterPdxToJsonConverter.java index 6bd12de0..b3f079d6 100644 --- a/spring-geode/src/main/java/org/springframework/geode/data/json/converter/support/JSONFormatterPdxToJsonConverter.java +++ b/spring-geode/src/main/java/org/springframework/geode/data/json/converter/support/JSONFormatterPdxToJsonConverter.java @@ -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); + } } diff --git a/spring-geode/src/test/java/org/springframework/geode/data/json/converter/support/JSONFormatterPdxToJsonConverterUnitTests.java b/spring-geode/src/test/java/org/springframework/geode/data/json/converter/support/JSONFormatterPdxToJsonConverterUnitTests.java index da284ee8..cc3f7bf6 100644 --- a/spring-geode/src/test/java/org/springframework/geode/data/json/converter/support/JSONFormatterPdxToJsonConverterUnitTests.java +++ b/spring-geode/src/test/java/org/springframework/geode/data/json/converter/support/JSONFormatterPdxToJsonConverterUnitTests.java @@ -17,18 +17,35 @@ package org.springframework.geode.data.json.converter.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.doReturn; +import static org.mockito.Mockito.doThrow; 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 com.fasterxml.jackson.core.JsonGenerationException; +import com.fasterxml.jackson.core.JsonGenerator; +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.junit.Test; +import org.apache.geode.pdx.JSONFormatter; import org.apache.geode.pdx.PdxInstance; +import org.apache.geode.pdx.PdxInstanceFactory; + +import org.springframework.dao.DataRetrievalFailureException; +import org.springframework.geode.pdx.PdxInstanceBuilder; + +import example.app.crm.model.Customer; /** * Unit Tests for {@link JSONFormatterPdxToJsonConverter}. @@ -36,8 +53,14 @@ import org.apache.geode.pdx.PdxInstance; * @author John Blum * @see org.junit.Test * @see org.mockito.Mockito + * @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.PdxInstanceFactory * @see org.springframework.geode.data.json.converter.support.JSONFormatterPdxToJsonConverter + * @see org.springframework.geode.pdx.PdxInstanceBuilder * @since 1.3.0 */ public class JSONFormatterPdxToJsonConverterUnitTests { @@ -60,7 +83,7 @@ public class JSONFormatterPdxToJsonConverterUnitTests { } @Test - public void convertsPdxInstanceToJson() { + public void convertsPdxToJson() { String json = "{ \"name\": \"Jon Doe\" }"; @@ -70,10 +93,459 @@ public class JSONFormatterPdxToJsonConverterUnitTests { doReturn(json).when(converter).convertPdxToJson(eq(mockPdxInstance)); - assertThat(converter.convertPdxToJson(mockPdxInstance)).isEqualTo(json); + assertThat(converter.convert(mockPdxInstance)).isEqualTo(json); verify(converter, times(1)).convertPdxToJson(eq(mockPdxInstance)); verify(converter, never()).convertObjectToJson(any()); verifyNoInteractions(mockPdxInstance); } + + @Test + public void convertPdxToJsonCallsDecorateAndJsonFormatterToJson() { + + String json = "{ \"name\": \"Jon Doe\" }"; + + PdxInstance mockPdxInstance = mock(PdxInstance.class); + + JSONFormatterPdxToJsonConverter converter = spy(new JSONFormatterPdxToJsonConverter()); + + doReturn(json).when(converter).jsonFormatterToJson(eq(mockPdxInstance)); + doReturn(json).when(converter).decorate(eq(mockPdxInstance), eq(json)); + + assertThat(converter.convertPdxToJson(mockPdxInstance)).isEqualTo(json); + + verify(converter, times(1)).jsonFormatterToJson(eq(mockPdxInstance)); + verify(converter, times(1)).decorate(eq(mockPdxInstance), eq(json)); + verifyNoInteractions(mockPdxInstance); + } + + @Test + public void decoratePdxInstance() { + + PdxInstance mockPdxInstanceSource = mock(PdxInstance.class); + PdxInstance mockPdxInstanceTarget = mock(PdxInstance.class); + + PdxInstanceBuilder mockPdxInstanceBuilder = mock(PdxInstanceBuilder.class); + + PdxInstanceFactory mockPdxInstanceFactory = mock(PdxInstanceFactory.class); + + doReturn(false).when(mockPdxInstanceSource).hasField(anyString()); + doReturn("example.app.test.model.Type").when(mockPdxInstanceSource).getClassName(); + doReturn(mockPdxInstanceFactory).when(mockPdxInstanceBuilder).copy(eq(mockPdxInstanceSource)); + doReturn(mockPdxInstanceFactory).when(mockPdxInstanceFactory) + .writeString(eq(JSONFormatterPdxToJsonConverter.AT_TYPE_METADATA_PROPERTY_NAME), + eq("example.app.test.model.Type")); + doReturn(mockPdxInstanceTarget).when(mockPdxInstanceFactory).create(); + + JSONFormatterPdxToJsonConverter converter = spy(new JSONFormatterPdxToJsonConverter()); + + doReturn(mockPdxInstanceBuilder).when(converter).newPdxInstanceBuilder(); + + assertThat(converter.decorate(mockPdxInstanceSource)).isEqualTo(mockPdxInstanceTarget); + + verify(mockPdxInstanceSource, times(1)) + .hasField(eq(JSONFormatterPdxToJsonConverter.AT_TYPE_METADATA_PROPERTY_NAME)); + verify(mockPdxInstanceSource, times(2)).getClassName(); + verify(converter,times(1)).newPdxInstanceBuilder(); + verify(mockPdxInstanceBuilder, times(1)).copy(eq(mockPdxInstanceSource)); + verify(mockPdxInstanceFactory, times(1)) + .writeString(eq(JSONFormatterPdxToJsonConverter.AT_TYPE_METADATA_PROPERTY_NAME), + eq("example.app.test.model.Type")); + verify(mockPdxInstanceFactory, times(1)).create(); + verifyNoInteractions(mockPdxInstanceTarget); + + } + + @Test + public void decoratePdxInstanceIsUnnecessary() { + + PdxInstance mockPdxInstance = mock(PdxInstance.class); + + doReturn(true).when(mockPdxInstance) + .hasField(eq(JSONFormatterPdxToJsonConverter.AT_TYPE_METADATA_PROPERTY_NAME)); + + JSONFormatterPdxToJsonConverter converter = new JSONFormatterPdxToJsonConverter(); + + assertThat(converter.decorate(mockPdxInstance)).isSameAs(mockPdxInstance); + + verify(mockPdxInstance, times(1)) + .hasField(eq(JSONFormatterPdxToJsonConverter.AT_TYPE_METADATA_PROPERTY_NAME)); + verifyNoMoreInteractions(mockPdxInstance); + } + + @Test(expected = IllegalArgumentException.class) + public void decoratePdxInstanceWithInvalidClassName() { + + PdxInstance mockPdxInstance = mock(PdxInstance.class); + + doReturn(false).when(mockPdxInstance) + .hasField(eq(JSONFormatterPdxToJsonConverter.AT_TYPE_METADATA_PROPERTY_NAME)); + doReturn(JSONFormatter.JSON_CLASSNAME).when(mockPdxInstance).getClassName(); + + JSONFormatterPdxToJsonConverter converter = spy(new JSONFormatterPdxToJsonConverter()); + + try { + converter.decorate(mockPdxInstance); + } + catch (IllegalArgumentException expected) { + + assertThat(expected).hasMessage("Class name [%s] is required and cannot be equal to [%s]", + JSONFormatter.JSON_CLASSNAME); + assertThat(expected).hasNoCause(); + + throw expected; + } + finally { + verify(mockPdxInstance, times(1)) + .hasField(eq(JSONFormatterPdxToJsonConverter.AT_TYPE_METADATA_PROPERTY_NAME)); + verify(mockPdxInstance, times(2)).getClassName(); + verify(converter, never()).newPdxInstanceBuilder(); + } + } + + @Test(expected = IllegalArgumentException.class) + public void decoratePdxInstanceWithNoClassName() { + + PdxInstance mockPdxInstance = mock(PdxInstance.class); + + doReturn(false).when(mockPdxInstance) + .hasField(eq(JSONFormatterPdxToJsonConverter.AT_TYPE_METADATA_PROPERTY_NAME)); + doReturn(null).when(mockPdxInstance).getClassName(); + + JSONFormatterPdxToJsonConverter converter = spy(new JSONFormatterPdxToJsonConverter()); + + try { + converter.decorate(mockPdxInstance); + } + catch (IllegalArgumentException expected) { + + assertThat(expected).hasMessage("Class name [null] is required and cannot be equal to [%s]", + JSONFormatter.JSON_CLASSNAME); + assertThat(expected).hasNoCause(); + + throw expected; + } + finally { + verify(mockPdxInstance, times(1)) + .hasField(eq(JSONFormatterPdxToJsonConverter.AT_TYPE_METADATA_PROPERTY_NAME)); + verify(mockPdxInstance, times(2)).getClassName(); + verify(converter, never()).newPdxInstanceBuilder(); + } + } + + @Test + public void decorateWithNullPdxInstanceIsNullSafe() { + assertThat(new JSONFormatterPdxToJsonConverter().decorate(null)).isNull(); + } + + @Test + @SuppressWarnings("all") + public void decorateJson() throws JsonProcessingException { + + String sourceJson = "{ \"name\": \"Jon Doe\" }"; + String targetJson = String.format("{ \"%s\": \"%s\"\"name\": \"Jon Doe\" }", + JSONFormatterPdxToJsonConverter.AT_TYPE_METADATA_PROPERTY_NAME, Customer.class.getName()); + + PdxInstance mockPdxInstance = mock(PdxInstance.class); + + ObjectMapper mockObjectMapper = mock(ObjectMapper.class); + + JsonNode mockJsonNode = mock(ObjectNode.class); + + JSONFormatterPdxToJsonConverter converter = spy(JSONFormatterPdxToJsonConverter.class); + + doReturn(Customer.class.getName()).when(mockPdxInstance).getClassName(); + doReturn(false).when(mockPdxInstance) + .hasField(eq(JSONFormatterPdxToJsonConverter.AT_TYPE_METADATA_PROPERTY_NAME)); + doReturn(mockObjectMapper).when(converter).newObjectMapper(eq(sourceJson)); + doReturn(mockJsonNode).when(mockObjectMapper).readTree(eq(sourceJson)); + doReturn(false).when(mockJsonNode) + .has(eq(JSONFormatterPdxToJsonConverter.AT_TYPE_METADATA_PROPERTY_NAME)); + doReturn(targetJson).when(mockObjectMapper).writeValueAsString(eq(mockJsonNode)); + + assertThat(converter.decorate(mockPdxInstance, sourceJson)).isEqualTo(targetJson); + + verify(mockPdxInstance, times(1)) + .hasField(eq(JSONFormatterPdxToJsonConverter.AT_TYPE_METADATA_PROPERTY_NAME)); + verify(converter, times(1)).newObjectMapper(eq(sourceJson)); + verify(mockObjectMapper, times(1)).readTree(eq(sourceJson)); + verify(mockJsonNode, times(1)) + .has(eq(JSONFormatterPdxToJsonConverter.AT_TYPE_METADATA_PROPERTY_NAME)); + verify(mockPdxInstance, times(1)).getClassName(); + verify((ObjectNode) mockJsonNode, times(1)) + .put(eq(JSONFormatterPdxToJsonConverter.AT_TYPE_METADATA_PROPERTY_NAME), eq(Customer.class.getName())); + verify(mockObjectMapper, times(1)).writeValueAsString(eq(mockJsonNode)); + } + + @Test + public void decorateJsonForReal() { + + String json = "{ \"name\": \"Jon Doe\" }"; + String expectedJson = String.format("{\"name\":\"Jon Doe\",\"%s\":\"%s\"}", + JSONFormatterPdxToJsonConverter.AT_TYPE_METADATA_PROPERTY_NAME, Customer.class.getName()); + + PdxInstance mockPdxInstance = mock(PdxInstance.class); + + doReturn(Customer.class.getName()).when(mockPdxInstance).getClassName(); + doReturn(false).when(mockPdxInstance) + .hasField(eq(JSONFormatterPdxToJsonConverter.AT_TYPE_METADATA_PROPERTY_NAME)); + + JSONFormatterPdxToJsonConverter converter = new JSONFormatterPdxToJsonConverter(); + + assertThat(converter.decorate(mockPdxInstance, json)).isEqualTo(expectedJson); + + verify(mockPdxInstance, times(1)).getClassName(); + verify(mockPdxInstance, times(1)) + .hasField(eq(JSONFormatterPdxToJsonConverter.AT_TYPE_METADATA_PROPERTY_NAME)); + verifyNoMoreInteractions(mockPdxInstance); + } + + @Test + public void decorateJsonIsUnnecessary() { + + String json = "{ \"name\": \"Jon Doe\" }"; + + PdxInstance mockPdxInstance = mock(PdxInstance.class); + + doReturn(true).when(mockPdxInstance) + .hasField(eq(JSONFormatterPdxToJsonConverter.AT_TYPE_METADATA_PROPERTY_NAME)); + + JSONFormatterPdxToJsonConverter converter = spy(new JSONFormatterPdxToJsonConverter()); + + assertThat(converter.decorate(mockPdxInstance, json)).isEqualTo(json); + + verify(mockPdxInstance, times(1)) + .hasField(eq(JSONFormatterPdxToJsonConverter.AT_TYPE_METADATA_PROPERTY_NAME)); + verify(converter, never()).newObjectMapper(any()); + verifyNoMoreInteractions(mockPdxInstance); + } + + @Test + public void decorateJsonNodeIsUnnecessary() throws JsonProcessingException { + + String json = "{ \"name\": \"Jon Doe\" }"; + + PdxInstance mockPdxInstance = mock(PdxInstance.class); + + ObjectMapper mockObjectMapper = mock(ObjectMapper.class); + + JsonNode mockJsonNode = mock(ObjectNode.class); + + JSONFormatterPdxToJsonConverter converter = spy(new JSONFormatterPdxToJsonConverter()); + + doReturn(false).when(mockPdxInstance) + .hasField(eq(JSONFormatterPdxToJsonConverter.AT_TYPE_METADATA_PROPERTY_NAME)); + doReturn(mockObjectMapper).when(converter).newObjectMapper(eq(json)); + doReturn(mockJsonNode).when(mockObjectMapper).readTree(eq(json)); + doReturn(true).when(mockJsonNode) + .has(eq(JSONFormatterPdxToJsonConverter.AT_TYPE_METADATA_PROPERTY_NAME)); + + assertThat(converter.decorate(mockPdxInstance, json)).isEqualTo(json); + + verify(mockPdxInstance, times(1)) + .hasField(eq(JSONFormatterPdxToJsonConverter.AT_TYPE_METADATA_PROPERTY_NAME)); + verify(converter, times(1)).newObjectMapper(eq(json)); + verify(mockObjectMapper, times(1)).readTree(eq(json)); + verify(mockJsonNode, times(1)) + .has(eq(JSONFormatterPdxToJsonConverter.AT_TYPE_METADATA_PROPERTY_NAME)); + verifyNoMoreInteractions(mockPdxInstance, mockObjectMapper, mockJsonNode); + } + + @Test + public void decoratesJsonThrowsJsonProcessingExceptionWhenPdxInstanceClassNameIsSpecifiedAndValid() + throws JsonProcessingException { + + String json = "{ \"name\": \"Jon Doe\" }"; + + PdxInstance mockPdxInstance = mock(PdxInstance.class); + + ObjectMapper mockObjectMapper = mock(ObjectMapper.class); + + Customer jonDoe = Customer.newCustomer(1L, "Jon Doe"); + + JSONFormatterPdxToJsonConverter converter = spy(new JSONFormatterPdxToJsonConverter()); + + doReturn(jonDoe.getClass().getName()).when(mockPdxInstance).getClassName(); + doReturn(jonDoe).when(mockPdxInstance).getObject(); + doReturn(false).when(mockPdxInstance).hasField(anyString()); + doReturn(mockObjectMapper).when(converter).newObjectMapper(eq(json)); + doThrow(new JsonGenerationException("TEST", (JsonGenerator) null)).when(mockObjectMapper).readTree(eq(json)); + doReturn(json).when(converter).convertObjectToJson(eq(jonDoe)); + + assertThat(converter.decorate(mockPdxInstance, json)).isEqualTo(json); + + verify(mockPdxInstance, times(1)) + .hasField(eq(JSONFormatterPdxToJsonConverter.AT_TYPE_METADATA_PROPERTY_NAME)); + verify(converter, times(1)).newObjectMapper(eq(json)); + verify(mockObjectMapper, times(1)).readTree(eq(json)); + verify(mockPdxInstance, times(1)).getClassName(); + verify(mockPdxInstance, times(1)).getObject(); + verify(converter, times(1)).convertObjectToJson(eq(jonDoe)); + verifyNoMoreInteractions(mockPdxInstance, mockObjectMapper); + } + + @Test(expected = DataRetrievalFailureException.class) + public void decorateJsonThrowsDataRetrievalFailureException() throws JsonProcessingException { + + String json = "{ \"name\": \"Jon Doe\" }"; + + PdxInstance mockPdxInstance = mock(PdxInstance.class); + + ObjectMapper mockObjectMapper = mock(ObjectMapper.class); + + JSONFormatterPdxToJsonConverter converter = spy(new JSONFormatterPdxToJsonConverter()); + + doReturn(false).when(mockPdxInstance).hasField(anyString()); + doReturn(mockObjectMapper).when(converter).newObjectMapper(eq(json)); + doThrow(new JsonGenerationException("TEST", (JsonGenerator) null)).when(mockObjectMapper).readTree(eq(json)); + doReturn(JSONFormatter.JSON_CLASSNAME).when(mockPdxInstance).getClassName(); + + try { + converter.decorate(mockPdxInstance, json); + } + catch (DataRetrievalFailureException expected) { + + assertThat(expected).hasMessageStartingWith("Failed to parse JSON [%s]", json); + assertThat(expected).hasCauseInstanceOf(JsonGenerationException.class); + assertThat(expected.getCause()).hasMessage("TEST"); + assertThat(expected.getCause()).hasNoCause(); + + throw expected; + } + finally { + verify(mockPdxInstance, times(1)) + .hasField(eq(JSONFormatterPdxToJsonConverter.AT_TYPE_METADATA_PROPERTY_NAME)); + verify(converter, times(1)).newObjectMapper(eq(json)); + verify(mockObjectMapper, times(1)).readTree(eq(json)); + verify(mockPdxInstance, times(1)).getClassName(); + verifyNoMoreInteractions(mockPdxInstance, mockObjectMapper); + } + } + + @Test + public void hasValidClassNameWithPdxInstanceHavingValidClassName() { + + PdxInstance mockPdxInstance = mock(PdxInstance.class); + + doReturn(Customer.class.getName()).when(mockPdxInstance).getClassName(); + + assertThat(new JSONFormatterPdxToJsonConverter().hasValidClassName(mockPdxInstance)).isTrue(); + + verify(mockPdxInstance, times(1)).getClassName(); + verifyNoMoreInteractions(mockPdxInstance); + } + + @Test + public void hasValidClassNameWithPdxInstanceHavingNonExistingClassType() { + + PdxInstance mockPdxInstance = mock(PdxInstance.class); + + doReturn("non.existing.class.Type").when(mockPdxInstance).getClassName(); + + assertThat(new JSONFormatterPdxToJsonConverter().hasValidClassName(mockPdxInstance)).isTrue(); + + verify(mockPdxInstance, times(1)).getClassName(); + verifyNoMoreInteractions(mockPdxInstance); + } + + private void testHasValidClassNameWithPdxInstanceHavingInvalidClassName(String className) { + + PdxInstance mockPdxInstance = mock(PdxInstance.class); + + doReturn(className).when(mockPdxInstance).getClassName(); + + assertThat(new JSONFormatterPdxToJsonConverter().hasValidClassName(mockPdxInstance)).isFalse(); + + verify(mockPdxInstance, times(1)).getClassName(); + verifyNoMoreInteractions(mockPdxInstance); + } + + @Test + public void hasValidClassNameWithPdxInstanceHavingJsonFormatterJsonClassName() { + testHasValidClassNameWithPdxInstanceHavingInvalidClassName(JSONFormatter.JSON_CLASSNAME); + } + + @Test + public void hasValidClassNameWithPdxInstanceHavingBlankClassName() { + testHasValidClassNameWithPdxInstanceHavingInvalidClassName(" "); + } + + @Test + public void hasValidClassNameWithPdxInstanceHavingEmptyClassName() { + testHasValidClassNameWithPdxInstanceHavingInvalidClassName(""); + } + + @Test + public void hasValidClassNameWithPdxInstanceHavingNullClassName() { + testHasValidClassNameWithPdxInstanceHavingInvalidClassName(null); + } + + @Test + public void isNotObjectNodeWhenJsonNodeIsObjectReturnsFalse() { + + JsonNode mockJsonNode = mock(JsonNode.class); + + doReturn(false).when(mockJsonNode).isObject(); + + assertThat(new JSONFormatterPdxToJsonConverter().isObjectNode(mockJsonNode)).isFalse(); + + verify(mockJsonNode, times(1)).isObject(); + verifyNoMoreInteractions(mockJsonNode); + } + + @Test + public void isObjectNodeWhenJsonNodeIsObjectReturnsTrue() { + + JsonNode mockJsonNode = mock(JsonNode.class); + + doReturn(true).when(mockJsonNode).isObject(); + + assertThat(new JSONFormatterPdxToJsonConverter().isObjectNode(mockJsonNode)).isTrue(); + + verify(mockJsonNode, times(1)).isObject(); + verifyNoMoreInteractions(mockJsonNode); + } + + @Test + public void isObjectNodeWithObjectNode() { + assertThat(new JSONFormatterPdxToJsonConverter().isObjectNode(mock(ObjectNode.class))).isTrue(); + } + + @Test + public void isObjectNodeIsNullSafe() { + assertThat(new JSONFormatterPdxToJsonConverter().isObjectNode(null)).isFalse(); + } + + @Test + public void isValidJsonWithJson() { + + assertThat(new JSONFormatterPdxToJsonConverter() + .isValidJson("[{ \"name\": \"Jon Doe\" }, { \"name\": \"Jane Doe\" }]")).isTrue(); + assertThat(new JSONFormatterPdxToJsonConverter().isValidJson("{ \"name\": \"Jon Doe\" }")).isTrue(); + assertThat(new JSONFormatterPdxToJsonConverter().isValidJson("{}")).isTrue(); + assertThat(new JSONFormatterPdxToJsonConverter().isValidJson("[]")).isTrue(); + assertThat(new JSONFormatterPdxToJsonConverter().isValidJson("test")).isTrue(); + assertThat(new JSONFormatterPdxToJsonConverter().isValidJson("")).isTrue(); + assertThat(new JSONFormatterPdxToJsonConverter().isValidJson("")).isTrue(); + } + + @Test + public void isValidJsonWithNonJsonString() { + assertThat(new JSONFormatterPdxToJsonConverter().isValidJson("non-Json String")).isTrue(); + } + + @Test + public void isValidJsonWithBlankString() { + assertThat(new JSONFormatterPdxToJsonConverter().isValidJson(" ")).isFalse(); + } + + @Test + public void isValidJsonWithEmptyString() { + assertThat(new JSONFormatterPdxToJsonConverter().isValidJson("")).isFalse(); + } + + @Test + public void isValidJsonWithNull() { + assertThat(new JSONFormatterPdxToJsonConverter().isValidJson(null)).isFalse(); + } }