Skip to content

Commit 4afec93

Browse files
committed
[Avro] Add support for @Stringable annotation
1 parent 8321c68 commit 4afec93

File tree

9 files changed

+419
-41
lines changed

9 files changed

+419
-41
lines changed

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

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,15 @@
44
import com.fasterxml.jackson.databind.AnnotationIntrospector;
55
import com.fasterxml.jackson.databind.PropertyName;
66
import com.fasterxml.jackson.databind.introspect.Annotated;
7+
import com.fasterxml.jackson.databind.introspect.AnnotatedClass;
8+
import com.fasterxml.jackson.databind.introspect.AnnotatedConstructor;
79
import com.fasterxml.jackson.databind.introspect.AnnotatedMember;
10+
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
811

912
import org.apache.avro.reflect.AvroDefault;
1013
import org.apache.avro.reflect.AvroIgnore;
1114
import org.apache.avro.reflect.AvroName;
15+
import org.apache.avro.reflect.Stringable;
1216

1317
/**
1418
* Adds support for the following annotations from the Apache Avro implementation:
@@ -18,6 +22,8 @@
1822
* <li>{@link AvroDefault @AvroDefault("default value")} - Alias for <code>JsonProperty.defaultValue</code>, to
1923
* define default value for generated Schemas
2024
* </li>
25+
* <li>{@link Stringable @Stringable} - Alias for <code>JsonCreator</code> on the constructor and <code>JsonValue</code> on
26+
* the {@link #toString()} method. </li>
2127
* </ul>
2228
*
2329
* @since 2.9
@@ -57,4 +63,23 @@ protected PropertyName _findName(Annotated a)
5763
AvroName ann = _findAnnotation(a, AvroName.class);
5864
return (ann == null) ? null : PropertyName.construct(ann.value());
5965
}
66+
67+
@Override
68+
public boolean hasCreatorAnnotation(Annotated a) {
69+
AnnotatedConstructor constructor = a instanceof AnnotatedConstructor ? (AnnotatedConstructor) a : null;
70+
AnnotatedClass parentClass =
71+
a instanceof AnnotatedConstructor && ((AnnotatedConstructor) a).getTypeContext() instanceof AnnotatedClass
72+
? (AnnotatedClass) ((AnnotatedConstructor) a).getTypeContext()
73+
: null;
74+
return constructor != null && parentClass != null && parentClass.hasAnnotation(Stringable.class)
75+
&& constructor.getParameterCount() == 1 && String.class.equals(constructor.getRawParameterType(0));
76+
}
77+
78+
@Override
79+
public Object findSerializer(Annotated a) {
80+
if (a instanceof AnnotatedClass && a.hasAnnotation(Stringable.class)) {
81+
return ToStringSerializer.class;
82+
}
83+
return null;
84+
}
6085
}

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

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

3-
import java.io.*;
3+
import java.io.IOException;
4+
import java.io.InputStream;
5+
import java.io.Writer;
6+
import java.math.BigDecimal;
47

58
import com.fasterxml.jackson.core.*;
69
import com.fasterxml.jackson.core.base.ParserBase;
@@ -281,6 +284,17 @@ public JsonLocation getCurrentLocation()
281284
@Override
282285
public abstract JsonToken nextToken() throws IOException;
283286

287+
@Override
288+
protected void convertNumberToBigDecimal() throws IOException {
289+
// ParserBase uses _textValue instead of _numberDouble for some reason when NR_DOUBLE is set, but _textValue is not set by setNumber()
290+
// Catch and use _numberDouble instead
291+
if ((_numTypesValid & NR_DOUBLE) != 0 && _textValue == null) {
292+
_numberBigDecimal = BigDecimal.valueOf(_numberDouble);
293+
return;
294+
}
295+
super.convertNumberToBigDecimal();
296+
}
297+
284298
/*
285299
/**********************************************************
286300
/* String value handling

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

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,6 @@
77
import java.net.URL;
88
import java.util.*;
99

10-
import org.apache.avro.Schema;
11-
import org.apache.avro.Schema.Parser;
12-
import org.apache.avro.reflect.Stringable;
13-
import org.apache.avro.specific.SpecificData;
14-
1510
import com.fasterxml.jackson.core.JsonParser;
1611
import com.fasterxml.jackson.databind.BeanDescription;
1712
import com.fasterxml.jackson.databind.JavaType;
@@ -20,6 +15,11 @@
2015
import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonFormatTypes;
2116
import com.fasterxml.jackson.databind.util.ClassUtil;
2217

18+
import org.apache.avro.Schema;
19+
import org.apache.avro.Schema.Parser;
20+
import org.apache.avro.reflect.Stringable;
21+
import org.apache.avro.specific.SpecificData;
22+
2323
public abstract class AvroSchemaHelper
2424
{
2525
/**
@@ -145,14 +145,15 @@ public static Schema numericAvroSchema(JsonParser.NumberType type) {
145145
switch (type) {
146146
case INT:
147147
return Schema.create(Schema.Type.INT);
148-
case BIG_INTEGER:
149148
case LONG:
150149
return Schema.create(Schema.Type.LONG);
151150
case FLOAT:
152151
return Schema.create(Schema.Type.FLOAT);
153-
case BIG_DECIMAL:
154152
case DOUBLE:
155153
return Schema.create(Schema.Type.DOUBLE);
154+
case BIG_INTEGER:
155+
case BIG_DECIMAL:
156+
return Schema.create(Schema.Type.STRING);
156157
default:
157158
}
158159
throw new IllegalStateException("Unrecognized number type: "+type);
@@ -211,6 +212,17 @@ public static Schema parseJsonSchema(String json) {
211212
return parser.parse(json);
212213
}
213214

215+
/**
216+
* Constructs a new enum schema
217+
*
218+
* @param bean Enum type to use for name / description / namespace
219+
* @param values List of enum names
220+
* @return An {@link org.apache.avro.Schema.Type#ENUM ENUM} schema.
221+
*/
222+
public static Schema createEnumSchema(BeanDescription bean, List<String> values) {
223+
return Schema.createEnum(getName(bean.getType()), bean.findClassDescription(), getNamespace(bean.getType()), values);
224+
}
225+
214226
/**
215227
* Returns the Avro type ID for a given type
216228
*/

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

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

3-
import org.apache.avro.Schema;
4-
53
import com.fasterxml.jackson.core.JsonParser;
4+
import com.fasterxml.jackson.databind.JavaType;
65
import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonNumberFormatVisitor;
76

7+
import org.apache.avro.Schema;
8+
89
public class DoubleVisitor
910
extends JsonNumberFormatVisitor.Base
1011
implements SchemaBuilder
1112
{
13+
protected final JavaType _hint;
1214
protected JsonParser.NumberType _type;
1315

14-
public DoubleVisitor() { }
16+
public DoubleVisitor(JavaType typeHint) {
17+
_hint = typeHint;
18+
}
1519

1620
@Override
1721
public void numberType(JsonParser.NumberType type) {
@@ -25,6 +29,6 @@ public Schema builtAvroSchema() {
2529
// would require union most likely
2630
return AvroSchemaHelper.anyNumberSchema();
2731
}
28-
return AvroSchemaHelper.numericAvroSchema(_type);
32+
return AvroSchemaHelper.numericAvroSchema(_type, _hint);
2933
}
3034
}

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

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

3-
import org.apache.avro.Schema;
4-
53
import com.fasterxml.jackson.databind.JavaType;
64
import com.fasterxml.jackson.databind.JsonMappingException;
75
import com.fasterxml.jackson.databind.SerializerProvider;
86
import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonFormatVisitable;
97
import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonMapFormatVisitor;
108

9+
import org.apache.avro.Schema;
10+
1111
public class MapVisitor extends JsonMapFormatVisitor.Base
1212
implements SchemaBuilder
1313
{
1414
protected final JavaType _type;
15-
15+
1616
protected final DefinedSchemas _schemas;
1717

1818
protected Schema _valueSchema;
19-
19+
20+
protected JavaType _keyType;
21+
2022
public MapVisitor(SerializerProvider p, JavaType type, DefinedSchemas schemas)
2123
{
2224
super(p);
@@ -30,7 +32,23 @@ public Schema builtAvroSchema() {
3032
if (_valueSchema == null) {
3133
throw new IllegalStateException("Missing value type for "+_type);
3234
}
33-
return Schema.createMap(_valueSchema);
35+
36+
Schema schema = Schema.createMap(_valueSchema);
37+
38+
// add the key type if there is one
39+
if (_keyType != null && AvroSchemaHelper.isStringable(getProvider()
40+
.getConfig()
41+
.introspectClassAnnotations(_keyType)
42+
.getClassInfo())) {
43+
schema.addProp(AvroSchemaHelper.AVRO_SCHEMA_PROP_KEY_CLASS, AvroSchemaHelper.getTypeId(_keyType));
44+
} else if (_keyType != null && !_keyType.isEnumType()) {
45+
// Avro handles non-stringable keys by converting the map to an array of key/value records
46+
// TODO add support for these in the schema, and custom serializers / deserializers to handle map restructuring
47+
throw new UnsupportedOperationException(
48+
"Key " + _keyType + " is not stringable and non-stringable map keys are not supported yet.");
49+
}
50+
51+
return schema;
3452
}
3553

3654
/*
@@ -43,12 +61,7 @@ public Schema builtAvroSchema() {
4361
public void keyFormat(JsonFormatVisitable handler, JavaType keyType)
4462
throws JsonMappingException
4563
{
46-
/* We actually don't care here, since Avro only has String-keyed
47-
* Maps like JSON: meaning that anything Jackson can regularly
48-
* serialize must convert to Strings anyway.
49-
* If we do find problem cases, we can start verifying them here,
50-
* but for now assume it all "just works".
51-
*/
64+
_keyType = keyType;
5265
}
5366

5467
@Override

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

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,25 +3,29 @@
33
import java.util.ArrayList;
44
import java.util.Set;
55

6-
import org.apache.avro.Schema;
7-
86
import com.fasterxml.jackson.core.JsonParser.NumberType;
7+
import com.fasterxml.jackson.databind.BeanDescription;
98
import com.fasterxml.jackson.databind.JavaType;
9+
import com.fasterxml.jackson.databind.SerializerProvider;
1010
import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonStringFormatVisitor;
1111
import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonValueFormat;
1212
import com.fasterxml.jackson.databind.type.TypeFactory;
1313

14+
import org.apache.avro.Schema;
15+
1416
public class StringVisitor extends JsonStringFormatVisitor.Base
1517
implements SchemaBuilder
1618
{
19+
protected final SerializerProvider _provider;
1720
protected final JavaType _type;
1821
protected final DefinedSchemas _schemas;
1922

2023
protected Set<String> _enums;
2124

22-
public StringVisitor(DefinedSchemas schemas, JavaType t) {
25+
public StringVisitor(SerializerProvider provider, DefinedSchemas schemas, JavaType t) {
2326
_schemas = schemas;
2427
_type = t;
28+
_provider = provider;
2529
}
2630

2731
@Override
@@ -40,13 +44,17 @@ public Schema builtAvroSchema() {
4044
if (_type.hasRawClass(char.class) || _type.hasRawClass(Character.class)) {
4145
return AvroSchemaHelper.numericAvroSchema(NumberType.INT, TypeFactory.defaultInstance().constructType(Character.class));
4246
}
43-
if (_enums == null) {
44-
return Schema.create(Schema.Type.STRING);
47+
BeanDescription bean = _provider.getConfig().introspectClassAnnotations(_type);
48+
if (_enums != null) {
49+
Schema s = AvroSchemaHelper.createEnumSchema(bean, new ArrayList<>(_enums));
50+
_schemas.addSchema(_type, s);
51+
return s;
52+
}
53+
Schema schema = Schema.create(Schema.Type.STRING);
54+
// Stringable classes need to include the type
55+
if (AvroSchemaHelper.isStringable(bean.getClassInfo())) {
56+
schema.addProp(AvroSchemaHelper.AVRO_SCHEMA_PROP_CLASS, AvroSchemaHelper.getTypeId(_type));
4557
}
46-
Schema s = Schema.createEnum(AvroSchemaHelper.getName(_type), "",
47-
AvroSchemaHelper.getNamespace(_type),
48-
new ArrayList<String>(_enums));
49-
_schemas.addSchema(_type, s);
50-
return s;
58+
return schema;
5159
}
5260
}

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

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

3-
import org.apache.avro.Schema;
4-
53
import com.fasterxml.jackson.databind.JavaType;
64
import com.fasterxml.jackson.databind.SerializerProvider;
75
import com.fasterxml.jackson.databind.jsonFormatVisitors.*;
86

7+
import org.apache.avro.Schema;
8+
99
public class VisitorFormatWrapperImpl
1010
implements JsonFormatVisitorWrapper
1111
{
@@ -118,14 +118,14 @@ public JsonStringFormatVisitor expectStringFormat(JavaType type)
118118
_valueSchema = s;
119119
return null;
120120
}
121-
StringVisitor v = new StringVisitor(_schemas, type);
121+
StringVisitor v = new StringVisitor(_provider, _schemas, type);
122122
_builder = v;
123123
return v;
124124
}
125125

126126
@Override
127127
public JsonNumberFormatVisitor expectNumberFormat(JavaType convertedType) {
128-
DoubleVisitor v = new DoubleVisitor();
128+
DoubleVisitor v = new DoubleVisitor(convertedType);
129129
_builder = v;
130130
return v;
131131
}

avro/src/main/java/com/fasterxml/jackson/dataformat/avro/ser/NonBSGenericDatumWriter.java

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,13 @@
55
import java.util.ArrayList;
66
import java.util.List;
77

8+
import com.fasterxml.jackson.dataformat.avro.schema.AvroSchemaHelper;
9+
810
import org.apache.avro.Schema;
911
import org.apache.avro.Schema.Type;
1012
import org.apache.avro.generic.GenericDatumWriter;
1113
import org.apache.avro.io.Encoder;
12-
13-
import com.fasterxml.jackson.dataformat.avro.schema.AvroSchemaHelper;
14+
import org.apache.avro.reflect.Stringable;
1415

1516
/**
1617
* Need to sub-class to prevent encoder from crapping on writing an optional
@@ -71,8 +72,29 @@ public int resolveUnion(Schema union, Object datum) {
7172

7273
@Override
7374
protected void write(Schema schema, Object datum, Encoder out) throws IOException {
74-
if ((schema.getType() == Type.DOUBLE) && datum instanceof BigDecimal) {
75-
out.writeDouble(((BigDecimal)datum).doubleValue());
75+
// Cocerce numerical types, like BigDecimal -> double and BigInteger -> long
76+
if (datum instanceof Number) {
77+
switch (schema.getType()) {
78+
case LONG:
79+
super.write(schema, (((Number) datum).longValue()), out);
80+
return;
81+
case INT:
82+
super.write(schema, (((Number) datum).intValue()), out);
83+
return;
84+
case FLOAT:
85+
super.write(schema, (((Number) datum).floatValue()), out);
86+
return;
87+
case DOUBLE:
88+
super.write(schema, (((Number) datum).doubleValue()), out);
89+
return;
90+
case STRING:
91+
super.write(schema, datum.toString(), out);
92+
return;
93+
}
94+
}
95+
// Handle stringable classes
96+
if (schema.getType() == Type.STRING && datum != null && datum.getClass().getAnnotation(Stringable.class) != null) {
97+
super.write(schema, datum.toString(), out);
7698
return;
7799
}
78100
if (datum instanceof String) {

0 commit comments

Comments
 (0)