Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add jackson module #174

Merged
merged 24 commits into from
Nov 24, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
54edbaa
feat: add jackson module
iProdigy Sep 24, 2023
99963ba
docs: describe the utility of each class
iProdigy Sep 25, 2023
0ebe17c
Merge branch 'main' into feature/jackson-bridge
iProdigy Sep 29, 2023
7bdc97a
Merge branch 'main' into feature/jackson-bridge
iProdigy Oct 21, 2023
e2ca29f
chore: build with 2.16 rc1
iProdigy Oct 21, 2023
04f799c
chore: remove duplicate jackson class
iProdigy Oct 21, 2023
e99cd69
chore: add override annotations in XanthicJacksonCacheAdapter
iProdigy Oct 21, 2023
b8b48f8
chore: use named constants for default cache sizes
iProdigy Oct 21, 2023
0d0eea4
chore: import TypeFactory
iProdigy Oct 21, 2023
4467f21
chore: import DeserializerCache
iProdigy Oct 21, 2023
7c9e962
chore: declare rich version
iProdigy Oct 21, 2023
33cbe1d
feat: add XanthicJacksonCacheProvider.DEFAULT_INSTANCE
iProdigy Oct 21, 2023
86d1245
chore: add basic tests
iProdigy Oct 21, 2023
55dd573
chore(tests): ensure cache was used
iProdigy Oct 21, 2023
89ea736
chore(test): ensure independent caches across mappers
iProdigy Oct 21, 2023
c18ebce
chore: add test that uses XanthicJacksonCacheProvider.DEFAULT_INSTANCE
iProdigy Oct 21, 2023
5b5083d
chore: simplify tests
iProdigy Oct 23, 2023
b390370
refactor: remove public spec getters in XanthicJacksonCacheProvider
iProdigy Oct 23, 2023
107dfa5
Merge branch 'main' into feature/jackson-bridge
iProdigy Nov 5, 2023
7f56f2a
chore: bump jackson version
iProdigy Nov 15, 2023
c69b7ba
Merge branch 'main' into feature/jackson-bridge
iProdigy Nov 15, 2023
b78d9a0
refactor: rename default instance getter
iProdigy Nov 15, 2023
9ecc500
chore: add java serializable test
iProdigy Nov 16, 2023
1c39caa
Merge branch 'main' into feature/jackson-bridge
iProdigy Nov 21, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading