Skip to content

Commit d974c58

Browse files
authored
feat: add jackson module (#174)
1 parent d0013bc commit d974c58

File tree

8 files changed

+358
-0
lines changed

8 files changed

+358
-0
lines changed

build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ subprojects {
4747
dependencies {
4848
// annotations
4949
compileOnly("org.jetbrains:annotations:24.1.0")
50+
testCompileOnly("org.jetbrains:annotations:24.1.0")
5051

5152
// tests
5253
testImplementation(platform("org.junit:junit-bom:5.10.1"))

jackson/build.gradle.kts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
dependencies {
2+
api(project(":cache-core"))
3+
implementation("com.fasterxml.jackson.core:jackson-databind") {
4+
version {
5+
require("2.16.0") // imposes a lower bound on acceptable versions
6+
}
7+
}
8+
testImplementation(project(":cache-provider-caffeine"))
9+
}
10+
11+
publishing.publications.withType<MavenPublication> {
12+
pom {
13+
name.set("Xanthic - Jackson")
14+
description.set("Xanthic Cache Jackson Adapter")
15+
}
16+
}
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
package io.github.xanthic.jackson;
2+
3+
import com.fasterxml.jackson.databind.util.LookupCache;
4+
import io.github.xanthic.cache.api.Cache;
5+
import io.github.xanthic.cache.core.CacheApi;
6+
import io.github.xanthic.cache.core.CacheApiSpec;
7+
import lombok.RequiredArgsConstructor;
8+
import lombok.Value;
9+
import org.jetbrains.annotations.NotNull;
10+
11+
import java.util.function.BiConsumer;
12+
import java.util.function.Consumer;
13+
14+
/**
15+
* Wraps a Xanthic {@link Cache} for use as a Jackson {@link LookupCache}.
16+
* <p>
17+
* Most users should utilize {@link XanthicJacksonCacheProvider} rather than directly interact with this class.
18+
*
19+
* @param <K> The type of keys that form the cache
20+
* @param <V> The type of values contained in the cache
21+
*/
22+
@Value
23+
@RequiredArgsConstructor
24+
public class XanthicJacksonCacheAdapter<K, V> implements LookupCache<K, V> {
25+
26+
/**
27+
* The Xanthic cache to use as a Jackson {@link LookupCache}.
28+
*/
29+
Cache<K, V> cache;
30+
31+
/**
32+
* The specification associated with the constructed cache.
33+
*/
34+
Consumer<CacheApiSpec<K, V>> spec;
35+
36+
/**
37+
* Creates a Jackson {@link LookupCache} by wrapping a Xanthic cache with this adapter.
38+
*
39+
* @param spec the cache specification (note: specifying {@link CacheApiSpec#maxSize(Long)} is recommended)
40+
*/
41+
public XanthicJacksonCacheAdapter(@NotNull Consumer<CacheApiSpec<K, V>> spec) {
42+
this(CacheApi.create(spec), spec);
43+
}
44+
45+
@Override
46+
public int size() {
47+
return (int) cache.size();
48+
}
49+
50+
@Override
51+
@SuppressWarnings("unchecked")
52+
public V get(Object key) {
53+
return cache.get((K) key);
54+
}
55+
56+
@Override
57+
public V put(K key, V value) {
58+
return cache.put(key, value);
59+
}
60+
61+
@Override
62+
public V putIfAbsent(K key, V value) {
63+
return cache.putIfAbsent(key, value);
64+
}
65+
66+
@Override
67+
public void clear() {
68+
cache.clear();
69+
}
70+
71+
@Override
72+
public void contents(BiConsumer<K, V> consumer) {
73+
cache.forEach(consumer);
74+
}
75+
76+
@Override
77+
public XanthicJacksonCacheAdapter<K, V> emptyCopy() {
78+
return new XanthicJacksonCacheAdapter<>(spec);
79+
}
80+
}
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
package io.github.xanthic.jackson;
2+
3+
import com.fasterxml.jackson.databind.DeserializationConfig;
4+
import com.fasterxml.jackson.databind.JavaType;
5+
import com.fasterxml.jackson.databind.JsonDeserializer;
6+
import com.fasterxml.jackson.databind.JsonSerializer;
7+
import com.fasterxml.jackson.databind.SerializationConfig;
8+
import com.fasterxml.jackson.databind.cfg.CacheProvider;
9+
import com.fasterxml.jackson.databind.deser.DeserializerCache;
10+
import com.fasterxml.jackson.databind.ser.SerializerCache;
11+
import com.fasterxml.jackson.databind.type.TypeFactory;
12+
import com.fasterxml.jackson.databind.util.LookupCache;
13+
import com.fasterxml.jackson.databind.util.TypeKey;
14+
import io.github.xanthic.cache.core.CacheApiSpec;
15+
import io.github.xanthic.jackson.util.SerializableConsumer;
16+
import lombok.AccessLevel;
17+
import lombok.Getter;
18+
import lombok.RequiredArgsConstructor;
19+
import lombok.Value;
20+
21+
/**
22+
* Implementation of Jackson's {@link CacheProvider} that yields Xanthic {@link io.github.xanthic.cache.api.Cache} instances,
23+
* which are backed by any cache implementation of your choosing.
24+
* <p>
25+
* Example usage:
26+
* {@code ObjectMapper mapper = JsonMapper.builder().cacheProvider(XanthicJacksonCacheProvider.defaultInstance()).build(); }
27+
*/
28+
@Value
29+
@Getter(AccessLevel.PRIVATE)
30+
@RequiredArgsConstructor
31+
public class XanthicJacksonCacheProvider implements CacheProvider {
32+
private static final long serialVersionUID = 1L;
33+
private static final XanthicJacksonCacheProvider DEFAULT_INSTANCE = new XanthicJacksonCacheProvider();
34+
35+
/**
36+
* Specification for the deserializer cache.
37+
*/
38+
SerializableConsumer<CacheApiSpec<JavaType, JsonDeserializer<Object>>> deserializationSpec;
39+
40+
/**
41+
* Specification for the serializer cache.
42+
*/
43+
SerializableConsumer<CacheApiSpec<TypeKey, JsonSerializer<Object>>> serializationSpec;
44+
45+
/**
46+
* Specification for the type factory cache.
47+
*/
48+
SerializableConsumer<CacheApiSpec<Object, JavaType>> typeFactorySpec;
49+
50+
/**
51+
* Creates a Jackson {@link CacheProvider} backed by Xanthic, using the specified max cache sizes.
52+
*
53+
* @param maxDeserializerCacheSize the maximum size of the deserializer cache
54+
* @param maxSerializerCacheSize the maximum size of the serializer cache
55+
* @param maxTypeFactoryCacheSize the maximum size of the type factory cache
56+
*/
57+
public XanthicJacksonCacheProvider(long maxDeserializerCacheSize, long maxSerializerCacheSize, long maxTypeFactoryCacheSize) {
58+
this.deserializationSpec = spec -> spec.maxSize(maxDeserializerCacheSize);
59+
this.serializationSpec = spec -> spec.maxSize(maxSerializerCacheSize);
60+
this.typeFactorySpec = spec -> spec.maxSize(maxTypeFactoryCacheSize);
61+
}
62+
63+
/**
64+
* Creates a Jackson {@link CacheProvider} backed by Xanthic, using Jackson's recommended default max cache sizes.
65+
*/
66+
private XanthicJacksonCacheProvider() {
67+
this(DeserializerCache.DEFAULT_MAX_CACHE_SIZE, SerializerCache.DEFAULT_MAX_CACHE_SIZE, TypeFactory.DEFAULT_MAX_CACHE_SIZE);
68+
}
69+
70+
@Override
71+
public LookupCache<JavaType, JsonDeserializer<Object>> forDeserializerCache(DeserializationConfig config) {
72+
return new XanthicJacksonCacheAdapter<>(deserializationSpec);
73+
}
74+
75+
@Override
76+
public LookupCache<TypeKey, JsonSerializer<Object>> forSerializerCache(SerializationConfig config) {
77+
return new XanthicJacksonCacheAdapter<>(serializationSpec);
78+
}
79+
80+
@Override
81+
public LookupCache<Object, JavaType> forTypeFactory() {
82+
return new XanthicJacksonCacheAdapter<>(typeFactorySpec);
83+
}
84+
85+
/**
86+
* @return a Jackson {@link CacheProvider} backed by Xanthic, using Jackson's recommended default max cache sizes.
87+
*/
88+
public static XanthicJacksonCacheProvider defaultInstance() {
89+
return DEFAULT_INSTANCE;
90+
}
91+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package io.github.xanthic.jackson.util;
2+
3+
import java.io.Serializable;
4+
import java.util.function.Consumer;
5+
6+
/**
7+
* A serializable {@link Consumer} since {@link com.fasterxml.jackson.databind.cfg.CacheProvider}
8+
* must be {@link Serializable}, as it is stored in {@link com.fasterxml.jackson.databind.ObjectMapper}.
9+
*
10+
* @param <T> the type of the input for the consumer
11+
*/
12+
@FunctionalInterface
13+
public interface SerializableConsumer<T> extends Consumer<T>, Serializable {}
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
package io.github.xanthic.jackson;
2+
3+
import com.fasterxml.jackson.core.JsonProcessingException;
4+
import com.fasterxml.jackson.databind.JavaType;
5+
import com.fasterxml.jackson.databind.ObjectMapper;
6+
import com.fasterxml.jackson.databind.cfg.CacheProvider;
7+
import com.fasterxml.jackson.databind.deser.DeserializerCache;
8+
import com.fasterxml.jackson.databind.json.JsonMapper;
9+
import com.fasterxml.jackson.databind.ser.SerializerCache;
10+
import com.fasterxml.jackson.databind.type.TypeFactory;
11+
import io.github.xanthic.jackson.util.TrackedCacheProvider;
12+
import lombok.AccessLevel;
13+
import lombok.AllArgsConstructor;
14+
import lombok.Data;
15+
import lombok.NoArgsConstructor;
16+
import lombok.Setter;
17+
import org.junit.jupiter.api.Test;
18+
19+
import java.io.ByteArrayInputStream;
20+
import java.io.ByteArrayOutputStream;
21+
import java.io.IOException;
22+
import java.io.ObjectInputStream;
23+
import java.io.ObjectOutputStream;
24+
import java.util.List;
25+
26+
import static org.junit.jupiter.api.Assertions.assertEquals;
27+
import static org.junit.jupiter.api.Assertions.assertFalse;
28+
import static org.junit.jupiter.api.Assertions.assertNotNull;
29+
import static org.junit.jupiter.api.Assertions.assertTrue;
30+
31+
class XanthicJacksonCacheProviderTest {
32+
33+
@Test
34+
void defaults() throws JsonProcessingException {
35+
ObjectMapper mapper = JsonMapper.builder()
36+
.cacheProvider(XanthicJacksonCacheProvider.defaultInstance())
37+
.build();
38+
assertNotNull(mapper.readValue("{\"bar\":\"baz\"}", Foo.class));
39+
assertNotNull(mapper.writeValueAsString(new Foo("baz")));
40+
assertNotNull(mapper.getTypeFactory().constructParametricType(List.class, Integer.class));
41+
}
42+
43+
@Test
44+
void deserialize() throws JsonProcessingException {
45+
TrackedCacheProvider provider = new TrackedCacheProvider();
46+
ObjectMapper mapper = JsonMapper.builder()
47+
.cacheProvider(createCacheProvider(provider))
48+
.build();
49+
assertFalse(provider.getConstructedCaches().stream().anyMatch(c -> c.size() > 0));
50+
Foo foo = mapper.readValue("{\"bar\":\"baz\"}", Foo.class);
51+
assertNotNull(foo);
52+
assertEquals("baz", foo.getBar());
53+
assertTrue(provider.getConstructedCaches().stream().anyMatch(c -> c.size() > 0));
54+
}
55+
56+
@Test
57+
void serialize() throws JsonProcessingException {
58+
TrackedCacheProvider provider = new TrackedCacheProvider();
59+
ObjectMapper mapper = JsonMapper.builder()
60+
.cacheProvider(createCacheProvider(provider))
61+
.build();
62+
assertFalse(provider.getConstructedCaches().stream().anyMatch(c -> c.size() > 0));
63+
String json = mapper.writeValueAsString(new Foo("baz"));
64+
assertEquals("{\"bar\":\"baz\"}", json);
65+
assertTrue(provider.getConstructedCaches().stream().anyMatch(c -> c.size() > 0));
66+
}
67+
68+
@Test
69+
void constructType() {
70+
TrackedCacheProvider provider = new TrackedCacheProvider();
71+
ObjectMapper mapper = JsonMapper.builder()
72+
.cacheProvider(createCacheProvider(provider))
73+
.build();
74+
assertFalse(provider.getConstructedCaches().stream().anyMatch(c -> c.size() > 0));
75+
JavaType type = mapper.getTypeFactory().constructParametricType(List.class, Integer.class);
76+
assertNotNull(type);
77+
assertTrue(provider.getConstructedCaches().stream().anyMatch(c -> c.size() > 0));
78+
}
79+
80+
@Test
81+
void constructMultiple() throws JsonProcessingException {
82+
TrackedCacheProvider provider = new TrackedCacheProvider();
83+
assertEquals(0, provider.getConstructedCaches().size());
84+
85+
ObjectMapper m1 = JsonMapper.builder().cacheProvider(createCacheProvider(provider)).build();
86+
m1.readValue("{\"bar\":\"baz\"}", Foo.class);
87+
m1.writeValueAsString(new Foo("baz"));
88+
m1.getTypeFactory().constructParametricType(List.class, Integer.class);
89+
assertEquals(3, provider.getConstructedCaches().size());
90+
assertTrue(provider.getConstructedCaches().stream().allMatch(c -> c.size() > 0));
91+
92+
ObjectMapper m2 = JsonMapper.builder().cacheProvider(createCacheProvider(provider)).build();
93+
m2.readValue("{\"bar\":\"baz\"}", Foo.class);
94+
m2.writeValueAsString(new Foo("baz"));
95+
m2.getTypeFactory().constructParametricType(List.class, Integer.class);
96+
assertEquals(6, provider.getConstructedCaches().size());
97+
assertTrue(provider.getConstructedCaches().stream().allMatch(c -> c.size() > 0));
98+
}
99+
100+
@Test
101+
void serializable() throws IOException, ClassNotFoundException {
102+
ByteArrayOutputStream baos = new ByteArrayOutputStream();
103+
try (ObjectOutputStream oos = new ObjectOutputStream(baos)) {
104+
oos.writeObject(XanthicJacksonCacheProvider.defaultInstance());
105+
}
106+
107+
XanthicJacksonCacheProvider provider;
108+
try (ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(baos.toByteArray()))) {
109+
provider = (XanthicJacksonCacheProvider) ois.readObject();
110+
}
111+
112+
assertNotNull(provider);
113+
assertNotNull(provider.forTypeFactory());
114+
}
115+
116+
private static CacheProvider createCacheProvider(TrackedCacheProvider trackedProvider) {
117+
return new XanthicJacksonCacheProvider(
118+
spec -> spec.provider(trackedProvider).maxSize((long) DeserializerCache.DEFAULT_MAX_CACHE_SIZE),
119+
spec -> spec.provider(trackedProvider).maxSize((long) SerializerCache.DEFAULT_MAX_CACHE_SIZE),
120+
spec -> spec.provider(trackedProvider).maxSize((long) TypeFactory.DEFAULT_MAX_CACHE_SIZE)
121+
);
122+
}
123+
124+
@Data
125+
@Setter(AccessLevel.PRIVATE)
126+
@NoArgsConstructor
127+
@AllArgsConstructor
128+
static class Foo {
129+
private String bar;
130+
}
131+
132+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package io.github.xanthic.jackson.util;
2+
3+
import io.github.xanthic.cache.api.Cache;
4+
import io.github.xanthic.cache.api.CacheProvider;
5+
import io.github.xanthic.cache.api.ICacheSpec;
6+
import io.github.xanthic.cache.core.CacheApiSettings;
7+
import lombok.Value;
8+
9+
import java.util.ArrayList;
10+
import java.util.List;
11+
12+
@Value
13+
public class TrackedCacheProvider implements CacheProvider {
14+
CacheProvider underlyingProvider = CacheApiSettings.getInstance().getDefaultCacheProvider();
15+
List<Cache<?, ?>> constructedCaches = new ArrayList<>();
16+
17+
@Override
18+
public <K, V> Cache<K, V> build(ICacheSpec<K, V> spec) {
19+
Cache<K, V> cache = underlyingProvider.build(spec);
20+
constructedCaches.add(cache);
21+
return cache;
22+
}
23+
}

settings.gradle.kts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ include(
55
":api",
66
":core",
77
":kotlin",
8+
":jackson",
89
":spring",
910
":spring-java17",
1011
":provider-androidx",
@@ -22,6 +23,7 @@ project(":bom").name = "cache-bom"
2223
project(":api").name = "cache-api"
2324
project(":core").name = "cache-core"
2425
project(":kotlin").name = "cache-kotlin"
26+
project(":jackson").name = "cache-jackson"
2527
project(":spring").name = "cache-spring"
2628
project(":spring-java17").name = "cache-spring-java17"
2729
project(":provider-androidx").name = "cache-provider-androidx"

0 commit comments

Comments
 (0)