MapEntryDeserializer.java

package tools.jackson.databind.deser.jdk;

import java.lang.reflect.InvocationTargetException;
import java.util.*;

import com.fasterxml.jackson.annotation.JsonFormat;

import tools.jackson.core.*;
import tools.jackson.databind.*;
import tools.jackson.databind.annotation.JacksonStdImpl;
import tools.jackson.databind.deser.*;
import tools.jackson.databind.deser.std.ContainerDeserializerBase;
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;

/**
 * Basic serializer that can take JSON "Object" structure and
 * construct a {@link java.util.Map.Entry} instance, with typed contents.
 *<p>
 * Note: for untyped content (one indicated by passing Object.class
 * as the type), {@link UntypedObjectDeserializer} is used instead.
 * It can also construct {@link java.util.Map.Entry}s, but not with specific
 * POJO types, only other containers and primitives/wrappers.
 */
@JacksonStdImpl
public class MapEntryDeserializer
    extends ContainerDeserializerBase<Map.Entry<Object,Object>>
{
    /**
     * Key deserializer to use; either passed via constructor
     * (when indicated by annotations), or resolved when
     * {@link #createContextual} is called;
     */
    protected final KeyDeserializer _keyDeserializer;

    /**
     * Value deserializer.
     */
    protected final ValueDeserializer<Object> _valueDeserializer;

    /**
     * If value instances have polymorphic type information, this
     * is the type deserializer that can handle it
     */
    protected final TypeDeserializer _valueTypeDeserializer;

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

    public MapEntryDeserializer(JavaType type,
            KeyDeserializer keyDeser, ValueDeserializer<Object> valueDeser,
            TypeDeserializer valueTypeDeser)
    {
        super(type);
        if (type.containedTypeCount() != 2) { // sanity check
            throw new IllegalArgumentException("Missing generic type information for "+type);
        }
        _keyDeserializer = keyDeser;
        _valueDeserializer = valueDeser;
        _valueTypeDeserializer = valueTypeDeser;
    }

    protected MapEntryDeserializer(MapEntryDeserializer src,
            KeyDeserializer keyDeser, ValueDeserializer<Object> valueDeser,
            TypeDeserializer valueTypeDeser)
    {
        super(src);
        _keyDeserializer = keyDeser;
        _valueDeserializer = valueDeser;
        _valueTypeDeserializer = valueTypeDeser;
    }

    /**
     * Factory method for constructing initial (non-contextual) instances.
     *
     * @since 3.1
     */
    @SuppressWarnings("unchecked")
    public static ValueDeserializer<Object> construct(DeserializationContext ctxt,
            JavaType entryType,
            boolean pojoWrappedFormat)
    {
        ValueDeserializer<?> deser = pojoWrappedFormat
                ? constructAsPOJO(ctxt, entryType)
                : constructDefault(ctxt, entryType);
        return (ValueDeserializer<Object>) deser;
    }

    /**
     * Factory method for initial instance using the default ("natural") format,
     * in which an Object with a single entry is expected.
     *
     * @since 3.1
     */
    protected static MapEntryDeserializer constructDefault(DeserializationContext ctxt,
            JavaType entryType)
    {
        final JavaType keyType = entryType.containedTypeOrUnknown(0);
        final JavaType valueType = entryType.containedTypeOrUnknown(1);
        // 28-Apr-2015, tatu: TypeFactory does it all for us already so
        // 04-Jan-2025, tatu: Or does is? None of tests fails if following was
        //    removed.
        TypeDeserializer vts = (TypeDeserializer) valueType.getTypeHandler();
        if (vts == null) {
            vts = ctxt.findTypeDeserializer(valueType);
        }
        @SuppressWarnings("unchecked")
        ValueDeserializer<Object> valueDeser = (ValueDeserializer<Object>) valueType.getValueHandler();
        KeyDeserializer keyDes = (KeyDeserializer) keyType.getValueHandler();
        return new MapEntryDeserializer(entryType, keyDes, valueDeser, vts);
    }

    /**
     * Factory method for initial instance using the alternative ("as POJO" or
     * "POJO-wrapped") format, in which an Object with 2 separate entries -- "key"
     * and "value" -- are expected.
     *
     * @since 3.1
     */
    protected static POJOWrappedDeserializer constructAsPOJO(DeserializationContext ctxt,
            JavaType entryType)
    {
        return new POJOWrappedDeserializer(entryType);
    }

    /**
     * Fluent factory method used to create a copy with slightly
     * different settings.
     */
    @SuppressWarnings("unchecked")
    protected MapEntryDeserializer withResolved(KeyDeserializer keyDeser,
            TypeDeserializer valueTypeDeser, ValueDeserializer<?> valueDeser)
    {
        if ((_keyDeserializer == keyDeser) && (_valueDeserializer == valueDeser)
                && (_valueTypeDeserializer == valueTypeDeser)) {
            return this;
        }
        return new MapEntryDeserializer(this,
                keyDeser, (ValueDeserializer<Object>) valueDeser, valueTypeDeser);
    }

    @Override // since 2.12
    public LogicalType logicalType() {
        return LogicalType.Map;
    }

    /*
    /**********************************************************************
    /* Validation, post-processing
    /**********************************************************************
     */

    /**
     * Method called to finalize setup of this deserializer,
     * when it is known for which property deserializer is needed for.
     */
    @Override
    public ValueDeserializer<?> createContextual(DeserializationContext ctxt,
            BeanProperty property)
    {
        // [databind#1419]: Check if property has @JsonFormat(shape=POJO)
        if (Boolean.TRUE.equals(_shouldDeserializeAsPOJO(ctxt, property))) {
            return constructAsPOJO(ctxt, _containerType)
                    ._createContextual2(ctxt, property);
        }
        return _createContextual2(ctxt, property);
    }

    // Method called from "createContextual()"s after determining if
    // "shape-shifting" needed (and has been performed)
    protected ValueDeserializer<?> _createContextual2(DeserializationContext ctxt,
            BeanProperty property)
    {
        KeyDeserializer kd = _keyDeserializer;
        if (kd == null) {
            kd = ctxt.findKeyDeserializer(_containerType.containedType(0), property);
        } else {
            if (kd instanceof ContextualKeyDeserializer ckd) {
                kd = ckd.createContextual(ctxt, property);
            }
        }
        ValueDeserializer<?> vd = _valueDeserializer;
        vd = findConvertingContentDeserializer(ctxt, property, vd);
        JavaType contentType = _containerType.containedType(1);
        if (vd == null) {
            vd = ctxt.findContextualValueDeserializer(contentType, property);
        } else { // if directly assigned, probably not yet contextual, so:
            vd = ctxt.handleSecondaryContextualization(vd, property, contentType);
        }
        TypeDeserializer vtd = _valueTypeDeserializer;
        if (vtd != null) {
            vtd = vtd.forProperty(property);
        }
        return withResolved(kd, vtd, vd);
    }

    protected static Boolean _shouldDeserializeAsPOJO(DeserializationContext ctxt,
            BeanProperty property)
    {
        if (property != null) {
            JsonFormat.Value format = property.findPropertyFormat(ctxt.getConfig(), Map.Entry.class);

            switch (format.getShape()) {
            case NATURAL:
                return false;
            case POJO:
                return true;
            default: // fall through
            }
        }
        return null;
    }

    /*
    /**********************************************************************
    /* ContainerDeserializerBase API
    /**********************************************************************
     */

    @Override
    public JavaType getContentType() {
        return _containerType.containedType(1);
    }

    @Override
    public ValueDeserializer<Object> getContentDeserializer() {
        return _valueDeserializer;
    }

    // 31-May-2020, tatu: Should probably define but we don't have it yet
//    public ValueInstantiator getValueInstantiator() { }

    /*
    /**********************************************************************
    /* ValueDeserializer API
    /**********************************************************************
     */

    @SuppressWarnings("unchecked")
    @Override
    public Map.Entry<Object,Object> deserialize(JsonParser p, DeserializationContext ctxt)
        throws JacksonException
    {
        // Ok: must point to START_OBJECT, PROPERTY_NAME or END_OBJECT
        JsonToken t = p.currentToken();
        if (t == JsonToken.START_OBJECT) {
            t = p.nextToken();
        } else if (t != JsonToken.PROPERTY_NAME && t != JsonToken.END_OBJECT) {
            // Empty array, or single-value wrapped in array?
            if (t == JsonToken.START_ARRAY) {
                return _deserializeFromArray(p, ctxt);
            }
            return (Map.Entry<Object,Object>) ctxt.handleUnexpectedToken(getValueType(ctxt), p);
        }
        if (t != JsonToken.PROPERTY_NAME) {
            if (t == JsonToken.END_OBJECT) {
                return ctxt.reportInputMismatch(this,
                        "Cannot deserialize a `Map.Entry` out of empty Object");
            }
            return (Map.Entry<Object,Object>) ctxt.handleUnexpectedToken(getValueType(ctxt), p);
        }

        final KeyDeserializer keyDes = _keyDeserializer;
        final ValueDeserializer<Object> valueDes = _valueDeserializer;
        final TypeDeserializer typeDeser = _valueTypeDeserializer;

        final String keyStr = p.currentName();
        Object key = keyDes.deserializeKey(keyStr, ctxt);
        Object value = null;
        // And then the value...
        t = p.nextToken();
        try {
            // Note: must handle null explicitly here; value deserializers won't
            if (t == JsonToken.VALUE_NULL) {
                value = valueDes.getNullValue(ctxt);
            } else if (typeDeser == null) {
                value = valueDes.deserialize(p, ctxt);
            } else {
                value = valueDes.deserializeWithType(p, ctxt, typeDeser);
            }
        } catch (Exception e) {
            wrapAndThrow(ctxt, e, Map.Entry.class, keyStr);
        }

        // Close, but also verify that we reached the END_OBJECT
        t = p.nextToken();
        if (t != JsonToken.END_OBJECT) {
            if (t == JsonToken.PROPERTY_NAME) { // most likely
                ctxt.reportInputMismatch(this,
                        "Problem binding JSON into Map.Entry: more than one entry in JSON (second field: '%s')",
                        p.currentName());
            } else {
                // how would this occur?
                ctxt.reportInputMismatch(this,
                        "Problem binding JSON into Map.Entry: unexpected content after JSON Object entry: "+t);
            }
            return null;
        }
        return new AbstractMap.SimpleEntry<Object,Object>(key, value);
    }

    @Override
    public Map.Entry<Object,Object> deserialize(JsonParser p, DeserializationContext ctxt,
            Map.Entry<Object,Object> result) throws JacksonException
    {
        throw new IllegalStateException("Cannot update Map.Entry values");
    }

    @Override
    public Object deserializeWithType(JsonParser p, DeserializationContext ctxt,
            TypeDeserializer typeDeserializer)
        throws JacksonException
    {
        // In future could check current token... for now this should be enough:
        return typeDeserializer.deserializeTypedFromObject(p, ctxt);
    }

    /*
    /**********************************************************************
    /* Alternate handlers
    /**********************************************************************
     */

    /**
     * @since 3.1
     */
    protected static class POJOWrappedDeserializer
        extends StdDeserializer<Map.Entry<Object, Object>>
    {
        protected final ValueDeserializer<Object> _keyDeserializer;
        protected final TypeDeserializer _keyTypeDeserializer;

        protected final ValueDeserializer<Object> _valueDeserializer;
        protected final TypeDeserializer _valueTypeDeserializer;

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

        public POJOWrappedDeserializer(JavaType type)
        {
            super(type);
            _keyDeserializer = null;
            _keyTypeDeserializer = null;
            _valueDeserializer = null;
            _valueTypeDeserializer = null;
        }
    
        protected POJOWrappedDeserializer(POJOWrappedDeserializer src,
                ValueDeserializer<Object> keyDeser, TypeDeserializer valueTypeDeser,
                ValueDeserializer<Object> valueDeser, TypeDeserializer keyTypeDeser)
        {
            super(src);
            _keyDeserializer = keyDeser;
            _keyTypeDeserializer = keyTypeDeser;
            _valueDeserializer = valueDeser;
            _valueTypeDeserializer = valueTypeDeser;
        }

        /**
         * Fluent factory method used to create a copy with slightly
         * different settings.
         */
        @SuppressWarnings("unchecked")
        protected POJOWrappedDeserializer withResolved(ValueDeserializer<?> keyDeser,
                TypeDeserializer keyTypeDeser,
                ValueDeserializer<?> valueDeser, TypeDeserializer valueTypeDeser)
        {
            if ((_keyDeserializer == keyDeser)
                    && (_keyTypeDeserializer == keyTypeDeser)
                    && (_valueDeserializer == valueDeser)
                    && (_valueTypeDeserializer == valueTypeDeser)) {
                return this;
            }
            return new POJOWrappedDeserializer(this,
                    (ValueDeserializer<Object>) keyDeser, keyTypeDeser,
                    (ValueDeserializer<Object>) valueDeser, valueTypeDeser);
        }

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

        /*
        /**********************************************************************
        /* Validation, post-processing
        /**********************************************************************
         */

        /**
         * Method called to finalize setup of this deserializer,
         * when it is known for which property deserializer is needed for.
         */
        @Override
        public ValueDeserializer<?> createContextual(DeserializationContext ctxt,
                BeanProperty property)
        {
            // May override back to standard too:
            if (Boolean.FALSE.equals(_shouldDeserializeAsPOJO(ctxt, property))) {
                return constructAsPOJO(ctxt, _valueType)
                        ._createContextual2(ctxt, property);
            }
            return _createContextual2(ctxt, property);
        }

        // Method called from "createContextual()"s after determining if
        // "shape-shifting" needed (and has been performed)
        protected ValueDeserializer<?> _createContextual2(DeserializationContext ctxt,
                BeanProperty property)
        {
            ValueDeserializer<?> kd = _keyDeserializer;
            kd = findConvertingContentDeserializer(ctxt, property, kd);
            JavaType keyType = _valueType.containedTypeOrUnknown(0);
            if (kd == null) {
                kd = ctxt.findContextualValueDeserializer(keyType, property);
            } else { // if directly assigned, probably not yet contextual, so:
                kd = ctxt.handleSecondaryContextualization(kd, property, keyType);
            }
            TypeDeserializer ktd = _keyTypeDeserializer;
            if (ktd != null) {
                ktd = ktd.forProperty(property);
            }

            ValueDeserializer<?> vd = _valueDeserializer;
            vd = findConvertingContentDeserializer(ctxt, property, vd);
            JavaType valueType = _valueType.containedType(1);
            if (vd == null) {
                vd = ctxt.findContextualValueDeserializer(valueType, property);
            } else { // if directly assigned, probably not yet contextual, so:
                vd = ctxt.handleSecondaryContextualization(vd, property, valueType);
            }
            TypeDeserializer vtd = _valueTypeDeserializer;
            if (vtd != null) {
                vtd = vtd.forProperty(property);
            }
            return withResolved(kd, ktd, vd, vtd);
        }

        /*
        /**********************************************************************
        /* ValueDeserializer API
        /**********************************************************************
         */

        @SuppressWarnings("unchecked")
        @Override
        public Map.Entry<Object,Object> deserialize(JsonParser p, DeserializationContext ctxt)
            throws JacksonException
        {
            JsonToken t = p.currentToken();
            if (t == JsonToken.START_OBJECT) {
                t = p.nextToken();
            } else if (t != JsonToken.PROPERTY_NAME && t != JsonToken.END_OBJECT) {
                if (t == JsonToken.START_ARRAY) {
                    return _deserializeFromArray(p, ctxt);
                }
                return (Map.Entry<Object,Object>) ctxt.handleUnexpectedToken(_valueType, p);
            }

            Object key = null;
            Object value = null;

            // Read properties "key" and "value"
            while (t == JsonToken.PROPERTY_NAME) {
                String propName = p.currentName();
                t = p.nextToken(); // move to value

                if ("key".equals(propName)) {
                    try {
                        if (t == JsonToken.VALUE_NULL) {
                            key = _keyDeserializer.getNullValue(ctxt);
                        } else if (_keyTypeDeserializer != null) {
                            key = _keyDeserializer.deserializeWithType(p, ctxt, _keyTypeDeserializer);
                        } else {
                            key = _keyDeserializer.deserialize(p, ctxt);
                        }
                    } catch (Exception e) {
                        wrapAndThrow(ctxt, e, Map.Entry.class, propName);
                    }
                } else if ("value".equals(propName)) {
                    try {
                        if (t == JsonToken.VALUE_NULL) {
                            value = _valueDeserializer.getNullValue(ctxt);
                        } else if (_valueTypeDeserializer != null) {
                            value = _valueDeserializer.deserializeWithType(p, ctxt, _valueTypeDeserializer);
                        } else {
                            value = _valueDeserializer.deserialize(p, ctxt);
                        }
                    } catch (Exception e) {
                        wrapAndThrow(ctxt, e, Map.Entry.class, propName);
                    }
                } else {
                    // Unknown property: check if we should fail or skip
                    handleUnknownProperty(p, ctxt, _valueType, propName);
                }

                t = p.nextToken(); // move to next property or END_OBJECT
            }

            if (t != JsonToken.END_OBJECT) {
                ctxt.reportInputMismatch(this,
                        "Problem deserializing `Map.Entry`; unexpected content after Object value: "
                                +JsonToken.valueDescFor(t));
            }

            return new AbstractMap.SimpleEntry<>(key, value);
        }
        
        @Override
        public Map.Entry<Object,Object> deserialize(JsonParser p, DeserializationContext ctxt,
                Map.Entry<Object,Object> result) throws JacksonException
        {
            throw new IllegalStateException("Cannot update `Map.Entry` values");
        }

        @Override
        public Object deserializeWithType(JsonParser p, DeserializationContext ctxt,
                TypeDeserializer typeDeserializer)
            throws JacksonException
        {
            // In future could check current token... for now this should be enough:
            return typeDeserializer.deserializeTypedFromObject(p, ctxt);
        }

        // Copied from `ContainerDeserializerBase`
        protected <BOGUS> BOGUS wrapAndThrow(DeserializationContext ctxt,
                Throwable t, Object ref, String key) throws JacksonException
        {
            while (t instanceof InvocationTargetException && t.getCause() != null) {
                t = t.getCause();
            }
            ClassUtil.throwIfError(t);
            if (!ctxt.isEnabled(DeserializationFeature.WRAP_EXCEPTIONS)) {
                ClassUtil.throwIfRTE(t);
            }
            throw DatabindException.wrapWithPath(ctxt, t,
                        new JacksonException.Reference(ref, ClassUtil.nonNull(key, "N/A")));
        }
    }
}