DeserializationProblemHandler3349Test.java

package tools.jackson.databind.deser.filter;

import java.util.*;

import org.junit.jupiter.api.Test;

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;

import tools.jackson.core.JsonParser;
import tools.jackson.core.JsonToken;
import tools.jackson.databind.*;
import tools.jackson.databind.deser.DeserializationProblemHandler;
import tools.jackson.databind.node.ObjectNode;

import static org.junit.jupiter.api.Assertions.*;

import static tools.jackson.databind.testutil.DatabindTestUtil.*;

/**
 * Test for [databind#3349]: DeserializationProblemHandler::handleUnexpectedToken
 * should be invoked for container types (Collections, Maps, arrays) when given
 * an incompatible String token.
 */
public class DeserializationProblemHandler3349Test
{
    // Problem handler that tracks which handler method was called
    static class TrackingProblemHandler extends DeserializationProblemHandler {
        boolean handleUnexpectedTokenCalled = false;
        boolean handleInstantiationProblemCalled = false;
        boolean handleMissingInstantiatorCalled = false;

        @Override
        public Object handleUnexpectedToken(DeserializationContext ctxt,
                JavaType targetType, JsonToken t, JsonParser p,
                String failureMsg)
        {
            handleUnexpectedTokenCalled = true;
            if (targetType.isMapLikeType()) {
                return new HashMap<>();
            }
            if (targetType.isCollectionLikeType()) {
                return new ArrayList<>();
            }
            if (targetType.isArrayType()) {
                // Return zero-length array of correct type
                return java.lang.reflect.Array.newInstance(
                        targetType.getContentType().getRawClass(), 0);
            }
            return NOT_HANDLED;
        }

        @Override
        public Object handleInstantiationProblem(DeserializationContext ctxt,
                Class<?> instClass, Object argument, Throwable t)
        {
            handleInstantiationProblemCalled = true;
            return NOT_HANDLED;
        }

        @Override
        public Object handleMissingInstantiator(DeserializationContext ctxt,
                Class<?> instClass, tools.jackson.databind.deser.ValueInstantiator inst,
                JsonParser p, String msg)
        {
            handleMissingInstantiatorCalled = true;
            return NOT_HANDLED;
        }

        void reset() {
            handleUnexpectedTokenCalled = false;
            handleInstantiationProblemCalled = false;
            handleMissingInstantiatorCalled = false;
        }
    }

    static class ArrayHolder {
        private final Collection<String> prop;

        private ArrayHolder(Collection<String> prop) {
            this.prop = prop;
        }

        @JsonCreator
        static ArrayHolder create(@JsonProperty("prop") Iterable<String> prop) {
            ArrayList<String> list = new ArrayList<>();
            prop.forEach(list::add);
            return new ArrayHolder(list);
        }

        @JsonProperty("prop")
        public Iterable<String> getProp() {
            return prop;
        }
    }

    static class StringHolder {
        private final String prop;

        @JsonCreator
        StringHolder(@JsonProperty("prop") String prop) {
            this.prop = prop;
        }

        @JsonProperty("prop")
        public String getProp() {
            return prop;
        }
    }

    /*
    /**********************************************************************
    /* Test methods: baseline
    /**********************************************************************
     */

    // Baseline: verify that handleUnexpectedToken is called for String type
    // when given an array token (this should work fine, not affected by #3349)
    @Test
    public void testHandleUnexpectedTokenForStringProp() throws Exception
    {
        TrackingProblemHandler handler = new TrackingProblemHandler();
        ObjectMapper mapper = jsonMapperBuilder()
            .addHandler(handler)
            .build();

        ObjectNode input = mapper.createObjectNode();
        input.set("prop", mapper.createArrayNode());

        try {
            mapper.treeToValue(input, StringHolder.class);
        } catch (Exception e) {
            // May fail, but we just want to check which handler was called
        }

        _verifyHandleUnexpectedTokenCalled(handler);
    }

    /*
    /**********************************************************************
    /* Test methods: Collection types
    /**********************************************************************
     */

    // [databind#3349]: handleUnexpectedToken should be called for Collection/Iterable types
    // when given a string token instead of START_ARRAY
    @Test
    public void testHandleUnexpectedTokenForCollectionProp() throws Exception
    {
        TrackingProblemHandler handler = new TrackingProblemHandler();
        ObjectMapper mapper = jsonMapperBuilder()
            .addHandler(handler)
            .build();

        ObjectNode input = mapper.createObjectNode();
        input.put("prop", "someString");

        mapper.treeToValue(input, ArrayHolder.class);

        _verifyHandleUnexpectedTokenCalled(handler);
    }

    // [databind#3349]: direct Collection<String> (StringCollectionDeserializer)
    @Test
    public void testHandleUnexpectedTokenForStringCollection() throws Exception
    {
        TrackingProblemHandler handler = new TrackingProblemHandler();
        ObjectMapper mapper = jsonMapperBuilder()
            .addHandler(handler)
            .build();

        mapper.readValue(q("someString"),
            mapper.getTypeFactory().constructCollectionType(ArrayList.class, String.class));

        _verifyHandleUnexpectedTokenCalled(handler);
    }

    // [databind#3349]: Collection<Integer> (CollectionDeserializer)
    @Test
    public void testHandleUnexpectedTokenForObjectCollection() throws Exception
    {
        TrackingProblemHandler handler = new TrackingProblemHandler();
        ObjectMapper mapper = jsonMapperBuilder()
            .addHandler(handler)
            .build();

        mapper.readValue(q("someString"),
            mapper.getTypeFactory().constructCollectionType(ArrayList.class, Integer.class));

        _verifyHandleUnexpectedTokenCalled(handler);
    }

    /*
    /**********************************************************************
    /* Test methods: Map types
    /**********************************************************************
     */

    // [databind#3349]: Map<String,String> (MapDeserializer)
    @Test
    public void testHandleUnexpectedTokenForMap() throws Exception
    {
        TrackingProblemHandler handler = new TrackingProblemHandler();
        ObjectMapper mapper = jsonMapperBuilder()
            .addHandler(handler)
            .build();

        mapper.readValue(q("someString"),
            mapper.getTypeFactory().constructMapType(HashMap.class, String.class, String.class));

        _verifyHandleUnexpectedTokenCalled(handler);
    }

    /*
    /**********************************************************************
    /* Test methods: Array types
    /**********************************************************************
     */

    // [databind#3349]: Object[] (ObjectArrayDeserializer)
    @Test
    public void testHandleUnexpectedTokenForObjectArray() throws Exception
    {
        TrackingProblemHandler handler = new TrackingProblemHandler();
        ObjectMapper mapper = jsonMapperBuilder()
            .addHandler(handler)
            .build();

        mapper.readValue(q("someString"), Object[].class);

        _verifyHandleUnexpectedTokenCalled(handler);
    }

    // [databind#3349]: String[] (StringArrayDeserializer)
    @Test
    public void testHandleUnexpectedTokenForStringArray() throws Exception
    {
        TrackingProblemHandler handler = new TrackingProblemHandler();
        ObjectMapper mapper = jsonMapperBuilder()
            .addHandler(handler)
            .build();

        mapper.readValue(q("someString"), String[].class);

        _verifyHandleUnexpectedTokenCalled(handler);
    }

    // [databind#3349]: int[] (PrimitiveArrayDeserializers)
    @Test
    public void testHandleUnexpectedTokenForIntArray() throws Exception
    {
        TrackingProblemHandler handler = new TrackingProblemHandler();
        ObjectMapper mapper = jsonMapperBuilder()
            .addHandler(handler)
            .build();

        mapper.readValue(q("someString"), int[].class);

        _verifyHandleUnexpectedTokenCalled(handler);
    }

    // [databind#3349]: long[] (PrimitiveArrayDeserializers)
    @Test
    public void testHandleUnexpectedTokenForLongArray() throws Exception
    {
        TrackingProblemHandler handler = new TrackingProblemHandler();
        ObjectMapper mapper = jsonMapperBuilder()
            .addHandler(handler)
            .build();

        mapper.readValue(q("someString"), long[].class);

        _verifyHandleUnexpectedTokenCalled(handler);
    }

    // NOTE: double[] and float[] not tested here: they have special "packed binary
    // vector" handling that intercepts STRING tokens (base64) before handleNonArray

    // [databind#3349]: boolean[] (PrimitiveArrayDeserializers)
    @Test
    public void testHandleUnexpectedTokenForBooleanArray() throws Exception
    {
        TrackingProblemHandler handler = new TrackingProblemHandler();
        ObjectMapper mapper = jsonMapperBuilder()
            .addHandler(handler)
            .build();

        mapper.readValue(q("someString"), boolean[].class);

        _verifyHandleUnexpectedTokenCalled(handler);
    }

    /*
    /**********************************************************************
    /* Helper methods
    /**********************************************************************
     */

    private void _verifyHandleUnexpectedTokenCalled(TrackingProblemHandler handler) {
        assertTrue(handler.handleUnexpectedTokenCalled,
            "handleUnexpectedToken should have been called");
        assertFalse(handler.handleInstantiationProblemCalled,
            "handleInstantiationProblem should NOT have been called");
        assertFalse(handler.handleMissingInstantiatorCalled,
            "handleMissingInstantiator should NOT have been called");
    }
}