RangeSetDeserializer.java

package tools.jackson.datatype.guava.deser;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Objects;

import com.fasterxml.jackson.annotation.JsonFormat;

import tools.jackson.core.JacksonException;
import tools.jackson.core.JsonParser;
import tools.jackson.core.JsonToken;

import tools.jackson.databind.*;
import tools.jackson.databind.cfg.CoercionAction;
import tools.jackson.databind.cfg.CoercionInputShape;
import tools.jackson.databind.deser.NullValueProvider;
import tools.jackson.databind.deser.impl.NullsConstantProvider;
import tools.jackson.databind.deser.std.StdDeserializer;
import tools.jackson.databind.jsontype.TypeDeserializer;
import tools.jackson.databind.type.LogicalType;
import tools.jackson.databind.type.TypeFactory;
import tools.jackson.databind.util.AccessPattern;
import tools.jackson.databind.util.ClassUtil;
import tools.jackson.datatype.guava.util.NullElementHelper;

import com.google.common.collect.ImmutableRangeSet;
import com.google.common.collect.Range;
import com.google.common.collect.RangeSet;

public class RangeSetDeserializer
    extends StdDeserializer<RangeSet<?>>
{
    /**
     * Deserializer for individual {@link Range} elements
     */
    private final ValueDeserializer<Object> _deserializer;

    /**
     * If {@link Range} elements have polymorphic type information, type
     * deserializer to use
     */
    private final TypeDeserializer _typeDeserializer;

    private final NullValueProvider _nullProvider;

    private final boolean _skipNullValues;

    private final Boolean _unwrapSingle;

    public RangeSetDeserializer() {
        super(RangeSet.class);
        _deserializer = null;
        _typeDeserializer = null;
        _nullProvider = null;
        _skipNullValues = false;
        _unwrapSingle = null;
    }

    protected RangeSetDeserializer(RangeSetDeserializer base,
            ValueDeserializer<Object> deser, TypeDeserializer typeDeser,
            NullValueProvider nuller, Boolean unwrapSingle)
    {
        super(base);
        _deserializer = deser;
        _typeDeserializer = typeDeser;
        _nullProvider = nuller;
        _skipNullValues = NullsConstantProvider.isSkipper(nuller);
        _unwrapSingle = unwrapSingle;
    }

    @Override
    public LogicalType logicalType() {
        return LogicalType.Collection;
    }

    @Override
    public AccessPattern getEmptyAccessPattern() {
        return AccessPattern.CONSTANT;
    }

    @Override
    public Object getEmptyValue(DeserializationContext ctxt) {
        return ImmutableRangeSet.of();
    }

    @Override
    public ValueDeserializer<?> createContextual(DeserializationContext ctxt, BeanProperty property)
    {
        JavaType rangeType = _findRangeType(ctxt, ctxt.getContextualType());
        if (rangeType == null) {
            if (property != null) {
                rangeType = _findRangeType(ctxt, property.getType());
            }
            // Cannot locate generic type to use? Leave as-is, fail on attempt to deserialize
            if (rangeType == null) {
                return this;
            }
        }
        // Content converter (`@JsonDeserialize(contentConverter=...)`), if any
        @SuppressWarnings("unchecked")
        ValueDeserializer<Object> deser = (ValueDeserializer<Object>)
                findConvertingContentDeserializer(ctxt, property, null);
        if (deser == null) {
            deser = ctxt.findContextualValueDeserializer(rangeType, property);
        }
        TypeDeserializer typeDeser = ctxt.findTypeDeserializer(rangeType);
        if (typeDeser != null) {
            typeDeser = typeDeser.forProperty(property);
        }
        NullValueProvider nuller = findContentNullProvider(ctxt, property, deser);
        if (nuller == null) {
            nuller = deser;
        }
        Boolean unwrapSingle = findFormatFeature(ctxt, property, Collection.class,
                JsonFormat.Feature.ACCEPT_SINGLE_VALUE_AS_ARRAY);
        return new RangeSetDeserializer(this, deser, typeDeser, nuller, unwrapSingle);
    }

    private JavaType _findRangeType(DeserializationContext ctxt, JavaType base)
    {
        Class<?> raw = base.getRawClass();
        final TypeFactory tf = ctxt.getTypeFactory();
        if (RangeSet.class.isAssignableFrom(raw)) {
            JavaType valueType = tf.findFirstTypeParameter(base, RangeSet.class);
            if (valueType != null) {
                return tf.constructParametricType(Range.class, valueType);
            }
        }
        return null;
    }

    @Override
    public RangeSet<?> deserialize(JsonParser p, DeserializationContext ctxt)
        throws JacksonException
    {
        if (_deserializer == null) {
            ctxt.reportBadDefinition(handledType(),
"Not contextualized to have value deserializer or value type of `RangeSet` was not available via type parameters");
        }
        List<Range<Comparable<?>>> ranges = new ArrayList<>();
        if (p.isExpectedStartArrayToken()) {
            JsonToken t;
            int index = 0;
            while ((t = p.nextToken()) != JsonToken.END_ARRAY) {
                try {
                    _readRange(p, ctxt, t, ranges);
                } catch (Exception e) {
                    // As with databind `CollectionDeserializer`: add index to path
                    boolean wrap = ctxt.isEnabled(DeserializationFeature.WRAP_EXCEPTIONS);
                    if (!wrap) {
                        ClassUtil.throwIfRTE(e);
                    }
                    throw DatabindException.wrapWithPath(ctxt, e,
                            new JacksonException.Reference(ranges, index));
                }
                ++index;
            }
        } else {
            // Empty/blank String may be coerced into empty (or null) `RangeSet`
            // (same as with databind `CollectionDeserializer`)
            if (p.hasToken(JsonToken.VALUE_STRING)) {
                final String str = p.getString();
                CoercionAction act = null;
                String desc = null;
                if (str.isEmpty()) {
                    act = ctxt.findCoercionAction(logicalType(), handledType(),
                            CoercionInputShape.EmptyString);
                    desc = "empty String (\"\")";
                } else if (_isBlank(str)) {
                    act = ctxt.findCoercionFromBlankString(logicalType(), handledType(),
                            CoercionAction.Fail);
                    desc = "blank String (all whitespace)";
                }
                // `Fail` falls through: may still be accepted as single value
                if ((act != null) && (act != CoercionAction.Fail)) {
                    return (RangeSet<?>) _deserializeFromEmptyString(p, ctxt, act,
                            handledType(), desc);
                }
            }
            final boolean canWrap = (_unwrapSingle == Boolean.TRUE) ||
                    ((_unwrapSingle == null) && ctxt.isEnabled(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY));
            if (!canWrap) {
                return (RangeSet<?>) ctxt.handleUnexpectedToken(getValueType(ctxt), p);
            }
            _readRange(p, ctxt, p.currentToken(), ranges);
        }
        // Endpoints of unknown type are read as "natural" types, so numbers
        // may differ between ranges (like `Integer` and `Long`): if so, widen
        final Object rangeDeser = _deserializer;
        if ((rangeDeser instanceof RangeDeserializer)
                && ((RangeDeserializer) rangeDeser).hasUnknownEndpointType()) {
            ranges = RangeDeserializer.widenNumericRanges(ranges);
        }
        try {
            return ImmutableRangeSet.<Comparable<?>>builder().addAll(ranges).build();
        // `ClassCastException` if ranges have incompatible endpoint types (like
        // `Integer` and `Long` when endpoint type is not known)
        } catch (IllegalArgumentException | ClassCastException e) {
            return ctxt.reportInputMismatch(this,
                    "Failed to build %s from deserialized entries: %s",
                    ClassUtil.getTypeDescription(getValueType(ctxt)), e.getMessage());
        }
    }

    private void _readRange(JsonParser p, DeserializationContext ctxt, JsonToken t,
            List<Range<Comparable<?>>> ranges)
        throws JacksonException
    {
        final Object range;
        if (t == JsonToken.VALUE_NULL) {
            if (_skipNullValues) {
                return;
            }
            range = _nullProvider.getNullValue(ctxt);
        } else if (_typeDeserializer == null) {
            range = _deserializer.deserialize(p, ctxt);
        } else {
            range = _deserializer.deserializeWithType(p, ctxt, _typeDeserializer);
        }
        _addRange(p, ctxt, ranges, range);
    }

    private void _addRange(JsonParser p, DeserializationContext ctxt,
            List<Range<Comparable<?>>> ranges, Object range)
        throws JacksonException
    {
        NullElementHelper.addElement(p, ctxt, p.currentToken(),
                ctxt.constructType(Range.class), "`RangeSet`",
                range, r -> {
                    @SuppressWarnings("unchecked")
                    Range<Comparable<?>> typed = (Range<Comparable<?>>) r;
                    // Validate like `ImmutableRangeSet.Builder.add()` (which is only called
                    // later): `null` must fail with NPE (see `NullElementHelper`)
                    if (Objects.requireNonNull(typed).isEmpty()) {
                        return ctxt.reportInputMismatch(this,
                                "Cannot add empty `Range` (%s) to `RangeSet`", typed);
                    }
                    return ranges.add(typed);
                });
    }
}