From df466c0a62eacf7b803a0b9e45bb4424f2fb2250 Mon Sep 17 00:00:00 2001 From: John Blum Date: Tue, 12 May 2020 19:01:17 -0700 Subject: [PATCH] Add PdxInstanceWrapper class to wrap (decorate) existing PdxInstances and more appropriately handle JSON to Object mappings returned by getObject(). --- .../geode/pdx/PdxInstanceWrapper.java | 363 ++++++++++++++ .../pdx/PdxInstanceWrapperUnitTests.java | 447 ++++++++++++++++++ 2 files changed, 810 insertions(+) create mode 100644 apache-geode-extensions/src/main/java/org/springframework/geode/pdx/PdxInstanceWrapper.java create mode 100644 apache-geode-extensions/src/test/java/org/springframework/geode/pdx/PdxInstanceWrapperUnitTests.java diff --git a/apache-geode-extensions/src/main/java/org/springframework/geode/pdx/PdxInstanceWrapper.java b/apache-geode-extensions/src/main/java/org/springframework/geode/pdx/PdxInstanceWrapper.java new file mode 100644 index 00000000..3ab006ba --- /dev/null +++ b/apache-geode-extensions/src/main/java/org/springframework/geode/pdx/PdxInstanceWrapper.java @@ -0,0 +1,363 @@ +/* + * 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.pdx; + +import static org.springframework.geode.util.GeodeAssertions.assertThat; + +import java.io.DataOutput; +import java.io.IOException; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.MapperFeature; +import com.fasterxml.jackson.databind.ObjectMapper; + +import org.apache.geode.internal.Sendable; +import org.apache.geode.pdx.JSONFormatter; +import org.apache.geode.pdx.PdxInstance; +import org.apache.geode.pdx.WritablePdxInstance; + +/** + * The {@link PdxInstanceWrapper} class is an implementation of the {@link PdxInstance} interface + * wrapping an existing {@link PdxInstance} object and decorating the functionality. + * + * @author John Blum + * @see java.util.function.Function + * @see com.fasterxml.jackson.databind.ObjectMapper + * @see org.apache.geode.pdx.JSONFormatter + * @see org.apache.geode.pdx.PdxInstance + * @see org.apache.geode.pdx.WritablePdxInstance + * @since 1.3.0 + */ +public class PdxInstanceWrapper implements PdxInstance, Sendable { + + public static final String AT_IDENTIFIER_FIELD_NAME = "@identifier"; + public static final String AT_TYPE_FIELD_NAME = "@type"; + public static final String CLASS_NAME_PROPERTY = "className"; + public static final String ID_FIELD_NAME = "id"; + + protected static final String ARRAY_BEGIN = "["; + protected static final String ARRAY_END = "]"; + protected static final String COMMA = ","; + protected static final String EMPTY_STRING = ""; + protected static final String FIELD_TYPE_VALUE = "\"%1$s\"(%2$s): \"%3$s\""; + protected static final String INDENT_STRING = "\t"; + protected static final String NEW_LINE = "\n"; + protected static final String COMMA_NEW_LINE = "," + NEW_LINE; + protected static final String COMMA_SPACE = COMMA + " "; + protected static final String OBJECT_BEGIN = "{"; + protected static final String OBJECT_END = "}"; + + /** + * Smart, {@literal null-safe} factory method used to evaluate the given {@link Object} and wrap the {@link Object} + * in a new instance of {@link PdxInstanceWrapper} if the {@link Object} is an instance of {@link PdxInstance} + * or return the given {@link Object} as is. + * + * @param target {@link Object} to evaluate + * @return the {@link Object} wrapped in a new instance of {@link PdxInstanceWrapper} if {@link Object} + * is an instance of {@link PdxInstance}, otherwise returns the given {@link Object}. + * @see org.apache.geode.pdx.PdxInstance + * @see java.lang.Object + * @see #from(PdxInstance) + */ + public static Object from(Object target) { + return target instanceof PdxInstance ? from((PdxInstance) target) : target; + } + + /** + * Factory method used to construct a new instance of {@link PdxInstanceWrapper} initialized with the given, + * required {@link PdxInstance} used to back the wrapper. + * + * @param pdxInstance {@link PdxInstance} object used to back this wrapper; must not be {@literal null}. + * @return a new instance of {@link PdxInstanceWrapper} initialized with the given {@link PdxInstance}. + * @throws IllegalArgumentException if {@link PdxInstance} is {@literal null}. + * @see org.apache.geode.pdx.PdxInstance + * @see #PdxInstanceWrapper(PdxInstance) + */ + public static PdxInstanceWrapper from(PdxInstance pdxInstance) { + return new PdxInstanceWrapper(pdxInstance); + } + + private final PdxInstance delegate; + + /** + * Constructs a new instance of {@link PdxInstanceWrapper} initialized with the given, required {@link PdxInstance} + * object used to back this wrapper. + * + * @param pdxInstance {@link PdxInstance} object used to back this wrapper; must not be {@literal null}. + * @throws IllegalArgumentException if {@link PdxInstance} is {@literal null}. + * @see org.apache.geode.pdx.PdxInstance + */ + public PdxInstanceWrapper(PdxInstance pdxInstance) { + + assertThat(pdxInstance).isNotNull(); + + this.delegate = pdxInstance; + } + + /** + * Returns a reference to the configured, underlying {@link PdxInstance} backing this wrapper. + * + * @return a reference to the configured, underlying {@link PdxInstance} backing this wrapper; + * never {@literal null}. + * @see org.apache.geode.pdx.PdxInstance + */ + protected PdxInstance getDelegate() { + return this.delegate; + } + + /** + * Returns an {@link Optional} reference to a configured Jackson {@link ObjectMapper} used to + * deserialize the {@link String JSON} generated from {@link PdxInstance PDX} back into an {@link Object}. + * + * This method is meant ot be overridden by {@link Class subclasses}. + * + * @return an {@link Optional} {@link ObjectMapper}. + * @see com.fasterxml.jackson.databind.ObjectMapper + * @see java.util.Optional + */ + protected Optional getObjectMapper() { + + ObjectMapper objectMapper = newObjectMapper() + .configure(DeserializationFeature.FAIL_ON_IGNORED_PROPERTIES, false) + .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) + .configure(MapperFeature.ACCEPT_CASE_INSENSITIVE_ENUMS, true) + .findAndRegisterModules(); + + return Optional.of(objectMapper); + } + + /** + * Constructs a new instance of Jackson's {@link ObjectMapper}. + * + * @return a new instance of Jackson's {@link ObjectMapper}; never {@literal null}. + * @see com.fasterxml.jackson.databind.ObjectMapper + */ + ObjectMapper newObjectMapper() { + return new ObjectMapper(); + } + + /** + * @inheritDoc + */ + @Override + public String getClassName() { + return getDelegate().getClassName(); + } + + /** + * @inheritDoc + */ + @Override + public boolean isDeserializable() { + return getDelegate().isDeserializable(); + } + + /** + * @inheritDoc + */ + @Override + public boolean isEnum() { + return getDelegate().isEnum(); + } + + /** + * @inheritDoc + */ + @Override + public boolean isIdentityField(String fieldName) { + return getDelegate().isIdentityField(fieldName); + } + + /** + * @inheritDoc + */ + @Override + public Object getField(String fieldName) { + return getDelegate().getField(fieldName); + } + + /** + * @inheritDoc + */ + @Override + public List getFieldNames() { + return getDelegate().getFieldNames(); + } + + /** + * @inheritDoc + */ + @Override + public Object getObject() { + + return getObjectMapper() + .filter(objectMapper -> JSONFormatter.JSON_CLASSNAME.equals(getClassName())) + .filter(objectMapper -> hasField(AT_TYPE_FIELD_NAME)) + .map(objectMapper -> { + try { + + String typeName = String.valueOf(getField(AT_TYPE_FIELD_NAME)); + + Class type = Class.forName(typeName); + + String json = jsonFormatterToJson(getDelegate()); + + return objectMapper.readValue(json, type); + } + catch (Throwable ignore) { + // TODO Add log statement + return null; + } + }) + .orElseGet(() -> getDelegate().getObject()); + } + + /** + * Calls {@link JSONFormatter#toJSON(PdxInstance)} to convert the {@link PdxInstance} into {@link String JSON}. + * + * @param pdxInstance {@link PdxInstance} to convert to {@link String JSON}. + * @return {@link String JSON} generated from the given {@link PdxInstance}. + * @see org.apache.geode.pdx.JSONFormatter#toJSON(PdxInstance) + * @see org.apache.geode.pdx.PdxInstance + */ + String jsonFormatterToJson(PdxInstance pdxInstance) { + return JSONFormatter.toJSON(pdxInstance); + } + + /** + * @inheritDoc + */ + @Override + public WritablePdxInstance createWriter() { + return getDelegate().createWriter(); + } + + /** + * @inheritDoc + */ + @Override + public boolean hasField(String fieldName) { + return getDelegate().hasField(fieldName); + } + + /** + * @inheritDoc + */ + @Override + public void sendTo(DataOutput out) throws IOException { + + PdxInstance delegate = getDelegate(); + + if (delegate instanceof Sendable) { + ((Sendable) delegate).sendTo(out); + } + } + + /** + * Returns a {@link String} representation of this {@link PdxInstance}. + * + * @return a {@link String} representation of this {@link PdxInstance}. + * @see java.lang.String + */ + @Override + public String toString() { + //return getDelegate().toString(); + return toString(this); + } + + private String toString(PdxInstance pdx) { + return toString(pdx, ""); + } + + private String toString(PdxInstance pdx, String indent) { + + if (Objects.nonNull(pdx)) { + + StringBuilder buffer = new StringBuilder(OBJECT_BEGIN).append(NEW_LINE); + + String fieldIndent = indent + INDENT_STRING; + + buffer.append(fieldIndent).append(formatFieldValue(CLASS_NAME_PROPERTY, pdx.getClassName())); + + for (String fieldName : nullSafeList(pdx.getFieldNames())) { + + Object fieldValue = pdx.getField(fieldName); + + String valueString = toStringObject(fieldValue, fieldIndent); + + buffer.append(COMMA_NEW_LINE); + buffer.append(fieldIndent).append(formatFieldValue(fieldName, valueString)); + } + + buffer.append(NEW_LINE).append(indent).append(OBJECT_END); + + return buffer.toString(); + } + else { + return null; + } + } + + private String toStringArray(Object value, String indent) { + + Object[] array = (Object[]) value; + + StringBuilder buffer = new StringBuilder(ARRAY_BEGIN); + + boolean addComma = false; + + for (Object element : array) { + buffer.append(addComma ? COMMA_SPACE : EMPTY_STRING); + buffer.append(toStringObject(element, indent)); + addComma = true; + } + + buffer.append(ARRAY_END); + + return buffer.toString(); + } + + private String toStringObject(Object value, String indent) { + + return isPdxInstance(value) ? toString((PdxInstance) value, indent) + : isArray(value) ? toStringArray(value, indent) + : String.valueOf(value); + } + + private String formatFieldValue(String fieldName, Object fieldValue) { + return String.format(FIELD_TYPE_VALUE, fieldName, nullSafeType(fieldValue), fieldValue); + } + + private boolean isArray(Object value) { + return Objects.nonNull(value) && value.getClass().isArray(); + } + + private boolean isPdxInstance(Object value) { + return value instanceof PdxInstance; + } + + private List nullSafeList(List list) { + return list != null ? list : Collections.emptyList(); + } + + private Class nullSafeType(Object value) { + return value != null ? value.getClass() : Object.class; + } +} diff --git a/apache-geode-extensions/src/test/java/org/springframework/geode/pdx/PdxInstanceWrapperUnitTests.java b/apache-geode-extensions/src/test/java/org/springframework/geode/pdx/PdxInstanceWrapperUnitTests.java new file mode 100644 index 00000000..d96d6e8a --- /dev/null +++ b/apache-geode-extensions/src/test/java/org/springframework/geode/pdx/PdxInstanceWrapperUnitTests.java @@ -0,0 +1,447 @@ +/* + * 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.pdx; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.atLeastOnce; +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 java.io.DataOutput; +import java.io.IOException; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; + +import com.fasterxml.jackson.core.JsonGenerationException; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.MapperFeature; +import com.fasterxml.jackson.databind.ObjectMapper; + +import org.junit.Test; + +import org.apache.geode.internal.Sendable; +import org.apache.geode.pdx.JSONFormatter; +import org.apache.geode.pdx.PdxInstance; +import org.apache.geode.pdx.WritablePdxInstance; + +/** + * Unit Tests for {@link PdxInstanceWrapper}. + * + * @author John Blum + * @see org.junit.Test + * @see org.mockito.Mockito + * @see com.fasterxml.jackson.core.JsonGenerator + * @see com.fasterxml.jackson.databind.ObjectMapper + * @see org.apache.geode.pdx.JSONFormatter + * @see org.apache.geode.pdx.PdxInstance + * @see org.apache.geode.pdx.WritablePdxInstance + * @since 1.3.0 + */ +public class PdxInstanceWrapperUnitTests { + + @Test + public void constructPdxInstanceWrapper() { + + PdxInstance mockPdxInstance = mock(PdxInstance.class); + + PdxInstanceWrapper wrapper = new PdxInstanceWrapper(mockPdxInstance); + + assertThat(wrapper).isNotNull(); + assertThat(wrapper.getDelegate()).isEqualTo(mockPdxInstance); + + verifyNoInteractions(mockPdxInstance); + } + + @Test(expected = IllegalArgumentException.class) + public void constructPdxInstanceWrapperWithNull() { + + try { + new PdxInstanceWrapper(null); + } + catch (IllegalArgumentException expected) { + + assertThat(expected).hasMessage("Argument must not be null"); + assertThat(expected).hasNoCause(); + + throw expected; + } + } + + @Test + public void fromObjectIsObject() { + assertThat(PdxInstanceWrapper.from("TEST")).isEqualTo("TEST"); + } + + @Test + public void fromNullIsNull() { + assertThat(PdxInstanceWrapper.from((Object) null)).isEqualTo(null); + } + + @Test + public void fromPdxInstanceIsWrapper() { + + PdxInstance mockPdxInstance = mock(PdxInstance.class); + + PdxInstanceWrapper wrapper = PdxInstanceWrapper.from(mockPdxInstance); + + assertThat(wrapper).isNotNull(); + assertThat(wrapper.getDelegate()).isEqualTo(mockPdxInstance); + + verifyNoInteractions(mockPdxInstance); + } + + @Test + public void objectMapperConfigurationIsCorrect() { + + PdxInstanceWrapper wrapper = spy(PdxInstanceWrapper.from(mock(PdxInstance.class))); + + ObjectMapper mockObjectMapper = mock(ObjectMapper.class); + + doReturn(mockObjectMapper).when(wrapper).newObjectMapper(); + doReturn(mockObjectMapper).when(mockObjectMapper).configure(any(DeserializationFeature.class), anyBoolean()); + doReturn(mockObjectMapper).when(mockObjectMapper).configure(any(MapperFeature.class), anyBoolean()); + doReturn(mockObjectMapper).when(mockObjectMapper).findAndRegisterModules(); + + ObjectMapper objectMapper = wrapper.getObjectMapper().orElse(null); + + assertThat(objectMapper).isNotNull(); + + verify(mockObjectMapper, times(1)).configure(eq(DeserializationFeature.FAIL_ON_IGNORED_PROPERTIES), eq(false)); + verify(mockObjectMapper, times(1)).configure(eq(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES), eq(false)); + verify(mockObjectMapper, times(1)).configure(eq(MapperFeature.ACCEPT_CASE_INSENSITIVE_ENUMS), eq(true)); + verify(mockObjectMapper, times(1)).findAndRegisterModules(); + verifyNoMoreInteractions(mockObjectMapper); + } + + @Test + public void getClassNameCallsPdxInstanceGetClassName() { + + PdxInstance mockPdxInstance = mock(PdxInstance.class); + + doReturn("example.app.test.model.Type").when(mockPdxInstance).getClassName(); + + assertThat(PdxInstanceWrapper.from(mockPdxInstance).getClassName()).isEqualTo("example.app.test.model.Type"); + + verify(mockPdxInstance, times(1)).getClassName(); + verifyNoMoreInteractions(mockPdxInstance); + } + + @Test + public void isDeserializaleCallsPdxInstanceIsDeserializable() { + + PdxInstance mockPdxInstance = mock(PdxInstance.class); + + doReturn(true).when(mockPdxInstance).isDeserializable(); + + assertThat(PdxInstanceWrapper.from(mockPdxInstance).isDeserializable()).isTrue(); + + verify(mockPdxInstance, times(1)).isDeserializable(); + verifyNoMoreInteractions(mockPdxInstance); + } + + @Test + public void isEnumCallsPdxInstanceIsEnum() { + + PdxInstance mockPdxInstance = mock(PdxInstance.class); + + doReturn(false).when(mockPdxInstance).isEnum(); + + assertThat(PdxInstanceWrapper.from(mockPdxInstance).isEnum()).isFalse(); + + verify(mockPdxInstance, times(1)).isEnum(); + verifyNoMoreInteractions(mockPdxInstance); + } + + @Test + public void isIdentityFieldCallsPdxInstanceIsIdentityField() { + + PdxInstance mockPdxInstance = mock(PdxInstance.class); + + doReturn(false).when(mockPdxInstance).isIdentityField(anyString()); + doReturn(true).when(mockPdxInstance).isIdentityField("id"); + + PdxInstanceWrapper wrapper = PdxInstanceWrapper.from(mockPdxInstance); + + assertThat(wrapper.isIdentityField("id")).isTrue(); + assertThat(wrapper.isIdentityField("randomField")).isFalse(); + + verify(mockPdxInstance, times(1)).isIdentityField(eq("id")); + verify(mockPdxInstance, times(1)).isIdentityField(eq("randomField")); + verifyNoMoreInteractions(mockPdxInstance); + } + + @Test + public void getFieldCallPdxInstanceGetField() { + + PdxInstance mockPdxInstance = mock(PdxInstance.class); + + doReturn("TestValue").when(mockPdxInstance).getField(eq("TestField")); + + assertThat(PdxInstanceWrapper.from(mockPdxInstance).getField("TestField")).isEqualTo("TestValue"); + + verify(mockPdxInstance, times(1)).getField(eq("TestField")); + verifyNoMoreInteractions(mockPdxInstance); + } + + @Test + public void getFieldNameCallsPdxInstanceGetFieldNames() { + + List fieldNames = Arrays.asList("FieldOne", "FieldTwo"); + + PdxInstance mockPdxInstance = mock(PdxInstance.class); + + doReturn(fieldNames).when(mockPdxInstance).getFieldNames(); + + assertThat(PdxInstanceWrapper.from(mockPdxInstance).getFieldNames()).isEqualTo(fieldNames); + + verify(mockPdxInstance, times(1)).getFieldNames(); + verifyNoMoreInteractions(mockPdxInstance); + } + + @Test + public void getObjectReturnsObject() throws JsonProcessingException { + + Account mockAccount = mock(Account.class); + + ObjectMapper mockObjectMapper = mock(ObjectMapper.class); + + PdxInstance mockPdxInstance = mock(PdxInstance.class); + + String json = String.format("{ \"@type\": \"%s\", \"name\": \"Savings\"}", Account.class.getName()); + + doReturn(JSONFormatter.JSON_CLASSNAME).when(mockPdxInstance).getClassName(); + doReturn(true).when(mockPdxInstance).hasField(eq(PdxInstanceWrapper.AT_TYPE_FIELD_NAME)); + doReturn(Account.class.getName()).when(mockPdxInstance).getField(eq(PdxInstanceWrapper.AT_TYPE_FIELD_NAME)); + + PdxInstanceWrapper wrapper = spy(PdxInstanceWrapper.from(mockPdxInstance)); + + assertThat(wrapper).isNotNull(); + assertThat(wrapper.getDelegate()).isEqualTo(mockPdxInstance); + + doReturn(Optional.of(mockObjectMapper)).when(wrapper).getObjectMapper(); + doReturn(json).when(wrapper).jsonFormatterToJson(eq(mockPdxInstance)); + doReturn(mockAccount).when(mockObjectMapper).readValue(eq(json), eq(Account.class)); + + assertThat(wrapper.getObject()).isEqualTo(mockAccount); + + verify(wrapper, atLeastOnce()).getDelegate(); + verify(wrapper, times(1)).getObjectMapper(); + verify(mockPdxInstance, times(1)).getClassName(); + verify(mockPdxInstance, times(1)).hasField(eq(PdxInstanceWrapper.AT_TYPE_FIELD_NAME)); + verify(mockPdxInstance, times(1)).getField(eq(PdxInstanceWrapper.AT_TYPE_FIELD_NAME)); + verify(wrapper, times(1)).jsonFormatterToJson(eq(mockPdxInstance)); + verify(mockObjectMapper, times(1)).readValue(eq(json), eq(Account.class)); + verify(mockPdxInstance, never()).getObject(); + verifyNoMoreInteractions(mockObjectMapper, mockPdxInstance); + } + + @Test + @SuppressWarnings("unchecked") + public void getObjectCallsPdxInstanceGetObjectWhenAtTypeFieldIsNotPresent() throws JsonProcessingException { + + Object value = new Object(); + + ObjectMapper mockObjectMapper = mock(ObjectMapper.class); + + PdxInstance mockPdxInstance = mock(PdxInstance.class); + + doReturn(JSONFormatter.JSON_CLASSNAME).when(mockPdxInstance).getClassName(); + doReturn(false).when(mockPdxInstance).hasField(eq(PdxInstanceWrapper.AT_TYPE_FIELD_NAME)); + doReturn(value).when(mockPdxInstance).getObject(); + + PdxInstanceWrapper wrapper = spy(PdxInstanceWrapper.from(mockPdxInstance)); + + assertThat(wrapper).isNotNull(); + assertThat(wrapper.getDelegate()).isEqualTo(mockPdxInstance); + + doReturn(Optional.of(mockObjectMapper)).when(wrapper).getObjectMapper(); + + assertThat(wrapper.getObject()).isEqualTo(value); + + verify(wrapper, atLeastOnce()).getDelegate(); + verify(wrapper, times(1)).getObjectMapper(); + verify(mockPdxInstance, times(1)).getClassName(); + verify(mockPdxInstance, times(1)).hasField(eq(PdxInstanceWrapper.AT_TYPE_FIELD_NAME)); + verify(mockPdxInstance, never()).getField(anyString()); + verify(wrapper, never()).jsonFormatterToJson(any()); + verify(mockObjectMapper, never()).readValue(anyString(), any(Class.class)); + verify(mockPdxInstance, times(1)).getObject(); + verifyNoMoreInteractions(mockObjectMapper, mockPdxInstance); + } + + @Test + @SuppressWarnings("unchecked") + public void getObjectCallsPdxInstanceGetObjectWhenClassNameIsNotGemFireJson() throws JsonProcessingException { + + ObjectMapper mockObjectMapper = mock(ObjectMapper.class); + + PdxInstance mockPdxInstance = mock(PdxInstance.class); + + doReturn("non.existing.class.Name").when(mockPdxInstance).getClassName(); + doReturn("TEST").when(mockPdxInstance).getObject(); + + PdxInstanceWrapper wrapper = spy(PdxInstanceWrapper.from(mockPdxInstance)); + + assertThat(wrapper).isNotNull(); + assertThat(wrapper.getDelegate()).isEqualTo(mockPdxInstance); + + doReturn(Optional.of(mockObjectMapper)).when(wrapper).getObjectMapper(); + + assertThat(wrapper.getObject()).isEqualTo("TEST"); + + verify(wrapper, atLeastOnce()).getDelegate(); + verify(wrapper, times(1)).getObjectMapper(); + verify(mockPdxInstance, times(1)).getClassName(); + verify(mockPdxInstance, never()).hasField(anyString()); + verify(mockPdxInstance, never()).getField(anyString()); + verify(wrapper, never()).jsonFormatterToJson(any()); + verify(mockObjectMapper, never()).readValue(anyString(), any(Class.class)); + verify(mockPdxInstance, times(1)).getObject(); + verifyNoMoreInteractions(mockObjectMapper, mockPdxInstance); + } + + @Test + @SuppressWarnings("unchecked") + public void getObjectCallsPdxInstanceGetObjectWhenExceptionIsThrown() throws JsonProcessingException { + + Object value = new Object(); + + ObjectMapper mockObjectMapper = mock(ObjectMapper.class); + + PdxInstance mockPdxInstance = mock(PdxInstance.class); + + String json = String.format("{ \"@type\": \"%s\", \"name\": \"Checking\"}", Account.class.getName()); + + doReturn(JSONFormatter.JSON_CLASSNAME).when(mockPdxInstance).getClassName(); + doReturn(true).when(mockPdxInstance).hasField(eq(PdxInstanceWrapper.AT_TYPE_FIELD_NAME)); + doReturn(Account.class.getName()).when(mockPdxInstance).getField(eq(PdxInstanceWrapper.AT_TYPE_FIELD_NAME)); + doReturn(value).when(mockPdxInstance).getObject(); + + PdxInstanceWrapper wrapper = spy(PdxInstanceWrapper.from(mockPdxInstance)); + + assertThat(wrapper).isNotNull(); + assertThat(wrapper.getDelegate()).isEqualTo(mockPdxInstance); + + doReturn(Optional.of(mockObjectMapper)).when(wrapper).getObjectMapper(); + doReturn(json).when(wrapper).jsonFormatterToJson(eq(mockPdxInstance)); + doThrow(new JsonGenerationException("TEST", mock(JsonGenerator.class))) + .when(mockObjectMapper).readValue(anyString(), any(Class.class)); + + assertThat(wrapper.getObject()).isEqualTo(value); + + verify(wrapper, times(1)).getObjectMapper(); + verify(mockPdxInstance, times(1)).getClassName(); + verify(mockPdxInstance, times(1)).hasField(eq(PdxInstanceWrapper.AT_TYPE_FIELD_NAME)); + verify(mockPdxInstance, times(1)).getField(eq(PdxInstanceWrapper.AT_TYPE_FIELD_NAME)); + verify(wrapper, atLeastOnce()).getDelegate(); + verify(wrapper, times(1)).jsonFormatterToJson(eq(mockPdxInstance)); + verify(mockObjectMapper, times(1)).readValue(eq(json), eq(Account.class)); + verify(mockPdxInstance, times((1))).getObject(); + verifyNoMoreInteractions(mockObjectMapper, mockPdxInstance); + } + + @Test + public void getObjectCallsPdxInstanceGetObjectWhenObjectMapperIsNotPresent() { + + PdxInstance mockPdxInstance = mock(PdxInstance.class); + + doReturn("non.existing.class.Name").when(mockPdxInstance).getClassName(); + doReturn("MOCK").when(mockPdxInstance).getObject(); + + PdxInstanceWrapper wrapper = spy(PdxInstanceWrapper.from(mockPdxInstance)); + + assertThat(wrapper).isNotNull(); + assertThat(wrapper.getDelegate()).isEqualTo(mockPdxInstance); + + doReturn(Optional.empty()).when(wrapper).getObjectMapper(); + + assertThat(wrapper.getObject()).isEqualTo("MOCK"); + + verify(wrapper, atLeastOnce()).getDelegate(); + verify(wrapper, times(1)).getObjectMapper(); + verify(wrapper, never()).jsonFormatterToJson(any()); + verify(mockPdxInstance, times(1)).getObject(); + verifyNoMoreInteractions(mockPdxInstance); + } + + @Test + public void createWriterCallsPdxInstanceCreateWriter() { + + PdxInstance mockPdxInstance = mock(PdxInstance.class); + + WritablePdxInstance mockWritablePdxInstance = mock(WritablePdxInstance.class); + + doReturn(mockWritablePdxInstance).when(mockPdxInstance).createWriter(); + + assertThat(PdxInstanceWrapper.from(mockPdxInstance).createWriter()).isEqualTo(mockWritablePdxInstance); + + verify(mockPdxInstance, times(1)).createWriter(); + verifyNoMoreInteractions(mockPdxInstance); + } + + @Test + public void hasFieldCallsPdxInstanceHasField() { + + PdxInstance mockPdxInstance = mock(PdxInstance.class); + + doReturn(true).when(mockPdxInstance).hasField("name"); + + assertThat(PdxInstanceWrapper.from(mockPdxInstance).hasField("name")).isTrue(); + + verify(mockPdxInstance, times(1)).hasField(eq("name")); + verifyNoMoreInteractions(mockPdxInstance); + } + + @Test + public void sendToCallsPdxInstanceSendTo() throws IOException { + + SendablePdxInstance mockSendablePdxInstance = mock(SendablePdxInstance.class); + + PdxInstanceWrapper wrapper = PdxInstanceWrapper.from(mockSendablePdxInstance); + + DataOutput mockOut = mock(DataOutput.class); + + assertThat(wrapper).isNotNull(); + assertThat(wrapper.getDelegate()).isEqualTo(mockSendablePdxInstance); + + wrapper.sendTo(mockOut); + + verify(mockSendablePdxInstance, times(1)).sendTo(eq(mockOut)); + verifyNoMoreInteractions(mockSendablePdxInstance); + verifyNoMoreInteractions(mockOut); + } + + interface Account { + String getName(); + } + + interface SendablePdxInstance extends PdxInstance, Sendable { } + +}