WithPathTest.java

package com.fasterxml.jackson.databind.node;

import org.junit.jupiter.api.Test;

import com.fasterxml.jackson.core.JsonPointer;
import com.fasterxml.jackson.databind.*;
import com.fasterxml.jackson.databind.JsonNode.OverwriteMode;
import com.fasterxml.jackson.databind.testutil.DatabindTestUtil;

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

// for [databind#1980] implementation
public class WithPathTest extends DatabindTestUtil
{
    private final ObjectMapper MAPPER = sharedMapper();

    /*
    /**********************************************************************
    /* Test methods, withObject()
    /**********************************************************************
     */

    @Test
    public void testValidWithObjectTrivial() throws Exception
    {
        ObjectNode root = MAPPER.createObjectNode();
        ObjectNode match = root.withObject(JsonPointer.empty());
        assertSame(root, match);
    }

    @Test
    public void testValidWithObjectSimpleExisting() throws Exception
    {
        _testValidWithObjectSimpleExisting(true);
        _testValidWithObjectSimpleExisting(false);
    }

    @Test
    public void testInvalidWithObjectTrivial() throws Exception
    {
        ArrayNode root = MAPPER.createArrayNode();
        try {
            root.withObject(JsonPointer.compile("/a"));
            fail("Should not pass");
        } catch (UnsupportedOperationException e) {
            verifyException(e, "Cannot replace context node");
            verifyException(e, "ArrayNode");
        }
    }

    private void _testValidWithObjectSimpleExisting(boolean compile) throws Exception
    {
        final String DOC_STR = a2q("{'a':{'b':42,'c':{'x':1}}}");
        JsonNode doc = MAPPER.readTree(DOC_STR);
        ObjectNode match = compile
                ? doc.withObject(JsonPointer.compile("/a"))
                : doc.withObject("/a");
        assertNotNull(match);
        assertTrue(match.isObject());
        assertEquals(a2q("{'b':42,'c':{'x':1}}"), match.toString());
        // should not modify the doc
        assertEquals(DOC_STR, doc.toString());

        match = compile
                ? doc.withObject(JsonPointer.compile("/a/c"))
                : doc.withObject("/a/c");
        assertNotNull(match);
        assertEquals(a2q("{'x':1}"), match.toString());
        // should not modify the doc
        assertEquals(DOC_STR, doc.toString());
    }

    @Test
    public void testValidWithObjectSimpleCreate() throws Exception {
        _testValidWithObjectSimpleCreate(true);
        _testValidWithObjectSimpleCreate(false);
    }

    private void _testValidWithObjectSimpleCreate(boolean compile) throws Exception
    {
        ObjectNode root = MAPPER.createObjectNode();
        ObjectNode match = compile
                ? root.withObject(JsonPointer.compile("/a/b"))
                : root.withObject("/a/b");
        assertTrue(match.isObject());
        assertEquals(a2q("{}"), match.toString());
        match.put("value", 42);

        assertEquals(a2q("{'a':{'b':{'value':42}}}"),
                root.toString());

        // and with that
        ObjectNode match2 = compile
                ? root.withObject(JsonPointer.compile("/a/b"))
                : root.withObject("/a/b");
        assertSame(match, match2);
        match.put("value2", true);

        assertEquals(a2q("{'a':{'b':{'value':42,'value2':true}}}"),
                root.toString());
    }

    @Test
    public void testValidWithObjectSimpleModify() throws Exception {
        _testValidWithObjectSimpleModify(true);
        _testValidWithObjectSimpleModify(false);
    }

    private void _testValidWithObjectSimpleModify(boolean compile) throws Exception
    {
        final String DOC_STR = a2q("{'a':{'b':42}}");
        JsonNode doc = MAPPER.readTree(DOC_STR);
        ObjectNode  match = compile
                ? doc.withObject(JsonPointer.compile("/a/d"))
                : doc.withObject("/a/d");
        assertNotNull(match);
        assertEquals("{}", match.toString());
        assertEquals(a2q("{'a':{'b':42,'d':{}}}"), doc.toString());
    }

    @Test
    public void testObjectPathWithReplace() throws Exception {
        _testObjectPathWithReplace(true);
        _testObjectPathWithReplace(false);
    }

    private void _testObjectPathWithReplace(boolean compile) throws Exception
    {
        final JsonPointer abPath = JsonPointer.compile("/a/b");
        ObjectNode root = MAPPER.createObjectNode();
        root.put("a", 13);

        // First, without replacement (default) get exception
        _verifyObjectReplaceFail(root, abPath, null);

        // Except fine via nulls (by default)
        root.putNull("a");
        if (compile) {
             root.withObject(abPath).put("value", 42);
        } else {
             root.withObject("/a/b").put("value", 42);
        }
        assertEquals(a2q("{'a':{'b':{'value':42}}}"),
                root.toString());

        // but not if prevented
        root = (ObjectNode) MAPPER.readTree(a2q("{'a':null}"));
        _verifyObjectReplaceFail(root, abPath, OverwriteMode.NONE);
    }

    @Test
    public void testValidWithObjectWithArray() throws Exception {
        _testValidWithObjectWithArray(true);
        _testValidWithObjectWithArray(false);
    }

    private void _testValidWithObjectWithArray(boolean compile) throws Exception
    {
        ObjectNode root = MAPPER.createObjectNode();
        root.putArray("arr");
        ObjectNode match = compile
                ? root.withObject(JsonPointer.compile("/arr/2"))
                : root.withObject("/arr/2");
        assertTrue(match.isObject());
        match.put("value", 42);
        assertEquals(a2q("{'arr':[null,null,{'value':42}]}"),
                root.toString());

        // But also verify we can match
        ObjectNode match2 = compile
                ? root.withObject(JsonPointer.compile("/arr/2"))
                : root.withObject("/arr/2");
        assertSame(match, match2);
        match2.put("value2", true);
        assertEquals(a2q("{'arr':[null,null,{'value':42,'value2':true}]}"),
                root.toString());

        // And even more! `null`s can be replaced by default
        ObjectNode match3 = compile
                ? root.withObject(JsonPointer.compile("/arr/0"))
                : root.withObject("/arr/0");
        assertEquals("{}", match3.toString());
        match3.put("value", "bar");
        assertEquals(a2q("{'arr':[{'value':'bar'},null,{'value':42,'value2':true}]}"),
                root.toString());

        // But not if prevented
        _verifyObjectReplaceFail(root, "/arr/1", OverwriteMode.NONE);
    }

    private void _verifyObjectReplaceFail(JsonNode doc, String ptrExpr, OverwriteMode mode) {
        _verifyObjectReplaceFail(doc, JsonPointer.compile(ptrExpr), mode);
    }

    private void _verifyObjectReplaceFail(JsonNode doc, JsonPointer ptr, OverwriteMode mode) {
        try {
            if (mode == null) {
                // default is "NULLS":
                mode = OverwriteMode.NULLS;
                doc.withObject(ptr);
            } else {
                doc.withObject(ptr, mode, true);
            }
            fail("Should not pass");
        } catch (UnsupportedOperationException e) {
            verifyException(e, "Cannot replace `JsonNode` of type ");
            verifyException(e, "(mode `OverwriteMode."+mode.name()+"`)");
        }
    }

    /*
    /**********************************************************************
    /* Test methods, withObjectProperty()/withObject(exprOrProperty)
    /**********************************************************************
     */

    // [databind#4095]
    @Test
    public void testWithObjectProperty() throws Exception
    {
        ObjectNode root = MAPPER.createObjectNode();

        // First: create new property value
        ObjectNode match = root.withObjectProperty("a");
        assertTrue(match.isObject());
        assertEquals(a2q("{}"), match.toString());
        match.put("value", 42);
        assertEquals(a2q("{'a':{'value':42}}"), root.toString());

        // Second: match existing Object property
        ObjectNode match2 = root.withObjectProperty("a");
        assertSame(match, match2);
        match.put("value2", true);

        assertEquals(a2q("{'a':{'value':42,'value2':true}}"),
                root.toString());

        // Third: match and overwrite existing null node
        JsonNode root2 = MAPPER.readTree("{\"b\": null}");
        ObjectNode match3 = root2.withObjectProperty("b");
        assertNotSame(match, match3);
        assertEquals("{\"b\":{}}", root2.toString());

        // and then failing case
        JsonNode root3 = MAPPER.readTree("{\"c\": 123}");
        try {
            root3.withObjectProperty("c");
            fail("Should not pass");
        } catch (UnsupportedOperationException e) {
            verifyException(e, "Cannot replace `JsonNode` of type ");
        }
    }

    // [databind#4096]
    @Test
    public void testWithObjectAdnExprOrProp() throws Exception
    {
        ObjectNode root = MAPPER.createObjectNode();

        // First: create new property value
        ObjectNode match = root.withObject("a");
        assertTrue(match.isObject());
        assertEquals(a2q("{}"), match.toString());
        match.put("value", 42);
        assertEquals(a2q("{'a':{'value':42}}"), root.toString());

        // and then with JsonPointer expr
        match = root.withObject("/a/b");
        assertTrue(match.isObject());
        assertEquals(a2q("{}"), match.toString());
        assertEquals(a2q("{'a':{'value':42,'b':{}}}"), root.toString());

        // Then existing prop:
        assertEquals(a2q("{'value':42,'b':{}}"),
                root.withObject("a").toString());
        assertEquals(a2q("{}"),
                root.withObject("/a/b").toString());

        // and then failing case
        JsonNode root3 = MAPPER.readTree("{\"c\": 123}");
        try {
            root3.withObject("c");
            fail("Should not pass");
        } catch (UnsupportedOperationException e) {
            verifyException(e, "Cannot replace `JsonNode` of type ");
        }
        try {
            root3.withObject("/c");
            fail("Should not pass");
        } catch (UnsupportedOperationException e) {
            verifyException(e, "Cannot replace `JsonNode` of type ");
        }
    }

    /*
    /**********************************************************************
    /* Test methods, withArray()
    /**********************************************************************
     */

    @Test
    public void testValidWithArrayTrivial() throws Exception
    {
        // First, empty path, existing Array
        ArrayNode root = MAPPER.createArrayNode();
        ArrayNode match = root.withArray(JsonPointer.empty());
        assertSame(root, match);

        // Then existing Object, empty Path -> fail
        ObjectNode rootOb = MAPPER.createObjectNode();
        try {
            rootOb.withArray(JsonPointer.empty());
            fail("Should not pass");
        } catch (UnsupportedOperationException e) {
            verifyException(e, "Can only call `withArray()` with empty");
            verifyException(e, "on `ArrayNode`");
        }
    }

    // From Javadoc example
    @Test
    public void testValidWithArraySimple() throws Exception {
        _testValidWithArraySimple(true);
        _testValidWithArraySimple(false);
    }

    @Test
    public void testInvalidWithArrayTrivial() throws Exception
    {
        ArrayNode root = MAPPER.createArrayNode();
        try {
            root.withArray(JsonPointer.compile("/a"));
            fail("Should not pass");
        } catch (UnsupportedOperationException e) {
            verifyException(e, "Cannot replace context node");
            verifyException(e, "ArrayNode");
        }
    }

    private void _testValidWithArraySimple(boolean compile) throws Exception
    {
        final String DOC_STR = a2q(
                "{'a':{'b':[1,2],'c':true}}"
                );
        JsonNode doc = MAPPER.readTree(DOC_STR);

        {
            ArrayNode match = compile
                    ? doc.withArray(JsonPointer.compile("/a/b"))
                    : doc.withArray("/a/b");
            assertEquals(a2q("[1,2]"), match.toString());
            // should not modify the doc
            assertEquals(DOC_STR, doc.toString());
        }
        {
            ArrayNode match = compile
                    ? doc.withArray(JsonPointer.compile("/a/x"))
                    : doc.withArray("/a/x");
            assertEquals(a2q("[]"), match.toString());
            // does modify the doc
            assertEquals(a2q(
                    "{'a':{'b':[1,2],'c':true,'x':[]}}"), doc.toString());
        }
        // And then replacements: first, fail
        _verifyArrayReplaceFail(doc, "/a/b/0", null);

        // then acceptable replacement
        {
            ArrayNode match = compile
                    ? doc.withArray(JsonPointer.compile("/a/b/0"), OverwriteMode.ALL, true)
                    : doc.withArray("/a/b/0", OverwriteMode.ALL, true);
            assertEquals(a2q("[]"), match.toString());
            // does further modify the doc
            assertEquals(a2q(
                    "{'a':{'b':[[],2],'c':true,'x':[]}}"), doc.toString());
        }
    }

    private void _verifyArrayReplaceFail(JsonNode doc, String ptrExpr, OverwriteMode mode) {
        _verifyArrayReplaceFail(doc, JsonPointer.compile(ptrExpr), mode);
    }

    private void _verifyArrayReplaceFail(JsonNode doc, JsonPointer ptr, OverwriteMode mode) {
        try {
            if (mode == null) {
                // default is "NULLS":
                mode = OverwriteMode.NULLS;
                doc.withArray(ptr);
            } else {
                doc.withArray(ptr, mode, true);
            }
            fail("Should not pass");
        } catch (UnsupportedOperationException e) {
            verifyException(e, "Cannot replace `JsonNode` of type ");
            verifyException(e, "(mode `OverwriteMode."+mode.name()+"`)");
        }
    }

    // [databind#3882]
    @Test
    public void testWithArray3882() throws Exception
    {
        ObjectNode root = MAPPER.createObjectNode();
        ArrayNode aN = root.withArray("/key/0/a",
                JsonNode.OverwriteMode.ALL, true);
        aN.add(123);
        assertEquals(a2q("{'key':[{'a':[123]}]}"),
                root.toString());

        // And then the original case
        root = MAPPER.createObjectNode();
        aN = root.withArray(JsonPointer.compile("/key1/array1/0/element1"),
            JsonNode.OverwriteMode.ALL, true);
        aN.add("v1");
        assertEquals(a2q("{'key1':{'array1':[{'element1':['v1']}]}}"),
                root.toString());
    }

    /*
    /**********************************************************************
    /* Test methods, withArrayProperty()/withArray(exprOrProperty)
    /**********************************************************************
     */

    // [databind#4095]
    @Test
    public void testWithArrayProperty() throws Exception
    {
        ObjectNode root = MAPPER.createObjectNode();

        // First: create new property value
        ArrayNode match = root.withArrayProperty("a");
        assertTrue(match.isArray());
        assertEquals(a2q("[]"), match.toString());
        match.add(42);
        assertEquals(a2q("{'a':[42]}"), root.toString());

        // Second: match existing Object property
        ArrayNode match2 = root.withArrayProperty("a");
        assertSame(match, match2);
        match.add(true);

        assertEquals(a2q("{'a':[42,true]}"), root.toString());

        // Third: match and overwrite existing null node
        JsonNode root2 = MAPPER.readTree("{\"b\": null}");
        ArrayNode match3 = root2.withArrayProperty("b");
        assertNotSame(match, match3);
        assertEquals("{\"b\":[]}", root2.toString());
        
        // and then failing case
        JsonNode root3 = MAPPER.readTree("{\"c\": 123}");
        try {
            root3.withArrayProperty("c");
            fail("Should not pass");
        } catch (UnsupportedOperationException e) {
            verifyException(e, "Cannot replace `JsonNode` of type ");
        }
    }

    // [databind#4096]
    @Test
    public void testWithArrayAndExprOrProp() throws Exception
    {
        ObjectNode root = MAPPER.createObjectNode();

        // First: create new property value
        ArrayNode match = root.withArray("a");
        assertTrue(match.isArray());
        assertEquals(a2q("[]"), match.toString());
        match.add(42);
        assertEquals(a2q("{'a':[42]}"), root.toString());

        match = root.withArray("/b");
        assertEquals(a2q("{'a':[42],'b':[]}"), root.toString());

        // Second: match existing Object property
        assertEquals(a2q("[42]"), root.withArray("a").toString());
        assertEquals(a2q("[42]"), root.withArray("/a").toString());

        // and then failing case
        JsonNode root3 = MAPPER.readTree("{\"c\": 123}");
        try {
            root3.withArrayProperty("c");
            fail("Should not pass");
        } catch (UnsupportedOperationException e) {
            verifyException(e, "Cannot replace `JsonNode` of type ");
        }
    }
}