RangeDeserializer.java
package tools.jackson.datatype.guava.deser;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.regex.Pattern;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.google.common.collect.BoundType;
import com.google.common.collect.Range;
import tools.jackson.core.*;
import tools.jackson.databind.*;
import tools.jackson.databind.deser.std.StdDeserializer;
import tools.jackson.databind.jsontype.TypeDeserializer;
import tools.jackson.databind.type.LogicalType;
import tools.jackson.databind.util.ClassUtil;
import tools.jackson.datatype.guava.deser.util.RangeFactory;
import tools.jackson.datatype.guava.deser.util.RangeHelper;
/**
* Jackson deserializer for a Guava {@link Range}.
*<p>
* TODO: I think it would make sense to re-implement this deserializer to
* use Delegating Deserializer, using a POJO as an intermediate form (properties
* could be of type {@link java.lang.Object})
* This would also also simplify the implementation a bit.
*/
public class RangeDeserializer
extends StdDeserializer<Range<?>>
{
protected final static Pattern PATTERN_DOUBLE_DOT = Pattern.compile("\\.\\.");
protected final JavaType _rangeType;
/**
* Deserializer used when getting JSON Object representation
*/
protected final ValueDeserializer<Object> _endpointDeserializer;
/**
* Deserializer used when getting JSON String (inline) representation.
*
* @since 2.17
*/
protected final KeyDeserializer _fromStringDeserializer;
protected final BoundType _defaultBoundType;
protected final RangeHelper.RangeProperties _fieldNames;
/**
* @since 2.17
*/
protected final JsonFormat.Shape _shape;
/*
/**********************************************************************
/* Life-cycle
/**********************************************************************
*/
public RangeDeserializer(JavaType rangeType, BoundType defaultBoundType) {
this(rangeType, null, defaultBoundType, RangeHelper.standardNames(),
JsonFormat.Shape.ANY, null);
}
@SuppressWarnings("unchecked")
protected RangeDeserializer(JavaType rangeType, ValueDeserializer<?> endpointDeser,
BoundType defaultBoundType, RangeHelper.RangeProperties fieldNames,
JsonFormat.Shape shape, KeyDeserializer fromStringDeserializer)
{
super(rangeType);
_rangeType = rangeType;
_endpointDeserializer = (ValueDeserializer<Object>) endpointDeser;
_fromStringDeserializer = fromStringDeserializer;
_defaultBoundType = defaultBoundType;
_fieldNames = fieldNames;
_shape = shape;
}
@Override
public ValueDeserializer<?> createContextual(DeserializationContext ctxt,
BeanProperty property)
{
final JsonFormat.Value format = findFormatOverrides(ctxt, property, handledType());
final JsonFormat.Shape shape = format.getShape();
final RangeHelper.RangeProperties fieldNames = RangeHelper.getPropertyNames(ctxt.getConfig(),
ctxt.getConfig().getPropertyNamingStrategy());
final JavaType endpointType = _rangeType.containedTypeOrUnknown(0);
ValueDeserializer<?> deser = _endpointDeserializer;
if (deser == null) {
// `Range<?>` resolves to the `Comparable` bound, which cannot be deserialized
// as such: read endpoints as "natural" types instead (which are `Comparable`)
deser = ctxt.findContextualValueDeserializer(endpointType.hasRawClass(Comparable.class)
? ctxt.constructType(Object.class) : endpointType, property);
} else {
// 04-Sep-2019, tatu: If we already have a deserialize, should contextualize, right?
deser = deser.createContextual(ctxt, property);
}
KeyDeserializer kd = _fromStringDeserializer;
if (shape == JsonFormat.Shape.STRING) {
kd = ctxt.findKeyDeserializer(endpointType, property);
}
if ((deser != _endpointDeserializer) || (fieldNames != _fieldNames)
|| (shape != _shape) || (kd != _fromStringDeserializer)) {
return new RangeDeserializer(_rangeType, deser, _defaultBoundType, fieldNames,
shape, kd);
}
return this;
}
/*
/**********************************************************************
/* Standard accessors
/**********************************************************************
*/
@Override
public JavaType getValueType() { return _rangeType; }
@Override // since 2.12
public LogicalType logicalType() {
// 30-May-2020, tatu: Not 100% sure, but looks a bit like POJO so...
return LogicalType.POJO;
}
/*
/**********************************************************************
/* Actual deserialization
/**********************************************************************
*/
@Override
public Object deserializeWithType(JsonParser p, DeserializationContext ctxt,
TypeDeserializer typeDeserializer)
throws JacksonException
{
return typeDeserializer.deserializeTypedFromObject(p, ctxt);
}
@Override
public Range<?> deserialize(JsonParser p, DeserializationContext ctxt)
throws JacksonException
{
// NOTE: either START_OBJECT _or_ PROPERTY_NAME fine; latter for polymorphic cases
JsonToken t = p.currentToken();
if (t == JsonToken.START_OBJECT) {
t = p.nextToken();
}
Comparable<?> lowerEndpoint = null;
Comparable<?> upperEndpoint = null;
BoundType lowerBoundType = _defaultBoundType;
BoundType upperBoundType = _defaultBoundType;
if (_shape == JsonFormat.Shape.STRING) {
expect(ctxt, JsonToken.VALUE_STRING, t);
return deserializeRangeFromString(ctxt, p);
}
for (; t != JsonToken.END_OBJECT; t = p.nextToken()) {
expect(ctxt, JsonToken.PROPERTY_NAME, t);
String fieldName = p.currentName();
if (fieldName.equals(_fieldNames.lowerEndpoint)) {
p.nextToken();
lowerEndpoint = deserializeEndpoint(ctxt, p);
} else if (fieldName.equals(_fieldNames.upperEndpoint)) {
p.nextToken();
upperEndpoint = deserializeEndpoint(ctxt, p);
} else if (fieldName.equals(_fieldNames.lowerBoundType)) {
p.nextToken();
lowerBoundType = deserializeBoundType(ctxt, p);
} else if (fieldName.equals(_fieldNames.upperBoundType)) {
p.nextToken();
upperBoundType = deserializeBoundType(ctxt, p);
} else {
// Note: should either return `true`, iff problem is handled (and
// content processed or skipped) or throw exception. So if we
// get back, we ought to be fine...
ctxt.handleUnknownProperty(p, this, Range.class, fieldName);
}
}
if ((lowerEndpoint != null) && (upperEndpoint != null)) {
// Endpoints of unknown type are read as "natural" types, so numbers
// may differ (like `1` and `2.5`): if so, widen to a common type
if (hasUnknownEndpointType()) {
Class<?> common = _commonNumberType(Arrays.asList(lowerEndpoint, upperEndpoint));
if (common != null) {
lowerEndpoint = _widenNumber(lowerEndpoint, common);
upperEndpoint = _widenNumber(upperEndpoint, common);
}
}
if (lowerEndpoint.getClass() != upperEndpoint.getClass()) {
return ctxt.reportBadDefinition(getValueType(ctxt), String.format(
"Endpoint types are not the same - '%s' deserialized to [%s], and '%s' deserialized to [%s].",
_fieldNames.lowerEndpoint,
lowerEndpoint.getClass().getName(),
_fieldNames.upperEndpoint,
upperEndpoint.getClass().getName()));
}
if (lowerBoundType == null) {
return ctxt.reportInputMismatch(getValueType(ctxt), String.format(
"'%s' field found, but not '%s'",
_fieldNames.lowerEndpoint,
_fieldNames.lowerBoundType));
}
if (upperBoundType == null) {
return ctxt.reportInputMismatch(getValueType(ctxt), String.format(
"'%s' field found, but not '%s'",
_fieldNames.upperEndpoint,
_fieldNames.upperBoundType));
}
try {
return RangeFactory.range(lowerEndpoint, lowerBoundType, upperEndpoint, upperBoundType);
} catch (IllegalArgumentException iae) {
return ctxt.reportInputMismatch(this,
"Failed to construct `Range` from (%s/'%s', %s/'%s'): %s",
lowerEndpoint, lowerBoundType, upperEndpoint, upperBoundType,
iae.getMessage());
}
}
if (lowerEndpoint != null) {
if (lowerBoundType == null) {
return ctxt.reportInputMismatch(getValueType(ctxt), String.format(
"'%s' field found, but not '%s'",
_fieldNames.lowerEndpoint,
_fieldNames.lowerBoundType));
}
return RangeFactory.downTo(lowerEndpoint, lowerBoundType);
}
if (upperEndpoint != null) {
if (upperBoundType == null) {
return ctxt.reportInputMismatch(getValueType(ctxt), String.format(
"'%s' field found, but not '%s'",
_fieldNames.upperEndpoint,
_fieldNames.upperBoundType));
}
return RangeFactory.upTo(upperEndpoint, upperBoundType);
}
return RangeFactory.all();
}
private Range<?> deserializeRangeFromString(DeserializationContext context, JsonParser p)
throws JacksonException
{
String rangeInterval = p.getString();
if (rangeInterval.isEmpty()) {
return null;
}
return RangeHelper.getRangeFromString(rangeInterval, context, _fromStringDeserializer, _rangeType, handledType());
}
private BoundType deserializeBoundType(DeserializationContext context, JsonParser p)
throws JacksonException
{
expect(context, JsonToken.VALUE_STRING, p.currentToken());
String name = p.getString();
if (name == null) {
name = "";
}
switch (name) {
case "OPEN":
return BoundType.OPEN;
case "CLOSED":
return BoundType.CLOSED;
}
if (context.isEnabled(MapperFeature.ACCEPT_CASE_INSENSITIVE_ENUMS)) {
if (name.equalsIgnoreCase("open")) {
return BoundType.OPEN;
}
if (name.equalsIgnoreCase("closed")) {
return BoundType.CLOSED;
}
}
return (BoundType) context.handleWeirdStringValue(BoundType.class, name,
"not a valid BoundType name (should be one of: %s)",
Arrays.asList(BoundType.values()));
}
/**
* Whether endpoint type is not specific enough to determine endpoint value types,
* in which case endpoints are read as "natural" types (and numbers may need widening).
*/
boolean hasUnknownEndpointType() {
JavaType endpointType = _rangeType.containedTypeOrUnknown(0);
return endpointType.hasRawClass(Object.class) || endpointType.hasRawClass(Comparable.class)
|| endpointType.hasRawClass(Number.class);
}
/**
* Widens numeric endpoints of given ranges to a common type, if they have
* different types (like {@code Integer} and {@code Long}): otherwise
* returns ranges as-is.
*/
static List<Range<Comparable<?>>> widenNumericRanges(List<Range<Comparable<?>>> ranges)
{
List<Object> endpoints = new ArrayList<>();
for (Range<?> range : ranges) {
endpoints.add(range.hasLowerBound() ? range.lowerEndpoint() : null);
endpoints.add(range.hasUpperBound() ? range.upperEndpoint() : null);
}
Class<?> common = _commonNumberType(endpoints);
if (common == null) {
return ranges;
}
List<Range<Comparable<?>>> result = new ArrayList<>(ranges.size());
for (Range<Comparable<?>> r : ranges) {
if (r.hasLowerBound() && r.hasUpperBound()) {
r = RangeFactory.range(_widenNumber(r.lowerEndpoint(), common), r.lowerBoundType(),
_widenNumber(r.upperEndpoint(), common), r.upperBoundType());
} else if (r.hasLowerBound()) {
r = RangeFactory.downTo(_widenNumber(r.lowerEndpoint(), common), r.lowerBoundType());
} else if (r.hasUpperBound()) {
r = RangeFactory.upTo(_widenNumber(r.upperEndpoint(), common), r.upperBoundType());
}
result.add(r);
}
return result;
}
/**
* Returns the type to widen given values ({@code null}s ignored) to, or {@code null}
* if no widening is needed (all of same type) or possible (not all numbers).
* Integral numbers are widened to {@code Long} or {@code BigInteger}, mixes
* with floating-point numbers to {@code BigDecimal} (to retain exact values;
* not possible for NaN and infinity).
*/
private static Class<?> _commonNumberType(List<?> values)
{
Set<Class<?>> types = new HashSet<>();
for (Object value : values) {
if (value != null) {
if (!(value instanceof Number)) {
return null;
}
types.add(value.getClass());
}
}
if (types.size() < 2) {
return null;
}
if (INTEGRAL_TYPES.containsAll(types)) {
return types.contains(BigInteger.class) ? BigInteger.class : Long.class;
}
for (Object value : values) {
if (((value instanceof Double) || (value instanceof Float))
&& !Double.isFinite(((Number) value).doubleValue())) {
return null;
}
}
return BigDecimal.class;
}
private final static Set<Class<?>> INTEGRAL_TYPES = new HashSet<>(Arrays.asList(
Byte.class, Short.class, Integer.class, Long.class, BigInteger.class));
private static Comparable<?> _widenNumber(Object n, Class<?> type) {
if (type == Long.class) {
return ((Number) n).longValue();
}
return (type == BigInteger.class) ? new BigInteger(n.toString()) : new BigDecimal(n.toString());
}
private Comparable<?> deserializeEndpoint(DeserializationContext context, JsonParser p)
throws JacksonException
{
Object obj = _endpointDeserializer.deserialize(p, context);
if (!(obj instanceof Comparable)) {
// 01-Oct-2015, tatu: Not sure if it's data or definition problem... for now,
// assume definition, but may need to reconsider
context.reportBadDefinition(_rangeType,
String.format(
"Field '%s' deserialized to a %s, which does not implement `Comparable`",
p.currentName(),
ClassUtil.classNameOf(obj)));
}
return (Comparable<?>) obj;
}
private void expect(DeserializationContext context, JsonToken expected, JsonToken actual)
{
if (actual != expected) {
context.reportInputMismatch(this, String.format("Problem deserializing %s: expecting %s, found %s",
ClassUtil.getTypeDescription(getValueType()), expected, actual));
}
}
}