Skip to content

Commit

Permalink
Support property naming strategy for Guava Ranges (#57)
Browse files Browse the repository at this point in the history
Implement #56, support for naming strategy for `Range` values.
  • Loading branch information
philleonard authored and cowtowncoder committed Sep 5, 2019
1 parent 43af85c commit 400f5be
Show file tree
Hide file tree
Showing 3 changed files with 150 additions and 29 deletions.
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
package com.fasterxml.jackson.datatype.guava.deser;

import static java.util.Arrays.asList;

import java.io.IOException;
import java.util.Arrays;

import com.google.common.base.Preconditions;
import com.google.common.collect.BoundType;
Expand All @@ -15,6 +16,8 @@
import com.fasterxml.jackson.databind.type.TypeFactory;

import com.fasterxml.jackson.datatype.guava.deser.util.RangeFactory;
import java.util.HashMap;
import java.util.Map;

/**
* Jackson deserializer for a Guava {@link Range}.
Expand All @@ -33,6 +36,8 @@ public class RangeDeserializer

protected final JsonDeserializer<Object> _endpointDeserializer;

private final Map<String, String> _fieldNames;

private BoundType _defaultBoundType;

/*
Expand All @@ -48,27 +53,35 @@ public class RangeDeserializer
public RangeDeserializer(JavaType rangeType) {
this(null, rangeType);
}

public RangeDeserializer(BoundType defaultBoundType, JavaType rangeType) {
this(rangeType, null);
this(rangeType, null, new HashMap<>());
_defaultBoundType = defaultBoundType;
}

@SuppressWarnings("unchecked")
public RangeDeserializer(JavaType rangeType, JsonDeserializer<?> endpointDeser)
public RangeDeserializer(JavaType rangeType, JsonDeserializer<?> endpointDeser, Map<String, String> fieldNames)
{
super(rangeType);
_rangeType = rangeType;
_endpointDeserializer = (JsonDeserializer<Object>) endpointDeser;
_fieldNames = fieldNames;
}

@SuppressWarnings("unchecked")
public RangeDeserializer(JavaType rangeType, JsonDeserializer<?> endpointDeser, BoundType defaultBoundType)
{
this(rangeType, endpointDeser, defaultBoundType, new HashMap<>());
}

@SuppressWarnings("unchecked")
public RangeDeserializer(JavaType rangeType, JsonDeserializer<?> endpointDeser, BoundType defaultBoundType, Map<String, String> fieldNames)
{
super(rangeType);
_rangeType = rangeType;
_endpointDeserializer = (JsonDeserializer<Object>) endpointDeser;
_defaultBoundType = defaultBoundType;
_fieldNames = fieldNames;
}

@Override
Expand All @@ -78,23 +91,40 @@ public RangeDeserializer(JavaType rangeType, JsonDeserializer<?> endpointDeser,
public JsonDeserializer<?> createContextual(DeserializationContext ctxt,
BeanProperty property) throws JsonMappingException
{
PropertyNamingStrategy propertyNamingStrategy = ctxt.getConfig().getPropertyNamingStrategy();
if (_endpointDeserializer == null) {
JavaType endpointType = _rangeType.containedType(0);
if (endpointType == null) { // should this ever occur?
endpointType = TypeFactory.unknownType();
}
JsonDeserializer<Object> deser = ctxt.findContextualValueDeserializer(endpointType, property);
return new RangeDeserializer(_rangeType, deser, _defaultBoundType);
if (propertyNamingStrategy != null) {
return new RangeDeserializer(_rangeType, deser, _defaultBoundType, getPropertyNames(ctxt, propertyNamingStrategy));
} else {
return new RangeDeserializer(_rangeType, deser, _defaultBoundType);
}
}
else if (propertyNamingStrategy != null) {
return new RangeDeserializer(_rangeType, _endpointDeserializer, _defaultBoundType, getPropertyNames(ctxt, propertyNamingStrategy));
}
return this;
}

private Map<String, String> getPropertyNames(DeserializationContext ctxt, PropertyNamingStrategy propertyNamingStrategy) {
DeserializationConfig config = ctxt.getConfig();
HashMap<String, String> fieldNames = new HashMap<>();
for (String field : asList("lowerEndpoint", "upperEndpoint", "lowerBoundType", "upperBoundType")) {
fieldNames.put(field, propertyNamingStrategy.nameForField(config, null, field));
}
return fieldNames;
}

/*
/**********************************************************
/* Actual deserialization
/**********************************************************
*/

@Override
public Object deserializeWithType(JsonParser jp, DeserializationContext ctxt,
TypeDeserializer typeDeserializer)
Expand Down Expand Up @@ -122,16 +152,16 @@ public Range<?> deserialize(JsonParser p, DeserializationContext context)
expect(context, JsonToken.FIELD_NAME, t);
String fieldName = p.currentName();
try {
if (fieldName.equals("lowerEndpoint")) {
if (fieldName.equals(getFieldName("lowerEndpoint"))) {
p.nextToken();
lowerEndpoint = deserializeEndpoint(context, p);
} else if (fieldName.equals("upperEndpoint")) {
} else if (fieldName.equals(getFieldName("upperEndpoint"))) {
p.nextToken();
upperEndpoint = deserializeEndpoint(context, p);
} else if (fieldName.equals("lowerBoundType")) {
} else if (fieldName.equals(getFieldName("lowerBoundType"))) {
p.nextToken();
lowerBoundType = deserializeBoundType(context, p);
} else if (fieldName.equals("upperBoundType")) {
} else if (fieldName.equals(getFieldName("upperBoundType"))) {
p.nextToken();
upperBoundType = deserializeBoundType(context, p);
} else {
Expand All @@ -148,19 +178,32 @@ public Range<?> deserialize(JsonParser p, DeserializationContext context)
try {
if ((lowerEndpoint != null) && (upperEndpoint != null)) {
Preconditions.checkState(lowerEndpoint.getClass() == upperEndpoint.getClass(),
"Endpoint types are not the same - 'lowerEndpoint' deserialized to [%s], and 'upperEndpoint' deserialized to [%s].",
"Endpoint types are not the same - '%s' deserialized to [%s], and '%s' deserialized to [%s].",
getFieldName("lowerEndpoint"),
lowerEndpoint.getClass().getName(),
getFieldName("upperEndpoint"),
upperEndpoint.getClass().getName());
Preconditions.checkState(lowerBoundType != null, "'lowerEndpoint' field found, but not 'lowerBoundType'");
Preconditions.checkState(upperBoundType != null, "'upperEndpoint' field found, but not 'upperBoundType'");
Preconditions.checkState(lowerBoundType != null,
"'%s' field found, but not '%s'",
getFieldName("lowerEndpoint"),
getFieldName("lowerBoundType"));
Preconditions.checkState(upperBoundType != null,
"'%s' field found, but not '%s'",
getFieldName("upperEndpoint"),
getFieldName("upperBoundType"));
return RangeFactory.range(lowerEndpoint, lowerBoundType, upperEndpoint, upperBoundType);
}
if (lowerEndpoint != null) {
Preconditions.checkState(lowerBoundType != null, "'lowerEndpoint' field found, but not 'lowerBoundType'");
Preconditions.checkState(lowerBoundType != null,
"'%s' field found, but not '%s'",
getFieldName("lowerEndpoint"),
getFieldName("lowerBoundType"));
return RangeFactory.downTo(lowerEndpoint, lowerBoundType);
}
if (upperEndpoint != null) {
Preconditions.checkState(upperBoundType != null, "'upperEndpoint' field found, but not 'upperBoundType'");
Preconditions.checkState(upperBoundType != null,
"'%s' field found, but not '%s'",
getFieldName("lowerEndpoint"));
return RangeFactory.upTo(upperEndpoint, upperBoundType);
}
return RangeFactory.all();
Expand All @@ -169,6 +212,11 @@ public Range<?> deserialize(JsonParser p, DeserializationContext context)
}
}

private String getFieldName(String fieldName) {
String name = _fieldNames.get(fieldName);
return name == null ? fieldName : name;
}

private BoundType deserializeBoundType(DeserializationContext context, JsonParser p) throws IOException
{
expect(context, JsonToken.VALUE_STRING, p.currentToken());
Expand All @@ -178,7 +226,7 @@ private BoundType deserializeBoundType(DeserializationContext context, JsonParse
} catch (IllegalArgumentException e) {
return (BoundType) context.handleWeirdStringValue(BoundType.class, name,
"not a valid BoundType name (should be one oF: %s)",
Arrays.asList(BoundType.values()));
asList(BoundType.values()));
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.fasterxml.jackson.datatype.guava.ser;

import static java.util.Arrays.asList;

import java.io.IOException;

import com.fasterxml.jackson.core.*;
Expand All @@ -12,6 +14,8 @@

import com.google.common.collect.BoundType;
import com.google.common.collect.Range;
import java.util.HashMap;
import java.util.Map;

/**
* Jackson serializer for a Guava {@link Range}.
Expand All @@ -23,6 +27,8 @@ public class RangeSerializer extends StdSerializer<Range<?>>

protected final JsonSerializer<Object> _endpointSerializer;

private final Map<String, String> _fieldNames;

/*
/**********************************************************
/* Life-cycle
Expand All @@ -31,12 +37,17 @@ public class RangeSerializer extends StdSerializer<Range<?>>

public RangeSerializer(JavaType type) { this(type, null); }

public RangeSerializer(JavaType type, JsonSerializer<?> endpointSer) {
this(type, endpointSer, new HashMap<>());
}

@SuppressWarnings("unchecked")
public RangeSerializer(JavaType type, JsonSerializer<?> endpointSer)
public RangeSerializer(JavaType type, JsonSerializer<?> endpointSer, Map<String, String> fieldNames)
{
super(type);
_rangeType = type;
_endpointSerializer = (JsonSerializer<Object>) endpointSer;
_fieldNames = fieldNames;
}

@Override
Expand All @@ -48,12 +59,17 @@ public boolean isEmpty(SerializerProvider prov, Range<?> value) {
public JsonSerializer<?> createContextual(SerializerProvider prov,
BeanProperty property) throws JsonMappingException
{
PropertyNamingStrategy propertyNamingStrategy = prov.getConfig().getPropertyNamingStrategy();
if (_endpointSerializer == null) {
JavaType endpointType = _rangeType.containedTypeOrUnknown(0);
// let's not consider "untyped" (java.lang.Object) to be meaningful here...
if (endpointType != null && !endpointType.hasRawClass(Object.class)) {
JsonSerializer<?> ser = prov.findSecondaryPropertySerializer(endpointType, property);
return new RangeSerializer(_rangeType, ser);
if (propertyNamingStrategy != null) {
return new RangeSerializer(_rangeType, ser, getPropertyNames(prov.getConfig(), propertyNamingStrategy));
} else {
return new RangeSerializer(_rangeType, ser);
}
}
/* 21-Sep-2014, tatu: Need to make sure all serializers get proper contextual
* access, in case they rely on annotations on properties... (or, more generally,
Expand All @@ -62,12 +78,28 @@ public JsonSerializer<?> createContextual(SerializerProvider prov,
} else {
JsonSerializer<?> cs = _endpointSerializer.createContextual(prov, property);
if (cs != _endpointSerializer) {
return new RangeSerializer(_rangeType, cs);
if (propertyNamingStrategy != null) {
return new RangeSerializer(_rangeType, cs, getPropertyNames(prov.getConfig(), propertyNamingStrategy));
} else {
return new RangeSerializer(_rangeType, cs);
}
}
}
if (propertyNamingStrategy != null) {
return new RangeSerializer(_rangeType, _endpointSerializer, getPropertyNames(prov.getConfig(), propertyNamingStrategy));
}
return this;
}

private Map<String, String> getPropertyNames(SerializationConfig config, PropertyNamingStrategy propertyNamingStrategy) {
final HashMap<String, String> fieldNames = new HashMap<>();
for (String field : asList("lowerEndpoint", "upperEndpoint", "lowerBoundType", "upperBoundType")) {
fieldNames.put(field, propertyNamingStrategy.nameForField(config, null, field));
}
return fieldNames;
}


/*
/**********************************************************
/* Serialization methods
Expand Down Expand Up @@ -101,27 +133,32 @@ private void _writeContents(Range<?> value, JsonGenerator g, SerializerProvider
{
if (value.hasLowerBound()) {
if (_endpointSerializer != null) {
g.writeFieldName("lowerEndpoint");
g.writeFieldName(getFieldName("lowerEndpoint"));
_endpointSerializer.serialize(value.lowerEndpoint(), g, provider);
} else {
provider.defaultSerializeField("lowerEndpoint", value.lowerEndpoint(), g);
provider.defaultSerializeField(getFieldName("lowerEndpoint"), value.lowerEndpoint(), g);
}
// 20-Mar-2016, tatu: Should not use default handling since it leads to
// [datatypes-collections#12] with default typing
g.writeStringField("lowerBoundType", value.lowerBoundType().name());
g.writeStringField(getFieldName("lowerBoundType"), value.lowerBoundType().name());
}
if (value.hasUpperBound()) {
if (_endpointSerializer != null) {
g.writeFieldName("upperEndpoint");
g.writeFieldName(getFieldName("upperEndpoint"));
_endpointSerializer.serialize(value.upperEndpoint(), g, provider);
} else {
provider.defaultSerializeField("upperEndpoint", value.upperEndpoint(), g);
provider.defaultSerializeField(getFieldName("upperEndpoint"), value.upperEndpoint(), g);
}
// same as above; should always be just String so
g.writeStringField("upperBoundType", value.upperBoundType().name());
g.writeStringField(getFieldName("upperBoundType"), value.upperBoundType().name());
}
}

private String getFieldName(String fieldName) {
String name = _fieldNames.get(fieldName);
return name == null ? fieldName : name;
}

@Override
public void acceptJsonFormatVisitor(JsonFormatVisitorWrapper visitor, JavaType typeHint)
throws JsonMappingException
Expand All @@ -135,10 +172,10 @@ public void acceptJsonFormatVisitor(JsonFormatVisitorWrapper visitor, JavaType t
// should probably keep track of `property`...
JsonSerializer<?> btSer = visitor.getProvider()
.findSecondaryPropertySerializer(btType, null);
objectVisitor.property("lowerEndpoint", _endpointSerializer, endpointType);
objectVisitor.property("lowerBoundType", btSer, btType);
objectVisitor.property("upperEndpoint", _endpointSerializer, endpointType);
objectVisitor.property("upperBoundType", btSer, btType);
objectVisitor.property(getFieldName("lowerEndpoint"), _endpointSerializer, endpointType);
objectVisitor.property(getFieldName("lowerBoundType"), btSer, btType);
objectVisitor.property(getFieldName("upperEndpoint"), _endpointSerializer, endpointType);
objectVisitor.property(getFieldName("upperBoundType"), btSer, btType);
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import com.fasterxml.jackson.databind.DefaultTyping;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.PropertyNamingStrategy;
import com.fasterxml.jackson.databind.json.JsonMapper;
import com.fasterxml.jackson.datatype.guava.deser.util.RangeFactory;
import com.google.common.collect.BoundType;
Expand Down Expand Up @@ -63,6 +64,21 @@ public void testSerialization() throws Exception
testSerialization(MAPPER, RangeFactory.singleton(1));
}

public void testSerializationWithPropertyNamingStrategy() throws Exception
{
ObjectMapper mappper = builderWithModule().propertyNamingStrategy(PropertyNamingStrategy.SNAKE_CASE).build();
testSerialization(mappper, RangeFactory.open(1, 10));
testSerialization(mappper, RangeFactory.openClosed(1, 10));
testSerialization(mappper, RangeFactory.closedOpen(1, 10));
testSerialization(mappper, RangeFactory.closed(1, 10));
testSerialization(mappper, RangeFactory.atLeast(1));
testSerialization(mappper, RangeFactory.greaterThan(1));
testSerialization(mappper, RangeFactory.atMost(10));
testSerialization(mappper, RangeFactory.lessThan(10));
testSerialization(mappper, RangeFactory.all());
testSerialization(mappper, RangeFactory.singleton(1));
}

public void testWrappedSerialization() throws Exception
{
testSerializationWrapped(MAPPER, RangeFactory.open(1, 10));
Expand Down Expand Up @@ -258,6 +274,26 @@ public void testDefaultBoundTypeBothBoundTypesClosedWithOpenConfigured() throws
assertEquals(BoundType.CLOSED, r.upperBoundType());
}

public void testSnakeCaseNamingStrategy() throws Exception
{
String json = "{\"lower_endpoint\": 12, \"lower_bound_type\": \"CLOSED\", \"upper_endpoint\": 33, \"upper_bound_type\": \"CLOSED\"}";

GuavaModule mod = new GuavaModule().defaultBoundType(BoundType.CLOSED);
ObjectMapper mapper = JsonMapper.builder()
.addModule(mod)
.propertyNamingStrategy(PropertyNamingStrategy.SNAKE_CASE)
.build();

@SuppressWarnings("unchecked")
Range<Integer> r = (Range<Integer>) mapper.readValue(json, Range.class);

assertEquals(Integer.valueOf(12), r.lowerEndpoint());
assertEquals(Integer.valueOf(33), r.upperEndpoint());

assertEquals(BoundType.CLOSED, r.lowerBoundType());
assertEquals(BoundType.CLOSED, r.upperBoundType());
}

// [datatypes-collections#12]
public void testRangeWithDefaultTyping() throws Exception
{
Expand Down

0 comments on commit 400f5be

Please sign in to comment.