From f3cb333489c38f558b24adf3f4c91f93ae15ee63 Mon Sep 17 00:00:00 2001 From: Bryan Harclerode Date: Thu, 2 Mar 2017 00:33:47 -0600 Subject: [PATCH] [Avro] Add support for @Stringable annotation --- .../avro/AvroAnnotationIntrospector.java | 35 ++- .../jackson/dataformat/avro/AvroParser.java | 16 +- .../avro/schema/AvroSchemaHelper.java | 16 +- .../dataformat/avro/schema/DoubleVisitor.java | 8 +- .../dataformat/avro/schema/MapVisitor.java | 31 +- .../dataformat/avro/schema/StringVisitor.java | 24 +- .../avro/schema/VisitorFormatWrapperImpl.java | 4 +- .../avro/ser/NonBSGenericDatumWriter.java | 34 ++- .../interop/annotations/StringableTest.java | 280 ++++++++++++++++++ 9 files changed, 417 insertions(+), 31 deletions(-) create mode 100644 avro/src/test/java/com/fasterxml/jackson/dataformat/avro/interop/annotations/StringableTest.java 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 c314a84d1..796140072 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 @@ -1,14 +1,20 @@ package com.fasterxml.jackson.dataformat.avro; +import java.io.File; + +import org.apache.avro.reflect.AvroDefault; +import org.apache.avro.reflect.AvroIgnore; +import org.apache.avro.reflect.AvroName; +import org.apache.avro.reflect.Stringable; + import com.fasterxml.jackson.core.Version; import com.fasterxml.jackson.databind.AnnotationIntrospector; import com.fasterxml.jackson.databind.PropertyName; 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 org.apache.avro.reflect.AvroDefault; -import org.apache.avro.reflect.AvroIgnore; -import org.apache.avro.reflect.AvroName; +import com.fasterxml.jackson.databind.ser.std.ToStringSerializer; /** * Adds support for the following annotations from the Apache Avro implementation: @@ -18,6 +24,8 @@ *
  • {@link AvroDefault @AvroDefault("default value")} - Alias for JsonProperty.defaultValue, to * define default value for generated Schemas *
  • + *
  • {@link Stringable @Stringable} - Alias for JsonCreator on the constructor and JsonValue on + * the {@link #toString()} method.
  • * * * @since 2.9 @@ -57,4 +65,23 @@ protected PropertyName _findName(Annotated a) AvroName ann = _findAnnotation(a, AvroName.class); return (ann == null) ? null : PropertyName.construct(ann.value()); } + + @Override + public boolean hasCreatorAnnotation(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; + return constructor != null && parentClass != null && parentClass.hasAnnotation(Stringable.class) + && constructor.getParameterCount() == 1 && String.class.equals(constructor.getRawParameterType(0)); + } + + @Override + public Object findSerializer(Annotated a) { + if (a instanceof AnnotatedClass && a.hasAnnotation(Stringable.class) || a.getRawType() == File.class) { + return ToStringSerializer.class; + } + return null; + } } 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 5368b25a0..79a772a78 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; @@ -281,6 +284,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/schema/AvroSchemaHelper.java b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/schema/AvroSchemaHelper.java index d4321e0a7..8d6d7e9d8 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 @@ -145,14 +145,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); @@ -211,6 +212,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/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 9db1559c0..25693eed0 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 @@ -9,6 +9,7 @@ import org.apache.avro.Schema.Type; 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; @@ -58,11 +59,17 @@ 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... @@ -71,8 +78,29 @@ public int resolveUnion(Schema union, Object datum) { @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()); + // Cocerce numerical types, like BigDecimal -> double and BigInteger -> long + 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; + } + } + // Handle stringable classes + if (schema.getType() == Type.STRING && datum != null && datum.getClass().getAnnotation(Stringable.class) != null) { + super.write(schema, datum.toString(), out); return; } if (datum instanceof String) { 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..1d1a1b9fd --- /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.math.BigDecimal; +import java.math.BigInteger; +import java.net.MalformedURLException; +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() { + // 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() { + // 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() { + BigDecimal original = new BigDecimal("0.7193789624775822761924891294139324921"); + // + BigDecimal result = roundTrip(original); + // + assertThat(result).isEqualTo(original); + } + + @Test + public void testBigDecimalArray() { + 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() { + 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() { + BigInteger original = new BigInteger("1236549816934246813682843621431493681279364198"); + // + BigInteger result = roundTrip(original); + // + assertThat(result).isEqualTo(original); + } + + @Test + public void testBigIntegerArray() { + 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() { + 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() { + CustomStringableKey original = new CustomStringableKey("one"); + // + CustomStringableKey result = roundTrip(original); + // + assertThat(result).isEqualTo(original); + } + + @Test + public void testCustomStringableArray() { + 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() { + 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() { + File original = new File("/a/cool/file"); + // + File result = roundTrip(original); + // + assertThat(result).isEqualTo(original); + } + + @Test + public void testFileArray() { + 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() { + 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 { + URI original = new URI("https://github.com"); + // + URI result = roundTrip(original); + // + assertThat(result).isEqualTo(original); + } + + @Test + public void testURIArray() throws URISyntaxException { + 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 { + 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 MalformedURLException { + URL original = new URL("https://github.com"); + // + URL result = roundTrip(original); + // + assertThat(result).isEqualTo(original); + } + + @Test + public void testURLArray() throws MalformedURLException { + 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 MalformedURLException { + 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); + } +}