SimpleModuleTest.java

package tools.jackson.databind.module;

import java.io.ByteArrayInputStream;
import java.io.StringReader;
import java.util.*;

import org.junit.jupiter.api.Test;

import com.fasterxml.jackson.annotation.JsonPropertyOrder;

import tools.jackson.core.*;
import tools.jackson.core.exc.StreamReadException;
import tools.jackson.databind.*;
import tools.jackson.databind.cfg.MapperBuilder;
import tools.jackson.databind.json.JsonMapper;
import tools.jackson.databind.ser.std.StdScalarSerializer;
import tools.jackson.databind.ser.std.StdSerializer;
import tools.jackson.databind.testutil.DatabindTestUtil;

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

@SuppressWarnings("serial")
public class SimpleModuleTest extends DatabindTestUtil
{
    /**
     * Trivial bean that requires custom serializer and deserializer
     */
    final static class CustomBean
    {
        protected String str;
        protected Integer num;

        public CustomBean(String s, Integer i) {
            str = s;
            num = i;
        }
    }

    static enum SimpleEnum { A, B; }

    // Extend SerializerBase to get access to declared handledType
    static class CustomBeanSerializer extends StdSerializer<CustomBean>
    {
        public CustomBeanSerializer() { super(CustomBean.class); }

        @Override
        public void serialize(CustomBean value, JsonGenerator g, SerializationContext provider)
        {
            // We will write it as a String, with '|' as delimiter
            g.writeString(value.str + "|" + value.num);
        }
    }

    static class CustomBeanDeserializer extends ValueDeserializer<CustomBean>
    {
        @Override
        public CustomBean deserialize(JsonParser p, DeserializationContext ctxt)
        {
            String text = p.getString();
            int ix = text.indexOf('|');
            if (ix < 0) {
                throw new StreamReadException(p, "Failed to parse String value of \""+text+"\"");
            }
            String str = text.substring(0, ix);
            int num = Integer.parseInt(text.substring(ix+1));
            return new CustomBean(str, num);
        }
    }

    static class SimpleEnumSerializer extends StdSerializer<SimpleEnum>
    {
        public SimpleEnumSerializer() { super(SimpleEnum.class); }

        @Override
        public void serialize(SimpleEnum value, JsonGenerator g, SerializationContext provider)
        {
            g.writeString(value.name().toLowerCase());
        }
    }

    static class SimpleEnumDeserializer extends ValueDeserializer<SimpleEnum>
    {
        @Override
        public SimpleEnum deserialize(JsonParser p, DeserializationContext ctxt)
        {
            return SimpleEnum.valueOf(p.getString().toUpperCase());
        }
    }

    interface Base {
        public String getText();
    }

    static class Impl1 implements Base {
        @Override
        public String getText() { return "1"; }
    }

    static class Impl2 extends Impl1 {
        @Override
        public String getText() { return "2"; }
    }

    static class BaseSerializer extends StdScalarSerializer<Base>
    {
        public BaseSerializer() { super(Base.class); }

        @Override
        public void serialize(Base value, JsonGenerator g, SerializationContext provider) {
            g.writeString("Base:"+value.getText());
        }
    }

    static class MixableBean {
        public int a = 1;
        public int b = 2;
        public int c = 3;
    }

    @JsonPropertyOrder({"c", "a", "b"})
    static class MixInForOrder { }

    protected static class MySimpleSerializers extends SimpleSerializers { }
    protected static class MySimpleDeserializers extends SimpleDeserializers { }

    /**
     * Test module which uses custom 'serializers' and 'deserializers' container; used
     * to trigger type problems.
     */
    protected static class MySimpleModule extends SimpleModule
    {
        public MySimpleModule(String name, Version version) {
            super(name, version);
            _deserializers = new MySimpleDeserializers();
            _serializers = new MySimpleSerializers();
        }
    }

    /**
     * Test module that is different from MySimpleModule. Used to test registration
     * of multiple modules.
     */
    protected static class AnotherSimpleModule extends SimpleModule
    {
        public AnotherSimpleModule(String name, Version version) {
            super(name, version);
        }
    }

    static class TestModule626 extends SimpleModule {
        final Class<?> mixin, target;
        public TestModule626(Class<?> t, Class<?> m) {
            super("Test");
            target = t;
            mixin = m;
        }

        @Override
        public void setupModule(SetupContext context) {
            context.setMixIn(target, mixin);
        }
    }

    // [databind#3787]
    static class Test3787Bean {
        public String value;
    }

    static class Deserializer3787A extends ValueDeserializer<Test3787Bean> {
        @Override
        public Test3787Bean deserialize(JsonParser p, DeserializationContext ctxt) {
            p.skipChildren(); // important to consume value
            Test3787Bean simpleTestBean = new Test3787Bean();
            simpleTestBean.value = "I am A";
            return simpleTestBean;
        }
    }

    static class Deserializer3787B extends ValueDeserializer<Test3787Bean> {
        @Override
        public Test3787Bean deserialize(JsonParser p, DeserializationContext ctxt)  {
            p.skipChildren(); // important to consume value
            Test3787Bean simpleTestBean = new Test3787Bean();
            simpleTestBean.value = "I am B";
            return simpleTestBean;
        }
    }

    static class Serializer3787A extends ValueSerializer<Test3787Bean> {
        @Override
        public void serialize(Test3787Bean value, JsonGenerator gen, SerializationContext serializers) {
            gen.writeRaw("a-result");
        }
    }

    static class Serializer3787B extends ValueSerializer<Test3787Bean> {
        @Override
        public void serialize(Test3787Bean value, JsonGenerator gen, SerializationContext serializers) {
            gen.writeRaw("b-result");
        }
    }

    // For [databind#5063]
    static class Module5063A extends SimpleModule {
        public Module5063A() {
            super(Version.unknownVersion());
        }
    }
    
    static class Module5063B extends SimpleModule {
        public Module5063B() {
            super(Version.unknownVersion());
        }
    }

    /*
    /**********************************************************************
    /* Unit tests; first, verifying need for custom handlers
    /**********************************************************************
     */

    @Test
    public void testDeserializationWithoutModule() throws Exception
    {
        ObjectMapper mapper = jsonMapperBuilder()
                // since 3.0 not enabled by default
                .enable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
                .build();
        final String DOC = "{\"str\":\"ab\",\"num\":2}";

        try {
            mapper.readValue(DOC, CustomBean.class);
            fail("Should have caused an exception");
        } catch (DatabindException e) {
            verifyException(e, "Unrecognized property");
        }

        // And then other variations
        try {
            mapper.readValue(new StringReader(DOC), CustomBean.class);
            fail("Should have caused an exception");
        } catch (DatabindException e) {
            verifyException(e, "Unrecognized property");
        }

        try {
            mapper.readValue(utf8Bytes(DOC), CustomBean.class);
            fail("Should have caused an exception");
        } catch (DatabindException e) {
            verifyException(e, "Unrecognized property");
        }

        try {
            mapper.readValue(new ByteArrayInputStream(utf8Bytes(DOC)), CustomBean.class);
            fail("Should have caused an exception");
        } catch (DatabindException e) {
            verifyException(e, "Unrecognized property");
        }
    }

    /**
     * Basic test to ensure we do not have functioning default
     * serializers for custom types used in tests.
     */
    @Test
    public void testSerializationWithoutModule() throws Exception
    {
        ObjectMapper mapper = jsonMapperBuilder()
                // since 3.0 not enabled by default
                .enable(SerializationFeature.FAIL_ON_EMPTY_BEANS)
                .build();

        // first: serialization failure:
        try {
            mapper.writeValueAsString(new CustomBean("foo", 3));
            fail("Should have caused an exception");
        } catch (DatabindException e) {
            verifyException(e, "No serializer found");
        }

        // and with another write call for test coverage
        try {
            mapper.writeValueAsBytes(new CustomBean("foo", 3));
            fail("Should have caused an exception");
        } catch (DatabindException e) {
            verifyException(e, "No serializer found");
        }
    }

    /*
    /**********************************************************************
    /* Unit tests; simple serializers
    /**********************************************************************
     */

    @Test
    public void testSimpleBeanSerializer() throws Exception
    {
        SimpleModule mod = new SimpleModule("test", Version.unknownVersion());
        mod.addSerializer(new CustomBeanSerializer());
        ObjectMapper mapper = JsonMapper.builder()
                .addModule(mod)
                .build();
        assertEquals(q("abcde|5"), mapper.writeValueAsString(new CustomBean("abcde", 5)));
    }

    @Test
    public void testSimpleEnumSerializer() throws Exception
    {
        SimpleModule mod = new SimpleModule("test", Version.unknownVersion());
        mod.addSerializer(new SimpleEnumSerializer());
        // for fun, call "multi-module" registration
        ObjectMapper mapper = JsonMapper.builder()
                .addModules(mod)
                .build();
        assertEquals(q("b"), mapper.writeValueAsString(SimpleEnum.B));
    }

    @Test
    public void testSimpleInterfaceSerializer() throws Exception
    {
        SimpleModule mod = new SimpleModule("test", Version.unknownVersion());
        mod.addSerializer(new BaseSerializer());
        // and another variant here too
        List<SimpleModule> mods = Arrays.asList(mod);
        ObjectMapper mapper = JsonMapper.builder()
                .addModules(mods)
                .build();
        assertEquals(q("Base:1"), mapper.writeValueAsString(new Impl1()));
        assertEquals(q("Base:2"), mapper.writeValueAsString(new Impl2()));
    }

    /*
    /**********************************************************************
    /* Unit tests; simple deserializers
    /**********************************************************************
     */

    @Test
    public void testSimpleBeanDeserializer() throws Exception
    {
        SimpleModule mod = new SimpleModule("test", Version.unknownVersion());
        mod.addDeserializer(CustomBean.class, new CustomBeanDeserializer());
        ObjectMapper mapper = jsonMapperBuilder()
                .addModule(mod)
                .build();
        CustomBean bean = mapper.readValue(q("xyz|3"), CustomBean.class);
        assertEquals("xyz", bean.str);
        assertEquals(3, bean.num);
    }

    @Test
    public void testSimpleEnumDeserializer() throws Exception
    {
        SimpleModule mod = new SimpleModule("test", Version.unknownVersion());
        mod.addDeserializer(SimpleEnum.class, new SimpleEnumDeserializer());
        ObjectMapper mapper = jsonMapperBuilder()
                .addModule(mod)
                .build();
        SimpleEnum result = mapper.readValue(q("a"), SimpleEnum.class);
        assertSame(SimpleEnum.A, result);
    }

    @Test
    public void testMultipleModules() throws Exception
    {
        MySimpleModule mod1 = new MySimpleModule("test1", Version.unknownVersion());
        SimpleModule mod2 = new SimpleModule("test2", Version.unknownVersion());
        mod1.addSerializer(SimpleEnum.class, new SimpleEnumSerializer());
        mod1.addDeserializer(CustomBean.class, new CustomBeanDeserializer());

        Map<Class<?>,ValueDeserializer<?>> desers = new HashMap<>();
        desers.put(SimpleEnum.class, new SimpleEnumDeserializer());
        mod2.setDeserializers(new SimpleDeserializers(desers));
        mod2.addSerializer(CustomBean.class, new CustomBeanSerializer());

        ObjectMapper mapper = jsonMapperBuilder()
                .addModule(mod1)
                .addModule(mod2)
                .build();
        assertEquals(q("b"), mapper.writeValueAsString(SimpleEnum.B));
        SimpleEnum result = mapper.readValue(q("a"), SimpleEnum.class);
        assertSame(SimpleEnum.A, result);

        // also let's try it with different order of registration, just in case
        mapper = jsonMapperBuilder()
                .addModule(mod2)
                .addModule(mod1)
                .build();
        assertEquals(q("b"), mapper.writeValueAsString(SimpleEnum.B));
        result = mapper.readValue(q("a"), SimpleEnum.class);
        assertSame(SimpleEnum.A, result);
    }

    @Test
    public void testGetRegisteredModules()
    {
        MySimpleModule mod1 = new MySimpleModule("test1", Version.unknownVersion());
        AnotherSimpleModule mod2 = new AnotherSimpleModule("test2", Version.unknownVersion());

        ObjectMapper mapper = jsonMapperBuilder()
                .addModule(mod1)
                .addModule(mod2)
                .build();

        List<JacksonModule> mods = _registeredModules(mapper);
        assertEquals(2, mods.size());
        // Should retain ordering even if not mandated
        assertEquals("test1", mods.get(0).getModuleName());
        assertEquals("test2", mods.get(1).getModuleName());

        // 01-Jul-2019, [databind#2374]: verify empty list is fine
        mapper = newJsonMapper();
        assertEquals(0, _registeredModules(mapper).size());

        // 07-Jun-2021, tatu [databind#3110] Casual SimpleModules ARE returned
        //    too!
        mapper = JsonMapper.builder()
                .addModule(new SimpleModule())
                .build();
        assertEquals(1, _registeredModules(mapper).size());
        Object id = mapper.registeredModules().iterator().next().getRegistrationId();
        // Id type won't be String but...
        if (!id.toString().startsWith("SimpleModule-")) {
            fail("SimpleModule registration id should start with 'SimpleModule-', does not: ["
                    +id+"]");
        }

        // And named ones retain their name
        final JacksonModule vsm = new SimpleModule("VerySpecialModule");
        mapper = JsonMapper.builder()
                .addModule(vsm)
                .build();
        Collection<JacksonModule> reg = _registeredModules(mapper);
        assertEquals(1, reg.size());
        assertSame(vsm, reg.iterator().next());
    }

    // More [databind#3110] testing
    @Test
    public void testMultipleSimpleModules()
    {
        final SimpleModule mod1 = new SimpleModule();
        final SimpleModule mod2 = new SimpleModule();
        ObjectMapper mapper = JsonMapper.builder()
                .addModule(mod1)
                .addModule(mod2)
                .build();
        assertEquals(2, _registeredModules(mapper).size());

        // Still avoid actual duplicates
        mapper = JsonMapper.builder()
                .addModule(mod1)
                .addModule(mod1)
                .build();
        assertEquals(1, _registeredModules(mapper).size());

        // Same for (anonymous) sub-classes
        final SimpleModule subMod1 = new SimpleModule() { };
        final SimpleModule subMod2 = new SimpleModule() { };
        mapper = JsonMapper.builder()
                .addModule(subMod1)
                .addModule(subMod2)
                .build();
        assertEquals(2, _registeredModules(mapper).size());

        mapper = JsonMapper.builder()
                .addModule(subMod1)
                .addModule(subMod1)
                .build();
        assertEquals(1, _registeredModules(mapper).size());
    }

    /*
    /**********************************************************************
    /* Unit tests; other
    /**********************************************************************
     */

    @Test
    public void testMixIns() throws Exception
    {
        SimpleModule module = new SimpleModule("test", Version.unknownVersion());
        module.setMixInAnnotation(MixableBean.class, MixInForOrder.class);
        ObjectMapper mapper = jsonMapperBuilder()
                .addModule(module)
                .build();
        Map<String,Object> props = writeAndMap(mapper, new MixableBean());
        assertEquals(3, props.size());
        assertEquals(Integer.valueOf(3), props.get("c"));
        assertEquals(Integer.valueOf(1), props.get("a"));
        assertEquals(Integer.valueOf(2), props.get("b"));
    }

    @Test
    public void testAccessToMapper() throws Exception
    {
        final JacksonModule module = new JacksonModule()
        {
            @Override
            public String getModuleName() { return "x"; }

            @Override
            public Version version() { return Version.unknownVersion(); }

            @Override
            public void setupModule(SetupContext context)
            {
                Object c = context.getOwner();
                if (!(c instanceof MapperBuilder<?,?>)) {
                    throw new RuntimeException("Owner should be a `MapperBuilder` but is not; is: "+c);
                }
            }
        };
        ObjectMapper mapper = jsonMapperBuilder()
                .addModule(module)
                .build();
        assertNotNull(mapper);
    }

    @Test
    public void testAutoDiscovery() throws Exception
    {
        List<?> mods = MapperBuilder.findModules();
        assertEquals(0, mods.size());
    }

    @Test
    public void testAddSerializerTwiceThenOnlyLatestIsKept() throws Exception {
        SimpleModule module = new SimpleModule()
            .addSerializer(Test3787Bean.class, new Serializer3787A())
            .addSerializer(Test3787Bean.class, new Serializer3787B());
        ObjectMapper objectMapper = JsonMapper.builder()
                .addModule(module)
                .build();
        assertEquals("b-result", objectMapper.writeValueAsString(new Test3787Bean()));
    }

    @Test
    public void testAddModuleWithSerializerTwiceThenOnlyLatestIsKept() throws Exception {
        SimpleModule firstModule = new SimpleModule()
            .addSerializer(Test3787Bean.class, new Serializer3787A());
        SimpleModule secondModule = new SimpleModule()
            .addSerializer(Test3787Bean.class, new Serializer3787B());
        ObjectMapper objectMapper = JsonMapper.builder()
                .addModule(firstModule)
                .addModule(secondModule)
                .build();
        Test3787Bean obj = new Test3787Bean();

        String result = objectMapper.writeValueAsString(obj);

        assertEquals("b-result", result);
    }

    @Test
    public void testAddModuleWithSerializerTwiceThenOnlyLatestIsKept_reverseOrder() throws Exception {
        SimpleModule firstModule = new SimpleModule()
            .addSerializer(Test3787Bean.class, new Serializer3787A());
        SimpleModule secondModule = new SimpleModule()
            .addSerializer(Test3787Bean.class, new Serializer3787B());
        ObjectMapper objectMapper = JsonMapper.builder()
                .addModule(secondModule)
                .addModule(firstModule)
                .build();

        assertEquals("a-result", objectMapper.writeValueAsString(new Test3787Bean()));
    }

    @Test
    public void testAddDeserializerTwiceThenOnlyLatestIsKept() throws Exception {
        SimpleModule module = new SimpleModule();
        module.addDeserializer(Test3787Bean.class, new Deserializer3787A())
            .addDeserializer(Test3787Bean.class, new Deserializer3787B());
        ObjectMapper objectMapper = JsonMapper.builder()
                .addModule(module)
                .build();

        Test3787Bean result = objectMapper.readValue(
            "{\"value\" : \"I am C\"}", Test3787Bean.class);

        assertEquals("I am B", result.value);
    }

    @Test
    public void testAddModuleWithDeserializerTwiceThenOnlyLatestIsKept() throws Exception {
        SimpleModule firstModule = new SimpleModule()
            .addDeserializer(Test3787Bean.class, new Deserializer3787A());
        SimpleModule secondModule = new SimpleModule()
            .addDeserializer(Test3787Bean.class, new Deserializer3787B());
        ObjectMapper objectMapper = JsonMapper.builder()
                .addModule(firstModule)
                .addModule(secondModule)
                .build();

        Test3787Bean result = objectMapper.readValue(
            "{\"value\" : \"I am C\"}", Test3787Bean.class);

        assertEquals("I am B", result.value);
    }

    @Test
    public void testAddModuleWithDeserializerTwiceThenOnlyLatestIsKept_reverseOrder() throws Exception
    {
        SimpleModule firstModule = new SimpleModule()
            .addDeserializer(Test3787Bean.class, new Deserializer3787A());
        SimpleModule secondModule = new SimpleModule()
            .addDeserializer(Test3787Bean.class, new Deserializer3787B());
        ObjectMapper objectMapper = JsonMapper.builder()
            .addModule(secondModule)
            .addModule(firstModule)
            .build();

        Test3787Bean result = objectMapper.readValue(
            "{\"value\" : \"I am C\"}", Test3787Bean.class);
        
        assertEquals("I am A", result.value);
    }

    // For [databind#5063]
    @Test
    public void testDuplicateModules5063() {
        ObjectMapper mapper = JsonMapper.builder()
                .addModule(new Module5063A())
                .addModule(new Module5063B())
                .build();
        assertEquals(2, _registeredModules(mapper).size());
    }

    private List<JacksonModule> _registeredModules(ObjectMapper mapper) {
        return new ArrayList<>(mapper.registeredModules());
    }
}