diff --git a/avro/pom.xml b/avro/pom.xml index d6fe2fb67..26459e796 100644 --- a/avro/pom.xml +++ b/avro/pom.xml @@ -21,7 +21,12 @@ abstractions. - + + + com.fasterxml.jackson.core + jackson-annotations + + com.fasterxml.jackson.core jackson-databind @@ -32,14 +37,7 @@ abstractions. 1.8.1 - - - com.fasterxml.jackson.core - jackson-annotations - test - - - + ch.qos.logback logback-classic diff --git a/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/AvroAnnotationIntrospector.java b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/AvroAnnotationIntrospector.java index 19350dba0..1203d0410 100644 --- a/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/AvroAnnotationIntrospector.java +++ b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/AvroAnnotationIntrospector.java @@ -5,12 +5,16 @@ import org.apache.avro.reflect.*; +import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.core.Version; import com.fasterxml.jackson.databind.AnnotationIntrospector; import com.fasterxml.jackson.databind.PropertyName; +import com.fasterxml.jackson.databind.cfg.MapperConfig; import com.fasterxml.jackson.databind.introspect.Annotated; +import com.fasterxml.jackson.databind.introspect.AnnotatedClass; +import com.fasterxml.jackson.databind.introspect.AnnotatedConstructor; import com.fasterxml.jackson.databind.introspect.AnnotatedMember; - +import com.fasterxml.jackson.databind.ser.std.ToStringSerializer; /** * Adds support for the following annotations from the Apache Avro implementation: * * * @since 2.9 @@ -76,4 +82,26 @@ public Boolean hasRequiredMarker(AnnotatedMember m) { } return null; } + + @Override + public JsonCreator.Mode findCreatorAnnotation(MapperConfig config, Annotated a) { + AnnotatedConstructor constructor = a instanceof AnnotatedConstructor ? (AnnotatedConstructor) a : null; + AnnotatedClass parentClass = + a instanceof AnnotatedConstructor && ((AnnotatedConstructor) a).getTypeContext() instanceof AnnotatedClass + ? (AnnotatedClass) ((AnnotatedConstructor) a).getTypeContext() + : null; + if (constructor != null && parentClass != null && parentClass.hasAnnotation(Stringable.class) + && constructor.getParameterCount() == 1 && String.class.equals(constructor.getRawParameterType(0))) { + return JsonCreator.Mode.DELEGATING; + } + return null; + } + + @Override + public Object findSerializer(Annotated a) { + if (a instanceof AnnotatedClass && a.hasAnnotation(Stringable.class)) { + return ToStringSerializer.class; + } + return null; + } } diff --git a/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/AvroModule.java b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/AvroModule.java index 6701bada2..1dfec9755 100644 --- a/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/AvroModule.java +++ b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/AvroModule.java @@ -1,5 +1,6 @@ package com.fasterxml.jackson.dataformat.avro; +import java.io.File; import java.io.IOException; import org.apache.avro.Schema; @@ -8,6 +9,7 @@ import com.fasterxml.jackson.databind.SerializerProvider; import com.fasterxml.jackson.databind.module.SimpleModule; import com.fasterxml.jackson.databind.ser.std.StdSerializer; +import com.fasterxml.jackson.databind.ser.std.ToStringSerializer; /** * Module that adds support for handling datatypes specific to the standard @@ -31,6 +33,7 @@ public AvroModule() { super(PackageVersion.VERSION); addSerializer(new SchemaSerializer()); + addSerializer(File.class, new ToStringSerializer(File.class)); // 08-Mar-2016, tatu: to fix [dataformat-avro#35], need to prune 'schema' property: setSerializerModifier(new AvroSerializerModifier()); } diff --git a/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/AvroParser.java b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/AvroParser.java index c19e987cd..34f6a3a38 100644 --- a/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/AvroParser.java +++ b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/AvroParser.java @@ -1,6 +1,9 @@ package com.fasterxml.jackson.dataformat.avro; -import java.io.*; +import java.io.IOException; +import java.io.InputStream; +import java.io.Writer; +import java.math.BigDecimal; import com.fasterxml.jackson.core.*; import com.fasterxml.jackson.core.base.ParserBase; @@ -255,6 +258,17 @@ public JsonLocation getCurrentLocation() @Override public abstract JsonToken nextToken() throws IOException; + @Override + protected void convertNumberToBigDecimal() throws IOException { + // ParserBase uses _textValue instead of _numberDouble for some reason when NR_DOUBLE is set, but _textValue is not set by setNumber() + // Catch and use _numberDouble instead + if ((_numTypesValid & NR_DOUBLE) != 0 && _textValue == null) { + _numberBigDecimal = BigDecimal.valueOf(_numberDouble); + return; + } + super.convertNumberToBigDecimal(); + } + /* /********************************************************** /* String value handling diff --git a/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/deser/AvroReaderFactory.java b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/deser/AvroReaderFactory.java index cf4673f33..7cad82161 100644 --- a/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/deser/AvroReaderFactory.java +++ b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/deser/AvroReaderFactory.java @@ -3,6 +3,7 @@ import java.util.*; import org.apache.avro.Schema; +import org.apache.avro.util.internal.JacksonUtils; import com.fasterxml.jackson.dataformat.avro.deser.ScalarDecoder.*; import com.fasterxml.jackson.dataformat.avro.schema.AvroSchemaHelper; @@ -329,8 +330,8 @@ protected AvroStructureReader createRecordReader(Schema writerSchema, Schema rea // Any defaults to consider? if (!defaultFields.isEmpty()) { for (Schema.Field defaultField : defaultFields) { - AvroFieldReader fr = AvroFieldDefaulters.createDefaulter(defaultField.name(), - defaultField.defaultValue()); + AvroFieldReader fr = + AvroFieldDefaulters.createDefaulter(defaultField.name(), JacksonUtils.toJsonNode(defaultField.defaultVal())); if (fr == null) { throw new IllegalArgumentException("Unsupported default type: "+defaultField.schema().getType()); } diff --git a/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/schema/AvroSchemaHelper.java b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/schema/AvroSchemaHelper.java index f64ec1cb0..800e98c92 100644 --- a/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/schema/AvroSchemaHelper.java +++ b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/schema/AvroSchemaHelper.java @@ -146,14 +146,15 @@ public static Schema numericAvroSchema(JsonParser.NumberType type) { switch (type) { case INT: return Schema.create(Schema.Type.INT); - case BIG_INTEGER: case LONG: return Schema.create(Schema.Type.LONG); case FLOAT: return Schema.create(Schema.Type.FLOAT); - case BIG_DECIMAL: case DOUBLE: return Schema.create(Schema.Type.DOUBLE); + case BIG_INTEGER: + case BIG_DECIMAL: + return Schema.create(Schema.Type.STRING); default: } throw new IllegalStateException("Unrecognized number type: "+type); @@ -212,6 +213,17 @@ public static Schema parseJsonSchema(String json) { return parser.parse(json); } + /** + * Constructs a new enum schema + * + * @param bean Enum type to use for name / description / namespace + * @param values List of enum names + * @return An {@link org.apache.avro.Schema.Type#ENUM ENUM} schema. + */ + public static Schema createEnumSchema(BeanDescription bean, List values) { + return Schema.createEnum(getName(bean.getType()), bean.findClassDescription(), getNamespace(bean.getType()), values); + } + /** * Returns the Avro type ID for a given type */ diff --git a/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/schema/DoubleVisitor.java b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/schema/DoubleVisitor.java index 8374a72bd..3daec32dc 100644 --- a/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/schema/DoubleVisitor.java +++ b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/schema/DoubleVisitor.java @@ -3,15 +3,19 @@ import org.apache.avro.Schema; import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.JavaType; import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonNumberFormatVisitor; public class DoubleVisitor extends JsonNumberFormatVisitor.Base implements SchemaBuilder { + protected final JavaType _hint; protected JsonParser.NumberType _type; - public DoubleVisitor() { } + public DoubleVisitor(JavaType typeHint) { + _hint = typeHint; + } @Override public void numberType(JsonParser.NumberType type) { @@ -25,6 +29,6 @@ public Schema builtAvroSchema() { // would require union most likely return AvroSchemaHelper.anyNumberSchema(); } - return AvroSchemaHelper.numericAvroSchema(_type); + return AvroSchemaHelper.numericAvroSchema(_type, _hint); } } diff --git a/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/schema/MapVisitor.java b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/schema/MapVisitor.java index e7528f0c1..2cc3a5ecb 100644 --- a/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/schema/MapVisitor.java +++ b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/schema/MapVisitor.java @@ -12,11 +12,13 @@ public class MapVisitor extends JsonMapFormatVisitor.Base implements SchemaBuilder { protected final JavaType _type; - + protected final DefinedSchemas _schemas; protected Schema _valueSchema; - + + protected JavaType _keyType; + public MapVisitor(SerializerProvider p, JavaType type, DefinedSchemas schemas) { super(p); @@ -30,7 +32,23 @@ public Schema builtAvroSchema() { if (_valueSchema == null) { throw new IllegalStateException("Missing value type for "+_type); } - return Schema.createMap(_valueSchema); + + Schema schema = Schema.createMap(_valueSchema); + + // add the key type if there is one + if (_keyType != null && AvroSchemaHelper.isStringable(getProvider() + .getConfig() + .introspectClassAnnotations(_keyType) + .getClassInfo())) { + schema.addProp(AvroSchemaHelper.AVRO_SCHEMA_PROP_KEY_CLASS, AvroSchemaHelper.getTypeId(_keyType)); + } else if (_keyType != null && !_keyType.isEnumType()) { + // Avro handles non-stringable keys by converting the map to an array of key/value records + // TODO add support for these in the schema, and custom serializers / deserializers to handle map restructuring + throw new UnsupportedOperationException( + "Key " + _keyType + " is not stringable and non-stringable map keys are not supported yet."); + } + + return schema; } /* @@ -43,12 +61,7 @@ public Schema builtAvroSchema() { public void keyFormat(JsonFormatVisitable handler, JavaType keyType) throws JsonMappingException { - /* We actually don't care here, since Avro only has String-keyed - * Maps like JSON: meaning that anything Jackson can regularly - * serialize must convert to Strings anyway. - * If we do find problem cases, we can start verifying them here, - * but for now assume it all "just works". - */ + _keyType = keyType; } @Override diff --git a/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/schema/RecordVisitor.java b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/schema/RecordVisitor.java index 434631717..ac4ff1cd5 100644 --- a/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/schema/RecordVisitor.java +++ b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/schema/RecordVisitor.java @@ -81,10 +81,6 @@ public void property(BeanProperty writer) throws JsonMappingException return; } _fields.add(schemaFieldForWriter(writer, false)); - /* - Schema schema = schemaForWriter(writer); - _fields.add(_field(writer, schema)); - */ } @Override @@ -274,7 +270,7 @@ protected Schema reorderUnionToMatchDefaultType(Schema schema, JsonNode defaultV } if (matchingIndex != null) { types.add(0, types.remove((int)matchingIndex)); - Map jsonProps = schema.getJsonProps(); + Map jsonProps = schema.getObjectProps(); schema = Schema.createUnion(types); // copy any properties over for (String property : jsonProps.keySet()) { diff --git a/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/schema/StringVisitor.java b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/schema/StringVisitor.java index b89136932..cfb88439d 100644 --- a/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/schema/StringVisitor.java +++ b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/schema/StringVisitor.java @@ -6,7 +6,9 @@ import org.apache.avro.Schema; import com.fasterxml.jackson.core.JsonParser.NumberType; +import com.fasterxml.jackson.databind.BeanDescription; import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.SerializerProvider; import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonStringFormatVisitor; import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonValueFormat; import com.fasterxml.jackson.databind.type.TypeFactory; @@ -14,14 +16,16 @@ public class StringVisitor extends JsonStringFormatVisitor.Base implements SchemaBuilder { + protected final SerializerProvider _provider; protected final JavaType _type; protected final DefinedSchemas _schemas; protected Set _enums; - public StringVisitor(DefinedSchemas schemas, JavaType t) { + public StringVisitor(SerializerProvider provider, DefinedSchemas schemas, JavaType t) { _schemas = schemas; _type = t; + _provider = provider; } @Override @@ -40,13 +44,17 @@ public Schema builtAvroSchema() { if (_type.hasRawClass(char.class) || _type.hasRawClass(Character.class)) { return AvroSchemaHelper.numericAvroSchema(NumberType.INT, TypeFactory.defaultInstance().constructType(Character.class)); } - if (_enums == null) { - return Schema.create(Schema.Type.STRING); + BeanDescription bean = _provider.getConfig().introspectClassAnnotations(_type); + if (_enums != null) { + Schema s = AvroSchemaHelper.createEnumSchema(bean, new ArrayList<>(_enums)); + _schemas.addSchema(_type, s); + return s; } - Schema s = Schema.createEnum(AvroSchemaHelper.getName(_type), "", - AvroSchemaHelper.getNamespace(_type), - new ArrayList(_enums)); - _schemas.addSchema(_type, s); - return s; + Schema schema = Schema.create(Schema.Type.STRING); + // Stringable classes need to include the type + if (AvroSchemaHelper.isStringable(bean.getClassInfo())) { + schema.addProp(AvroSchemaHelper.AVRO_SCHEMA_PROP_CLASS, AvroSchemaHelper.getTypeId(_type)); + } + return schema; } } diff --git a/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/schema/VisitorFormatWrapperImpl.java b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/schema/VisitorFormatWrapperImpl.java index a423d256a..4c8aac1e1 100644 --- a/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/schema/VisitorFormatWrapperImpl.java +++ b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/schema/VisitorFormatWrapperImpl.java @@ -118,14 +118,14 @@ public JsonStringFormatVisitor expectStringFormat(JavaType type) _valueSchema = s; return null; } - StringVisitor v = new StringVisitor(_schemas, type); + StringVisitor v = new StringVisitor(_provider, _schemas, type); _builder = v; return v; } @Override public JsonNumberFormatVisitor expectNumberFormat(JavaType convertedType) { - DoubleVisitor v = new DoubleVisitor(); + DoubleVisitor v = new DoubleVisitor(convertedType); _builder = v; return v; } diff --git a/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/ser/NonBSGenericDatumWriter.java b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/ser/NonBSGenericDatumWriter.java index 68e7effeb..c064d643f 100644 --- a/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/ser/NonBSGenericDatumWriter.java +++ b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/ser/NonBSGenericDatumWriter.java @@ -10,6 +10,7 @@ import org.apache.avro.generic.GenericData; import org.apache.avro.generic.GenericDatumWriter; import org.apache.avro.io.Encoder; +import org.apache.avro.reflect.Stringable; import com.fasterxml.jackson.dataformat.avro.schema.AvroSchemaHelper; @@ -61,44 +62,54 @@ public int resolveUnion(Schema union, Object datum) { } } } else if (datum instanceof BigDecimal) { + int subOptimal = -1; for (int i = 0, len = schemas.size(); i < len; i++) { - if (schemas.get(i).getType() == Type.DOUBLE) { + if (schemas.get(i).getType() == Type.STRING) { return i; + } else if (schemas.get(i).getType() == Type.DOUBLE) { + subOptimal = i; } } + if (subOptimal > -1) { + return subOptimal; + } } // otherwise just default to base impl, stupid as it is... return super.resolveUnion(union, datum); } - /* - <<<<<<< HEAD - @Override - protected void write(Schema schema, Object datum, Encoder out) throws IOException { - if ((schema.getType() == Type.DOUBLE) && datum instanceof BigDecimal) { - out.writeDouble(((BigDecimal)datum).doubleValue()); - } else if (schema.getType() == Type.ENUM) { - super.write(schema, GENERIC_DATA.createEnum(datum.toString(), schema), out); - } else { - super.write(schema, datum, out); - } - } - */ @Override protected void write(Schema schema, Object datum, Encoder out) throws IOException { - Schema.Type t = schema.getType(); - if (t == Type.DOUBLE) { - if (datum instanceof BigDecimal) { - out.writeDouble(((BigDecimal)datum).doubleValue()); + if (datum instanceof Number) { + switch (schema.getType()) { + case LONG: + super.write(schema, (((Number) datum).longValue()), out); + return; + case INT: + super.write(schema, (((Number) datum).intValue()), out); + return; + case FLOAT: + super.write(schema, (((Number) datum).floatValue()), out); + return; + case DOUBLE: + super.write(schema, (((Number) datum).doubleValue()), out); + return; + case STRING: + super.write(schema, datum.toString(), out); return; } - } else if (t == Type.ENUM) { + } + // Handle stringable classes + if (schema.getType() == Type.STRING && datum != null && datum.getClass().getAnnotation(Stringable.class) != null) { + super.write(schema, datum.toString(), out); + return; + } else if (schema.getType() == Type.ENUM) { super.write(schema, GENERIC_DATA.createEnum(datum.toString(), schema), out); return; } else if (datum instanceof String) { String str = (String) datum; final int len = str.length(); - if (t == Type.ARRAY && schema.getElementType().getType() == Type.INT) { + if (schema.getType() == Type.ARRAY && schema.getElementType().getType() == Type.INT) { ArrayList chars = new ArrayList<>(len); for (int i = 0; i < len; ++i) { chars.add((int) str.charAt(i)); @@ -106,7 +117,7 @@ protected void write(Schema schema, Object datum, Encoder out) throws IOExceptio super.write(schema, chars, out); return; } - if (len == 1 && t == Type.INT) { + if (len == 1 && schema.getType() == Type.INT) { super.write(schema, (int) str.charAt(0), out); return; } diff --git a/avro/src/test/java/com/fasterxml/jackson/dataformat/avro/interop/annotations/AvroDefaultTest.java b/avro/src/test/java/com/fasterxml/jackson/dataformat/avro/interop/annotations/AvroDefaultTest.java index bc2e3c56b..575d96101 100644 --- a/avro/src/test/java/com/fasterxml/jackson/dataformat/avro/interop/annotations/AvroDefaultTest.java +++ b/avro/src/test/java/com/fasterxml/jackson/dataformat/avro/interop/annotations/AvroDefaultTest.java @@ -1,11 +1,11 @@ package com.fasterxml.jackson.dataformat.avro.interop.annotations; -import com.fasterxml.jackson.dataformat.avro.interop.ApacheAvroInteropUtil; - import org.apache.avro.Schema; import org.apache.avro.reflect.AvroDefault; import org.junit.Test; +import com.fasterxml.jackson.dataformat.avro.interop.ApacheAvroInteropUtil; + import static org.assertj.core.api.Assertions.assertThat; public class AvroDefaultTest { @@ -23,7 +23,7 @@ public void testUnionBooleanDefault() { Schema apacheSchema = ApacheAvroInteropUtil.getApacheSchema(RecordWithDefaults.class); Schema jacksonSchema = ApacheAvroInteropUtil.getJacksonSchema(RecordWithDefaults.class); // - assertThat(jacksonSchema.getField("booleanField").defaultValue()).isEqualTo(apacheSchema.getField("booleanField").defaultValue()); + assertThat(jacksonSchema.getField("booleanField").defaultVal()).isEqualTo(apacheSchema.getField("booleanField").defaultVal()); } @Test @@ -31,7 +31,7 @@ public void testUnionIntegerDefault() { Schema apacheSchema = ApacheAvroInteropUtil.getApacheSchema(RecordWithDefaults.class); Schema jacksonSchema = ApacheAvroInteropUtil.getJacksonSchema(RecordWithDefaults.class); // - assertThat(jacksonSchema.getField("intField").defaultValue()).isEqualTo(apacheSchema.getField("intField").defaultValue()); + assertThat(jacksonSchema.getField("intField").defaultVal()).isEqualTo(apacheSchema.getField("intField").defaultVal()); } @Test @@ -39,6 +39,6 @@ public void testUnionStringDefault() { Schema apacheSchema = ApacheAvroInteropUtil.getApacheSchema(RecordWithDefaults.class); Schema jacksonSchema = ApacheAvroInteropUtil.getJacksonSchema(RecordWithDefaults.class); // - assertThat(jacksonSchema.getField("stringField").defaultValue()).isEqualTo(apacheSchema.getField("stringField").defaultValue()); + assertThat(jacksonSchema.getField("stringField").defaultVal()).isEqualTo(apacheSchema.getField("stringField").defaultVal()); } } diff --git a/avro/src/test/java/com/fasterxml/jackson/dataformat/avro/interop/annotations/StringableTest.java b/avro/src/test/java/com/fasterxml/jackson/dataformat/avro/interop/annotations/StringableTest.java new file mode 100644 index 000000000..35f6aed22 --- /dev/null +++ b/avro/src/test/java/com/fasterxml/jackson/dataformat/avro/interop/annotations/StringableTest.java @@ -0,0 +1,280 @@ +package com.fasterxml.jackson.dataformat.avro.interop.annotations; + +import java.io.File; +import java.io.IOException; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.apache.avro.reflect.AvroSchema; +import org.apache.avro.reflect.Stringable; +import org.apache.avro.specific.SpecificData; +import org.junit.Test; + +import com.fasterxml.jackson.dataformat.avro.interop.ApacheAvroInteropUtil; +import com.fasterxml.jackson.dataformat.avro.interop.InteropTestBase; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.Assume.assumeTrue; + +/** + * Tests support for using classes marked {@link Stringable @Stringable} as map keys. These classes must have a constructor which accepts a + * single string as an argument, and their {@link #toString()} must return a serialized version of the object that can be passed back into + * the constructor to recreate it. In addition, Avro considers the following classes {@link SpecificData#stringableClasses stringable by + * default}: + *
    + *
  • {@link File}
  • + *
  • {@link BigInteger}
  • + *
  • {@link BigDecimal}
  • + *
  • {@link URI}
  • + *
  • {@link URL}
  • + *
+ */ +public class StringableTest extends InteropTestBase { + @Stringable + @Data + public static class CustomStringableKey { + private final String test; + + public CustomStringableKey(String test) { + this.test = test; + } + + @Override + public String toString() { + return test; + } + } + + @Data + @NoArgsConstructor + @AllArgsConstructor + static class BigNumberWrapper { + @AvroSchema("\"double\"") + private BigDecimal bigDecimal; + @AvroSchema("\"long\"") + private BigInteger bigInteger; + } + + @Test + public void testBigDecimalWithDoubleSchema() throws IOException { + // Apache impl can't do coercion + assumeTrue(serializeFunctor != ApacheAvroInteropUtil.apacheSerializer); + assumeTrue(deserializeFunctor != ApacheAvroInteropUtil.apacheDeserializer); + + double value = 0.32198154657; + BigNumberWrapper wrapper = new BigNumberWrapper(new BigDecimal(value), BigInteger.ONE); + // + BigNumberWrapper result = roundTrip(wrapper); + // + assertThat(result.bigDecimal.doubleValue()).isEqualTo(value); + } + + @Test + public void testBigIntegerWithDoubleSchema() throws IOException { + // Apache impl can't do coercion + assumeTrue(serializeFunctor != ApacheAvroInteropUtil.apacheSerializer); + assumeTrue(deserializeFunctor != ApacheAvroInteropUtil.apacheDeserializer); + + long value = 948241716844286248L; + BigNumberWrapper wrapper = new BigNumberWrapper(BigDecimal.ZERO, BigInteger.valueOf(value)); + // + BigNumberWrapper result = roundTrip(wrapper); + // + assertThat(result.bigInteger.longValue()).isEqualTo(value); + } + + @Test + public void testBigDecimal() throws IOException { + BigDecimal original = new BigDecimal("0.7193789624775822761924891294139324921"); + // + BigDecimal result = roundTrip(original); + // + assertThat(result).isEqualTo(original); + } + + @Test + public void testBigDecimalArray() throws IOException { + ArrayList array = new ArrayList<>(); + array.add(new BigDecimal("32165498701061140.034501381101601018405251061")); + array.add(new BigDecimal("0.7193789624775822761924891294139324921")); + // + ArrayList result = roundTrip(type(ArrayList.class, BigDecimal.class), array); + // + assertThat(result).isEqualTo(array); + } + + @Test + public void testBigDecimalKeys() throws IOException { + Map map = new HashMap<>(); + map.put(new BigDecimal("32165498701061140.034501381101601018405251061"), "one"); + map.put(new BigDecimal("0.7193789624775822761924891294139324921"), "two"); + // + Map result = roundTrip(type(Map.class, BigDecimal.class, String.class), map); + // + assertThat(result).isEqualTo(map); + } + + @Test + public void testBigInteger() throws IOException { + BigInteger original = new BigInteger("1236549816934246813682843621431493681279364198"); + // + BigInteger result = roundTrip(original); + // + assertThat(result).isEqualTo(original); + } + + @Test + public void testBigIntegerArray() throws IOException { + ArrayList array = new ArrayList<>(); + array.add(new BigInteger("32165498701061140034501381101601018405251061")); + array.add(new BigInteger("7193789624775822761924891294139324921")); + // + ArrayList result = roundTrip(type(ArrayList.class, BigInteger.class), array); + // + assertThat(result).isEqualTo(array); + } + + @Test + public void testBigIntegerKeys() throws IOException { + Map map = new HashMap<>(); + map.put(new BigInteger("32165498701061140034501381101601018405251061"), "one"); + map.put(new BigInteger("7193789624775822761924891294139324921"), "two"); + // + Map result = roundTrip(type(Map.class, BigInteger.class, String.class), map); + // + assertThat(result).isEqualTo(map); + } + + @Test + public void testCustomStringable() throws IOException { + CustomStringableKey original = new CustomStringableKey("one"); + // + CustomStringableKey result = roundTrip(original); + // + assertThat(result).isEqualTo(original); + } + + @Test + public void testCustomStringableArray() throws IOException { + ArrayList array = new ArrayList<>(); + array.add(new CustomStringableKey("one")); + array.add(new CustomStringableKey("two")); + // + ArrayList result = roundTrip(type(ArrayList.class, CustomStringableKey.class), array); + // + assertThat(result).isEqualTo(array); + } + + @Test + public void testCustomStringableKeyWithScalarValue() throws IOException { + Map object = new HashMap<>(); + object.put(new CustomStringableKey("one"), "two"); + object.put(new CustomStringableKey("three"), "four"); + // + Map result = roundTrip(type(Map.class, CustomStringableKey.class, String.class), object); + // + assertThat(result).isEqualTo(object); + } + + @Test + public void testFile() throws IOException { + File original = new File("/a/cool/file"); + // + File result = roundTrip(original); + // + assertThat(result).isEqualTo(original); + } + + @Test + public void testFileArray() throws IOException { + ArrayList array = new ArrayList<>(); + array.add(new File("/some/path")); + array.add(new File("/some/other/path")); + // + ArrayList result = roundTrip(type(ArrayList.class, File.class), array); + // + assertThat(result).isEqualTo(array); + } + + @Test + public void testFileKeys() throws IOException { + Map object = new HashMap<>(); + object.put(new File("/some/path"), "one"); + object.put(new File("/some/other/path"), "two"); + // + Map result = roundTrip(type(Map.class, File.class, String.class), object); + // + assertThat(result).isEqualTo(object); + } + + @Test + public void testURI() throws URISyntaxException, IOException { + URI original = new URI("https://github.com"); + // + URI result = roundTrip(original); + // + assertThat(result).isEqualTo(original); + } + + @Test + public void testURIArray() throws URISyntaxException, IOException { + ArrayList array = new ArrayList<>(); + array.add(new URI("http://fasterxml.com")); + array.add(new URI("https://github.com")); + // + ArrayList result = roundTrip(type(ArrayList.class, URI.class), array); + // + assertThat(result).isEqualTo(array); + } + + @Test + public void testURIKeys() throws URISyntaxException, IOException { + Map object = new HashMap<>(); + object.put(new URI("http://fasterxml.com"), "one"); + object.put(new URI("https://github.com"), "two"); + // + Map result = roundTrip(type(Map.class, URI.class, String.class), object); + // + assertThat(result).isEqualTo(object); + } + + @Test + public void testURL() throws IOException { + URL original = new URL("https://github.com"); + // + URL result = roundTrip(original); + // + assertThat(result).isEqualTo(original); + } + + @Test + public void testURLArray() throws IOException { + ArrayList array = new ArrayList<>(); + array.add(new URL("http://fasterxml.com")); + array.add(new URL("https://github.com")); + // + ArrayList result = roundTrip(type(ArrayList.class, URL.class), array); + // + assertThat(result).isEqualTo(array); + } + + @Test + public void testURLKeys() throws IOException { + Map map = new HashMap<>(); + map.put(new URL("http://fasterxml.com"), "one"); + map.put(new URL("https://github.com"), "two"); + // + Map result = roundTrip(type(Map.class, URL.class, String.class), map); + // + assertThat(result).isEqualTo(map); + } +}