FastutilMapDeserializers.java
package tools.jackson.datatype.fastutil.deser.map;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Supplier;
import tools.jackson.core.JacksonException;
import tools.jackson.core.JsonParser;
import tools.jackson.core.JsonToken;
import tools.jackson.databind.BeanProperty;
import tools.jackson.databind.DeserializationConfig;
import tools.jackson.databind.DeserializationContext;
import tools.jackson.databind.JavaType;
import tools.jackson.databind.KeyDeserializer;
import tools.jackson.databind.ValueDeserializer;
import tools.jackson.databind.deser.NullValueProvider;
import tools.jackson.databind.jsontype.TypeDeserializer;
import tools.jackson.databind.type.LogicalType;
import tools.jackson.databind.type.MapLikeType;
import tools.jackson.databind.type.MapType;
import tools.jackson.databind.util.AccessPattern;
import tools.jackson.datatype.fastutil.deser.BoxedDelegatingDeserializer;
import tools.jackson.datatype.fastutil.deser.ContainerFamily;
import tools.jackson.datatype.fastutil.util.ContentNulls;
import tools.jackson.datatype.fastutil.util.EmptyStrings;
import tools.jackson.datatype.fastutil.util.Merging;
import tools.jackson.datatype.fastutil.util.OptionalTypes;
import tools.jackson.datatype.primitive_collections_base.deser.map.KeyHandler;
import tools.jackson.datatype.primitive_collections_base.deser.map.MapDeserializer;
import tools.jackson.datatype.primitive_collections_base.deser.map.RefKeyHandler;
import tools.jackson.datatype.primitive_collections_base.deser.map.RefValueHandler;
import tools.jackson.datatype.primitive_collections_base.deser.map.TypeHandlerPair;
import tools.jackson.datatype.primitive_collections_base.deser.map.ValueHandler;
import it.unimi.dsi.fastutil.objects.*;
/**
* Deserializers for fastutil maps that have primitive keys, primitive values or both
* ({@code Int2LongMap}, {@code Object2IntMap}, {@code Long2ObjectMap} and so on).
* Maps with both reference keys and values are handled by standard Jackson
* {@code Map} deserializers.
*/
public final class FastutilMapDeserializers
{
private static final List<Entry<?, ?, ?>> ENTRIES = new ArrayList<>();
// Maps with identity-based reference keys and primitive values, read as the
// matching (insertion-ordered) `Object2XMap`, so that values are handled the same way
private static final List<ReferenceEntry> REFERENCE_ENTRIES = new ArrayList<>();
static {
TypeHandlerPairs.register();
// Not included in `fastutil-core`
_addReference(() -> new ReferenceEntry(new ContainerFamily<Map<?, ?>>(Reference2BooleanMap.class)
.add(Reference2BooleanSortedMap.class, Reference2BooleanLinkedOpenHashMap::new, Reference2BooleanLinkedOpenHashMap::new)
.add(Reference2BooleanMap.class, Reference2BooleanOpenHashMap::new, Reference2BooleanLinkedOpenHashMap::new),
Object2BooleanLinkedOpenHashMap.class));
_addReference(() -> new ReferenceEntry(new ContainerFamily<Map<?, ?>>(Reference2ByteMap.class)
.add(Reference2ByteSortedMap.class, Reference2ByteLinkedOpenHashMap::new, Reference2ByteLinkedOpenHashMap::new)
.add(Reference2ByteMap.class, Reference2ByteOpenHashMap::new, Reference2ByteLinkedOpenHashMap::new),
Object2ByteLinkedOpenHashMap.class));
_addReference(() -> new ReferenceEntry(new ContainerFamily<Map<?, ?>>(Reference2ShortMap.class)
.add(Reference2ShortSortedMap.class, Reference2ShortLinkedOpenHashMap::new, Reference2ShortLinkedOpenHashMap::new)
.add(Reference2ShortMap.class, Reference2ShortOpenHashMap::new, Reference2ShortLinkedOpenHashMap::new),
Object2ShortLinkedOpenHashMap.class));
_addReference(() -> new ReferenceEntry(new ContainerFamily<Map<?, ?>>(Reference2CharMap.class)
.add(Reference2CharSortedMap.class, Reference2CharLinkedOpenHashMap::new, Reference2CharLinkedOpenHashMap::new)
.add(Reference2CharMap.class, Reference2CharOpenHashMap::new, Reference2CharLinkedOpenHashMap::new),
Object2CharLinkedOpenHashMap.class));
_addReference(() -> new ReferenceEntry(new ContainerFamily<Map<?, ?>>(Reference2IntMap.class)
.add(Reference2IntSortedMap.class, Reference2IntLinkedOpenHashMap::new, Reference2IntLinkedOpenHashMap::new)
.add(Reference2IntMap.class, Reference2IntOpenHashMap::new, Reference2IntLinkedOpenHashMap::new),
Object2IntLinkedOpenHashMap.class));
_addReference(() -> new ReferenceEntry(new ContainerFamily<Map<?, ?>>(Reference2LongMap.class)
.add(Reference2LongSortedMap.class, Reference2LongLinkedOpenHashMap::new, Reference2LongLinkedOpenHashMap::new)
.add(Reference2LongMap.class, Reference2LongOpenHashMap::new, Reference2LongLinkedOpenHashMap::new),
Object2LongLinkedOpenHashMap.class));
_addReference(() -> new ReferenceEntry(new ContainerFamily<Map<?, ?>>(Reference2FloatMap.class)
.add(Reference2FloatSortedMap.class, Reference2FloatLinkedOpenHashMap::new, Reference2FloatLinkedOpenHashMap::new)
.add(Reference2FloatMap.class, Reference2FloatOpenHashMap::new, Reference2FloatLinkedOpenHashMap::new),
Object2FloatLinkedOpenHashMap.class));
_addReference(() -> new ReferenceEntry(new ContainerFamily<Map<?, ?>>(Reference2DoubleMap.class)
.add(Reference2DoubleSortedMap.class, Reference2DoubleLinkedOpenHashMap::new, Reference2DoubleLinkedOpenHashMap::new)
.add(Reference2DoubleMap.class, Reference2DoubleOpenHashMap::new, Reference2DoubleLinkedOpenHashMap::new),
Object2DoubleLinkedOpenHashMap.class));
}
private static void _addReference(Supplier<ReferenceEntry> entry) {
OptionalTypes.registerIfPresent(() -> REFERENCE_ENTRIES.add(entry.get()));
}
private FastutilMapDeserializers() { }
static <M, K extends KeyHandler<K>, V extends ValueHandler<V>> void add(
boolean refKey, boolean refValue,
ContainerFamily<M> family, TypeHandlerPair<M, K, V> handlerPair)
{
ENTRIES.add(new Entry<>(refKey, refValue, family, handlerPair));
}
/**
* @return Whether given type is a fastutil map with primitive keys and/or values
* that can be constructed
*/
public static boolean handles(Class<?> rawType) {
ContainerFamily<?> family = _findFamily(rawType);
return (family != null) && (family.findCreator(rawType) != null);
}
/**
* @return Deserializer for given type if it is a fastutil map with primitive keys
* and/or values that can be constructed; {@code null} otherwise
*/
public static ValueDeserializer<?> findDeserializer(DeserializationConfig config, MapType type,
KeyDeserializer keyDeserializer,
TypeDeserializer valueTypeDeserializer, ValueDeserializer<?> valueDeserializer)
{
final Class<?> rawType = type.getRawClass();
Entry<?, ?, ?> entry = _findEntry(rawType);
if (entry != null) {
return entry.createDeserializer(config, type, keyDeserializer,
valueTypeDeserializer, valueDeserializer);
}
for (ReferenceEntry refEntry : REFERENCE_ENTRIES) {
if (refEntry.family().handles(rawType)) {
ContainerFamily.Creator<Map<?, ?>> creator = refEntry.family().findCreator(rawType);
if (creator == null) {
return null;
}
if (valueDeserializer != null) { // custom deserializer for primitive values
return new BoxedDelegatingDeserializer<>(type,
_boxedMapType(config, type, keyDeserializer, valueDeserializer), creator,
false, true);
}
Entry<?, ?, ?> intermediateEntry = _findEntry(refEntry.intermediateType());
if (intermediateEntry == null) { // never happens: registered together
return null;
}
return _createReferenceDeserializer(intermediateEntry, creator,
refEntry.intermediateType(), type, keyDeserializer);
}
}
return null;
}
/**
* @return Type of {@code LinkedHashMap} with same key and value types as given
* map type, using given custom deserializers (that need to be set explicitly:
* resolved types are cached without them)
*/
static JavaType _boxedMapType(DeserializationConfig config, MapType type,
KeyDeserializer keyDeserializer, ValueDeserializer<?> valueDeserializer)
{
MapLikeType boxed = config.getTypeFactory()
.constructMapType(LinkedHashMap.class, type.getKeyType(), type.getContentType());
if (keyDeserializer != null) {
boxed = boxed.withKeyValueHandler(keyDeserializer);
}
if (valueDeserializer != null) {
boxed = boxed.withContentValueHandler(valueDeserializer);
}
return boxed;
}
private static ContainerFamily<?> _findFamily(Class<?> rawType) {
Entry<?, ?, ?> entry = _findEntry(rawType);
if (entry != null) {
return entry.family();
}
for (ReferenceEntry refEntry : REFERENCE_ENTRIES) {
if (refEntry.family().handles(rawType)) {
return refEntry.family();
}
}
return null;
}
private static Entry<?, ?, ?> _findEntry(Class<?> rawType) {
for (Entry<?, ?, ?> entry : ENTRIES) {
if (entry.family().handles(rawType)) {
return entry;
}
}
return null;
}
/**
* Maps with identity-based reference keys and primitive values: populated as
* an (insertion-ordered) {@code Object2XMap} of given type, then copied.
*/
private record ReferenceEntry(ContainerFamily<Map<?, ?>> family, Class<?> intermediateType) { }
private static <M> ValueDeserializer<?> _createReferenceDeserializer(Entry<M, ?, ?> entry,
ContainerFamily.Creator<Map<?, ?>> creator, Class<?> intermediateType,
MapType type, KeyDeserializer keyDeserializer)
{
ContainerFamily.Creator<M> intermediate = entry.family().findCreator(intermediateType);
return entry.createDeserializer(type, keyDeserializer, null, null,
new CopyingConstruction<>(intermediate, creator));
}
private record Entry<M, K extends KeyHandler<K>, V extends ValueHandler<V>>(
boolean refKey, boolean refValue,
ContainerFamily<M> family, TypeHandlerPair<M, K, V> typeHandlerPair)
{
ValueDeserializer<?> createDeserializer(DeserializationConfig config, MapType type,
KeyDeserializer keyDeserializer,
TypeDeserializer valueTypeDeserializer, ValueDeserializer<?> valueDeserializer)
{
final ContainerFamily.Creator<M> creator = family.findCreator(type.getRawClass());
if (creator == null) {
return null;
}
// Custom deserializers for primitive keys or values produce boxed values:
// let standard Jackson handling use them
if ((!refKey && keyDeserializer != null) || (!refValue && valueDeserializer != null)) {
return new BoxedDelegatingDeserializer<>(type,
_boxedMapType(config, type, keyDeserializer, valueDeserializer), creator,
!refKey, !refValue);
}
return createDeserializer(type, keyDeserializer, valueTypeDeserializer, valueDeserializer,
new DirectConstruction<>(creator));
}
@SuppressWarnings("unchecked")
<T> ValueDeserializer<?> createDeserializer(MapType type, KeyDeserializer keyDeserializer,
TypeDeserializer valueTypeDeserializer, ValueDeserializer<?> valueDeserializer,
Construction<T, M> construction)
{
K keyHandler = refKey
? (K) new RefKeyHandler(type.getKeyType(), keyDeserializer)
: typeHandlerPair.keyHandler(type.getKeyType());
V valueHandler = refValue
? (V) new RefValueHandler(type.getContentType(), valueDeserializer, valueTypeDeserializer)
: typeHandlerPair.valueHandler(type.getContentType());
return new FastutilMapDeserializer<>(type, keyHandler, valueHandler,
typeHandlerPair, construction, null);
}
}
/**
* How maps are constructed: an intermediate map is populated, then converted into
* the result. Maps to create depend on the requested type (not just on key and value
* types), so {@link TypeHandlerPair#createEmpty} is not used.
*
* @param <T> Type of maps returned
* @param <I> Type of intermediate maps populated
*/
interface Construction<T, I>
{
/**
* @param ctxt Context for reporting failures; may be {@code null}
*/
I createIntermediate(DeserializationContext ctxt) throws JacksonException;
T finish(DeserializationContext ctxt, I intermediate) throws JacksonException;
/**
* @return Whether existing maps (when merging) can be populated directly,
* that is, whether intermediate and result types are the same
*/
boolean canPopulateExisting();
}
// Intermediate map is the result (possibly wrapped, or copied by `Creator`)
private record DirectConstruction<M>(ContainerFamily.Creator<M> creator)
implements Construction<M, M>
{
@Override
public M createIntermediate(DeserializationContext ctxt) throws JacksonException {
return creator.create(ctxt);
}
@Override
public M finish(DeserializationContext ctxt, M intermediate) throws JacksonException {
return creator.finish(ctxt, intermediate);
}
@Override
public boolean canPopulateExisting() {
return true;
}
}
// Intermediate map (of a different type) is copied into the result
private record CopyingConstruction<I>(ContainerFamily.Creator<I> intermediate,
ContainerFamily.Creator<Map<?, ?>> result)
implements Construction<Map<?, ?>, I>
{
@Override
public I createIntermediate(DeserializationContext ctxt) throws JacksonException {
return intermediate.create(ctxt);
}
@SuppressWarnings("unchecked")
@Override
public Map<?, ?> finish(DeserializationContext ctxt, I populated) throws JacksonException {
Map<?, ?> map = result.create(ctxt);
((Map<Object, Object>) map).putAll((Map<?, ?>) populated);
return result.finish(ctxt, map);
}
@Override
public boolean canPopulateExisting() {
return false;
}
}
/**
* Deserializer that creates maps using given {@link Construction}.
*<p>
* Also handles {@code @JsonSetter(contentNulls = SKIP / FAIL)} for values,
* coercion of empty Strings and merging.
*/
static final class FastutilMapDeserializer<T, I, K extends KeyHandler<K>, V extends ValueHandler<V>>
extends MapDeserializer<T, I, K, V>
{
private final JavaType _mapType;
private final Construction<T, I> _construction;
// Handler for JSON nulls as values, if not the default one
private final NullValueProvider _nuller;
FastutilMapDeserializer(JavaType mapType, K keyHandler, V valueHandler,
TypeHandlerPair<I, K, V> typeHandlerPair, Construction<T, I> construction,
NullValueProvider nuller) {
super(mapType.getRawClass(), keyHandler, valueHandler, typeHandlerPair,
construction::finish);
_mapType = mapType;
_construction = construction;
_nuller = nuller;
}
@Override
public ValueDeserializer<?> createContextual(DeserializationContext ctxt, BeanProperty property)
{
FastutilMapDeserializer<T, I, K, V> deser = (FastutilMapDeserializer<T, I, K, V>)
super.createContextual(ctxt, property);
NullValueProvider nuller = ContentNulls.findNuller(ctxt, property, _mapType);
if (nuller == deser._nuller) {
return deser;
}
return new FastutilMapDeserializer<>(_mapType, deser._keyHandler, deser._valueHandler,
_typeHandlerPair, _construction, nuller);
}
@Override
protected MapDeserializer<T, ?, ?, ?> withResolved(K keyHandler, V valueHandler) {
return new FastutilMapDeserializer<>(_mapType, keyHandler, valueHandler,
_typeHandlerPair, _construction, _nuller);
}
@Override
protected I createIntermediate() {
return _construction.createIntermediate(null);
}
@Override
protected I createIntermediate(DeserializationContext ctxt) throws JacksonException {
return _construction.createIntermediate(ctxt);
}
@Override
protected void deserializeEntry(I target, DeserializationContext ctxt, String key,
JsonParser valueParser)
throws JacksonException
{
if ((_nuller != null) && valueParser.hasToken(JsonToken.VALUE_NULL)) {
_nuller.getNullValue(ctxt); // fails, or skips
return;
}
super.deserializeEntry(target, ctxt, key, valueParser);
}
@SuppressWarnings("unchecked")
@Override
public T deserialize(JsonParser p, DeserializationContext ctxt)
throws JacksonException
{
// Empty String may be coerced (like for `java.util` maps)
if (p.hasToken(JsonToken.VALUE_STRING)) {
return (T) EmptyStrings.deserializeFromString(p, ctxt, this);
}
return super.deserialize(p, ctxt);
}
// Merging (`@JsonMerge`, `ObjectReader.readerForUpdating()`) adds entries,
// replacing (not merging) existing values
@SuppressWarnings("unchecked")
@Override
public T deserialize(JsonParser p, DeserializationContext ctxt, T intoValue)
throws JacksonException
{
if (_construction.canPopulateExisting()) {
JsonToken t = p.currentToken();
if (t == JsonToken.START_OBJECT) {
t = p.nextToken();
}
if (t == JsonToken.PROPERTY_NAME || t == JsonToken.END_OBJECT) {
try {
deserializeEntries(p, ctxt, (I) intoValue);
} catch (UnsupportedOperationException e) {
throw Merging.failure(ctxt, intoValue, e);
}
return intoValue;
}
}
return Merging.putAll(ctxt, intoValue, (Map<?, ?>) deserialize(p, ctxt));
}
@Override
public Boolean supportsUpdate(DeserializationConfig config) {
return Boolean.TRUE;
}
@Override
public Object getEmptyValue(DeserializationContext ctxt) {
return _construction.finish(ctxt, _construction.createIntermediate(ctxt));
}
@Override
public AccessPattern getEmptyAccessPattern() {
return AccessPattern.DYNAMIC;
}
@Override
public LogicalType logicalType() {
return LogicalType.Map;
}
}
}