Skip to content

Commit

Permalink
Fix for owlike#123: Initial refactoring based on PR feedback
Browse files Browse the repository at this point in the history
  • Loading branch information
aseovic committed May 20, 2018
1 parent ed60adc commit fa5ab83
Show file tree
Hide file tree
Showing 8 changed files with 398 additions and 15 deletions.
14 changes: 12 additions & 2 deletions genson/src/main/java/com/owlike/genson/Genson.java
Original file line number Diff line number Diff line change
Expand Up @@ -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.*;

/**
Expand Down Expand Up @@ -72,6 +73,7 @@ public final class Genson {
private final EncodingAwareReaderFactory readerFactory = new EncodingAwareReaderFactory();
private final Map<Class<?>, Object> defaultValues;
private final RuntimePropertyFilter runtimePropertyFilter;
private final UnknownPropertyHandler unknownPropertyHandler;

/**
* The default constructor will use the default configuration provided by the {@link GensonBuilder}.
Expand All @@ -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);
}

/**
Expand All @@ -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<Converter<?>> converterFactory, BeanDescriptorProvider beanDescProvider,
boolean skipNull, boolean htmlSafe, Map<String, Class<?>> classAliases, boolean withClassMetadata,
boolean strictDoubleParse, boolean indent, boolean withMetadata, boolean failOnMissingProperty,
Map<Class<?>, Object> defaultValues, RuntimePropertyFilter runtimePropertyFilter) {
Map<Class<?>, Object> defaultValues, RuntimePropertyFilter runtimePropertyFilter,
UnknownPropertyHandler unknownPropertyHandler) {
this.converterFactory = converterFactory;
this.beanDescriptorFactory = beanDescProvider;
this.skipNull = skipNull;
Expand All @@ -129,6 +134,7 @@ public Genson(Factory<Converter<?>> converterFactory, BeanDescriptorProvider bea
this.indent = indent;
this.withMetadata = withClassMetadata || withMetadata;
this.failOnMissingProperty = failOnMissingProperty;
this.unknownPropertyHandler = unknownPropertyHandler;
}

/**
Expand Down Expand Up @@ -609,6 +615,10 @@ public RuntimePropertyFilter runtimePropertyFilter() {
return runtimePropertyFilter;
}

public UnknownPropertyHandler unknownPropertyHandler() {
return unknownPropertyHandler;
}

/**
* @deprecated use GensonBuilder
*/
Expand Down
9 changes: 8 additions & 1 deletion genson/src/main/java/com/owlike/genson/GensonBuilder.java
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ public class GensonBuilder {
private final Map<Class<?>, Object> defaultValues = new HashMap<Class<?>, Object>();
private boolean failOnNullPrimitive = false;
private RuntimePropertyFilter runtimePropertyFilter = RuntimePropertyFilter.noFilter;
private UnknownPropertyHandler unknownPropertyHandler;

public GensonBuilder() {
defaultValues.put(int.class, 0);
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -824,7 +830,8 @@ protected Genson create(Factory<Converter<?>> converterFactory,
Map<String, Class<?>> classAliases) {
return new Genson(converterFactory, getBeanDescriptorProvider(),
isSkipNull(), isHtmlSafe(), classAliases, withClassMetadata,
strictDoubleParse, indent, metadata, failOnMissingProperty, defaultValues, runtimePropertyFilter);
strictDoubleParse, indent, metadata, failOnMissingProperty,
defaultValues, runtimePropertyFilter, unknownPropertyHandler);
}

/**
Expand Down
40 changes: 28 additions & 12 deletions genson/src/main/java/com/owlike/genson/reflect/BeanDescriptor.java
Original file line number Diff line number Diff line change
@@ -1,12 +1,7 @@
package com.owlike.genson.reflect;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.*;
import java.util.function.Consumer;

import com.owlike.genson.*;
import com.owlike.genson.reflect.BeanCreator.BeanCreatorProperty;
Expand Down Expand Up @@ -46,6 +41,7 @@ public class BeanDescriptor<T> implements Converter<T> {
private final boolean _noArgCtr;

private static final Object MISSING = new Object();

// Used as a cache so we just copy it instead of recreating and assigning the default values
private Object[] globalCreatorArgs;

Expand Down Expand Up @@ -86,11 +82,16 @@ 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) {
unknownPropertyHandler.writeUnknownProperties(obj, writer, ctx);
}
writer.endObject();
}

Expand All @@ -110,8 +111,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();
Expand All @@ -122,6 +125,8 @@ public void deserialize(T into, ObjectReader reader, Context ctx) {
} else {
reader.skipValue();
}
} else if (unknownPropertyHandler != null) {
unknownPropertyHandler.onUnknownProperty(into, propName, reader, ctx);
} else if (failOnMissingProperty) throw missingPropertyException(propName);
else reader.skipValue();
}
Expand All @@ -130,9 +135,12 @@ public void deserialize(T into, ObjectReader reader, Context ctx) {


protected T _deserWithCtrArgs(ObjectReader reader, Context ctx) {
List<String> names = new ArrayList<String>();
List<Object> values = new ArrayList<Object>();
List<String> names = new ArrayList<>();
List<Object> values = new ArrayList<>();
List<Consumer<T>> unknownProperties = new ArrayList<>();

RuntimePropertyFilter runtimePropertyFilter = ctx.genson.runtimePropertyFilter();
UnknownPropertyHandler unknownPropertyHandler = ctx.genson.unknownPropertyHandler();

reader.beginObject();
for (; reader.hasNext(); ) {
Expand All @@ -148,6 +156,9 @@ protected T _deserWithCtrArgs(ObjectReader reader, Context ctx) {
} else {
reader.skipValue();
}
} else if (unknownPropertyHandler != null) {
Consumer<T> callback = unknownPropertyHandler.onUnknownProperty(null, propName, reader, ctx);
unknownProperties.add(callback);
} else if (failOnMissingProperty) throw missingPropertyException(propName);
else reader.skipValue();
}
Expand Down Expand Up @@ -175,7 +186,12 @@ 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]);
}
}
if (!unknownProperties.isEmpty()) {
unknownProperties.forEach(callback -> callback.accept(bean));
}
reader.endObject();
return bean;
Expand Down
30 changes: 30 additions & 0 deletions genson/src/main/java/com/owlike/genson/reflect/Evolvable.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package com.owlike.genson.reflect;

import java.util.Map;

/**
* An interface that can be implemented by data classes
* in order to support schema evolution.
* <p>
* This interface is used in combination with {@link EvolvableHandler}
* in order to prevent data loss during serialization across different
* versions of data classes.
*
* @author Aleksandar Seovic 2018.05.20
*/
interface Evolvable {
/**
* Add unknown property to this instance.
*
* @param propName property name
* @param propValue property value
*/
void addUnknownProperty(String propName, Object propValue);

/**
* Return a map of unknown properties.
*
* @return a map of unknown properties
*/
Map<String, Object> unknownProperties();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package com.owlike.genson.reflect;

import com.owlike.genson.Context;
import com.owlike.genson.GenericType;
import com.owlike.genson.stream.ObjectReader;
import com.owlike.genson.stream.ObjectWriter;

import java.util.Map;
import java.util.function.Consumer;

/**
* An implementation of an {@link UnknownPropertyHandler} that supports
* evolution of data classes via {@link Evolvable} interface.
* <p>
* If the target object we are deserializing into is {@link Evolvable},
* this handler will add any unknown properties encountered during
* deserialization into {@link Evolvable#unknownProperties()} map,
* and will write them out along with all known properties during
* subsequent serialization.
* <p>
* This prevents data loss when serializing and deserializing the same
* JSON payload using different versions of Java data classes.
*
* @author Aleksandar Seovic 2018.05.20
*/
public class EvolvableHandler implements UnknownPropertyHandler {
private static final GenericType<Object> UNKNOWN = new GenericType<Object>() {};

@Override
public <T> Consumer<T> onUnknownProperty(T target, String propName, ObjectReader reader, Context ctx) {
// TODO: change this to read property as an opaque value, using ObjectReader directly
Object propValue = ctx.genson.deserialize(UNKNOWN, reader, ctx);

if (target == null) {
// this is a bit ugly...
// the issue is that we may not have a target object while parsing JSON when using creators,
// so we need to store the parsed value somewhere and apply it later
return objTarget -> {
if (objTarget instanceof Evolvable) {
((Evolvable) objTarget).addUnknownProperty(propName, propValue);
}
};
}

if (target instanceof Evolvable) {
((Evolvable) target).addUnknownProperty(propName, propValue);
}
return null;
}

@Override
public <T> void writeUnknownProperties(T source, ObjectWriter writer, Context ctx) {
if (source instanceof Evolvable) {
Map<String, Object> props = ((Evolvable) source).unknownProperties();
if (props != null) {
for (String propName : props.keySet()) {
writer.writeName(propName);
// TODO: change this to write property as an opaque value, using ObjectWriter directly
ctx.genson.serialize(props.get(propName), writer, ctx);
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.owlike.genson.reflect;

import com.owlike.genson.annotation.JsonIgnore;

import java.util.HashMap;
import java.util.Map;

/**
* Convenience base class for {@link Evolvable} data classes.
*
* @author Aleksandar Seovic 2018.05.20
*/
public abstract class EvolvableObject implements Evolvable {
@JsonIgnore
private Map<String, Object> unknownProperties;

@Override
public void addUnknownProperty(String propName, Object propValue) {
if (unknownProperties == null) {
unknownProperties = new HashMap<>();
}
unknownProperties.put(propName, propValue);
}

@Override
public Map<String, Object> unknownProperties() {
return unknownProperties;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package com.owlike.genson.reflect;

import com.owlike.genson.Context;
import com.owlike.genson.stream.ObjectReader;
import com.owlike.genson.stream.ObjectWriter;

import java.util.function.Consumer;

/**
* 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.
* <p>
* 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}.
* <p>
* Typically, the implementation of this interface concerned
* with schema evolution will handle this event by storing
* property value somewhere so it can be written later by the
* {@link #writeUnknownProperties} method.
*
* @param target the object we are deserializing JSON into, if known
* @param propName the name of the unknown property
* @param reader the ObjectReader to read property value from
* @param ctx deserialization context
*
* @return the optional Consumer that will be called once the target object is known
*/
<T> Consumer<T> onUnknownProperty(T target, String propName, ObjectReader reader, Context ctx);

/**
* Write unknown properties encountered during deserialization.
* <p>
* This method can be optionally implemented by {@code UnknownPropertyHandler}s
* that want to write unknown properties during serialization. The default
* implementation is a no-op.
*
* @param source the object we are serializing into JSON
* @param writer the ObjectReader to read property value from
* @param ctx serialization context
*
* @return a map of unknown properties
*/
default <T> void writeUnknownProperties(T source, ObjectWriter writer, Context ctx) {
}
}
Loading

0 comments on commit fa5ab83

Please sign in to comment.