From 392b428fafbab43c96116a74c6a28c37fe31785b Mon Sep 17 00:00:00 2001 From: Joshua Barr Date: Wed, 2 Dec 2020 16:38:21 -0800 Subject: [PATCH] Allow disabling native type ids in IonMapper This change makes the behavior of the Ion format more similar to YAMLMapper in `jackson-dataformat-yaml`. * Native type ids may be disabled in `Ion{Generator,Factory,Mapper}` * IonParser capability introspection `canReadTypeId => true` * IonGenerator#writeTypePrefix override removed, enabling non-native Together these changes allow `AsPropertyDeserializer` with an `IonParser` backing to deserialize either style while the `IonGenerator` will honor its constructed feature set. --- .../jackson/dataformat/ion/IonFactory.java | 131 ++++++++++++++- .../jackson/dataformat/ion/IonGenerator.java | 154 ++++++++++++------ .../dataformat/ion/IonObjectMapper.java | 38 ++++- .../jackson/dataformat/ion/IonParser.java | 99 ++++++++--- .../IonAnnotationTypeDeserializer.java | 2 +- .../jackson/dataformat/ion/IonParserTest.java | 27 ++- .../SerializationAnnotationsTest.java | 123 ++++++++++++++ 7 files changed, 486 insertions(+), 88 deletions(-) create mode 100644 ion/src/test/java/com/fasterxml/jackson/dataformat/ion/polymorphism/SerializationAnnotationsTest.java diff --git a/ion/src/main/java/com/fasterxml/jackson/dataformat/ion/IonFactory.java b/ion/src/main/java/com/fasterxml/jackson/dataformat/ion/IonFactory.java index fd81223ba..7fe43f6ef 100644 --- a/ion/src/main/java/com/fasterxml/jackson/dataformat/ion/IonFactory.java +++ b/ion/src/main/java/com/fasterxml/jackson/dataformat/ion/IonFactory.java @@ -56,7 +56,23 @@ public class IonFactory extends JsonFactory { * Whether we will produce binary or text Ion writers: default is textual. */ protected boolean _cfgCreateBinaryWriters = false; - + + /** + * Bitfield (set of flags) of all parser features that are enabled + * by default. + */ + protected final static int DEFAULT_ION_PARSER_FEATURE_FLAGS = IonParser.Feature.collectDefaults(); + + /** + * Bitfield (set of flags) of all generator features that are enabled + * by default. + */ + protected final static int DEFAULT_ION_GENERATOR_FEATURE_FLAGS = IonGenerator.Feature.collectDefaults(); + + protected int _ionParserFeatures = DEFAULT_ION_PARSER_FEATURE_FLAGS; + + protected int _ionGeneratorFeatures = DEFAULT_ION_GENERATOR_FEATURE_FLAGS; + public IonFactory() { this((ObjectCodec) null); } @@ -64,7 +80,7 @@ public IonFactory() { public IonFactory(ObjectCodec mapper) { this(mapper, IonSystemBuilder.standard().build()); } - + public IonFactory(ObjectCodec mapper, IonSystem system) { super(mapper); _system = system; @@ -110,7 +126,7 @@ public static IonFactory forBinaryWriters() { public static IonFactoryBuilder builderForBinaryWriters() { return new IonFactoryBuilder(true); } - + /** * Method for creating {@link IonFactory} that will * create textual (not binary) writers. @@ -144,7 +160,7 @@ public Version version() { public String getFormatName() { return FORMAT_NAME_ION; } - + public void setCreateBinaryWriters(boolean b) { _cfgCreateBinaryWriters = b; } @@ -164,6 +180,107 @@ public boolean canUseCharArrays() { return false; } + /* + /********************************************************** + /* Configuration, parser settings + /********************************************************** + */ + + /** + * Method for enabling or disabling specified parser feature + * (check {@link IonParser.Feature} for list of features) + */ + public final IonFactory configure(IonParser.Feature f, boolean state) + { + if (state) { + enable(f); + } else { + disable(f); + } + return this; + } + + /** + * Method for enabling specified parser feature + * (check {@link IonParser.Feature} for list of features) + */ + public IonFactory enable(IonParser.Feature f) { + _ionParserFeatures |= f.getMask(); + return this; + } + + /** + * Method for disabling specified parser features + * (check {@link IonParser.Feature} for list of features) + */ + public IonFactory disable(IonParser.Feature f) { + _ionParserFeatures &= ~f.getMask(); + return this; + } + + /** + * Checked whether specified parser feature is enabled. + */ + public final boolean isEnabled(IonParser.Feature f) { + return (_ionParserFeatures & f.getMask()) != 0; + } + + @Override + public int getFormatParserFeatures() { + return _ionParserFeatures; + } + + /* + /********************************************************** + /* Configuration, generator settings + /********************************************************** + */ + + /** + * Method for enabling or disabling specified generator feature + * (check {@link IonGenerator.Feature} for list of features) + */ + public final IonFactory configure(IonGenerator.Feature f, boolean state) { + if (state) { + enable(f); + } else { + disable(f); + } + return this; + } + + + /** + * Method for enabling specified generator features + * (check {@link IonGenerator.Feature} for list of features) + */ + public IonFactory enable(IonGenerator.Feature f) { + _ionGeneratorFeatures |= f.getMask(); + return this; + } + + /** + * Method for disabling specified generator feature + * (check {@link IonGenerator.Feature} for list of features) + */ + public IonFactory disable(IonGenerator.Feature f) { + _ionGeneratorFeatures &= ~f.getMask(); + return this; + } + + /** + * Check whether specified generator feature is enabled. + */ + public final boolean isEnabled(IonGenerator.Feature f) { + return (_ionGeneratorFeatures & f.getMask()) != 0; + } + + @Override + public int getFormatGeneratorFeatures() { + return _ionGeneratorFeatures; + } + + /* *************************************************************** * Extended API @@ -305,7 +422,7 @@ protected String _readAll(Reader r, IOContext ctxt) throws IOException main_loop: while (true) { - + while (offset < buf.length) { int count = r.read(buf, offset, buf.length - offset); if (count < 0) { @@ -355,6 +472,6 @@ protected IonGenerator _createGenerator(OutputStream out, JsonEncoding enc, bool protected IonGenerator _createGenerator(IonWriter ion, boolean ionWriterIsManaged, IOContext ctxt, Closeable dst) { - return new IonGenerator(_generatorFeatures, _objectCodec, ion, ionWriterIsManaged, ctxt, dst); - } + return new IonGenerator(_generatorFeatures, _ionGeneratorFeatures, _objectCodec, ion, ionWriterIsManaged, ctxt, dst); + } } diff --git a/ion/src/main/java/com/fasterxml/jackson/dataformat/ion/IonGenerator.java b/ion/src/main/java/com/fasterxml/jackson/dataformat/ion/IonGenerator.java index fac89883e..99019c1b9 100644 --- a/ion/src/main/java/com/fasterxml/jackson/dataformat/ion/IonGenerator.java +++ b/ion/src/main/java/com/fasterxml/jackson/dataformat/ion/IonGenerator.java @@ -22,17 +22,17 @@ import java.util.Calendar; import com.fasterxml.jackson.core.Base64Variant; +import com.fasterxml.jackson.core.FormatFeature; import com.fasterxml.jackson.core.JsonGenerationException; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.core.JsonToken; import com.fasterxml.jackson.core.ObjectCodec; import com.fasterxml.jackson.core.StreamWriteCapability; import com.fasterxml.jackson.core.Version; import com.fasterxml.jackson.core.base.GeneratorBase; import com.fasterxml.jackson.core.io.IOContext; import com.fasterxml.jackson.core.json.JsonWriteContext; -import com.fasterxml.jackson.core.type.WritableTypeId; +import com.fasterxml.jackson.core.util.JacksonFeature; import com.fasterxml.jackson.core.util.JacksonFeatureSet; import com.fasterxml.jackson.dataformat.ion.polymorphism.IonAnnotationTypeSerializer; @@ -48,11 +48,61 @@ public class IonGenerator extends GeneratorBase { + /** + * Enumeration that defines all toggleable features for Ion generators + */ + public enum Feature implements FormatFeature // since 2.12 + { + /** + * Whether to use Ion native Type Id construct for indicating type (true); + * or "generic" type property (false) when writing. Former works better for + * systems that are Ion-centric; latter may be better choice for interoperability, + * when converting between formats or accepting other formats. Enabled by default + * for backwards compatibility as that has been the behavior of + * `jackson-dataformat-ion` since 2.9. + * + * @see getReadCapabilities() { /***************************************************************** /* JsonParser implementation: state handling /***************************************************************** - */ - + */ + @Override public boolean isClosed() { return _closed; } - + @Override public void close() throws IOException { if (!_closed) { @@ -172,7 +216,7 @@ public void close() throws IOException { /***************************************************************** /* JsonParser implementation: Text value access /***************************************************************** - */ + */ @Override public String getText() throws IOException @@ -217,12 +261,12 @@ public int getTextLength() throws IOException { public int getTextOffset() throws IOException { return 0; } - + /* /***************************************************************** /* JsonParser implementation: Numeric value access /***************************************************************** - */ + */ @Override public BigInteger getBigIntegerValue() throws IOException { @@ -314,8 +358,8 @@ public final Number getNumberValueExact() throws IOException { /**************************************************************** /* JsonParser implementation: Access to other (non-text/number) values /***************************************************************** - */ - + */ + @Override public byte[] getBinaryValue(Base64Variant arg0) throws IOException { @@ -362,11 +406,28 @@ public Object getEmbeddedObject() throws IOException { return getIonValue(); } + /* + /********************************************************** + /* Public API, Native Ids (type, object) + /********************************************************** + */ + + /* getTypeId() wants to return a single type, but there may be multiple type annotations on an Ion value. + * @see MultipleTypeIdResolver... + * MultipleClassNameIdResolver#selectId + */ + @Override + public Object getTypeId() throws IOException { + String[] typeAnnotations = getTypeAnnotations(); + // getTypeAnnotations signals "none" with an empty array, but getTypeId is allowed to return null + return typeAnnotations.length == 0 ? null : typeAnnotations[0]; + } + /* /***************************************************************** /* JsonParser implementation: traversal /***************************************************************** - */ + */ @Override public JsonLocation getCurrentLocation() { @@ -456,7 +517,7 @@ public JsonParser skipChildren() throws IOException while (true) { JsonToken t = nextToken(); if (t == null) { - _handleEOF(); // won't return in this case... + _handleEOF(); // won't return in this case... return this; } switch (t) { @@ -479,15 +540,15 @@ public JsonParser skipChildren() throws IOException ***************************************************************** * Internal helper methods ***************************************************************** - */ - + */ + protected JsonToken _tokenFromType(IonType type) { // One twist: Ion exposes nulls as typed ones... so: if (_reader.isNullValue()) { return JsonToken.VALUE_NULL; } - + switch (type) { case BOOL: return _reader.booleanValue() ? JsonToken.VALUE_TRUE : JsonToken.VALUE_FALSE; @@ -517,7 +578,7 @@ protected JsonToken _tokenFromType(IonType type) // (BLOB, CLOB) return JsonToken.VALUE_EMBEDDED_OBJECT; } - + /** * Method called when an EOF is encountered between tokens. */ diff --git a/ion/src/main/java/com/fasterxml/jackson/dataformat/ion/polymorphism/IonAnnotationTypeDeserializer.java b/ion/src/main/java/com/fasterxml/jackson/dataformat/ion/polymorphism/IonAnnotationTypeDeserializer.java index 5925bd425..6217b2b58 100644 --- a/ion/src/main/java/com/fasterxml/jackson/dataformat/ion/polymorphism/IonAnnotationTypeDeserializer.java +++ b/ion/src/main/java/com/fasterxml/jackson/dataformat/ion/polymorphism/IonAnnotationTypeDeserializer.java @@ -64,7 +64,7 @@ private IonParser ionParser(JsonParser p) throws JsonParseException { return (IonParser) p; } throw new JsonParseException(p, - "Can only use IonAnnotationTypeDeserializer with IonGenerator"); + "Can only use IonAnnotationTypeDeserializer with IonParser"); } private Object _deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException { diff --git a/ion/src/test/java/com/fasterxml/jackson/dataformat/ion/IonParserTest.java b/ion/src/test/java/com/fasterxml/jackson/dataformat/ion/IonParserTest.java index a79733c6a..fe7aad8e7 100644 --- a/ion/src/test/java/com/fasterxml/jackson/dataformat/ion/IonParserTest.java +++ b/ion/src/test/java/com/fasterxml/jackson/dataformat/ion/IonParserTest.java @@ -13,7 +13,7 @@ */ package com.fasterxml.jackson.dataformat.ion; - + import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.core.JsonToken; @@ -35,35 +35,35 @@ public class IonParserTest @Test public void testGetNumberTypeAndValue() throws Exception { IonSystem ion = IonSystemBuilder.standard().build(); - + Integer intValue = Integer.MAX_VALUE; IonValue ionInt = ion.newInt(intValue); IonParser intParser = new IonFactory().createParser(ionInt); Assert.assertEquals(JsonToken.VALUE_NUMBER_INT, intParser.nextToken()); Assert.assertEquals(JsonParser.NumberType.INT, intParser.getNumberType()); Assert.assertEquals(intValue, intParser.getNumberValue()); - + Long longValue = Long.MAX_VALUE; IonValue ionLong = ion.newInt(longValue); IonParser longParser = new IonFactory().createParser(ionLong); Assert.assertEquals(JsonToken.VALUE_NUMBER_INT, longParser.nextToken()); Assert.assertEquals(JsonParser.NumberType.LONG, longParser.getNumberType()); Assert.assertEquals(longValue, longParser.getNumberValue()); - + BigInteger bigIntValue = new BigInteger(Long.MAX_VALUE + "1"); IonValue ionBigInt = ion.newInt(bigIntValue); IonParser bigIntParser = new IonFactory().createParser(ionBigInt); Assert.assertEquals(JsonToken.VALUE_NUMBER_INT, bigIntParser.nextToken()); Assert.assertEquals(JsonParser.NumberType.BIG_INTEGER, bigIntParser.getNumberType()); Assert.assertEquals(bigIntValue, bigIntParser.getNumberValue()); - + Double decimalValue = Double.MAX_VALUE; IonValue ionDecimal = ion.newDecimal(decimalValue); IonParser decimalParser = new IonFactory().createParser(ionDecimal); Assert.assertEquals(JsonToken.VALUE_NUMBER_FLOAT, decimalParser.nextToken()); Assert.assertEquals(JsonParser.NumberType.BIG_DECIMAL, decimalParser.getNumberType()); Assert.assertTrue(new BigDecimal("" + decimalValue).compareTo((BigDecimal)decimalParser.getNumberValue()) == 0); - + Double floatValue = Double.MAX_VALUE; IonValue ionFloat = ion.newFloat(floatValue); IonParser floatParser = new IonFactory().createParser(ionFloat); @@ -94,4 +94,19 @@ public void testFloatType() throws IOException { final IonParser floatParser = new IonFactory().createParser(reader); Assert.assertEquals(JsonParser.NumberType.DOUBLE, floatParser.getNumberType()); } + + @Test + public void testGetTypeId() throws IOException { + String className = "com.example.Struct"; + final byte[] data = ("'" + className + "'::{ foo: \"bar\" }").getBytes(); + + IonSystem ion = IonSystemBuilder.standard().build(); + IonReader reader = ion.newReader(data, 0, data.length); + IonFactory factory = new IonFactory(); + IonParser parser = factory.createParser(reader); + + parser.nextToken(); // advance to find START_OBJECT + + Assert.assertEquals(className, parser.getTypeId()); + } } diff --git a/ion/src/test/java/com/fasterxml/jackson/dataformat/ion/polymorphism/SerializationAnnotationsTest.java b/ion/src/test/java/com/fasterxml/jackson/dataformat/ion/polymorphism/SerializationAnnotationsTest.java new file mode 100644 index 000000000..b61f6e1b4 --- /dev/null +++ b/ion/src/test/java/com/fasterxml/jackson/dataformat/ion/polymorphism/SerializationAnnotationsTest.java @@ -0,0 +1,123 @@ +package com.fasterxml.jackson.dataformat.ion.polymorphism; + +import com.amazon.ion.IonValue; +import com.amazon.ion.system.IonSystemBuilder; +import com.amazon.ion.util.Equivalence; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.fasterxml.jackson.dataformat.ion.IonGenerator; +import com.fasterxml.jackson.dataformat.ion.IonObjectMapper; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import java.io.IOException; + +public class SerializationAnnotationsTest { + + private static final String SUBCLASS_TYPE_NAME = + SerializationAnnotationsTest.Subclass.class.getTypeName(); + + private static final IonValue SUBCLASS_TYPED_AS_PROPERTY = asIonValue( + "{" + + " '@class':\"" + SUBCLASS_TYPE_NAME + "\"," + + " someString:\"some value\"," + + " anInt:42" + + "}"); + + private static final IonValue SUBCLASS_TYPED_BY_ANNOTATION = asIonValue( + "'" + SUBCLASS_TYPE_NAME + "'::{" + + " someString:\"some value\"," + + " anInt:42" + + "}"); + + private Subclass subclass; + + @Before + public void setup() { + this.subclass = new Subclass("some value", 42); + } + + @Test + public void testNativeTypeIdsEnabledOnWriteByDefault() throws IOException { + IonObjectMapper mapper = new IonObjectMapper(); + IonValue subclassAsIon = mapper.writeValueAsIonValue(subclass); + + assertEqualIonValues(SUBCLASS_TYPED_BY_ANNOTATION, subclassAsIon); + + BaseClass roundTripInstance = mapper.readValue(subclassAsIon, BaseClass.class); + + assertCorrectlyTypedAndFormed(subclass, roundTripInstance); + } + + + @Test + public void testNativeTypeIdsCanBeDisabledOnWrite() throws IOException { + IonObjectMapper mapper = new IonObjectMapper() + .disable(IonGenerator.Feature.USE_NATIVE_TYPE_ID); + + IonValue subclassAsIon = mapper.writeValueAsIonValue(subclass); + assertEqualIonValues(SUBCLASS_TYPED_AS_PROPERTY, subclassAsIon); + + BaseClass roundTripInstance = mapper.readValue(subclassAsIon, BaseClass.class); + + assertCorrectlyTypedAndFormed(subclass, roundTripInstance); + } + + @Test + public void testNativeTypeIdsDisabledStillReadsNativeTypesSuccessfully() throws IOException { + IonObjectMapper writer = new IonObjectMapper(); // native type ids enabled by default + + IonValue subclassAsIon = writer.writeValueAsIonValue(subclass); + + assertEqualIonValues(SUBCLASS_TYPED_BY_ANNOTATION, subclassAsIon); + + IonObjectMapper reader = new IonObjectMapper() + .disable(IonGenerator.Feature.USE_NATIVE_TYPE_ID); + + BaseClass roundTripInstance = reader.readValue(subclassAsIon, BaseClass.class); + + assertCorrectlyTypedAndFormed(subclass, roundTripInstance); + } + + /* + /********************************************************** + /* Helper methods etc. + /********************************************************** + */ + + + @JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, include = JsonTypeInfo.As.PROPERTY, property = "@class") + static public abstract class BaseClass { /* empty */ } + + public static class Subclass extends BaseClass { + public String someString; + public int anInt; + + public Subclass() {}; + public Subclass(String s, int i) { + this.someString = s; + this.anInt = i; + } + } + + private static IonValue asIonValue(final String ionStr) { + return IonSystemBuilder.standard().build().singleValue(ionStr); + } + + private static void assertCorrectlyTypedAndFormed(final Subclass expectedSubclass, final BaseClass actualBaseclass) { + Assert.assertTrue(actualBaseclass instanceof Subclass); + assertEquals(expectedSubclass, (Subclass) actualBaseclass); + } + private static void assertEquals(Subclass expected, Subclass actual) { + Assert.assertEquals(expected.someString, ((Subclass) actual).someString); + Assert.assertEquals(expected.anInt, ((Subclass) actual).anInt); + } + + private static void assertEqualIonValues(IonValue expected, IonValue actual) { + if (!Equivalence.ionEquals(expected, actual)) { + String message = String.format("Expected %s but found %s", + expected.toPrettyString(), actual.toPrettyString()); + throw new AssertionError(message); + } + } +}