From 0a1ba4b33803dddf2895dd9f9e95d7585ddf690c Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Fri, 21 Aug 2020 17:35:02 -0700 Subject: [PATCH] Update release notes wrt #43, minor cleanup --- release-notes/CREDITS-2.x | 5 + release-notes/VERSION-2.x | 3 + .../impl/AsDeductionTypeDeserializer.java | 230 +++++++++--------- .../jsontype/impl/StdTypeResolverBuilder.java | 15 +- .../jsontype/TestPolymorphicDeduction.java | 26 +- 5 files changed, 141 insertions(+), 138 deletions(-) diff --git a/release-notes/CREDITS-2.x b/release-notes/CREDITS-2.x index eb8d7e0467..c3dfd07a89 100644 --- a/release-notes/CREDITS-2.x +++ b/release-notes/CREDITS-2.x @@ -1147,6 +1147,11 @@ Daniel Hrabovcak (TheSpiritXIII@github) * Reported #2796: `TypeFactory.constructType()` does not take `TypeBindings` correctly (2.11.2) +Mark Carter (drekbour@github) + * Contributed #43 implementation: Add option to resolve type from multiple existing properties, + `@JsonTypeInfo(use=DEDUCTION)` + (2.12.0) + Mike Gilbode (gilbode@github) * Reported #792: Deserialization Not Working Right with Generic Types and Builders (2.12.0) diff --git a/release-notes/VERSION-2.x b/release-notes/VERSION-2.x index 03cbe45b66..ab0c605611 100644 --- a/release-notes/VERSION-2.x +++ b/release-notes/VERSION-2.x @@ -6,6 +6,9 @@ Project: jackson-databind 2.12.0 (not yet released) +#43: Add option to resolve type from multiple existing properties, + `@JsonTypeInfo(use=DEDUCTION)` + (contributed by drekbour@github) #426: `@JsonIgnoreProperties` does not prevent Exception Conflicting getter/setter definitions for property (reported by gmkll@github) diff --git a/src/main/java/com/fasterxml/jackson/databind/jsontype/impl/AsDeductionTypeDeserializer.java b/src/main/java/com/fasterxml/jackson/databind/jsontype/impl/AsDeductionTypeDeserializer.java index 1f10df4617..78f073b929 100644 --- a/src/main/java/com/fasterxml/jackson/databind/jsontype/impl/AsDeductionTypeDeserializer.java +++ b/src/main/java/com/fasterxml/jackson/databind/jsontype/impl/AsDeductionTypeDeserializer.java @@ -1,27 +1,20 @@ package com.fasterxml.jackson.databind.jsontype.impl; import java.io.IOException; -import java.util.BitSet; -import java.util.Collection; -import java.util.HashMap; -import java.util.Iterator; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; +import java.util.*; import com.fasterxml.jackson.annotation.JsonTypeInfo; + import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.core.JsonToken; -import com.fasterxml.jackson.databind.BeanProperty; -import com.fasterxml.jackson.databind.DeserializationConfig; -import com.fasterxml.jackson.databind.DeserializationContext; -import com.fasterxml.jackson.databind.JavaType; -import com.fasterxml.jackson.databind.MapperFeature; + +import com.fasterxml.jackson.databind.*; import com.fasterxml.jackson.databind.exc.InvalidTypeIdException; import com.fasterxml.jackson.databind.introspect.BeanPropertyDefinition; import com.fasterxml.jackson.databind.jsontype.NamedType; import com.fasterxml.jackson.databind.jsontype.TypeDeserializer; import com.fasterxml.jackson.databind.jsontype.TypeIdResolver; +import com.fasterxml.jackson.databind.util.ClassUtil; import com.fasterxml.jackson.databind.util.TokenBuffer; /** @@ -29,124 +22,125 @@ * is limited to the names of child fields (not their values or, consequently, any nested descendants). * Exceptions will be thrown if not enough unique information is present to select a single subtype. */ -public class AsDeductionTypeDeserializer extends AsPropertyTypeDeserializer { - - // Fieldname -> bitmap-index of every field discovered, across all subtypes - private final Map fieldBitIndex; - // Bitmap of available fields in each subtype (including its parents) - private final Map subtypeFingerprints; - - public AsDeductionTypeDeserializer(JavaType bt, TypeIdResolver idRes, JavaType defaultImpl, DeserializationConfig config, Collection subtypes) { - super(bt, idRes, null, false, defaultImpl); - fieldBitIndex = new HashMap<>(); - subtypeFingerprints = buildFingerprints(config, subtypes); - } - - public AsDeductionTypeDeserializer(AsDeductionTypeDeserializer src, BeanProperty property) { - super(src, property); - fieldBitIndex = src.fieldBitIndex; - subtypeFingerprints = src.subtypeFingerprints; - } - - @Override - public JsonTypeInfo.As getTypeInclusion() { - return null; - } - - @Override - public TypeDeserializer forProperty(BeanProperty prop) { - return (prop == _property) ? this : new AsDeductionTypeDeserializer(this, prop); - } - - protected Map buildFingerprints(DeserializationConfig config, Collection subtypes) { - boolean ignoreCase = config.isEnabled(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES); - - int nextField = 0; - Map fingerprints = new HashMap<>(); - - for (NamedType subtype : subtypes) { - JavaType subtyped = config.getTypeFactory().constructType(subtype.getType()); - List properties = config.introspect(subtyped).findProperties(); - - BitSet fingerprint = new BitSet(nextField + properties.size()); - for (BeanPropertyDefinition property : properties) { - String name = property.getName(); - if (ignoreCase) name = name.toLowerCase(); - Integer bitIndex = fieldBitIndex.get(name); - if (bitIndex == null) { - bitIndex = nextField; - fieldBitIndex.put(name, nextField++); - } - fingerprint.set(bitIndex); - } +public class AsDeductionTypeDeserializer extends AsPropertyTypeDeserializer +{ + private static final long serialVersionUID = 1L; + + // Fieldname -> bitmap-index of every field discovered, across all subtypes + private final Map fieldBitIndex; + // Bitmap of available fields in each subtype (including its parents) + private final Map subtypeFingerprints; + + public AsDeductionTypeDeserializer(JavaType bt, TypeIdResolver idRes, JavaType defaultImpl, DeserializationConfig config, Collection subtypes) { + super(bt, idRes, null, false, defaultImpl); + fieldBitIndex = new HashMap<>(); + subtypeFingerprints = buildFingerprints(config, subtypes); + } - String existingFingerprint = fingerprints.put(fingerprint, subtype.getType().getName()); + public AsDeductionTypeDeserializer(AsDeductionTypeDeserializer src, BeanProperty property) { + super(src, property); + fieldBitIndex = src.fieldBitIndex; + subtypeFingerprints = src.subtypeFingerprints; + } - // Validate uniqueness - if (existingFingerprint != null) { - throw new IllegalStateException( - String.format("Subtypes %s and %s have the same signature and cannot be uniquely deduced.", existingFingerprint, subtype.getType().getName()) - ); - } + @Override + public JsonTypeInfo.As getTypeInclusion() { + return null; + } + @Override + public TypeDeserializer forProperty(BeanProperty prop) { + return (prop == _property) ? this : new AsDeductionTypeDeserializer(this, prop); } - return fingerprints; - } - - @Override - public Object deserializeTypedFromObject(JsonParser p, DeserializationContext ctxt) throws IOException { - - JsonToken t = p.currentToken(); - if (t == JsonToken.START_OBJECT) { - t = p.nextToken(); - } else { - /* This is most likely due to the fact that not all Java types are - * serialized as JSON Objects; so if "as-property" inclusion is requested, - * serialization of things like Lists must be instead handled as if - * "as-wrapper-array" was requested. - * But this can also be due to some custom handling: so, if "defaultImpl" - * is defined, it will be asked to handle this case. - */ - return _deserializeTypedUsingDefaultImpl(p, ctxt, null); + + protected Map buildFingerprints(DeserializationConfig config, Collection subtypes) { + boolean ignoreCase = config.isEnabled(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES); + + int nextField = 0; + Map fingerprints = new HashMap<>(); + + for (NamedType subtype : subtypes) { + JavaType subtyped = config.getTypeFactory().constructType(subtype.getType()); + List properties = config.introspect(subtyped).findProperties(); + + BitSet fingerprint = new BitSet(nextField + properties.size()); + for (BeanPropertyDefinition property : properties) { + String name = property.getName(); + if (ignoreCase) name = name.toLowerCase(); + Integer bitIndex = fieldBitIndex.get(name); + if (bitIndex == null) { + bitIndex = nextField; + fieldBitIndex.put(name, nextField++); + } + fingerprint.set(bitIndex); + } + + String existingFingerprint = fingerprints.put(fingerprint, subtype.getType().getName()); + + // Validate uniqueness + if (existingFingerprint != null) { + throw new IllegalStateException( + String.format("Subtypes %s and %s have the same signature and cannot be uniquely deduced.", existingFingerprint, subtype.getType().getName()) + ); + } + } + return fingerprints; } - List candidates = new LinkedList<>(subtypeFingerprints.keySet()); + @Override + public Object deserializeTypedFromObject(JsonParser p, DeserializationContext ctxt) throws IOException { + + JsonToken t = p.currentToken(); + if (t == JsonToken.START_OBJECT) { + t = p.nextToken(); + } else { + /* This is most likely due to the fact that not all Java types are + * serialized as JSON Objects; so if "as-property" inclusion is requested, + * serialization of things like Lists must be instead handled as if + * "as-wrapper-array" was requested. + * But this can also be due to some custom handling: so, if "defaultImpl" + * is defined, it will be asked to handle this case. + */ + return _deserializeTypedUsingDefaultImpl(p, ctxt, null); + } + + List candidates = new LinkedList<>(subtypeFingerprints.keySet()); - // Record processed tokens as we must rewind once after deducing the deserializer to use - TokenBuffer tb = new TokenBuffer(p, ctxt); - boolean ignoreCase = ctxt.isEnabled(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES); + // Record processed tokens as we must rewind once after deducing the deserializer to use + TokenBuffer tb = new TokenBuffer(p, ctxt); + boolean ignoreCase = ctxt.isEnabled(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES); - for (; t == JsonToken.FIELD_NAME; t = p.nextToken()) { - String name = p.getCurrentName(); - if (ignoreCase) name = name.toLowerCase(); + for (; t == JsonToken.FIELD_NAME; t = p.nextToken()) { + String name = p.currentName(); + if (ignoreCase) name = name.toLowerCase(); - tb.copyCurrentStructure(p); + tb.copyCurrentStructure(p); - Integer bit = fieldBitIndex.get(name); - if (bit != null) { - // field is known by at least one subtype - prune(candidates, bit); - if (candidates.size() == 1) { - return _deserializeTypedForId(p, ctxt, tb, subtypeFingerprints.get(candidates.get(0))); + Integer bit = fieldBitIndex.get(name); + if (bit != null) { + // field is known by at least one subtype + prune(candidates, bit); + if (candidates.size() == 1) { + return _deserializeTypedForId(p, ctxt, tb, subtypeFingerprints.get(candidates.get(0))); + } + } } - } - } - throw new InvalidTypeIdException( - p, - String.format("Cannot deduce unique subtype of %s (%d candidates match)", _baseType.toString(), candidates.size()), - _baseType - , "DEDUCED" - ); - } - - // Keep only fingerprints containing this field - private static void prune(List candidates, int bit) { - for (Iterator iter = candidates.iterator(); iter.hasNext(); ) { - if (!iter.next().get(bit)) { - iter.remove(); - } + throw new InvalidTypeIdException(p, + String.format("Cannot deduce unique subtype of %s (%d candidates match)", + ClassUtil.getTypeDescription(_baseType), + candidates.size()), + _baseType + , "DEDUCED" + ); } - } + // Keep only fingerprints containing this field + private static void prune(List candidates, int bit) { + for (Iterator iter = candidates.iterator(); iter.hasNext(); ) { + if (!iter.next().get(bit)) { + iter.remove(); + } + } + } } diff --git a/src/main/java/com/fasterxml/jackson/databind/jsontype/impl/StdTypeResolverBuilder.java b/src/main/java/com/fasterxml/jackson/databind/jsontype/impl/StdTypeResolverBuilder.java index 594843f8ce..c948c0880e 100644 --- a/src/main/java/com/fasterxml/jackson/databind/jsontype/impl/StdTypeResolverBuilder.java +++ b/src/main/java/com/fasterxml/jackson/databind/jsontype/impl/StdTypeResolverBuilder.java @@ -3,19 +3,14 @@ import java.util.Collection; import com.fasterxml.jackson.annotation.JsonTypeInfo; + import com.fasterxml.jackson.databind.DeserializationConfig; import com.fasterxml.jackson.databind.JavaType; import com.fasterxml.jackson.databind.MapperFeature; import com.fasterxml.jackson.databind.SerializationConfig; import com.fasterxml.jackson.databind.annotation.NoClass; import com.fasterxml.jackson.databind.cfg.MapperConfig; -import com.fasterxml.jackson.databind.jsontype.NamedType; -import com.fasterxml.jackson.databind.jsontype.PolymorphicTypeValidator; -import com.fasterxml.jackson.databind.jsontype.PolymorphicTypeValidator.Validity; -import com.fasterxml.jackson.databind.jsontype.TypeDeserializer; -import com.fasterxml.jackson.databind.jsontype.TypeIdResolver; -import com.fasterxml.jackson.databind.jsontype.TypeResolverBuilder; -import com.fasterxml.jackson.databind.jsontype.TypeSerializer; +import com.fasterxml.jackson.databind.jsontype.*; import com.fasterxml.jackson.databind.util.ClassUtil; /** @@ -332,13 +327,13 @@ protected PolymorphicTypeValidator verifyBaseTypeValidity(MapperConfig config { final PolymorphicTypeValidator ptv = subTypeValidator(config); if (_idType == JsonTypeInfo.Id.CLASS || _idType == JsonTypeInfo.Id.MINIMAL_CLASS) { - final Validity validity = ptv.validateBaseType(config, baseType); + final PolymorphicTypeValidator.Validity validity = ptv.validateBaseType(config, baseType); // If no subtypes are legal (that is, base type itself is invalid), indicate problem - if (validity == Validity.DENIED) { + if (validity == PolymorphicTypeValidator.Validity.DENIED) { return reportInvalidBaseType(config, baseType, ptv); } // If there's indication that any and all subtypes are fine, replace validator itself: - if (validity == Validity.ALLOWED) { + if (validity == PolymorphicTypeValidator.Validity.ALLOWED) { return LaissezFaireSubTypeValidator.instance; } // otherwise just return validator, is to be called for each distinct type diff --git a/src/test/java/com/fasterxml/jackson/databind/jsontype/TestPolymorphicDeduction.java b/src/test/java/com/fasterxml/jackson/databind/jsontype/TestPolymorphicDeduction.java index fb47498e2c..e2a22ea30f 100644 --- a/src/test/java/com/fasterxml/jackson/databind/jsontype/TestPolymorphicDeduction.java +++ b/src/test/java/com/fasterxml/jackson/databind/jsontype/TestPolymorphicDeduction.java @@ -12,10 +12,12 @@ import com.fasterxml.jackson.databind.MapperFeature; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.exc.InvalidTypeIdException; +import com.fasterxml.jackson.databind.json.JsonMapper; import com.fasterxml.jackson.databind.type.TypeFactory; import static com.fasterxml.jackson.annotation.JsonSubTypes.Type; import static com.fasterxml.jackson.annotation.JsonTypeInfo.Id.DEDUCTION; +// for [databind#43], deduction-based polymorphism public class TestPolymorphicDeduction extends BaseMapTest { @JsonTypeInfo(use = DEDUCTION) @@ -84,8 +86,9 @@ public void testSimpleInference() throws Exception { } public void testCaseInsensitiveInference() throws Exception { - Cat cat = newJsonMapper() // Don't use shared mapper! + Cat cat = JsonMapper.builder() // Don't use shared mapper! .configure(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES, true) + .build() .readValue(deadCatJson.toUpperCase(), Cat.class); assertTrue(cat instanceof DeadCat); assertSame(cat.getClass(), DeadCat.class); @@ -95,8 +98,8 @@ public void testCaseInsensitiveInference() throws Exception { // TODO not currently supported // public void testCaseInsensitivePerFieldInference() throws Exception { -// ObjectMapper mapper = newJsonMapper(); // Don't use shared mapper! -// mapper.configOverride(DeadCat.class) +// ObjectMapper mapper = JsonMapper.builder() // Don't use shared mapper! +// .configOverride(DeadCat.class) // .setFormat(JsonFormat.Value.empty() // .withFeature(JsonFormat.Feature.ACCEPT_CASE_INSENSITIVE_PROPERTIES)); // Cat cat = mapper.readValue(deadCatJson.replace("causeOfDeath", "CAUSEOFDEATH"), Cat.class); @@ -160,21 +163,24 @@ static class AnotherLiveCat extends Cat { public void testAmbiguousClasses() throws Exception { try { - ObjectMapper mapper = newJsonMapper(); // Don't use shared mapper! - mapper.registerSubtypes(AnotherLiveCat.class); - Cat cat = mapper.readValue(liveCatJson, Cat.class); + ObjectMapper mapper = JsonMapper.builder() // Don't use shared mapper! + .registerSubtypes(AnotherLiveCat.class) + .build(); + /*Cat cat =*/ mapper.readValue(liveCatJson, Cat.class); fail("Should not get here"); } catch (IllegalStateException e) { - // NO OP + verifyException(e, "Subtypes "); + verifyException(e, "have the same signature"); + verifyException(e, "cannot be uniquely deduced"); } } public void testAmbiguousProperties() throws Exception { try { - Cat cat = sharedMapper().readValue(ambiguousCatJson, Cat.class); + /*Cat cat =*/ sharedMapper().readValue(ambiguousCatJson, Cat.class); fail("Should not get here"); } catch (InvalidTypeIdException e) { - // NO OP + verifyException(e, "Cannot deduce unique subtype of"); } } @@ -184,7 +190,7 @@ public void testSimpleSerialization() throws Exception { List list = sharedMapper().readValue(arrayOfCatsJson, listOfCats); Cat cat = list.get(0); // When: - String json = sharedMapper().writeValueAsString(list.get(0)); + String json = sharedMapper().writeValueAsString(cat); // Then: assertEquals(liveCatJson, json); }