CacheProviderTest.java

package com.fasterxml.jackson.databind.cfg;

import java.util.HashMap;
import java.util.List;

import org.junit.jupiter.api.Test;

import com.fasterxml.jackson.databind.*;
import com.fasterxml.jackson.databind.json.JsonMapper;
import com.fasterxml.jackson.databind.util.LRUMap;
import com.fasterxml.jackson.databind.util.LookupCache;
import com.fasterxml.jackson.databind.util.TypeKey;

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

/**
 * <a href="https://github.com/FasterXML/jackson-databind/issues/2502">
 * [databind#2502] Test for adding a way to configure Caches Jackson uses</a>
 *
 * @since 2.16
 */
@SuppressWarnings("serial")
public class CacheProviderTest
{
    static class RandomBean {
        public int point;
    }

    static class AnotherBean {
        public int height;
    }

    static class SerBean {
        public int slide = 123;
    }

    static class SimpleTestCache implements LookupCache<JavaType, JsonDeserializer<Object>> {

        final HashMap<JavaType, JsonDeserializer<Object>> _cachedDeserializers;

        boolean invokedAtLeastOnce = false;

        public SimpleTestCache(int cacheSize) {
            _cachedDeserializers = new HashMap<>(cacheSize);
        }

        @Override
        public int size() {
            return _cachedDeserializers.size();
        }

        @Override
        public JsonDeserializer<Object> get(Object key) {
            invokedAtLeastOnce = true;
            return _cachedDeserializers.get(key);
        }

        @Override
        public JsonDeserializer<Object> put(JavaType key, JsonDeserializer<Object> value) {
            invokedAtLeastOnce = true;
            return _cachedDeserializers.put(key, value);
        }

        @Override
        public JsonDeserializer<Object> putIfAbsent(JavaType key, JsonDeserializer<Object> value) {
            invokedAtLeastOnce = true;
            return _cachedDeserializers.putIfAbsent(key, value);
        }

        @Override
        public void clear() {
            _cachedDeserializers.clear();
        }

        boolean isInvokedAtLeastOnce() {
            return invokedAtLeastOnce;
        }
    }

    static class CustomCacheProvider implements CacheProvider {

        final SimpleTestCache _cache;
        int createCacheCount = 0;

        public CustomCacheProvider(SimpleTestCache cache) {
            _cache = cache;
        }

        @Override
        public LookupCache<JavaType, JsonDeserializer<Object>> forDeserializerCache(DeserializationConfig config) {
            createCacheCount++;
            return _cache;
        }

        @Override
        public LookupCache<Object, JavaType> forTypeFactory() {
            return new LRUMap<>(16, 64);
        }

        @Override
        public LookupCache<TypeKey, JsonSerializer<Object>> forSerializerCache(SerializationConfig config) {
            return new LRUMap<>(8, 64);
        }

        int createCacheCount() {
            return createCacheCount;
        }
    }

    static class CustomSerCacheProvider implements CacheProvider {

        final CustomTestSerializerCache _cache = new CustomTestSerializerCache();

        @Override
        public LookupCache<JavaType, JsonDeserializer<Object>> forDeserializerCache(DeserializationConfig config) {
            return new LRUMap<>(16, 64);
        }

        @Override
        public LookupCache<Object, JavaType> forTypeFactory() {
            return new LRUMap<>(16, 64);
        }

        @Override
        public LookupCache<TypeKey, JsonSerializer<Object>> forSerializerCache(SerializationConfig config) {
            return _cache;
        }
    }

    static class CustomTestSerializerCache extends LRUMap<TypeKey, JsonSerializer<Object>> {

        public boolean _isInvoked = false;
        public CustomTestSerializerCache() {
            super(8, 64);
        }

        @Override
        public JsonSerializer<Object> put(TypeKey key, JsonSerializer<Object> value) {
            _isInvoked = true;
            return super.put(key, value);
        }
    }

    static class CustomTypeFactoryCacheProvider implements CacheProvider {

        final CustomTestTypeFactoryCache _cache = new CustomTestTypeFactoryCache();

        @Override
        public LookupCache<JavaType, JsonDeserializer<Object>> forDeserializerCache(DeserializationConfig config) {
            return new LRUMap<>(16, 64);
        }

        @Override
        public LookupCache<Object, JavaType> forTypeFactory() {
            return _cache;
        }

        @Override
        public LookupCache<TypeKey, JsonSerializer<Object>> forSerializerCache(SerializationConfig config) {
            return new LRUMap<>(16, 64);
        }
    }

    static class CustomTestTypeFactoryCache extends LRUMap<Object,JavaType> {

        public boolean _isInvoked = false;

        public CustomTestTypeFactoryCache() {
            super(8, 16);
        }

        @Override
        public JavaType putIfAbsent(Object key, JavaType value) {
            _isInvoked = true;
            return super.putIfAbsent(key, value);
        }
    }
    
    /*
    /**********************************************************************
    /* Unit tests
    /**********************************************************************
     */

    @Test
    public void testDefaultCacheProviderConfigDeserializerCache() throws Exception
    {
        CacheProvider cacheProvider = DefaultCacheProvider.builder()
                .maxDeserializerCacheSize(1234)
                .build();
        ObjectMapper mapper = JsonMapper.builder()
                .cacheProvider(cacheProvider).build();

        assertNotNull(mapper.readValue("{\"point\":24}", RandomBean.class));
    }

    @Test
    public void testDefaultCacheProviderConfigDeserializerCacheSizeZero() throws Exception
    {
        CacheProvider cacheProvider = DefaultCacheProvider.builder()
                .maxDeserializerCacheSize(0)
                .build();
        ObjectMapper mapper = JsonMapper.builder()
                .cacheProvider(cacheProvider)
                .build();

        assertNotNull(mapper.readValue("{\"point\":24}", RandomBean.class));
    }

    @Test
    public void testCustomCacheProviderConfig() throws Exception
    {
        SimpleTestCache cache = new SimpleTestCache(123);
        ObjectMapper mapper = JsonMapper.builder()
                .cacheProvider(new CustomCacheProvider(cache))
                .build();

        assertNotNull(mapper.readValue("{\"point\":24}", RandomBean.class));
        assertTrue(cache.isInvokedAtLeastOnce());
    }

    @Test
    public void testDefaultCacheProviderSharesCache() throws Exception
    {
        // Arrange
        // 1. shared CacheProvider
        CustomCacheProvider cacheProvider = new CustomCacheProvider(new SimpleTestCache(123));
        // 2. two different mapper instances
        ObjectMapper mapper1 = JsonMapper.builder()
                .cacheProvider(cacheProvider)
                .build();
        ObjectMapper mapper2 = JsonMapper.builder()
                .cacheProvider(cacheProvider)
                .build();

        // Act
        // 3. Add two different types to each mapper cache
        mapper1.readValue("{\"point\":24}", RandomBean.class);
        mapper2.readValue("{\"height\":24}", AnotherBean.class);

        // Assert
        // 4. Should have created two cache instance
        assertEquals(2, cacheProvider.createCacheCount());
    }

    @Test
    public void testBuilderValueValidation() throws Exception
    {
        // success cases
        DefaultCacheProvider.builder()
                .build();
        DefaultCacheProvider.builder()
                .maxDeserializerCacheSize(0)
                .maxSerializerCacheSize(0)
                .maxTypeFactoryCacheSize(0)
                .build();
        DefaultCacheProvider.builder()
                .maxDeserializerCacheSize(Integer.MAX_VALUE)
                .maxSerializerCacheSize(Integer.MAX_VALUE)
                .maxTypeFactoryCacheSize(Integer.MAX_VALUE)
                .build();

        // fail cases
        try {
            DefaultCacheProvider.builder().maxDeserializerCacheSize(-1);
            fail("Should not reach here");
        } catch (IllegalArgumentException e) {
            assertTrue(e.getMessage().contains("Cannot set maxDeserializerCacheSize to a negative value"));
        }
        try {
            DefaultCacheProvider.builder().maxSerializerCacheSize(-1);
            fail("Should not reach here");
        } catch (IllegalArgumentException e) {
            assertTrue(e.getMessage().contains("Cannot set maxSerializerCacheSize to a negative value"));
        }
        try {
            DefaultCacheProvider.builder().maxTypeFactoryCacheSize(-1);
            fail("Should not reach here");
        } catch (IllegalArgumentException e) {
            assertTrue(e.getMessage().contains("Cannot set maxTypeFactoryCacheSize to a negative value"));
        }
    }

    /**
     * Sanity test for serialization with {@link CacheProvider#forSerializerCache(SerializationConfig)}
     */
    @Test
    public void sanityCheckSerializerCacheSize() throws Exception
    {
        // with positive value
        _verifySerializeSuccess(_defaultProviderWithSerCache(1234));
        // with zero value
        _verifySerializeSuccess(_defaultProviderWithSerCache(0));

        // custom
        CustomSerCacheProvider customProvider = new CustomSerCacheProvider();
        _verifySerializeSuccess(customProvider);
        assertTrue(customProvider._cache._isInvoked); // -- verify that custom cache is actually used
    }

    private CacheProvider _defaultProviderWithSerCache(int maxSerializerCacheSize)
    {
        return DefaultCacheProvider.builder()
                .maxSerializerCacheSize(maxSerializerCacheSize)
                .build();
    }

    private void _verifySerializeSuccess(CacheProvider cacheProvider) throws Exception
    {
        ObjectMapper mapper = JsonMapper.builder()
                .cacheProvider(cacheProvider)
                .build();
        assertEquals("{\"slide\":123}",
                mapper.writeValueAsString(new SerBean()));
    }

    /**
     * Sanity test for serialization with {@link CacheProvider#forTypeFactory()}
     */
    @Test
    public void sanityCheckTypeFactoryCacheSize() throws Exception
    {
        // custom
        CustomTypeFactoryCacheProvider customProvider = new CustomTypeFactoryCacheProvider();
        ObjectMapper mapper = JsonMapper.builder()
                .cacheProvider(customProvider)
                .build();
        mapper.getTypeFactory().constructParametricType(List.class, HashMap.class);
        assertTrue(customProvider._cache._isInvoked);
    }
}