diff --git a/guava/src/main/java/com/fasterxml/jackson/datatype/guava/GuavaDeserializers.java b/guava/src/main/java/com/fasterxml/jackson/datatype/guava/GuavaDeserializers.java index 9eea265d..cddbd4a8 100644 --- a/guava/src/main/java/com/fasterxml/jackson/datatype/guava/GuavaDeserializers.java +++ b/guava/src/main/java/com/fasterxml/jackson/datatype/guava/GuavaDeserializers.java @@ -1,6 +1,9 @@ package com.fasterxml.jackson.datatype.guava; import com.google.common.base.Optional; +import com.google.common.cache.Cache; +import com.google.common.cache.ForwardingCache; +import com.google.common.cache.LoadingCache; import com.google.common.collect.*; import com.google.common.hash.HashCode; import com.google.common.net.HostAndPort; @@ -260,10 +263,42 @@ public JsonDeserializer findMapLikeDeserializer(MapLikeType type, if (Table.class.isAssignableFrom(raw)) { // !!! TODO } + // @since 2.16 : support Cache deserialization + java.util.Optional> cacheDeserializer = findCacheDeserializer(raw, type, config, + beanDesc, keyDeserializer, elementTypeDeserializer, elementDeserializer); + if (cacheDeserializer.isPresent()) { + return cacheDeserializer.get(); + } return null; } + /** + * Find matching implementation of {@link Cache} deserializers by checking + * if the parameter {@code raw} type is assignable. + * + * NOTE: Make sure the cache implementations are checked in such a way that more concrete classes are + * compared first before more abstract ones. + * + * @return An optional {@link JsonDeserializer} for the cache type, if found. + * @since 2.16 + */ + private java.util.Optional> findCacheDeserializer(Class raw, MapLikeType type, + DeserializationConfig config, BeanDescription beanDesc, KeyDeserializer keyDeserializer, + TypeDeserializer elementTypeDeserializer, JsonDeserializer elementDeserializer) + { + /* // Example implementations + if (LoadingCache.class.isAssignableFrom(raw)) { + return ....your implementation....; + } + */ + if (Cache.class.isAssignableFrom(raw)) { + return java.util.Optional.of( + new SimpleCacheDeserializer(type, keyDeserializer, elementTypeDeserializer, elementDeserializer)); + } + return java.util.Optional.empty(); + } + @Override // since 2.7 public JsonDeserializer findReferenceDeserializer(ReferenceType refType, DeserializationConfig config, BeanDescription beanDesc, diff --git a/guava/src/main/java/com/fasterxml/jackson/datatype/guava/SimpleCacheDeserializer.java b/guava/src/main/java/com/fasterxml/jackson/datatype/guava/SimpleCacheDeserializer.java new file mode 100644 index 00000000..3dc56936 --- /dev/null +++ b/guava/src/main/java/com/fasterxml/jackson/datatype/guava/SimpleCacheDeserializer.java @@ -0,0 +1,57 @@ +package com.fasterxml.jackson.datatype.guava; + +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.KeyDeserializer; +import com.fasterxml.jackson.databind.deser.NullValueProvider; +import com.fasterxml.jackson.databind.jsontype.TypeDeserializer; +import com.fasterxml.jackson.databind.type.MapLikeType; +import com.fasterxml.jackson.datatype.guava.deser.cache.GuavaCacheDeserializer; +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; + +/** + * {@link GuavaCacheDeserializer} class implementation for deserializing Guava {@link Cache} instances. + * + * @since 2.16 + */ +public class SimpleCacheDeserializer + extends GuavaCacheDeserializer> +{ + + /* + /********************************************************** + /* Life-cycle + /********************************************************** + */ + + public SimpleCacheDeserializer(MapLikeType type, KeyDeserializer keyDeserializer, + TypeDeserializer elementTypeDeserializer, JsonDeserializer elementDeserializer) + { + super(type, keyDeserializer, elementTypeDeserializer, elementDeserializer); + } + + public SimpleCacheDeserializer(MapLikeType type, KeyDeserializer keyDeserializer, + TypeDeserializer elementTypeDeserializer, JsonDeserializer elementDeserializer, + NullValueProvider nvp) + { + super(type, keyDeserializer, elementTypeDeserializer, elementDeserializer, nvp); + } + + /* + /********************************************************************** + /* Abstract method overrides + /********************************************************************** + */ + + @Override + protected Cache createCache() { + return CacheBuilder.newBuilder().build(); + } + + @Override + protected JsonDeserializer _createContextual(MapLikeType t, + KeyDeserializer kd, TypeDeserializer vtd, JsonDeserializer vd, NullValueProvider np) + { + return new SimpleCacheDeserializer(t, kd, vtd, vd, np); + } +} diff --git a/guava/src/main/java/com/fasterxml/jackson/datatype/guava/deser/cache/GuavaCacheDeserializer.java b/guava/src/main/java/com/fasterxml/jackson/datatype/guava/deser/cache/GuavaCacheDeserializer.java new file mode 100644 index 00000000..bbda7cb2 --- /dev/null +++ b/guava/src/main/java/com/fasterxml/jackson/datatype/guava/deser/cache/GuavaCacheDeserializer.java @@ -0,0 +1,161 @@ +package com.fasterxml.jackson.datatype.guava.deser.cache; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonToken; +import com.fasterxml.jackson.databind.*; +import com.fasterxml.jackson.databind.deser.ContextualDeserializer; +import com.fasterxml.jackson.databind.deser.NullValueProvider; +import com.fasterxml.jackson.databind.deser.impl.NullsConstantProvider; +import com.fasterxml.jackson.databind.deser.std.StdDeserializer; +import com.fasterxml.jackson.databind.jsontype.TypeDeserializer; +import com.fasterxml.jackson.databind.type.LogicalType; +import com.fasterxml.jackson.databind.type.MapLikeType; +import com.google.common.cache.Cache; + +import java.io.IOException; +import java.lang.reflect.InvocationTargetException; + +public abstract class GuavaCacheDeserializer> + extends StdDeserializer implements ContextualDeserializer +{ + private static final long serialVersionUID = 1L; + + private final MapLikeType type; + private final KeyDeserializer keyDeserializer; + private final TypeDeserializer elementTypeDeserializer; + private final JsonDeserializer elementDeserializer; + + /* + * @since 2.16 : in 3.x demote to `ContainerDeserializerBase` + */ + private final NullValueProvider nullProvider; + private final boolean skipNullValues; + + /* + /********************************************************** + /* Life-cycle + /********************************************************** + */ + + public GuavaCacheDeserializer(MapLikeType type, KeyDeserializer keyDeserializer, + TypeDeserializer elementTypeDeserializer, JsonDeserializer elementDeserializer) { + this(type, keyDeserializer, elementTypeDeserializer, elementDeserializer, null); + } + + public GuavaCacheDeserializer(MapLikeType type, KeyDeserializer keyDeserializer, + TypeDeserializer elementTypeDeserializer, JsonDeserializer elementDeserializer, + NullValueProvider nvp) + { + super(type); + this.type = type; + this.keyDeserializer = keyDeserializer; + this.elementTypeDeserializer = elementTypeDeserializer; + this.elementDeserializer = elementDeserializer; + this.nullProvider = nvp; + skipNullValues = (nvp == null) ? false : NullsConstantProvider.isSkipper(nvp); + } + + /* + /********************************************************** + /* Post-processing (contextualization) + /********************************************************** + */ + + @Override + public JsonDeserializer createContextual(DeserializationContext ctxt, + BeanProperty property) throws JsonMappingException + { + KeyDeserializer kd = keyDeserializer; + if (kd == null) { + kd = ctxt.findKeyDeserializer(type.getKeyType(), property); + } + JsonDeserializer valueDeser = elementDeserializer; + final JavaType vt = type.getContentType(); + if (valueDeser == null) { + valueDeser = ctxt.findContextualValueDeserializer(vt, property); + } else { // if directly assigned, probably not yet contextual, so: + valueDeser = ctxt.handleSecondaryContextualization(valueDeser, property, vt); + } + // Type deserializer is slightly different; must be passed, but needs to become contextual: + TypeDeserializer vtd = elementTypeDeserializer; + if (vtd != null) { + vtd = vtd.forProperty(property); + } + return _createContextual(type, kd, vtd, valueDeser, + findContentNullProvider(ctxt, property, valueDeser)); + } + + /* + /********************************************************************** + /* Abstract methods for subclasses + /********************************************************************** + */ + + protected abstract T createCache(); + + protected abstract JsonDeserializer _createContextual(MapLikeType t, KeyDeserializer kd, + TypeDeserializer vtd, JsonDeserializer vd, NullValueProvider np); + + /* + /********************************************************************** + /* Implementations + /********************************************************************** + */ + + @Override + public LogicalType logicalType() { + return LogicalType.Map; + } + + @Override + public T deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { + return deserializeContents(p, ctxt); + } + + private T deserializeContents(JsonParser p, DeserializationContext ctxt) + throws IOException + { + T cache = createCache(); + + JsonToken currToken = p.currentToken(); + if (currToken != JsonToken.FIELD_NAME) { + // 01-Mar-2023, tatu: [datatypes-collections#104] Handle empty Maps too + if (currToken != JsonToken.END_OBJECT) { + expect(p, JsonToken.START_OBJECT); + currToken = p.nextToken(); + } + } + + for (; currToken == JsonToken.FIELD_NAME; currToken = p.nextToken()) { + final Object key; + if (keyDeserializer != null) { + key = keyDeserializer.deserializeKey(p.currentName(), ctxt); + } else { + key = p.currentName(); + } + + p.nextToken(); + + final Object value; + if (p.currentToken() == JsonToken.VALUE_NULL) { + if (skipNullValues) { + continue; + } + value = nullProvider.getNullValue(ctxt); + } else if (elementTypeDeserializer != null) { + value = elementDeserializer.deserializeWithType(p, ctxt, elementTypeDeserializer); + } else { + value = elementDeserializer.deserialize(p, ctxt); + } + cache.put(key, value); + } + return cache; + } + + private void expect(JsonParser p, JsonToken token) throws IOException { + if (p.getCurrentToken() != token) { + throw new JsonMappingException(p, "Expecting " + token + " to start `Cache` value, found " + p.currentToken(), + p.getCurrentLocation()); + } + } +} diff --git a/guava/src/test/java/com/fasterxml/jackson/datatype/guava/CacheDeserializationTest.java b/guava/src/test/java/com/fasterxml/jackson/datatype/guava/CacheDeserializationTest.java new file mode 100644 index 00000000..3e9dfa69 --- /dev/null +++ b/guava/src/test/java/com/fasterxml/jackson/datatype/guava/CacheDeserializationTest.java @@ -0,0 +1,179 @@ +package com.fasterxml.jackson.datatype.guava; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonValue; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.base.Optional; +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; + +import java.util.Map; +import java.util.Objects; + +/** + * Unit tests for verifying deserialization of Guava's {@link Cache} type. + * + * @since 2.16 + */ +public class CacheDeserializationTest extends ModuleTestBase { + + /* + /********************************************************** + /* Set up + /********************************************************** + */ + + // [datatype-collections#96] + static class CacheProperties { + @JsonProperty + Cache cache; + + @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) + public CacheProperties(@JsonProperty("cache") Cache cache) { + this.cache = cache; + } + } + + // [datatype-collections#96] + static class CacheDelegating { + Cache cache; + + @JsonCreator(mode = JsonCreator.Mode.DELEGATING) + public CacheDelegating(Cache cache) { + this.cache = cache; + } + + @JsonValue + Cache mapValue() { + return cache; + } + } + + private enum MyEnum { + YAY, + BOO + } + + /* + /********************************************************** + /* Tests + /********************************************************** + */ + + private final ObjectMapper MAPPER = mapperWithModule(); + + public void testGuavaCacheApi() throws Exception { + Cache cache = CacheBuilder.newBuilder().build(); + // Cache does not allow null key + try { + cache.put(null, "value"); + fail("should not pass"); + } catch (NullPointerException e) {} + + // Cache does not allow null value + try { + cache.put("key", null); + fail("should not pass"); + } catch (NullPointerException e) {} + } + + public void testCacheDeserializationSimple() throws Exception { + // Create a delegate cache using CacheBuilder + Cache delegateCache = CacheBuilder.newBuilder().build(); + delegateCache.put("key1", 1); + + Cache s = MAPPER.readValue(a2q("{'a':'foo'}"), + new TypeReference>() {}); + + assertEquals(1, s.size()); + assertEquals("foo", s.getIfPresent("a")); + } + + public void testCacheDeserRoundTrip() throws Exception { + Cache cache = CacheBuilder.newBuilder().build(); + cache.put("key1", 1); + cache.put("key2", 2); + String json = MAPPER.writeValueAsString(cache); + + Cache deserializedCache = MAPPER.readValue(json, + new TypeReference>() {}); + + int value1 = Objects.requireNonNull(deserializedCache.getIfPresent("key1")); + int value2 = Objects.requireNonNull(deserializedCache.getIfPresent("key2")); + assertEquals(1, value1); + assertEquals(2, value2); + } + + // [datatype-collections#96] + public void testCacheSerialization() throws Exception { + Cache cache = CacheBuilder.newBuilder().build(); + cache.put(1L, 1); + cache.put(2L, 2); + + // properties + String json = MAPPER.writeValueAsString(new CacheProperties(cache)); + CacheProperties propertiesCache = MAPPER.readValue(json, CacheProperties.class); + _verifySizeTwoAndContains(propertiesCache.cache.asMap()); + + // Delegating + String delegatingJson = MAPPER.writeValueAsString(new CacheDelegating(cache)); + CacheDelegating delegtingCache = MAPPER.readValue(delegatingJson, CacheDelegating.class); + _verifySizeTwoAndContains(delegtingCache.cache.asMap()); + } + + // [datatype-collections#96] + private void _verifySizeTwoAndContains(Map map) { + assertEquals(2, map.size()); + assertEquals(1, map.get(1L).intValue()); + assertEquals(2, map.get(2L).intValue()); + } + + public void testEnumKey() throws Exception { + final TypeReference> type = new TypeReference>() {}; + final Cache cache = CacheBuilder.newBuilder().build(); + cache.put(MyEnum.YAY, 5); + cache.put(MyEnum.BOO, 2); + + // test serialization + final String serializedForm = MAPPER.writerFor(type).writeValueAsString(cache); + assertEquals(serializedForm, MAPPER.writeValueAsString(cache)); + + // test deserialization + final Cache deserializedCache = MAPPER.readValue(serializedForm, type); + assertEquals( + cache.asMap().entrySet(), + deserializedCache.asMap().entrySet()); + } + + @JsonInclude(JsonInclude.Include.NON_EMPTY) + private static class CacheWrapper { + @JsonProperty + private Cache cache = CacheBuilder.newBuilder().build(); + } + + public void testEmptyCacheExclusion() throws Exception { + String json = MAPPER.writeValueAsString(new CacheWrapper()); + assertEquals("{}", json); + } + + public void testWithGuavaOptional() throws Exception { + // set up + Cache> cache = CacheBuilder.newBuilder().build(); + cache.put("a", Optional.of(6.0)); + cache.put("b", Optional.absent()); + + // test ser + String jsonStr = MAPPER.writeValueAsString(cache); + assertEquals(a2q("{'a':6.0,'b':null}"), jsonStr); + + // test deser + Cache> deserializedCache = MAPPER.readValue(jsonStr, + new TypeReference>>() {}); + + // test before and after + assertEquals(cache.asMap().entrySet(), deserializedCache.asMap().entrySet()); + } +}