Skip to content

Commit c06c030

Browse files
authored
Merge pull request #47 from baharclerode/bah.SimpleAvroFixes
Avro Array/Character/char[] fixes and Test Suite
2 parents b66ab0f + 623b004 commit c06c030

22 files changed

+1602
-32
lines changed

avro/pom.xml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,20 @@ abstractions.
4545
<version>1.1.3</version>
4646
<scope>test</scope>
4747
</dependency>
48+
<!-- A bit of help to reduce boiler-plate in dummy test classes -->
49+
<dependency>
50+
<groupId>org.projectlombok</groupId>
51+
<artifactId>lombok</artifactId>
52+
<version>1.16.12</version>
53+
<scope>test</scope>
54+
</dependency>
55+
<!-- For validating more complex comparisons -->
56+
<dependency>
57+
<groupId>org.assertj</groupId>
58+
<artifactId>assertj-core</artifactId>
59+
<version>3.6.2</version>
60+
<scope>test</scope>
61+
</dependency>
4862

4963
</dependencies>
5064

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package com.fasterxml.jackson.dataformat.avro;
2+
3+
import com.fasterxml.jackson.databind.PropertyName;
4+
import com.fasterxml.jackson.databind.introspect.Annotated;
5+
import com.fasterxml.jackson.databind.introspect.JacksonAnnotationIntrospector;
6+
import org.apache.avro.reflect.AvroIgnore;
7+
import org.apache.avro.reflect.AvroName;
8+
import org.codehaus.jackson.annotate.JsonIgnore;
9+
import org.codehaus.jackson.annotate.JsonProperty;
10+
11+
/**
12+
* Adds support for the following annotations from the Apache Avro implementation:
13+
* <ul>
14+
* <li>{@link AvroIgnore @AvroIgnore} - Alias for {@link JsonIgnore @JsonIgnore(true)}</li>
15+
* <li>{@link AvroName @AvroName("custom Name")} - Alias for {@link JsonProperty @JsonProperty("custom name")}</li>
16+
* </ul>
17+
*/
18+
public class AvroAnnotationIntrospector extends JacksonAnnotationIntrospector {
19+
20+
@Override
21+
protected boolean _isIgnorable(Annotated a) {
22+
return a.getAnnotation(AvroIgnore.class) != null || super._isIgnorable(a);
23+
}
24+
25+
@Override
26+
public PropertyName findNameForSerialization(Annotated a) {
27+
if (a.hasAnnotation(AvroName.class)) {
28+
return PropertyName.construct(a.getAnnotation(AvroName.class).value());
29+
}
30+
return super.findNameForSerialization(a);
31+
}
32+
33+
@Override
34+
public PropertyName findNameForDeserialization(Annotated a) {
35+
if (a.hasAnnotation(AvroName.class)) {
36+
return PropertyName.construct(a.getAnnotation(AvroName.class).value());
37+
}
38+
return super.findNameForDeserialization(a);
39+
}
40+
}

avro/src/main/java/com/fasterxml/jackson/dataformat/avro/AvroModule.java

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,8 @@
44

55
import org.apache.avro.Schema;
66

7-
import com.fasterxml.jackson.core.*;
8-
9-
import com.fasterxml.jackson.databind.*;
7+
import com.fasterxml.jackson.core.JsonGenerator;
8+
import com.fasterxml.jackson.databind.SerializerProvider;
109
import com.fasterxml.jackson.databind.module.SimpleModule;
1110
import com.fasterxml.jackson.databind.ser.std.StdSerializer;
1211

@@ -34,6 +33,12 @@ public AvroModule()
3433
@Deprecated // 08-Mar-2016, tatu: How on earth did this end up here?!?
3534
public Schema schema;
3635

36+
@Override
37+
public void setupModule(SetupContext context) {
38+
super.setupModule(context);
39+
context.insertAnnotationIntrospector(new AvroAnnotationIntrospector());
40+
}
41+
3742
/*
3843
/**********************************************************
3944
/* Helper classes (as long as number is small)

avro/src/main/java/com/fasterxml/jackson/dataformat/avro/deser/AvroReaderFactory.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import org.apache.avro.Schema;
66

77
import com.fasterxml.jackson.dataformat.avro.deser.ScalarDecoder.*;
8+
import com.fasterxml.jackson.dataformat.avro.schema.AvroSchemaHelper;
89

910
/**
1011
* Helper class used for constructing a hierarchic reader for given
@@ -20,6 +21,7 @@ public abstract class AvroReaderFactory
2021
protected final static ScalarDecoder READER_LONG = new LongReader();
2122
protected final static ScalarDecoder READER_NULL = new NullReader();
2223
protected final static ScalarDecoder READER_STRING = new StringReader();
24+
protected final static ScalarDecoder READER_CHAR = new CharReader();
2325

2426
/**
2527
* To resolve cyclic types, need to keep track of resolved named
@@ -65,6 +67,9 @@ public ScalarDecoder createScalarValueDecoder(Schema type)
6567
case FLOAT:
6668
return READER_FLOAT;
6769
case INT:
70+
if (Character.class.getName().equals(type.getProp(AvroSchemaHelper.AVRO_SCHEMA_PROP_CLASS))) {
71+
return READER_CHAR;
72+
}
6873
return READER_INT;
6974
case LONG:
7075
return READER_LONG;

avro/src/main/java/com/fasterxml/jackson/dataformat/avro/deser/ScalarDecoder.java

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,42 @@ public void skipValue(BinaryDecoder decoder) throws IOException {
214214
}
215215
}
216216
}
217+
218+
protected final static class CharReader extends ScalarDecoder {
219+
@Override
220+
public JsonToken decodeValue(AvroParserImpl parser, Decoder decoder) throws IOException {
221+
return parser.setString(Character.toString((char)decoder.readInt()));
222+
}
223+
224+
@Override
225+
protected void skipValue(Decoder decoder) throws IOException {
226+
// ints use variable-length zigzagging; alas, no native skipping
227+
decoder.readInt();
228+
}
229+
230+
@Override
231+
public AvroFieldReader asFieldReader(String name, boolean skipper) {
232+
return new FR(name, skipper);
233+
}
234+
235+
private final static class FR extends AvroFieldReader {
236+
public FR(String name, boolean skipper) {
237+
super(name, skipper);
238+
}
239+
240+
@Override
241+
public JsonToken readValue(
242+
AvroReadContext parent, AvroParserImpl parser, BinaryDecoder decoder
243+
) throws IOException {
244+
return parser.setString(Character.toString((char) decoder.readInt()));
245+
}
246+
247+
@Override
248+
public void skipValue(BinaryDecoder decoder) throws IOException {
249+
decoder.readInt();
250+
}
251+
}
252+
}
217253

218254
protected final static class LongReader extends ScalarDecoder
219255
{

avro/src/main/java/com/fasterxml/jackson/dataformat/avro/schema/ArrayVisitor.java

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,17 @@
11
package com.fasterxml.jackson.dataformat.avro.schema;
22

33
import org.apache.avro.Schema;
4+
import org.apache.avro.Schema.Type;
45

5-
import com.fasterxml.jackson.databind.*;
6-
import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonFormatTypes;
6+
import com.fasterxml.jackson.databind.JavaType;
7+
import com.fasterxml.jackson.databind.JsonMappingException;
8+
import com.fasterxml.jackson.databind.SerializerProvider;
79
import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonArrayFormatVisitor;
10+
import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonFormatTypes;
811
import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonFormatVisitable;
912

13+
import static com.fasterxml.jackson.dataformat.avro.schema.AvroSchemaHelper.AVRO_SCHEMA_PROP_CLASS;
14+
1015
public class ArrayVisitor
1116
extends JsonArrayFormatVisitor.Base
1217
implements SchemaBuilder
@@ -29,7 +34,11 @@ public Schema builtAvroSchema() {
2934
if (_elementSchema == null) {
3035
throw new IllegalStateException("No element schema created for: "+_type);
3136
}
32-
return Schema.createArray(_elementSchema);
37+
Schema schema = Schema.createArray(_elementSchema);
38+
if (_type.isArrayType()) {
39+
schema.addProp(AVRO_SCHEMA_PROP_CLASS, _type.toCanonical());
40+
}
41+
return schema;
3342
}
3443

3544
/*
@@ -50,6 +59,11 @@ public void itemsFormat(JsonFormatVisitable visitable, JavaType type)
5059
@Override
5160
public void itemsFormat(JsonFormatTypes type) throws JsonMappingException
5261
{
53-
_elementSchema = AvroSchemaHelper.simpleSchema(type, _type.getContentType());
62+
// Unlike Jackson, Avro treats character arrays as an array of ints with the java.lang.Character class type.
63+
if (_type.hasRawClass(char[].class)) {
64+
_elementSchema = AvroSchemaHelper.typedSchema(Type.INT, _type.getContentType());
65+
} else {
66+
_elementSchema = AvroSchemaHelper.simpleSchema(type, _type.getContentType());
67+
}
5468
}
5569
}

avro/src/main/java/com/fasterxml/jackson/dataformat/avro/schema/AvroSchemaHelper.java

Lines changed: 83 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,20 @@
11
package com.fasterxml.jackson.dataformat.avro.schema;
22

3-
import java.util.ArrayList;
4-
import java.util.Arrays;
5-
import java.util.List;
3+
import java.io.File;
4+
import java.math.BigDecimal;
5+
import java.math.BigInteger;
6+
import java.net.URI;
7+
import java.net.URL;
8+
import java.util.*;
69

710
import org.apache.avro.Schema;
11+
import org.apache.avro.reflect.Stringable;
812
import org.apache.avro.specific.SpecificData;
913

1014
import com.fasterxml.jackson.core.JsonParser;
1115
import com.fasterxml.jackson.databind.JavaType;
16+
import com.fasterxml.jackson.databind.introspect.AnnotatedClass;
17+
import com.fasterxml.jackson.databind.introspect.AnnotatedConstructor;
1218
import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonFormatTypes;
1319

1420
public abstract class AvroSchemaHelper
@@ -20,7 +26,52 @@ public abstract class AvroSchemaHelper
2026
*
2127
* @since 2.8.7
2228
*/
23-
protected final static String AVRO_SCHEMA_PROP_CLASS = SpecificData.CLASS_PROP;
29+
public static final String AVRO_SCHEMA_PROP_CLASS = SpecificData.CLASS_PROP;
30+
/**
31+
* Constant used by native Avro Schemas for indicating more specific
32+
* physical class of a map key; referenced indirectly to reduce direct
33+
* dependencies to the standard avro library.
34+
*
35+
* @since 2.8.7
36+
*/
37+
public static final String AVRO_SCHEMA_PROP_KEY_CLASS = SpecificData.KEY_CLASS_PROP;
38+
/**
39+
* Default stringable classes
40+
*
41+
* @since 2.8.7
42+
*/
43+
protected static final Set<Class<?>> STRINGABLE_CLASSES = new HashSet<Class<?>>(Arrays.asList(URI.class,
44+
URL.class,
45+
File.class,
46+
BigInteger.class,
47+
BigDecimal.class,
48+
String.class
49+
));
50+
51+
/**
52+
* Checks if a given type is "Stringable", that is one of the default {@link #STRINGABLE_CLASSES}, or is annotated with
53+
* {@link Stringable @Stringable} and has a constructor that takes a single string argument capable of deserializing the output of its
54+
* {@code toString()} method.
55+
*
56+
* @param type
57+
* Type to check if it can be serialized to a Avro string schema
58+
*
59+
* @return {@code true} if it can be stored in a string schema, otherwise {@code false}
60+
*/
61+
public static boolean isStringable(AnnotatedClass type) {
62+
if (STRINGABLE_CLASSES.contains(type.getRawType())) {
63+
return true;
64+
}
65+
if (!type.hasAnnotation(Stringable.class)) {
66+
return false;
67+
}
68+
for (AnnotatedConstructor constructor : type.getConstructors()) {
69+
if (constructor.getParameterCount() == 1 && constructor.getRawParameterType(0) == String.class) {
70+
return true;
71+
}
72+
}
73+
return false;
74+
}
2475

2576
protected static String getNamespace(JavaType type) {
2677
Class<?> cls = type.getRawClass();
@@ -106,7 +157,7 @@ public static Schema numericAvroSchema(JsonParser.NumberType type) {
106157
public static Schema numericAvroSchema(JsonParser.NumberType type, JavaType hint) {
107158
Schema schema = numericAvroSchema(type);
108159
if (hint != null) {
109-
schema.addProp(AVRO_SCHEMA_PROP_CLASS, hint.toCanonical());
160+
schema.addProp(AVRO_SCHEMA_PROP_CLASS, getTypeId(hint));
110161
}
111162
return schema;
112163
}
@@ -118,7 +169,7 @@ public static Schema numericAvroSchema(JsonParser.NumberType type, JavaType hint
118169
*/
119170
public static Schema typedSchema(Schema.Type nativeType, JavaType javaType) {
120171
Schema schema = Schema.create(nativeType);
121-
schema.addProp(AVRO_SCHEMA_PROP_CLASS, javaType.toCanonical());
172+
schema.addProp(AVRO_SCHEMA_PROP_CLASS, getTypeId(javaType));
122173
return schema;
123174
}
124175

@@ -134,4 +185,30 @@ public static Schema anyNumberSchema()
134185
protected static <T> T throwUnsupported() {
135186
throw new UnsupportedOperationException("Format variation not supported");
136187
}
188+
189+
/**
190+
* Returns the Avro type ID for a given type
191+
*/
192+
protected static String getTypeId(JavaType type) {
193+
// Primitives use the name of the wrapper class as their type ID
194+
String canonical = type.toCanonical();
195+
switch (canonical) {
196+
case "byte":
197+
return Byte.class.getName();
198+
case "short":
199+
return Short.class.getName();
200+
case "char":
201+
return Character.class.getName();
202+
case "int":
203+
return Integer.class.getName();
204+
case "long":
205+
return Long.class.getName();
206+
case "float":
207+
return Float.class.getName();
208+
case "double":
209+
return Double.class.getName();
210+
default:
211+
return canonical;
212+
}
213+
}
137214
}

avro/src/main/java/com/fasterxml/jackson/dataformat/avro/schema/MapVisitor.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.fasterxml.jackson.dataformat.avro.schema;
22

33
import org.apache.avro.Schema;
4+
import org.apache.avro.Schema.Type;
45

56
import com.fasterxml.jackson.databind.JavaType;
67
import com.fasterxml.jackson.databind.JsonMappingException;

avro/src/main/java/com/fasterxml/jackson/dataformat/avro/schema/StringVisitor.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
package com.fasterxml.jackson.dataformat.avro.schema;
22

3-
import java.util.*;
3+
import java.util.ArrayList;
4+
import java.util.Set;
45

56
import org.apache.avro.Schema;
67

8+
import com.fasterxml.jackson.core.JsonParser.NumberType;
79
import com.fasterxml.jackson.databind.JavaType;
810
import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonStringFormatVisitor;
911
import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonValueFormat;
12+
import com.fasterxml.jackson.databind.type.TypeFactory;
1013

1114
public class StringVisitor extends JsonStringFormatVisitor.Base
1215
implements SchemaBuilder
@@ -33,6 +36,10 @@ public void enumTypes(Set<String> enums) {
3336

3437
@Override
3538
public Schema builtAvroSchema() {
39+
// Unlike Jackson, Avro treats characters as an int with the java.lang.Character class type.
40+
if (_type.hasRawClass(char.class) || _type.hasRawClass(Character.class)) {
41+
return AvroSchemaHelper.numericAvroSchema(NumberType.INT, TypeFactory.defaultInstance().constructType(Character.class));
42+
}
3643
if (_enums == null) {
3744
return Schema.create(Schema.Type.STRING);
3845
}

0 commit comments

Comments
 (0)