DeserializationProblemHandlerTest.java

package tools.jackson.databind.deser.filter;

import java.util.*;

import org.junit.jupiter.api.Test;

import com.fasterxml.jackson.annotation.*;
import com.fasterxml.jackson.annotation.JsonInclude.Include;

import tools.jackson.core.*;
import tools.jackson.databind.*;
import tools.jackson.databind.deser.DeserializationProblemHandler;
import tools.jackson.databind.deser.ValueInstantiator;
import tools.jackson.databind.exc.InvalidDefinitionException;
import tools.jackson.databind.exc.InvalidFormatException;
import tools.jackson.databind.exc.InvalidTypeIdException;
import tools.jackson.databind.exc.MismatchedInputException;
import tools.jackson.databind.exc.ValueInstantiationException;
import tools.jackson.databind.json.JsonMapper;
import tools.jackson.databind.jsontype.TypeIdResolver;
import tools.jackson.databind.node.ObjectNode;
import tools.jackson.databind.testutil.NoCheckSubTypeValidator;

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

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

/**
 * Tests to exercise handler methods of {@link DeserializationProblemHandler}.
 */
public class DeserializationProblemHandlerTest
{
    /*
    /**********************************************************
    /* Test handler types
    /**********************************************************
     */

    static class WeirdKeyHandler
        extends DeserializationProblemHandler
    {
        protected final Object key;

        public WeirdKeyHandler(Object key0) {
            key = key0;
        }

        @Override
        public Object handleWeirdKey(DeserializationContext ctxt,
                Class<?> rawKeyType, String keyValue,
                String failureMsg)
        {
            return key;
        }
    }

    static class WeirdNumberHandler
        extends DeserializationProblemHandler
    {
        protected final Object value;

        public WeirdNumberHandler(Object v0) {
            value = v0;
        }

        @Override
        public Object handleWeirdNumberValue(DeserializationContext ctxt,
                Class<?> targetType, Number n,
                String failureMsg)
        {
            return value;
        }
    }

    static class WeirdStringHandler
        extends DeserializationProblemHandler
    {
        protected final Object value;

        public WeirdStringHandler(Object v0) {
            value = v0;
        }

        @Override
        public Object handleWeirdStringValue(DeserializationContext ctxt,
                Class<?> targetType, String v,
                String failureMsg)
        {
            return value;
        }
    }

    static class InstantiationProblemHandler
        extends DeserializationProblemHandler
    {
        protected final Object value;

        public InstantiationProblemHandler(Object v0) {
            value = v0;
        }

        @Override
        public Object handleInstantiationProblem(DeserializationContext ctxt,
                Class<?> instClass, Object argument, Throwable t)
        {
            if (!(t instanceof ValueInstantiationException)) {
                throw new IllegalArgumentException("Should have gotten `ValueInstantiationException`, instead got: "+t);
            }
            return value;
        }
    }

    static class MissingInstantiationHandler
        extends DeserializationProblemHandler
    {
        protected final Object value;

        public MissingInstantiationHandler(Object v0) {
            value = v0;
        }

        @Override
        public Object handleMissingInstantiator(DeserializationContext ctxt,
                Class<?> instClass, ValueInstantiator inst, JsonParser p, String msg)
        {
            p.skipChildren();
            return value;
        }
    }

    static class WeirdTokenHandler
        extends DeserializationProblemHandler
    {
        protected final Object value;

        public WeirdTokenHandler(Object v) {
            value = v;
        }

        @Override
        public Object handleUnexpectedToken(DeserializationContext ctxt,
                JavaType targetType, JsonToken t, JsonParser p,
                String failureMsg)
        {
            p.skipChildren();
            return value;
        }
    }

    static class UnknownTypeIdHandler
        extends DeserializationProblemHandler
    {
        protected final Class<?> raw;

        public UnknownTypeIdHandler(Class<?> r) { raw = r; }

        @Override
        public JavaType handleUnknownTypeId(DeserializationContext ctxt,
                JavaType baseType, String subTypeId, TypeIdResolver idResolver,
                String failureMsg)
        {
            return ctxt.constructType(raw);
        }
    }

    static class MissingTypeIdHandler
        extends DeserializationProblemHandler
    {
        protected final Class<?> raw;

        public MissingTypeIdHandler(Class<?> r) { raw = r; }

        @Override
        public JavaType handleMissingTypeId(DeserializationContext ctxt,
                JavaType baseType, TypeIdResolver idResolver,
                String failureMsg)
        {
            return ctxt.constructType(raw);
        }
    }

    // [databind#1767]
    static class IntHandler extends DeserializationProblemHandler
    {
        @Override
        public Object handleWeirdStringValue(DeserializationContext ctxt,
                Class<?> targetType,
                String valueToConvert,
                String failureMsg)
        {
            if (targetType != Integer.TYPE) {
                return NOT_HANDLED;
            }
            return 1;
        }
    }

    // [databind#2973]
    static class WeirdTokenHandler2973
        extends DeserializationProblemHandler
    {
        @Override
        public Object handleUnexpectedToken(DeserializationContext ctxt,
                JavaType targetType, JsonToken t, JsonParser p,
                String failureMsg)
        {
            String result = p.currentToken().toString();
            p.skipChildren();
            return result;
        }
    }

    // [databind#3349]
    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, ValueInstantiator inst,
                JsonParser p, String msg)
        {
            handleMissingInstantiatorCalled = true;
            return NOT_HANDLED;
        }

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

    // [databind#3450]
    static class LenientDeserializationProblemHandler extends DeserializationProblemHandler {
        @Override
        public Object handleWeirdStringValue(DeserializationContext ctxt, Class<?> targetType,
                String valueToConvert, String failureMsg)
        {
            // I just want to ignore badly formatted value
            return null;
        }
    }

    // [databind#4656]
    static class ProblemHandler4656 extends DeserializationProblemHandler
    {
        protected static final String NUMBER_LONG_KEY = "$numberLong";

        @Override
        public Object handleUnexpectedToken(DeserializationContext ctxt, JavaType targetType,
                JsonToken t, JsonParser p, String failureMsg)
        {
            if (targetType.getRawClass().equals(Long.class) && t == JsonToken.START_OBJECT) {
                JsonNode tree = p.readValueAsTree();
                if (tree.get(NUMBER_LONG_KEY) != null) {
                    try {
                        return Long.parseLong(tree.get(NUMBER_LONG_KEY).asString());
                    } catch (NumberFormatException e) { }
                }
            }
            return NOT_HANDLED;
        }
    }

    // [databind#5469]
    private static int hitCountFirst5469 = 0;
    static class ProblemHandler5469 extends DeserializationProblemHandler
    {
        @Override
        public Object handleNullForPrimitives(DeserializationContext ctxt, Class<?> targetType,
                JsonParser p, ValueDeserializer<?> deser, String failureMsg
        ) throws JacksonException {
            hitCountFirst5469++;
            return 5469L;
        }
    }

    private static int hitCountSecond5469 = 0;
    static class MoreProblemHandler5469 extends DeserializationProblemHandler
    {
        @Override
        public Object handleNullForPrimitives(DeserializationContext ctxt, Class<?> targetType,
                JsonParser p, ValueDeserializer<?> deser, String failureMsg
        ) throws JacksonException {
            hitCountSecond5469++;
            return "THIS  IS AN ERROR";
        }
    }

    // [databind#1440]
    static class DeserializationProblem {
        public List<String> unknownProperties = new ArrayList<>();

        public DeserializationProblem() { }

        public void addUnknownProperty(final String prop) {
            unknownProperties.add(prop);
        }
        public boolean foundProblems() {
            return !unknownProperties.isEmpty();
        }

        @Override
        public String toString() {
            return "DeserializationProblem{" +"unknownProperties=" + unknownProperties +'}';
        }
    }

    static class DeserializationProblemLogger extends DeserializationProblemHandler {

        public DeserializationProblem probs = new DeserializationProblem();

        public List<String> problems() {
            return probs.unknownProperties;
        }

        @Override
        public boolean handleUnknownProperty(final DeserializationContext ctxt, final JsonParser p,
                ValueDeserializer<?> deserializer, Object beanOrClass, String propertyName)
        {
            final TokenStreamContext parsingContext = p.streamReadContext();
            final List<String> pathList = new ArrayList<>();
            addParent(parsingContext, pathList);
            Collections.reverse(pathList);
            final String path = _join(".", pathList) + "#" + propertyName;

            probs.addUnknownProperty(path);

            p.skipChildren();
            return true;
        }

        static String _join(String sep, Collection<String> parts) {
            StringBuilder sb = new StringBuilder();
            for (String part : parts) {
                if (sb.length() > 0) {
                    sb.append(sep);
                }
                sb.append(part);
            }
            return sb.toString();
        }

        private void addParent(final TokenStreamContext streamContext, final List<String> pathList) {
            if (streamContext != null && streamContext.currentName() != null) {
                pathList.add(streamContext.currentName());
                addParent(streamContext.getParent(), pathList);
            }
        }
    }

    /*
    /**********************************************************
    /* Other helper types
    /**********************************************************
     */

    static class IntKeyMapWrapper {
        public Map<Integer,String> stuff;
    }

    @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type")
    static class Base { }
    static class BaseImpl extends Base {
        public int a;
    }

    static class BaseWrapper {
        public Base value;
    }

    @JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, property = "clazz")
    static class Base2 { }
    static class Base2Impl extends Base2 {
        public int a;
    }

    static class Base2Wrapper {
        public Base2 value;
    }

    enum SingleValuedEnum {
        A;
    }

    static class BustedCtor {
        public final static BustedCtor INST = new BustedCtor(true);

        public BustedCtor() {
            throw new RuntimeException("Fail! (to be caught by handler)");
        }
        private BustedCtor(boolean b) { }
    }

    static class NoDefaultCtor {
        public int value;

        public NoDefaultCtor(int v) { value = v; }
    }

    // [databind#1767]
    static class TestBean1767 {
        int a;

        public int getA() {
            return a;
        }

        public void setA(int a) {
            this.a = a;
        }
    }

    // [databind#3349]
    static class ArrayHolder3349 {
        private final Collection<String> prop;

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

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

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

    // [databind#3349]
    static class StringHolder3349 {
        private final String prop;

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

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

    // [databind#3450]
    static class TestPojo3450Int {
        public Integer myInteger;
    }

    static class TestPojo3450Long {
        public Long myLong;
    }

    // [databind#4656]
    static class Person4656 {
        public String id;
        public String name;
        public Long age;
    }

    // [databind#5469]
    static class Person5469 {
        public String id;
        public String name;
        public long age;
    }

    // [databind#1440]
    @JsonInclude(JsonInclude.Include.NON_NULL)
    static class Activity1440 {
        public ActivityEntity1440 actor;
        public String verb;
        public ActivityEntity1440 object;
        public ActivityEntity1440 target;

        @JsonCreator
        public Activity1440(@JsonProperty("actor") final ActivityEntity1440 actor, @JsonProperty("object") final ActivityEntity1440 object, @JsonProperty("target") final ActivityEntity1440 target, @JsonProperty("verb") final String verb) {
            this.actor = actor;
            this.verb = verb;
            this.object = object;
            this.target = target;
        }
    }

    @JsonInclude(JsonInclude.Include.NON_NULL)
    static class ActivityEntity1440 {
        public String id;
        public String type;
        public String status;
        public String context;

        @JsonCreator
        public ActivityEntity1440(@JsonProperty("id") final String id, @JsonProperty("type") final String type, @JsonProperty("status") final String status, @JsonProperty("context") final String context) {
            this.id = id;
            this.type = type;
            this.status = status;
            this.context = context;
        }
    }

    // [databind#2221]
    @SuppressWarnings("rawtypes")
    @JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, include = JsonTypeInfo.As.PROPERTY, property = "_class")
    @JsonInclude(Include.NON_EMPTY)
    static class GenericContent2221 {

        private Collection innerObjects;

        public Collection getInnerObjects() {
            return innerObjects;
        }

        public void setInnerObjects(Collection innerObjects) {
            this.innerObjects = innerObjects;
        }
    }

    static class DummyContent2221 {
        private String aField;

        public DummyContent2221() {
            super();
        }

        public DummyContent2221(String aField) {
            super();
            this.aField = aField;
        }

        public String getaField() {
            return aField;
        }

        public void setaField(String aField) {
            this.aField = aField;
        }

        @Override
        public String toString() {
            return "DummyContent [aField=" + aField + "]";
        }
    }

    /*
    /**********************************************************
    /* Test methods
    /**********************************************************
     */

    private final ObjectMapper MAPPER = newJsonMapper();

    @Test
    public void testWeirdKeyHandling() throws Exception
    {
        ObjectMapper mapper = jsonMapperBuilder()
            .addHandler(new WeirdKeyHandler(7))
            .build();
        IntKeyMapWrapper w = mapper.readValue("{\"stuff\":{\"foo\":\"abc\"}}",
                IntKeyMapWrapper.class);
        Map<Integer,String> map = w.stuff;
        assertEquals(1, map.size());
        assertEquals("abc", map.values().iterator().next());
        assertEquals(Integer.valueOf(7), map.keySet().iterator().next());
    }

    @Test
    public void testWeirdNumberHandling() throws Exception
    {
        ObjectMapper mapper = jsonMapperBuilder()
            .addHandler(new WeirdNumberHandler(SingleValuedEnum.A))
            .build();
        SingleValuedEnum result = mapper.readValue("3", SingleValuedEnum.class);
        assertEquals(SingleValuedEnum.A, result);

        mapper = jsonMapperBuilder()
                .addHandler(new WeirdNumberHandler("foo"))
                .build();
        try {
            mapper.readValue("3", SingleValuedEnum.class);
            fail("Should not pass");
        } catch (InvalidFormatException e) {
            verifyException(e, "returned value of type `java.lang.String`");
        }
    }

    @Test
    public void testWeirdStringHandling() throws Exception
    {
        ObjectMapper mapper = jsonMapperBuilder()
            .addHandler(new WeirdStringHandler(SingleValuedEnum.A))
            .build();
        SingleValuedEnum result = mapper.readValue("\"B\"", SingleValuedEnum.class);
        assertEquals(SingleValuedEnum.A, result);

        // also, write [databind#1629] try this
        mapper = jsonMapperBuilder()
                .addHandler(new WeirdStringHandler(null))
                .build();
        UUID result2 = mapper.readValue(q("not a uuid!"), UUID.class);
        assertNull(result2);

        mapper = jsonMapperBuilder()
                .addHandler(new WeirdStringHandler("foo"))
                .build();
        try {
            mapper.readValue(q("not a uuid!"), UUID.class);
            fail("Should not pass");
        } catch (InvalidFormatException e) {
            verifyException(e, "returned value of type `java.lang.String`");
        }
    }

    // [databind#3784]: Base64 decoding
    @Test
    public void testWeirdStringForBase64() throws Exception
    {
        ObjectMapper mapper = jsonMapperBuilder()
                .addHandler(new WeirdStringHandler(new byte[0]))
                .build();
        byte[] binary = mapper.readValue(q("foobar"), byte[].class);
        assertNotNull(binary);
        assertEquals(0, binary.length);

        JsonNode tree = mapper.readTree(q("foobar"));
        binary = mapper.treeToValue(tree, byte[].class);
        assertNotNull(binary);
        assertEquals(0, binary.length);
    }

    @Test
    public void testInvalidTypeId() throws Exception
    {
        ObjectMapper mapper = jsonMapperBuilder()
            .addHandler(new UnknownTypeIdHandler(BaseImpl.class))
            .build();
        BaseWrapper w = mapper.readValue("{\"value\":{\"type\":\"foo\",\"a\":4}}",
                BaseWrapper.class);
        assertNotNull(w);
        assertEquals(BaseImpl.class, w.value.getClass());

        mapper = jsonMapperBuilder()
                .addHandler(new UnknownTypeIdHandler(String.class))
                .build();
        try {
            mapper.readValue("{\"value\":{\"type\":\"foo\",\"a\":4}}",
                    BaseWrapper.class);
            fail("Should not pass");
        } catch (InvalidTypeIdException e) {
            verifyException(e, "into non-subtype: `java.lang.String`");
        }
    }

    @Test
    public void testInvalidClassAsId() throws Exception
    {
        ObjectMapper mapper = jsonMapperBuilder()
            .addHandler(new UnknownTypeIdHandler(Base2Impl.class))
            .build();
        Base2Wrapper w = mapper.readValue("{\"value\":{\"clazz\":\"com.fizz\",\"a\":4}}",
                Base2Wrapper.class);
        assertNotNull(w);
        assertEquals(Base2Impl.class, w.value.getClass());
    }

    // 2.9: missing type id, distinct from unknown

    @Test
    public void testMissingTypeId() throws Exception
    {
        ObjectMapper mapper = jsonMapperBuilder()
            .addHandler(new MissingTypeIdHandler(BaseImpl.class))
            .build();
        BaseWrapper w = mapper.readValue("{\"value\":{\"a\":4}}",
                BaseWrapper.class);
        assertNotNull(w);
        assertEquals(BaseImpl.class, w.value.getClass());

        mapper = jsonMapperBuilder()
                .addHandler(new MissingTypeIdHandler(String.class))
                .build();
        try {
            mapper.readValue("{\"value\":{\"a\":4}}",
                    BaseWrapper.class);
            fail("Should not pass");
        } catch (InvalidTypeIdException e) {
            verifyException(e, "into non-subtype: `java.lang.String`");
        }
    }

    @Test
    public void testMissingClassAsId() throws Exception
    {
        ObjectMapper mapper = jsonMapperBuilder()
            .addHandler(new MissingTypeIdHandler(Base2Impl.class))
            .build();
        Base2Wrapper w = mapper.readValue("{\"value\":{\"a\":4}}",
                Base2Wrapper.class);
        assertNotNull(w);
        assertEquals(Base2Impl.class, w.value.getClass());
    }

    // verify that by default we get special exception type
    @Test
    public void testInvalidTypeIdFail() throws Exception
    {
        try {
            MAPPER.readValue("{\"value\":{\"type\":\"foo\",\"a\":4}}",
                BaseWrapper.class);
            fail("Should not pass");
        } catch (InvalidTypeIdException e) {
            verifyException(e, "Could not resolve type id 'foo'");
            assertEquals(Base.class, e.getBaseType().getRawClass());
            assertEquals("foo", e.getTypeId());
        }
    }

    @Test
    public void testInstantiationExceptionHandling() throws Exception
    {
        ObjectMapper mapper = jsonMapperBuilder()
            .addHandler(new InstantiationProblemHandler(BustedCtor.INST))
            .build();
        BustedCtor w = mapper.readValue("{ }", BustedCtor.class);
        assertNotNull(w);

        // and then broken handling
        mapper = jsonMapperBuilder()
                .addHandler(new InstantiationProblemHandler("foo"))
                .build();
        try {
            mapper.readValue("{ }", BustedCtor.class);
            fail("Should not pass");
        } catch (InvalidDefinitionException e) {
            verifyException(e, "returned value of type `java.lang.String`");
        }
    }

    @Test
    public void testMissingInstantiatorHandling() throws Exception
    {
        ObjectMapper mapper = jsonMapperBuilder()
            // 14-Jan-2025, tatu: Need to disable trailing tokens (for 3.0)
            //   for this to work (handler not consuming all tokens as it should
            //   but no time to fully fix right now)
            .disable(DeserializationFeature.FAIL_ON_TRAILING_TOKENS)
            .addHandler(new MissingInstantiationHandler(new NoDefaultCtor(13)))
            .build();
        NoDefaultCtor w = mapper.readValue("{ \"x\" : true }", NoDefaultCtor.class);
        assertNotNull(w);
        assertEquals(13, w.value);

        // And then broken case
        mapper = jsonMapperBuilder()
                .disable(DeserializationFeature.FAIL_ON_TRAILING_TOKENS)
                .addHandler(new MissingInstantiationHandler("foo"))
                .build();
        try {
            mapper.readValue("{ \"x\" : true }", NoDefaultCtor.class);
            fail("Should not pass");
        } catch (InvalidDefinitionException e) {
            verifyException(e, "returned value of type `java.lang.String`");
        }
    }

    @Test
    public void testUnexpectedTokenHandling() throws Exception
    {
        ObjectMapper mapper = jsonMapperBuilder()
            .addHandler(new WeirdTokenHandler(Integer.valueOf(13)))
            .build();
        Integer v = mapper.readValue("true", Integer.class);
        assertEquals(Integer.valueOf(13), v);

        mapper = jsonMapperBuilder()
                .addHandler(new WeirdTokenHandler("foo"))
                .build();
        try{
            mapper.readValue("true", Integer.class);
            fail("Should not pass");
        } catch (InvalidDefinitionException e) {
            verifyException(e, "returned value of type `java.lang.String`");
        }
    }

    /*
    /**********************************************************
    /* Test methods, [databind#1767]
    /**********************************************************
     */

    @Test
    public void testPrimitivePropertyWithHandler1767() throws Exception {
        final ObjectMapper mapper = jsonMapperBuilder()
                .addHandler(new IntHandler())
                .build();
        TestBean1767 result = mapper.readValue(a2q("{'a': 'not-a-number'}"), TestBean1767.class);
        assertNotNull(result);
        assertEquals(1, result.a);
    }

    /*
    /**********************************************************
    /* Test methods, [databind#2973]
    /**********************************************************
     */

    @Test
    public void testUnexpectedToken2973() throws Exception
    {
        // First: without handler, should get certain failure
        ObjectMapper mapper = sharedMapper();
        try {
            mapper.readValue("{ }", String.class);
            fail("Should not pass");
        } catch (MismatchedInputException e) {
            verifyException(e, "Cannot deserialize value of type `java.lang.String` from Object value");
        }

        // But DeserializationProblemHandler should resolve:
        mapper = jsonMapperBuilder()
            .addHandler(new WeirdTokenHandler2973())
            .build();

        String str = mapper.readValue("{ }", String.class);
        assertEquals("START_OBJECT", str);
    }

    /*
    /**********************************************************************
    /* Test methods, [databind#3349]
    /**********************************************************************
     */

    // 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 testHandleUnexpectedTokenForStringProp3349() 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, StringHolder3349.class);
        } catch (Exception e) {
            // May fail, but we just want to check which handler was called
        }

        _verifyHandleUnexpectedTokenCalled(handler);
    }

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

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

        mapper.treeToValue(input, ArrayHolder3349.class);

        _verifyHandleUnexpectedTokenCalled(handler);
    }

    // [databind#3349]: direct Collection<String> (StringCollectionDeserializer)
    @Test
    public void testHandleUnexpectedTokenForStringCollection3349() 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 testHandleUnexpectedTokenForObjectCollection3349() 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);
    }

    // [databind#3349]: Map<String,String> (MapDeserializer)
    @Test
    public void testHandleUnexpectedTokenForMap3349() 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);
    }

    // [databind#3349]: Object[] (ObjectArrayDeserializer)
    @Test
    public void testHandleUnexpectedTokenForObjectArray3349() 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 testHandleUnexpectedTokenForStringArray3349() 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 testHandleUnexpectedTokenForIntArray3349() 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 testHandleUnexpectedTokenForLongArray3349() 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 testHandleUnexpectedTokenForBooleanArray3349() throws Exception
    {
        TrackingProblemHandler handler = new TrackingProblemHandler();
        ObjectMapper mapper = jsonMapperBuilder()
            .addHandler(handler)
            .build();

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

        _verifyHandleUnexpectedTokenCalled(handler);
    }

    /*
    /**********************************************************
    /* Test methods, [databind#3450]
    /**********************************************************
     */

    private final ObjectMapper LENIENT_MAPPER =
            JsonMapper.builder().addHandler(new LenientDeserializationProblemHandler()).build();

    // [databind#3450]
    @Test
    public void testIntegerCoercion3450() throws Exception
    {
        TestPojo3450Int pojo;

        // First expected coercion into `null` from empty String
        pojo = LENIENT_MAPPER.readValue("{\"myInteger\" : \"\"}", TestPojo3450Int.class);
        assertNull(pojo.myInteger);

        // and then coercion into `null` by our problem handler
        pojo = LENIENT_MAPPER.readValue("{\"myInteger\" : \"notInt\"}", TestPojo3450Int.class);
        assertNull(pojo.myInteger);
    }

    @Test
    public void testLongCoercion3450() throws Exception
    {
        TestPojo3450Long pojo;

        // First expected coercion into `null` from empty String
        pojo = LENIENT_MAPPER.readValue("{\"myLong\" : \"\"}", TestPojo3450Long.class);
        assertNull(pojo.myLong);

        // and then coercion into `null` by our problem handler
        pojo = LENIENT_MAPPER.readValue("{\"myLong\" : \"notSoLong\"}", TestPojo3450Long.class);
        assertNull(pojo.myLong);
    }

    /*
    /**********************************************************
    /* Test methods, [databind#4656]
    /**********************************************************
     */

    @Test
    public void testUnexpectedToken4656() throws Exception {
        ObjectMapper mapper = JsonMapper.builder()
                .addHandler(new ProblemHandler4656())
                .build();
        final String json = "{\"id\":  \"12ab\", \"name\": \"Bob\", \"age\": {\"$numberLong\": \"10\"}}";
        Person4656 person = mapper.readValue(json, Person4656.class);
        assertNotNull(person);
        assertEquals("12ab", person.id);
        assertEquals("Bob", person.name);
        assertEquals(10L, person.age);
    }

    /*
    /**********************************************************
    /* Test methods, [databind#5469]
    /**********************************************************
     */

    // SUCCESS Test when problem handler was implemented as required.
    @Test
    public void testNullForPrimitivesHappyCase5469()
        throws Exception
    {
        // Given
        assertEquals(0, hitCountFirst5469);
        ObjectMapper mapper = JsonMapper.builder()
                .enable(DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES)
                .addHandler(new ProblemHandler5469())
                .build();

        // When
        Person5469 person = mapper.readValue(
            "{\"id\":  \"12ab\", \"name\": \"Bob\", " +
            // Input is NULL, but....
            "\"age\": null}", Person5469.class);

        // Then
        assertNotNull(person);
        assertEquals("12ab", person.id);
        assertEquals("Bob", person.name);
        // We get the MAGIC NUMBER as age
        assertEquals(5469L, person.age);
        // Sanity check, we hit the code path as we wanted
        assertEquals(1, hitCountFirst5469);
    }

    // FAIL! Test when problem handler was implemented WRONG
    @Test
    public void testNullForPrimitivesBadImpl5469()
        throws Exception
    {
        // Given
        assertEquals(0, hitCountSecond5469);
        ObjectMapper mapper = JsonMapper.builder()
                .enable(DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES)
                .addHandler(new MoreProblemHandler5469())
                .build();

        // When
        try {
            mapper.readValue("{\"id\":  \"12ab\", \"name\": \"Bob\", " +
                    // Input is NULL, to cause problem
                    "\"age\": null}", Person5469.class);
            // Sanity check, we hit the code path as we wanted
            assertEquals(1, hitCountSecond5469);
            fail("Should not reach here.");
        } catch (InvalidFormatException e) {
            // Then
            verifyException(e,
                    "`DeserializationProblemHandler.handleNullForPrimitives()` for type `long` returned value of type `java.lang.String`");
        }
    }

    /*
    /**********************************************************
    /* Test methods, [databind#1440]
    /**********************************************************
     */

    @Test
    public void testIncorrectContext1440() throws Exception
    {
        // need invalid to trigger problem:
        final String invalidInput = a2q(
"{'actor': {'id': 'actor_id','type': 'actor_type',"
+"'status': 'actor_status','context':'actor_context','invalid_1': 'actor_invalid_1'},"
+"'verb': 'verb','object': {'id': 'object_id','type': 'object_type',"
+"'invalid_2': 'object_invalid_2','status': 'object_status','context': 'object_context'},"
+"'target': {'id': 'target_id','type': 'target_type','invalid_3': 'target_invalid_3',"
+"'invalid_4': 'target_invalid_4','status': 'target_status','context': 'target_context'}}"
);
        final DeserializationProblemLogger logger = new DeserializationProblemLogger();

        ObjectMapper mapper = jsonMapperBuilder()
                .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
                .addHandler(logger)
                .build();
        mapper.readValue(invalidInput, Activity1440.class);

        List<String> probs = logger.problems();
        assertEquals(4, probs.size());
        assertEquals("actor.invalid_1#invalid_1", probs.get(0));
        assertEquals("object.invalid_2#invalid_2", probs.get(1));
        assertEquals("target.invalid_3#invalid_3", probs.get(2));
        assertEquals("target.invalid_4#invalid_4", probs.get(3));
    }

    /*
    /**********************************************************
    /* Test methods, [databind#2221]
    /**********************************************************
     */

    private final static String CLASS_GENERIC_CONTENT_2221 = GenericContent2221.class.getName();
    private final static String CLASS_DUMMY_CONTENT_2221 = DummyContent2221.class.getName();
    private final static String JSON_2221 = a2q(
"{\n" +
"          \"_class\":\""+CLASS_GENERIC_CONTENT_2221+"\",\n" +
"          \"innerObjects\":\n" +
"               [\n" +
"                    \"java.util.ArrayList\",\n" +
"                    [\n" +
"                         [\n" +
"                              \""+CLASS_DUMMY_CONTENT_2221+"\",\n" +
"                              {\n" +
"                                   \"aField\":\"some value\"\n" +
"                              }\n" +
"                         ],\n" +
"                         [\n" +
"                              \"tools.jackson.databind.deser.NoSuchClass$AnInventedClassBeingNotOnTheClasspath\",\n" +
"                              {\n" +
"                                   \"aField\":\"some value\"\n" +
"                              }\n" +
"                         ]\n" +
"                    ]\n" +
"               ]\n" +
"     }"
);

    @Test
    public void testWithDeserializationProblemHandler2221() throws Exception {
        final ObjectMapper mapper = jsonMapperBuilder()
                .activateDefaultTyping(NoCheckSubTypeValidator.instance)
                .addHandler(new DeserializationProblemHandler() {
                    @Override
                    public JavaType handleUnknownTypeId(DeserializationContext ctxt, JavaType baseType, String subTypeId, TypeIdResolver idResolver, String failureMsg) {
                        return ctxt.constructType(Void.class);
                    }
                })
        .build();

        GenericContent2221 processableContent = mapper.readValue(JSON_2221, GenericContent2221.class);
        assertNotNull(processableContent.getInnerObjects());
        assertEquals(2, processableContent.getInnerObjects().size());
    }

    @Test
    public void testWithDisabledFailOnInvalidSubtype2221() throws Exception {
        final ObjectMapper mapper = jsonMapperBuilder()
                .disable(DeserializationFeature.FAIL_ON_INVALID_SUBTYPE)
                .activateDefaultTyping(NoCheckSubTypeValidator.instance)
                .build();
        GenericContent2221 processableContent = mapper.readValue(JSON_2221, GenericContent2221.class);
        assertNotNull(processableContent.getInnerObjects());
        assertEquals(2, processableContent.getInnerObjects().size());
    }

    /*
    /**********************************************************
    /* 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");
    }
}