RangeDeserializer.java

package com.fasterxml.jackson.datatype.guava.deser;

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

import com.fasterxml.jackson.annotation.JsonFormat;
import com.google.common.base.Preconditions;
import com.google.common.collect.BoundType;
import com.google.common.collect.Range;

import com.fasterxml.jackson.core.*;

import com.fasterxml.jackson.databind.*;
import com.fasterxml.jackson.databind.deser.ContextualDeserializer;
import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
import com.fasterxml.jackson.databind.jsontype.TypeDeserializer;
import com.fasterxml.jackson.databind.type.LogicalType;
import com.fasterxml.jackson.databind.type.TypeFactory;
import com.fasterxml.jackson.databind.util.ClassUtil;
import com.fasterxml.jackson.datatype.guava.deser.util.RangeFactory;
import com.fasterxml.jackson.datatype.guava.deser.util.RangeHelper;

/**
 * Jackson deserializer for a Guava {@link Range}.
 *<p>
 * TODO: I think it would make sense to reimplement 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<?>>
    implements ContextualDeserializer
{
    private static final long serialVersionUID = 1L;

    // @since 2.17
    protected final static Pattern PATTERN_DOUBLE_DOT = Pattern.compile("\\.\\.");

    protected final JavaType _rangeType;

    /**
     * Deserializer used when getting JSON Object representation
     */
    protected final JsonDeserializer<Object> _endpointDeserializer;

    /**
     * Deserializer used when getting JSON String (inline) representation.
     *
     * @since 2.17
     */
    protected final KeyDeserializer _fromStringDeserializer;
    
    protected final BoundType _defaultBoundType;

    /**
     * @since 2.10
     */
    protected final RangeHelper.RangeProperties _fieldNames;

    /**
     * @since 2.17
     */
    protected final JsonFormat.Shape _shape;

    /*
    /**********************************************************
    /* Life-cycle
    /**********************************************************
     */

    public RangeDeserializer(BoundType defaultBoundType, JavaType rangeType) {
        this(rangeType, null, defaultBoundType, RangeHelper.standardNames(),
                JsonFormat.Shape.ANY, null);
    }

    @Deprecated // since 2.12
    public RangeDeserializer(JavaType rangeType, JsonDeserializer<?> endpointDeser) {
        this(rangeType, endpointDeser, null);
    }

    @Deprecated // since 2.10
    public RangeDeserializer(JavaType rangeType, JsonDeserializer<?> endpointDeser,
            BoundType defaultBoundType)
    {
        this(rangeType, endpointDeser, defaultBoundType, RangeHelper.standardNames());
    }

    /**
     * @since 2.10
     * @deprecated Since 2.17 use other constructor(s)
     */
    @Deprecated // since 2.17
    protected RangeDeserializer(JavaType rangeType, JsonDeserializer<?> endpointDeser,
            BoundType defaultBoundType, RangeHelper.RangeProperties fieldNames)
    {
        this(rangeType, endpointDeser, defaultBoundType, fieldNames,
                JsonFormat.Shape.ANY, null);
    }

    /**
     * @since 2.17
     */
    @SuppressWarnings("unchecked")
    protected RangeDeserializer(JavaType rangeType, JsonDeserializer<?> endpointDeser,
            BoundType defaultBoundType, RangeHelper.RangeProperties fieldNames,
            JsonFormat.Shape shape, KeyDeserializer fromStringDeserializer)
    {
        super(rangeType);
        _rangeType = rangeType;
        _endpointDeserializer = (JsonDeserializer<Object>) endpointDeser;
        _fromStringDeserializer = fromStringDeserializer;
        _defaultBoundType = defaultBoundType;
        _fieldNames = fieldNames;
        _shape = shape;
    }

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

    @Override
    public JsonDeserializer<?> createContextual(DeserializationContext ctxt,
            BeanProperty property) throws JsonMappingException
    {
        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());
        JavaType endpointType = _rangeType.containedType(0);
        if (endpointType == null) { // should this ever occur?
            endpointType = TypeFactory.unknownType();
        }
        JsonDeserializer<?> deser = _endpointDeserializer;
        if (deser == null) {
            deser = ctxt.findContextualValueDeserializer(endpointType, property);
        } else if (deser instanceof ContextualDeserializer) {
            // 04-Sep-2019, tatu: If we already have a deserialize, should contextualize, right?
            deser = ((ContextualDeserializer) 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;
    }

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

    @Override
    public Object deserializeWithType(JsonParser p, DeserializationContext ctxt,
            TypeDeserializer typeDeserializer)
        throws IOException
    {
        return typeDeserializer.deserializeTypedFromObject(p, ctxt);
    }

    @Override
    public Range<?> deserialize(JsonParser p, DeserializationContext context)
        throws IOException
    {
        // NOTE: either START_OBJECT _or_ FIELD_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(context, JsonToken.VALUE_STRING, t);
            return deserializeRangeFromString(context, p);
        }
        for (; t != JsonToken.END_OBJECT; t = p.nextToken()) {
            expect(context, JsonToken.FIELD_NAME, t);
            String fieldName = p.currentName();
            try {
                if (fieldName.equals(_fieldNames.lowerEndpoint)) {
                    p.nextToken();
                    lowerEndpoint = deserializeEndpoint(context, p);
                } else if (fieldName.equals(_fieldNames.upperEndpoint)) {
                    p.nextToken();
                    upperEndpoint = deserializeEndpoint(context, p);
                } else if (fieldName.equals(_fieldNames.lowerBoundType)) {
                    p.nextToken();
                    lowerBoundType = deserializeBoundType(context, p);
                } else if (fieldName.equals(_fieldNames.upperBoundType)) {
                    p.nextToken();
                    upperBoundType = deserializeBoundType(context, 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...
                    context.handleUnknownProperty(p, this, Range.class, fieldName);
                }
            } catch (IllegalStateException e) {
                return context.reportBadDefinition(handledType(), e.getMessage());
            }
        }
        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].",
                        lowerEndpoint.getClass().getName(),
                        upperEndpoint.getClass().getName());
                Preconditions.checkState(lowerBoundType != null, "'lowerEndpoint' field found, but not 'lowerBoundType'");
                Preconditions.checkState(upperBoundType != null, "'upperEndpoint' field found, but not 'upperBoundType'");
                return RangeFactory.range(lowerEndpoint, lowerBoundType, upperEndpoint, upperBoundType);
            }
            if (lowerEndpoint != null) {
                Preconditions.checkState(lowerBoundType != null, "'lowerEndpoint' field found, but not 'lowerBoundType'");
                return RangeFactory.downTo(lowerEndpoint, lowerBoundType);
            }
            if (upperEndpoint != null) {
                Preconditions.checkState(upperBoundType != null, "'upperEndpoint' field found, but not 'upperBoundType'");
                return RangeFactory.upTo(upperEndpoint, upperBoundType);
            }
            return RangeFactory.all();
        } catch (IllegalStateException e) {
            return context.reportBadDefinition(getValueType(context), e.getMessage());
        }
    }

    private Range<?> deserializeRangeFromString(DeserializationContext context, JsonParser p)
            throws IOException
    {
        String rangeInterval = p.getText();

        if (rangeInterval.isEmpty()) {
            return null;
        }

        if (isValidBracketNotation(rangeInterval)) {
            BoundType lowerBoundType = rangeInterval.startsWith("[") ? BoundType.CLOSED : BoundType.OPEN;
            BoundType upperBoundType = rangeInterval.endsWith("]") ? BoundType.CLOSED : BoundType.OPEN;

            rangeInterval = rangeInterval.substring(1, rangeInterval.length() - 1);
            String[] parts = PATTERN_DOUBLE_DOT.split(rangeInterval);

            if (parts.length == 2) {
                boolean isLowerInfinite = parts[0].equals("-���");
                boolean isUpperInfinite = parts[1].equals("+���");

                if (isLowerInfinite && isUpperInfinite) {
                    return RangeFactory.all();
                } else if (isLowerInfinite) {
                    return RangeFactory.upTo(deserializeStringified(context, parts[1]), upperBoundType);
                } else if (isUpperInfinite) {
                    return RangeFactory.downTo(deserializeStringified(context, parts[0]), lowerBoundType);
                } else {
                    return RangeFactory.range(deserializeStringified(context, parts[0]),
                            lowerBoundType,
                            deserializeStringified(context, parts[1]),
                            upperBoundType);
                }
            }
        } else {
            String msg = "Invalid Range: should start with '[' or '(', end with ')' or ']'";
            return (Range<?>) context.handleWeirdStringValue(handledType(), rangeInterval, msg);
        }

        // Give generic failure if no specific reason can be given.
        // Although most likely will happen because `..` is absent, since we are validating brackets above.
        return (Range<?>) context.handleWeirdStringValue(handledType(), rangeInterval,
                "Invalid bracket-notation representation (possibly missing \"..\" delimiter in your Stringified Range)");
    }

    private BoundType deserializeBoundType(DeserializationContext context, JsonParser p) throws IOException
    {
        expect(context, JsonToken.VALUE_STRING, p.currentToken());
        String name = p.getText();
        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()));
    }

    private Comparable<?> deserializeEndpoint(DeserializationContext context, JsonParser p) throws IOException
    {
        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 Comparable<?> deserializeStringified(DeserializationContext context, String value) throws IOException
    {
        Object obj = _fromStringDeserializer.deserializeKey(value, context);
        if (!(obj instanceof Comparable)) {
            // 02-Jan-2024, tatu: Not sure this is possible but let's double-check
            context.reportBadDefinition(_rangeType,
                    String.format(
                            "Stringified endpoint '%s' deserialized to a %s, which does not implement `Comparable`",
                            value,
                            ClassUtil.classNameOf(obj)));
        }
        return (Comparable<?>) obj;
    }
    
    private void expect(DeserializationContext context, JsonToken expected, JsonToken actual) throws JsonMappingException
    {
        if (actual != expected) {
            context.reportInputMismatch(this, String.format("Problem deserializing %s: expecting %s, found %s",
                    ClassUtil.getTypeDescription(getValueType()), expected, actual));
        }
    }

    private boolean isValidBracketNotation(String range) {
        char first = range.charAt(0);
        char last = range.charAt(range.length() - 1);

        return (first == '[' || first == '(') && (last == ']' || last == ')');
    }
}