From f484b07a521981f84d20697b41df48f0ccff911d Mon Sep 17 00:00:00 2001 From: Aleksandar Seovic Date: Wed, 9 May 2018 23:11:04 -0400 Subject: [PATCH 1/2] Fix for #123 - Add support for preservation of unknown properties --- .../main/java/com/owlike/genson/Genson.java | 14 +- .../java/com/owlike/genson/GensonBuilder.java | 9 +- .../owlike/genson/reflect/BeanDescriptor.java | 33 +++- .../reflect/UnknownPropertyHandler.java | 43 ++++ .../reflect/UnknownPropertyHandlerTest.java | 187 ++++++++++++++++++ pom.xml | 6 +- 6 files changed, 282 insertions(+), 10 deletions(-) create mode 100644 genson/src/main/java/com/owlike/genson/reflect/UnknownPropertyHandler.java create mode 100644 genson/src/test/java/com/owlike/genson/reflect/UnknownPropertyHandlerTest.java diff --git a/genson/src/main/java/com/owlike/genson/Genson.java b/genson/src/main/java/com/owlike/genson/Genson.java index 23cb7659..92122704 100644 --- a/genson/src/main/java/com/owlike/genson/Genson.java +++ b/genson/src/main/java/com/owlike/genson/Genson.java @@ -9,6 +9,7 @@ import com.owlike.genson.reflect.BeanDescriptor; import com.owlike.genson.reflect.BeanDescriptorProvider; import com.owlike.genson.reflect.RuntimePropertyFilter; +import com.owlike.genson.reflect.UnknownPropertyHandler; import com.owlike.genson.stream.*; /** @@ -72,6 +73,7 @@ public final class Genson { private final EncodingAwareReaderFactory readerFactory = new EncodingAwareReaderFactory(); private final Map, Object> defaultValues; private final RuntimePropertyFilter runtimePropertyFilter; + private final UnknownPropertyHandler unknownPropertyHandler; /** * The default constructor will use the default configuration provided by the {@link GensonBuilder}. @@ -81,7 +83,8 @@ public Genson() { this(_default.converterFactory, _default.beanDescriptorFactory, _default.skipNull, _default.htmlSafe, _default.aliasClassMap, _default.withClassMetadata, _default.strictDoubleParse, _default.indent, - _default.withMetadata, _default.failOnMissingProperty, _default.defaultValues, _default.runtimePropertyFilter); + _default.withMetadata, _default.failOnMissingProperty, _default.defaultValues, + _default.runtimePropertyFilter, _default.unknownPropertyHandler); } /** @@ -108,11 +111,13 @@ public Genson() { * @param failOnMissingProperty throw a JsonBindingException when a key in the json stream does not match a property in the Java Class. * @param defaultValues contains a mapping from the raw class to the default value that should be used when the property is missing. * @param runtimePropertyFilter is used to define what bean properties should be excluded from ser/de at runtime. + * @param unknownPropertyHandler is used to handle unknown properties during ser/de. */ public Genson(Factory> converterFactory, BeanDescriptorProvider beanDescProvider, boolean skipNull, boolean htmlSafe, Map> classAliases, boolean withClassMetadata, boolean strictDoubleParse, boolean indent, boolean withMetadata, boolean failOnMissingProperty, - Map, Object> defaultValues, RuntimePropertyFilter runtimePropertyFilter) { + Map, Object> defaultValues, RuntimePropertyFilter runtimePropertyFilter, + UnknownPropertyHandler unknownPropertyHandler) { this.converterFactory = converterFactory; this.beanDescriptorFactory = beanDescProvider; this.skipNull = skipNull; @@ -129,6 +134,7 @@ public Genson(Factory> converterFactory, BeanDescriptorProvider bea this.indent = indent; this.withMetadata = withClassMetadata || withMetadata; this.failOnMissingProperty = failOnMissingProperty; + this.unknownPropertyHandler = unknownPropertyHandler; } /** @@ -609,6 +615,10 @@ public RuntimePropertyFilter runtimePropertyFilter() { return runtimePropertyFilter; } + public UnknownPropertyHandler unknownPropertyHandler() { + return unknownPropertyHandler; + } + /** * @deprecated use GensonBuilder */ diff --git a/genson/src/main/java/com/owlike/genson/GensonBuilder.java b/genson/src/main/java/com/owlike/genson/GensonBuilder.java index 147712d0..5932f9c7 100644 --- a/genson/src/main/java/com/owlike/genson/GensonBuilder.java +++ b/genson/src/main/java/com/owlike/genson/GensonBuilder.java @@ -76,6 +76,7 @@ public class GensonBuilder { private final Map, Object> defaultValues = new HashMap, Object>(); private boolean failOnNullPrimitive = false; private RuntimePropertyFilter runtimePropertyFilter = RuntimePropertyFilter.noFilter; + private UnknownPropertyHandler unknownPropertyHandler; public GensonBuilder() { defaultValues.put(int.class, 0); @@ -739,6 +740,11 @@ public GensonBuilder useRuntimePropertyFilter(RuntimePropertyFilter filter) { return this; } + public GensonBuilder useUnknownPropertyHandler(UnknownPropertyHandler handler) { + this.unknownPropertyHandler = handler; + return this; + } + /** * Creates an instance of Genson. You may use this method as many times you want. It wont * change the state of the builder, in sense that the returned instance will have always the @@ -824,7 +830,8 @@ protected Genson create(Factory> converterFactory, Map> classAliases) { return new Genson(converterFactory, getBeanDescriptorProvider(), isSkipNull(), isHtmlSafe(), classAliases, withClassMetadata, - strictDoubleParse, indent, metadata, failOnMissingProperty, defaultValues, runtimePropertyFilter); + strictDoubleParse, indent, metadata, failOnMissingProperty, + defaultValues, runtimePropertyFilter, unknownPropertyHandler); } /** diff --git a/genson/src/main/java/com/owlike/genson/reflect/BeanDescriptor.java b/genson/src/main/java/com/owlike/genson/reflect/BeanDescriptor.java index 753754b9..ae92861f 100644 --- a/genson/src/main/java/com/owlike/genson/reflect/BeanDescriptor.java +++ b/genson/src/main/java/com/owlike/genson/reflect/BeanDescriptor.java @@ -4,7 +4,6 @@ import java.util.Arrays; import java.util.Collections; import java.util.Comparator; -import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -46,6 +45,8 @@ public class BeanDescriptor implements Converter { private final boolean _noArgCtr; private static final Object MISSING = new Object(); + private static final GenericType UNKNOWN = new GenericType() {}; + // Used as a cache so we just copy it instead of recreating and assigning the default values private Object[] globalCreatorArgs; @@ -86,11 +87,22 @@ public boolean isWritable() { } public void serialize(T obj, ObjectWriter writer, Context ctx) { + RuntimePropertyFilter runtimePropertyFilter = ctx.genson.runtimePropertyFilter(); + UnknownPropertyHandler unknownPropertyHandler = ctx.genson.unknownPropertyHandler(); + writer.beginObject(); - RuntimePropertyFilter runtimePropertyFilter = ctx.genson.runtimePropertyFilter(); for (PropertyAccessor accessor : accessibleProperties) { if (runtimePropertyFilter.shouldInclude(accessor, ctx)) accessor.serialize(obj, writer, ctx); } + if (unknownPropertyHandler != null) { + Map props = unknownPropertyHandler.getUnknownProperties(obj); + if (props != null) { + for (String propName : props.keySet()) { + writer.writeName(propName); + ctx.genson.serialize(props.get(propName), writer, ctx); + } + } + } writer.endObject(); } @@ -110,8 +122,10 @@ public T deserialize(ObjectReader reader, Context ctx) { } public void deserialize(T into, ObjectReader reader, Context ctx) { + RuntimePropertyFilter runtimePropertyFilter = ctx.genson.runtimePropertyFilter(); + UnknownPropertyHandler unknownPropertyHandler = ctx.genson.unknownPropertyHandler(); + reader.beginObject(); - RuntimePropertyFilter runtimePropertyFilter = ctx.genson.runtimePropertyFilter(); for (; reader.hasNext(); ) { reader.next(); String propName = reader.name(); @@ -122,6 +136,8 @@ public void deserialize(T into, ObjectReader reader, Context ctx) { } else { reader.skipValue(); } + } else if (unknownPropertyHandler != null) { + unknownPropertyHandler.onUnknownProperty(into, propName, ctx.genson.deserialize(UNKNOWN, reader, ctx)); } else if (failOnMissingProperty) throw missingPropertyException(propName); else reader.skipValue(); } @@ -133,6 +149,7 @@ protected T _deserWithCtrArgs(ObjectReader reader, Context ctx) { List names = new ArrayList(); List values = new ArrayList(); RuntimePropertyFilter runtimePropertyFilter = ctx.genson.runtimePropertyFilter(); + UnknownPropertyHandler unknownPropertyHandler = ctx.genson.unknownPropertyHandler(); reader.beginObject(); for (; reader.hasNext(); ) { @@ -148,6 +165,10 @@ protected T _deserWithCtrArgs(ObjectReader reader, Context ctx) { } else { reader.skipValue(); } + } else if (unknownPropertyHandler != null) { + Object propValue = ctx.genson.deserialize(UNKNOWN, reader, ctx); + names.add(propName); + values.add(propValue); } else if (failOnMissingProperty) throw missingPropertyException(propName); else reader.skipValue(); } @@ -175,7 +196,11 @@ protected T _deserWithCtrArgs(ObjectReader reader, Context ctx) { T bean = ofClass.cast(creator.create(creatorArgs)); for (int i = 0; i < size; i++) { PropertyMutator property = mutableProperties.get(newNames[i]); - if (property != null) property.mutate(bean, newValues[i]); + if (property != null) { + property.mutate(bean, newValues[i]); + } else if (unknownPropertyHandler != null) { + unknownPropertyHandler.onUnknownProperty(bean, newNames[i], newValues[i]); + } } reader.endObject(); return bean; diff --git a/genson/src/main/java/com/owlike/genson/reflect/UnknownPropertyHandler.java b/genson/src/main/java/com/owlike/genson/reflect/UnknownPropertyHandler.java new file mode 100644 index 00000000..e7a4cfb6 --- /dev/null +++ b/genson/src/main/java/com/owlike/genson/reflect/UnknownPropertyHandler.java @@ -0,0 +1,43 @@ +package com.owlike.genson.reflect; + +import java.util.Map; + +/** + * An interface that defines callbacks that will be called when an + * unknown properties are encountered during deserialization, as well + * as to check if there are any unknown properties that should be + * written out during serialization. + *

+ * The main purpose of this interface is to support schema evolution + * of objects that use JSON as a long term storage format, without + * loss of unknown properties across clients and severs using different + * versions of Java classes. + * + * @author Aleksandar Seovic 2018.05.09 + */ +public interface UnknownPropertyHandler { + /** + * Called whenever a property is encountered in a JSON document + * that doesn't have a corresponding {@link PropertyMutator}. + *

+ * Typically, the implementation of this interface concerned + * with schema evolution will handle this event by storing + * property value somewhere so it can be returned by the + * {@link #getUnknownProperties} method. + * + * @param target the object we are deserializing JSON into + * @param propName the name of the unknown property + * @param propValue the value of the unknown property + */ + void onUnknownProperty(Object target, String propName, Object propValue); + + /** + * Return a map of unknown properties encountered during + * deserialization, keyed by property name. + * + * @param source the object we are serializing into JSON + * + * @return a map of unknown properties + */ + Map getUnknownProperties(Object source); +} diff --git a/genson/src/test/java/com/owlike/genson/reflect/UnknownPropertyHandlerTest.java b/genson/src/test/java/com/owlike/genson/reflect/UnknownPropertyHandlerTest.java new file mode 100644 index 00000000..3bd9a406 --- /dev/null +++ b/genson/src/test/java/com/owlike/genson/reflect/UnknownPropertyHandlerTest.java @@ -0,0 +1,187 @@ +package com.owlike.genson.reflect; + +import com.owlike.genson.Genson; +import com.owlike.genson.GensonBuilder; + +import com.owlike.genson.annotation.JsonCreator; +import org.junit.Test; + +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Objects; + +import static org.junit.Assert.assertEquals; + +/** + * @author Aleksandar Seovic 2018.05.09 + */ +public class UnknownPropertyHandlerTest { + private static final Genson GENSON = new GensonBuilder() + .useClassMetadata(true) + .useConstructorWithArguments(true) + .useUnknownPropertyHandler(new EvolvableHandler()) + .useIndentation(true) + .create(); + + @Test + public void testDeserialization() { + String json = "{\n" + + " \"@class\":\"com.owlike.genson.reflect.UnknownPropertyHandlerTest$EvolvablePerson\",\n" + + " \"age\":50,\n" + + " \"name\":\"Homer\",\n" + + " \"spouse\":{\n" + + " \"@class\":\"com.owlike.genson.reflect.UnknownPropertyHandlerTest$EvolvablePerson\",\n" + + " \"age\":40,\n" + + " \"name\":\"Marge\"\n" + + " },\n" + + " \"children\":[\n" + + " \"Bart\",\n" + + " \"Lisa\",\n" + + " \"Maggie\"\n" + + " ],\n" + + " \"salary\":10000.0,\n" + + " \"donutLover\":true\n" + + "}"; + + EvolvablePerson homer = GENSON.deserialize(json, EvolvablePerson.class); + assertEquals("Homer", homer.name); + assertEquals(50, homer.age); + assertEquals(Arrays.asList("Bart", "Lisa", "Maggie"), homer.unknownProperties.get("children")); + assertEquals(10_000d, homer.unknownProperties.get("salary")); + assertEquals(true, homer.unknownProperties.get("donutLover")); + } + + @Test + public void testCtorDeserialization() { + String json = "{\n" + + " \"@class\":\"com.owlike.genson.reflect.UnknownPropertyHandlerTest$CtorEvolvablePerson\",\n" + + " \"age\":50,\n" + + " \"name\":\"Homer\",\n" + + " \"spouse\":{\n" + + " \"@class\":\"com.owlike.genson.reflect.UnknownPropertyHandlerTest$CtorEvolvablePerson\",\n" + + " \"age\":40,\n" + + " \"name\":\"Marge\"\n" + + " },\n" + + " \"children\":[\n" + + " \"Bart\",\n" + + " \"Lisa\",\n" + + " \"Maggie\"\n" + + " ],\n" + + " \"salary\":10000.0,\n" + + " \"donutLover\":true\n" + + "}"; + + EvolvablePerson homer = GENSON.deserialize(json, CtorEvolvablePerson.class); + assertEquals("Homer", homer.name); + assertEquals(50, homer.age); + assertEquals(Arrays.asList("Bart", "Lisa", "Maggie"), homer.unknownProperties.get("children")); + assertEquals(10_000d, homer.unknownProperties.get("salary")); + assertEquals(true, homer.unknownProperties.get("donutLover")); + } + + @Test + public void testRoundTrip() { + EvolvablePerson homer = new EvolvablePerson("Homer", 50); + homer.unknownProperties().put("spouse", new EvolvablePerson("Marge", 40)); + homer.unknownProperties().put("children", Arrays.asList("Bart", "Lisa", "Maggie")); + homer.unknownProperties().put("salary", 10_000d); + homer.unknownProperties().put("donutLover", true); + + String json = GENSON.serialize(homer); + EvolvablePerson homer2 = GENSON.deserialize(json, EvolvablePerson.class); + + assertEquals(homer, homer2); + } + + interface Evolvable { + Map unknownProperties(); + } + + static class EvolvableHandler implements UnknownPropertyHandler { + + @Override + public void onUnknownProperty(Object target, String propName, Object propValue) { + if (target instanceof Evolvable) { + ((Evolvable) target).unknownProperties().put(propName, propValue); + } + } + + @Override + public Map getUnknownProperties(Object source) { + return source instanceof Evolvable + ? ((Evolvable) source).unknownProperties() + : null; + } + } + + static class EvolvablePerson implements Evolvable { + private Map unknownProperties = new LinkedHashMap(); + private String name; + private int age; + + public EvolvablePerson() { + } + + public EvolvablePerson(String name, int age) { + this.name = name; + this.age = age; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public int getAge() { + return age; + } + + public void setAge(int age) { + this.age = age; + } + + @Override + public Map unknownProperties() { + return unknownProperties; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + EvolvablePerson that = (EvolvablePerson) o; + return age == that.age && + Objects.equals(unknownProperties, that.unknownProperties) && + Objects.equals(name, that.name); + } + + @Override + public int hashCode() { + return Objects.hash(unknownProperties, name, age); + } + + @Override + public String toString() { + return "EvolvablePerson{" + + "name='" + name + '\'' + + ", age=" + age + + ", unknownProperties=" + unknownProperties + + '}'; + } + } + + static class CtorEvolvablePerson extends EvolvablePerson { + private CtorEvolvablePerson() { + throw new RuntimeException("shouldn't be called"); + } + + @JsonCreator + public CtorEvolvablePerson(String name, int age) { + super(name, age); + } + } +} diff --git a/pom.xml b/pom.xml index b43af8f4..dc05d08a 100644 --- a/pom.xml +++ b/pom.xml @@ -51,7 +51,7 @@ UTF-8 - 1.6 + 1.8 @@ -174,9 +174,9 @@ Required to build using JDK 8, unless all JavaDoc is revamped to handle the stricter DocLint enabled within JDK 8. --> - + From 25456ed1343fe4cba64ea8932f85ab3b2ac6d2bf Mon Sep 17 00:00:00 2001 From: Aleksandar Seovic Date: Wed, 9 May 2018 23:11:04 -0400 Subject: [PATCH 2/2] Fix for #123 - Add support for preservation of unknown properties --- pom.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pom.xml b/pom.xml index dc05d08a..c415c877 100644 --- a/pom.xml +++ b/pom.xml @@ -51,7 +51,7 @@ UTF-8 - 1.8 + 1.7 @@ -174,9 +174,9 @@ Required to build using JDK 8, unless all JavaDoc is revamped to handle the stricter DocLint enabled within JDK 8. --> - +