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));
        }
    }
}