Skip to content

Commit

Permalink
feat: add jackson module (#174)
Browse files Browse the repository at this point in the history
  • Loading branch information
iProdigy committed Nov 24, 2023
1 parent d0013bc commit d974c58
Show file tree
Hide file tree
Showing 8 changed files with 358 additions and 0 deletions.
1 change: 1 addition & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ subprojects {
dependencies {
// annotations
compileOnly("org.jetbrains:annotations:24.1.0")
testCompileOnly("org.jetbrains:annotations:24.1.0")

// tests
testImplementation(platform("org.junit:junit-bom:5.10.1"))
Expand Down
16 changes: 16 additions & 0 deletions jackson/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
dependencies {
api(project(":cache-core"))
implementation("com.fasterxml.jackson.core:jackson-databind") {
version {
require("2.16.0") // imposes a lower bound on acceptable versions
}
}
testImplementation(project(":cache-provider-caffeine"))
}

publishing.publications.withType<MavenPublication> {
pom {
name.set("Xanthic - Jackson")
description.set("Xanthic Cache Jackson Adapter")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package io.github.xanthic.jackson;

import com.fasterxml.jackson.databind.util.LookupCache;
import io.github.xanthic.cache.api.Cache;
import io.github.xanthic.cache.core.CacheApi;
import io.github.xanthic.cache.core.CacheApiSpec;
import lombok.RequiredArgsConstructor;
import lombok.Value;
import org.jetbrains.annotations.NotNull;

import java.util.function.BiConsumer;
import java.util.function.Consumer;

/**
* Wraps a Xanthic {@link Cache} for use as a Jackson {@link LookupCache}.
* <p>
* Most users should utilize {@link XanthicJacksonCacheProvider} rather than directly interact with this class.
*
* @param <K> The type of keys that form the cache
* @param <V> The type of values contained in the cache
*/
@Value
@RequiredArgsConstructor
public class XanthicJacksonCacheAdapter<K, V> implements LookupCache<K, V> {

/**
* The Xanthic cache to use as a Jackson {@link LookupCache}.
*/
Cache<K, V> cache;

/**
* The specification associated with the constructed cache.
*/
Consumer<CacheApiSpec<K, V>> spec;

/**
* Creates a Jackson {@link LookupCache} by wrapping a Xanthic cache with this adapter.
*
* @param spec the cache specification (note: specifying {@link CacheApiSpec#maxSize(Long)} is recommended)
*/
public XanthicJacksonCacheAdapter(@NotNull Consumer<CacheApiSpec<K, V>> spec) {
this(CacheApi.create(spec), spec);
}

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

@Override
@SuppressWarnings("unchecked")
public V get(Object key) {
return cache.get((K) key);
}

@Override
public V put(K key, V value) {
return cache.put(key, value);
}

@Override
public V putIfAbsent(K key, V value) {
return cache.putIfAbsent(key, value);
}

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

@Override
public void contents(BiConsumer<K, V> consumer) {
cache.forEach(consumer);
}

@Override
public XanthicJacksonCacheAdapter<K, V> emptyCopy() {
return new XanthicJacksonCacheAdapter<>(spec);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
package io.github.xanthic.jackson;

import com.fasterxml.jackson.databind.DeserializationConfig;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializationConfig;
import com.fasterxml.jackson.databind.cfg.CacheProvider;
import com.fasterxml.jackson.databind.deser.DeserializerCache;
import com.fasterxml.jackson.databind.ser.SerializerCache;
import com.fasterxml.jackson.databind.type.TypeFactory;
import com.fasterxml.jackson.databind.util.LookupCache;
import com.fasterxml.jackson.databind.util.TypeKey;
import io.github.xanthic.cache.core.CacheApiSpec;
import io.github.xanthic.jackson.util.SerializableConsumer;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.Value;

/**
* Implementation of Jackson's {@link CacheProvider} that yields Xanthic {@link io.github.xanthic.cache.api.Cache} instances,
* which are backed by any cache implementation of your choosing.
* <p>
* Example usage:
* {@code ObjectMapper mapper = JsonMapper.builder().cacheProvider(XanthicJacksonCacheProvider.defaultInstance()).build(); }
*/
@Value
@Getter(AccessLevel.PRIVATE)
@RequiredArgsConstructor
public class XanthicJacksonCacheProvider implements CacheProvider {
private static final long serialVersionUID = 1L;
private static final XanthicJacksonCacheProvider DEFAULT_INSTANCE = new XanthicJacksonCacheProvider();

/**
* Specification for the deserializer cache.
*/
SerializableConsumer<CacheApiSpec<JavaType, JsonDeserializer<Object>>> deserializationSpec;

/**
* Specification for the serializer cache.
*/
SerializableConsumer<CacheApiSpec<TypeKey, JsonSerializer<Object>>> serializationSpec;

/**
* Specification for the type factory cache.
*/
SerializableConsumer<CacheApiSpec<Object, JavaType>> typeFactorySpec;

/**
* Creates a Jackson {@link CacheProvider} backed by Xanthic, using the specified max cache sizes.
*
* @param maxDeserializerCacheSize the maximum size of the deserializer cache
* @param maxSerializerCacheSize the maximum size of the serializer cache
* @param maxTypeFactoryCacheSize the maximum size of the type factory cache
*/
public XanthicJacksonCacheProvider(long maxDeserializerCacheSize, long maxSerializerCacheSize, long maxTypeFactoryCacheSize) {
this.deserializationSpec = spec -> spec.maxSize(maxDeserializerCacheSize);
this.serializationSpec = spec -> spec.maxSize(maxSerializerCacheSize);
this.typeFactorySpec = spec -> spec.maxSize(maxTypeFactoryCacheSize);
}

/**
* Creates a Jackson {@link CacheProvider} backed by Xanthic, using Jackson's recommended default max cache sizes.
*/
private XanthicJacksonCacheProvider() {
this(DeserializerCache.DEFAULT_MAX_CACHE_SIZE, SerializerCache.DEFAULT_MAX_CACHE_SIZE, TypeFactory.DEFAULT_MAX_CACHE_SIZE);
}

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

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

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

/**
* @return a Jackson {@link CacheProvider} backed by Xanthic, using Jackson's recommended default max cache sizes.
*/
public static XanthicJacksonCacheProvider defaultInstance() {
return DEFAULT_INSTANCE;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package io.github.xanthic.jackson.util;

import java.io.Serializable;
import java.util.function.Consumer;

/**
* A serializable {@link Consumer} since {@link com.fasterxml.jackson.databind.cfg.CacheProvider}
* must be {@link Serializable}, as it is stored in {@link com.fasterxml.jackson.databind.ObjectMapper}.
*
* @param <T> the type of the input for the consumer
*/
@FunctionalInterface
public interface SerializableConsumer<T> extends Consumer<T>, Serializable {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
package io.github.xanthic.jackson;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.cfg.CacheProvider;
import com.fasterxml.jackson.databind.deser.DeserializerCache;
import com.fasterxml.jackson.databind.json.JsonMapper;
import com.fasterxml.jackson.databind.ser.SerializerCache;
import com.fasterxml.jackson.databind.type.TypeFactory;
import io.github.xanthic.jackson.util.TrackedCacheProvider;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.junit.jupiter.api.Test;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.List;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;

class XanthicJacksonCacheProviderTest {

@Test
void defaults() throws JsonProcessingException {
ObjectMapper mapper = JsonMapper.builder()
.cacheProvider(XanthicJacksonCacheProvider.defaultInstance())
.build();
assertNotNull(mapper.readValue("{\"bar\":\"baz\"}", Foo.class));
assertNotNull(mapper.writeValueAsString(new Foo("baz")));
assertNotNull(mapper.getTypeFactory().constructParametricType(List.class, Integer.class));
}

@Test
void deserialize() throws JsonProcessingException {
TrackedCacheProvider provider = new TrackedCacheProvider();
ObjectMapper mapper = JsonMapper.builder()
.cacheProvider(createCacheProvider(provider))
.build();
assertFalse(provider.getConstructedCaches().stream().anyMatch(c -> c.size() > 0));
Foo foo = mapper.readValue("{\"bar\":\"baz\"}", Foo.class);
assertNotNull(foo);
assertEquals("baz", foo.getBar());
assertTrue(provider.getConstructedCaches().stream().anyMatch(c -> c.size() > 0));
}

@Test
void serialize() throws JsonProcessingException {
TrackedCacheProvider provider = new TrackedCacheProvider();
ObjectMapper mapper = JsonMapper.builder()
.cacheProvider(createCacheProvider(provider))
.build();
assertFalse(provider.getConstructedCaches().stream().anyMatch(c -> c.size() > 0));
String json = mapper.writeValueAsString(new Foo("baz"));
assertEquals("{\"bar\":\"baz\"}", json);
assertTrue(provider.getConstructedCaches().stream().anyMatch(c -> c.size() > 0));
}

@Test
void constructType() {
TrackedCacheProvider provider = new TrackedCacheProvider();
ObjectMapper mapper = JsonMapper.builder()
.cacheProvider(createCacheProvider(provider))
.build();
assertFalse(provider.getConstructedCaches().stream().anyMatch(c -> c.size() > 0));
JavaType type = mapper.getTypeFactory().constructParametricType(List.class, Integer.class);
assertNotNull(type);
assertTrue(provider.getConstructedCaches().stream().anyMatch(c -> c.size() > 0));
}

@Test
void constructMultiple() throws JsonProcessingException {
TrackedCacheProvider provider = new TrackedCacheProvider();
assertEquals(0, provider.getConstructedCaches().size());

ObjectMapper m1 = JsonMapper.builder().cacheProvider(createCacheProvider(provider)).build();
m1.readValue("{\"bar\":\"baz\"}", Foo.class);
m1.writeValueAsString(new Foo("baz"));
m1.getTypeFactory().constructParametricType(List.class, Integer.class);
assertEquals(3, provider.getConstructedCaches().size());
assertTrue(provider.getConstructedCaches().stream().allMatch(c -> c.size() > 0));

ObjectMapper m2 = JsonMapper.builder().cacheProvider(createCacheProvider(provider)).build();
m2.readValue("{\"bar\":\"baz\"}", Foo.class);
m2.writeValueAsString(new Foo("baz"));
m2.getTypeFactory().constructParametricType(List.class, Integer.class);
assertEquals(6, provider.getConstructedCaches().size());
assertTrue(provider.getConstructedCaches().stream().allMatch(c -> c.size() > 0));
}

@Test
void serializable() throws IOException, ClassNotFoundException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
try (ObjectOutputStream oos = new ObjectOutputStream(baos)) {
oos.writeObject(XanthicJacksonCacheProvider.defaultInstance());
}

XanthicJacksonCacheProvider provider;
try (ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(baos.toByteArray()))) {
provider = (XanthicJacksonCacheProvider) ois.readObject();
}

assertNotNull(provider);
assertNotNull(provider.forTypeFactory());
}

private static CacheProvider createCacheProvider(TrackedCacheProvider trackedProvider) {
return new XanthicJacksonCacheProvider(
spec -> spec.provider(trackedProvider).maxSize((long) DeserializerCache.DEFAULT_MAX_CACHE_SIZE),
spec -> spec.provider(trackedProvider).maxSize((long) SerializerCache.DEFAULT_MAX_CACHE_SIZE),
spec -> spec.provider(trackedProvider).maxSize((long) TypeFactory.DEFAULT_MAX_CACHE_SIZE)
);
}

@Data
@Setter(AccessLevel.PRIVATE)
@NoArgsConstructor
@AllArgsConstructor
static class Foo {
private String bar;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package io.github.xanthic.jackson.util;

import io.github.xanthic.cache.api.Cache;
import io.github.xanthic.cache.api.CacheProvider;
import io.github.xanthic.cache.api.ICacheSpec;
import io.github.xanthic.cache.core.CacheApiSettings;
import lombok.Value;

import java.util.ArrayList;
import java.util.List;

@Value
public class TrackedCacheProvider implements CacheProvider {
CacheProvider underlyingProvider = CacheApiSettings.getInstance().getDefaultCacheProvider();
List<Cache<?, ?>> constructedCaches = new ArrayList<>();

@Override
public <K, V> Cache<K, V> build(ICacheSpec<K, V> spec) {
Cache<K, V> cache = underlyingProvider.build(spec);
constructedCaches.add(cache);
return cache;
}
}
2 changes: 2 additions & 0 deletions settings.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ include(
":api",
":core",
":kotlin",
":jackson",
":spring",
":spring-java17",
":provider-androidx",
Expand All @@ -22,6 +23,7 @@ project(":bom").name = "cache-bom"
project(":api").name = "cache-api"
project(":core").name = "cache-core"
project(":kotlin").name = "cache-kotlin"
project(":jackson").name = "cache-jackson"
project(":spring").name = "cache-spring"
project(":spring-java17").name = "cache-spring-java17"
project(":provider-androidx").name = "cache-provider-androidx"
Expand Down

0 comments on commit d974c58

Please sign in to comment.