JsonFilterTest.java

package tools.jackson.databind.ser.filter;

import java.util.*;

import org.junit.jupiter.api.Test;

import com.fasterxml.jackson.annotation.*;

import tools.jackson.core.JsonGenerator;
import tools.jackson.core.TokenStreamContext;
import tools.jackson.databind.*;
import tools.jackson.databind.exc.InvalidDefinitionException;
import tools.jackson.databind.ser.FilterProvider;
import tools.jackson.databind.ser.PropertyWriter;
import tools.jackson.databind.ser.std.SimpleBeanPropertyFilter;
import tools.jackson.databind.ser.std.SimpleFilterProvider;
import tools.jackson.databind.testutil.DatabindTestUtil;

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

/**
 * Tests for verifying that bean property filtering using {@link JsonFilter}
 * works as expected, including {@link SimpleFilterProvider} registration.
 */
public class JsonFilterTest extends DatabindTestUtil
{
    @JsonFilter("RootFilter")
    @JsonPropertyOrder({ "a", "b" })
    static class Bean {
        public String a = "a";
        public String b = "b";
    }

    @JsonFilter("checkSiblingContextFilter")
    static class CheckSiblingContextBean {
        public A a = new A();
        public B b = new B();
        @JsonFilter("checkSiblingContextFilter")
        static class A { }
        @JsonFilter("checkSiblingContextFilter")
        static class B {
            public C c = new C();
            @JsonFilter("checkSiblingContextFilter")
            static class C { }
        }
    }

    @JsonFilter("filterB")
    @JsonPropertyOrder({ "a", "b", "c"})
    static class BeanB {
        public String a;
        public String b;
        public String c;

        public BeanB(String a, String b, String c) {
            this.a = a;
            this.b = b;
            this.c = c;
        }
    }

    static class CheckSiblingContextFilter extends SimpleBeanPropertyFilter {
        @Override
        public void serializeAsProperty(Object bean, JsonGenerator jgen, SerializationContext prov,
                PropertyWriter writer) throws Exception {
            TokenStreamContext sc = jgen.streamWriteContext();

            if (writer.getName() != null && writer.getName().equals("c")) {
                assertEquals("b", sc.getParent().currentName());
            }
            writer.serializeAsProperty(bean, jgen, prov);
        }
    }

    // [databind#89]
    static class Pod
    {
        protected String username;
        protected String userPassword;

        public String getUsername() { return username; }
        public void setUsername(String value) { this.username = value; }

        @JsonIgnore
        @JsonProperty(value = "user_password")
        public String getUserPassword() { return userPassword; }

        @JsonProperty(value = "user_password")
        public void setUserPassword(String value) { this.userPassword = value; }
    }

    // [databind#306]: @JsonFilter for properties too
    @JsonPropertyOrder(alphabetic=true)
    static class FilteredProps
    {
        // will default to using "RootFilter", only including 'a'
        public Bean first = new Bean();

        // but minimal includes 'b'
        @JsonFilter("b")
        public Bean second = new Bean();
    }

    // For SimpleFilterProvider tests
    @JsonFilter("filterB")
    public static class AnyBeanB
    {
        public String a;
        public String b;

        public AnyBeanB(String a, String b) {
            this.a = a;
            this.b = b;
        }
    }

    @JsonFilter(value = "")
    public static class AnyBeanC
    {
        public String c;
        public String d;

        public AnyBeanC(String c, String d) {
            this.c = c;
            this.d = d;
        }
    }

    /*
    /**********************************************************************
    /* Test methods, @JsonFilter annotation
    /**********************************************************************
     */

    private final ObjectMapper MAPPER = newJsonMapper();

    @Test
    public void testSimpleInclusionFilter() throws Exception
    {
        FilterProvider prov = new SimpleFilterProvider().addFilter("RootFilter",
                SimpleBeanPropertyFilter.filterOutAllExcept("a"));
        assertEquals("{\"a\":\"a\"}", MAPPER.writer(prov).writeValueAsString(new Bean()));

        ObjectMapper mapper = jsonMapperBuilder()
                .filterProvider(prov)
                .build();
        assertEquals("{\"a\":\"a\"}", mapper.writeValueAsString(new Bean()));
    }

    @Test
    public void testIncludeAllFilter() throws Exception
    {
        FilterProvider prov = new SimpleFilterProvider().addFilter("RootFilter",
                SimpleBeanPropertyFilter.serializeAll());
        assertEquals("{\"a\":\"a\",\"b\":\"b\"}", MAPPER.writer(prov).writeValueAsString(new Bean()));
    }

    @Test
    public void testExcludeAllFilter() throws Exception
    {
        FilterProvider prov = new SimpleFilterProvider().addFilter("RootFilter",
            SimpleBeanPropertyFilter.filterOutAll());
        assertEquals("{}", MAPPER.writer(prov).writeValueAsString(new Bean()));
    }

    @Test
    public void testSimpleExclusionFilter() throws Exception
    {
        FilterProvider prov = new SimpleFilterProvider().addFilter("RootFilter",
                SimpleBeanPropertyFilter.serializeAllExcept("a"));
        assertEquals("{\"b\":\"b\"}", MAPPER.writer(prov).writeValueAsString(new Bean()));
    }

    @Test
    public void testMissingFilter() throws Exception
    {
        // First: default behavior should be to throw an exception
        try {
            MAPPER.writeValueAsString(new Bean());
            fail("Should have failed without configured filter");
        } catch (InvalidDefinitionException e) {
            verifyException(e, "Cannot resolve PropertyFilter with id 'RootFilter'");
        }

        // but when changing behavior, should work differently
        SimpleFilterProvider fp = new SimpleFilterProvider().setFailOnUnknownId(false);
        ObjectMapper mapper = jsonMapperBuilder()
                .filterProvider(fp)
                .build();
        assertEquals("{\"a\":\"a\",\"b\":\"b\"}", mapper.writeValueAsString(new Bean()));
    }

    @Test
    public void testDefaultFilter() throws Exception
    {
        FilterProvider prov = new SimpleFilterProvider().setDefaultFilter(SimpleBeanPropertyFilter.filterOutAllExcept("b"));
        assertEquals("{\"b\":\"b\"}", MAPPER.writer(prov).writeValueAsString(new Bean()));
    }

    // [databind#89] combining @JsonIgnore, @JsonProperty
    @Test
    public void testIssue89() throws Exception
    {
        Pod pod = new Pod();
        pod.username = "Bob";
        pod.userPassword = "s3cr3t!";

        assertEquals("{\"username\":\"Bob\"}", MAPPER.writeValueAsString(pod));

        Pod pod2 = MAPPER.readValue("{\"username\":\"Bill\",\"user_password\":\"foo!\"}", Pod.class);
        assertEquals("Bill", pod2.username);
        assertEquals("foo!", pod2.userPassword);
    }

    // [databind#306]
    @Test
    public void testFilterOnProperty() throws Exception
    {
        FilterProvider prov = new SimpleFilterProvider()
            .addFilter("RootFilter", SimpleBeanPropertyFilter.filterOutAllExcept("a"))
            .addFilter("b", SimpleBeanPropertyFilter.filterOutAllExcept("b"));

        assertEquals("{\"first\":{\"a\":\"a\"},\"second\":{\"b\":\"b\"}}",
                MAPPER.writer(prov).writeValueAsString(new FilteredProps()));
    }

    @Test
    public void testAllFiltersWithSameOutput() throws Exception
    {
        SimpleBeanPropertyFilter[] allPossibleFilters = new SimpleBeanPropertyFilter[]{
            SimpleBeanPropertyFilter.filterOutAllExcept("a", "b"),
            SimpleBeanPropertyFilter.filterOutAllExcept(setOf("a", "b")),
            SimpleBeanPropertyFilter.serializeAllExcept("c"),
            SimpleBeanPropertyFilter.serializeAllExcept(setOf("c")),
            new SimpleBeanPropertyFilter.SerializeExceptFilter(setOf("c")),
            SimpleBeanPropertyFilter.SerializeExceptFilter.serializeAllExcept("c"),
            SimpleBeanPropertyFilter.SerializeExceptFilter.serializeAllExcept(setOf("c")),
            SimpleBeanPropertyFilter.SerializeExceptFilter.filterOutAllExcept("a", "b"),
            SimpleBeanPropertyFilter.SerializeExceptFilter.filterOutAllExcept(setOf("a", "b")),
            new SimpleBeanPropertyFilter.FilterExceptFilter(setOf("a", "b")),
            SimpleBeanPropertyFilter.FilterExceptFilter.serializeAllExcept("c"),
            SimpleBeanPropertyFilter.FilterExceptFilter.serializeAllExcept(setOf("c")),
            SimpleBeanPropertyFilter.FilterExceptFilter.filterOutAllExcept(setOf("a", "b")),
            SimpleBeanPropertyFilter.FilterExceptFilter.filterOutAllExcept("a", "b")
        };

        for (SimpleBeanPropertyFilter filter : allPossibleFilters) {
            BeanB beanB = new BeanB("aa", "bb", "cc");
            SimpleFilterProvider prov = new SimpleFilterProvider().addFilter("filterB", filter);
            assertEquals(a2q("{'a':'aa','b':'bb'}"), MAPPER.writer(prov).writeValueAsString(beanB));
        }
    }

    @Test
    public void testCheckSiblingContextFilter() {
        FilterProvider prov = new SimpleFilterProvider().addFilter("checkSiblingContextFilter",
                new CheckSiblingContextFilter());

        ObjectMapper mapper = jsonMapperBuilder()
                .filterProvider(prov)
                .disable(SerializationFeature.FAIL_ON_EMPTY_BEANS)
                .build();
        mapper.valueToTree(new CheckSiblingContextBean());
    }

    /*
    /**********************************************************************
    /* Test methods, SimpleFilterProvider registration
    /**********************************************************************
     */

    @Test
    public void testAddFilterLastOneRemains() throws Exception {
        FilterProvider prov = new SimpleFilterProvider()
                .addFilter("filterB", SimpleBeanPropertyFilter.serializeAll())
                .addFilter("filterB", SimpleBeanPropertyFilter.filterOutAllExcept());
        AnyBeanB beanB = new AnyBeanB("1a", "2b");

        assertEquals("{}", MAPPER.writer(prov).writeValueAsString(beanB));
    }

    @Test
    public void testAddFilterLastOneRemainsFlip() throws Exception {
        FilterProvider prov = new SimpleFilterProvider()
                .addFilter("filterB", SimpleBeanPropertyFilter.filterOutAllExcept("a"))
                .addFilter("filterB", SimpleBeanPropertyFilter.serializeAll());
        AnyBeanB beanB = new AnyBeanB("1a", "2b");

        String jsonString = MAPPER.writer(prov).writeValueAsString(beanB);
        Map<?,?> actualMap = MAPPER.readValue(jsonString, Map.class);
        Map<String, Object> expectedMap = new LinkedHashMap<>();
        expectedMap.put("a", "1a");
        expectedMap.put("b", "2b");

        assertEquals(expectedMap, actualMap);
    }

    @Test
    public void testAddFilterWithEmptyStringId() throws Exception {
        FilterProvider prov = new SimpleFilterProvider()
                .addFilter("", SimpleBeanPropertyFilter.filterOutAllExcept("d"));
        AnyBeanC bean = new AnyBeanC(null, "D is filtered");

        String jsonString = MAPPER.writer(prov).writeValueAsString(bean);
        Map<?,?> actualMap = MAPPER.readValue(jsonString, Map.class);
        Map<String, Object> expectedMap = new LinkedHashMap<>();
        expectedMap.put("c", null);
        expectedMap.put("d", "D is filtered");

        assertEquals(expectedMap, actualMap);
    }

    @Test
    public void testAddingNullFilter2ThrowsException() throws Exception {
        FilterProvider prov = new SimpleFilterProvider()
                .addFilter("filterB", null);
        ObjectWriter writer = MAPPER.writer(prov);
        AnyBeanB beanD = new AnyBeanB("1a", "2b");

        try {
            writer.writeValueAsString(beanD);
            fail("Should not have passed");
        } catch (DatabindException e) {
            verifyException(e, "No filter configured with id 'filterB'");
        }
    }

    @Test
    public void testAddingNullFilterIdThrowsException() throws Exception {
        FilterProvider prov = new SimpleFilterProvider()
                .addFilter(null, SimpleBeanPropertyFilter.serializeAll());
        ObjectWriter writer = MAPPER.writer(prov);
        AnyBeanB beanD = new AnyBeanB("1a", "2b");

        try {
            writer.writeValueAsString(beanD);
            fail("Should not have passed");
        } catch (DatabindException e) {
            verifyException(e, "No filter configured with id 'filterB'");
        }
    }

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

    private Set<String> setOf(String... properties) {
        Set<String> set = new HashSet<>(properties.length);
        set.addAll(Arrays.asList(properties));
        return set;
    }
}