Skip to content

Commit ca58cfc

Browse files
authored
Merge pull request #57 from baharclerode/bah.StringableAnnotationMaster
[Avro] Add support for @Stringable annotation
2 parents d9d6e60 + ee71b6a commit ca58cfc

File tree

14 files changed

+435
-67
lines changed

14 files changed

+435
-67
lines changed

avro/pom.xml

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,12 @@ abstractions.
2121
</properties>
2222

2323
<dependencies>
24-
<!-- Hmmh. Need databind for Avro Schema generation... -->
24+
<!-- Hmmh. Need annotations for introspection... -->
25+
<dependency>
26+
<groupId>com.fasterxml.jackson.core</groupId>
27+
<artifactId>jackson-annotations</artifactId>
28+
</dependency>
29+
<!-- and databind for Avro Schema generation... -->
2530
<dependency>
2631
<groupId>com.fasterxml.jackson.core</groupId>
2732
<artifactId>jackson-databind</artifactId>
@@ -32,14 +37,7 @@ abstractions.
3237
<version>1.8.1</version>
3338
</dependency>
3439

35-
<!-- and for testing we need annotations -->
36-
<dependency>
37-
<groupId>com.fasterxml.jackson.core</groupId>
38-
<artifactId>jackson-annotations</artifactId>
39-
<scope>test</scope>
40-
</dependency>
41-
42-
<!-- plus logback -->
40+
<!-- and for testing we need logback -->
4341
<dependency>
4442
<groupId>ch.qos.logback</groupId>
4543
<artifactId>logback-classic</artifactId>

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

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,16 @@
55

66
import org.apache.avro.reflect.*;
77

8+
import com.fasterxml.jackson.annotation.JsonCreator;
89
import com.fasterxml.jackson.core.Version;
910
import com.fasterxml.jackson.databind.AnnotationIntrospector;
1011
import com.fasterxml.jackson.databind.PropertyName;
12+
import com.fasterxml.jackson.databind.cfg.MapperConfig;
1113
import com.fasterxml.jackson.databind.introspect.Annotated;
14+
import com.fasterxml.jackson.databind.introspect.AnnotatedClass;
15+
import com.fasterxml.jackson.databind.introspect.AnnotatedConstructor;
1216
import com.fasterxml.jackson.databind.introspect.AnnotatedMember;
13-
17+
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
1418
/**
1519
* Adds support for the following annotations from the Apache Avro implementation:
1620
* <ul>
@@ -20,6 +24,8 @@
2024
* define default value for generated Schemas
2125
* </li>
2226
* <li>{@link Nullable @Nullable} - Alias for <code>JsonProperty(required = false)</code></li>
27+
* <li>{@link Stringable @Stringable} - Alias for <code>JsonCreator</code> on the constructor and <code>JsonValue</code> on
28+
* the {@link #toString()} method. </li>
2329
* </ul>
2430
*
2531
* @since 2.9
@@ -76,4 +82,26 @@ public Boolean hasRequiredMarker(AnnotatedMember m) {
7682
}
7783
return null;
7884
}
85+
86+
@Override
87+
public JsonCreator.Mode findCreatorAnnotation(MapperConfig<?> config, Annotated a) {
88+
AnnotatedConstructor constructor = a instanceof AnnotatedConstructor ? (AnnotatedConstructor) a : null;
89+
AnnotatedClass parentClass =
90+
a instanceof AnnotatedConstructor && ((AnnotatedConstructor) a).getTypeContext() instanceof AnnotatedClass
91+
? (AnnotatedClass) ((AnnotatedConstructor) a).getTypeContext()
92+
: null;
93+
if (constructor != null && parentClass != null && parentClass.hasAnnotation(Stringable.class)
94+
&& constructor.getParameterCount() == 1 && String.class.equals(constructor.getRawParameterType(0))) {
95+
return JsonCreator.Mode.DELEGATING;
96+
}
97+
return null;
98+
}
99+
100+
@Override
101+
public Object findSerializer(Annotated a) {
102+
if (a instanceof AnnotatedClass && a.hasAnnotation(Stringable.class)) {
103+
return ToStringSerializer.class;
104+
}
105+
return null;
106+
}
79107
}

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

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

3+
import java.io.File;
34
import java.io.IOException;
45

56
import org.apache.avro.Schema;
@@ -8,6 +9,7 @@
89
import com.fasterxml.jackson.databind.SerializerProvider;
910
import com.fasterxml.jackson.databind.module.SimpleModule;
1011
import com.fasterxml.jackson.databind.ser.std.StdSerializer;
12+
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
1113

1214
/**
1315
* Module that adds support for handling datatypes specific to the standard
@@ -31,6 +33,7 @@ public AvroModule()
3133
{
3234
super(PackageVersion.VERSION);
3335
addSerializer(new SchemaSerializer());
36+
addSerializer(File.class, new ToStringSerializer(File.class));
3437
// 08-Mar-2016, tatu: to fix [dataformat-avro#35], need to prune 'schema' property:
3538
setSerializerModifier(new AvroSerializerModifier());
3639
}

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;
@@ -255,6 +258,17 @@ public JsonLocation getCurrentLocation()
255258
@Override
256259
public abstract JsonToken nextToken() throws IOException;
257260

261+
@Override
262+
protected void convertNumberToBigDecimal() throws IOException {
263+
// ParserBase uses _textValue instead of _numberDouble for some reason when NR_DOUBLE is set, but _textValue is not set by setNumber()
264+
// Catch and use _numberDouble instead
265+
if ((_numTypesValid & NR_DOUBLE) != 0 && _textValue == null) {
266+
_numberBigDecimal = BigDecimal.valueOf(_numberDouble);
267+
return;
268+
}
269+
super.convertNumberToBigDecimal();
270+
}
271+
258272
/*
259273
/**********************************************************
260274
/* String value handling

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import java.util.*;
44

55
import org.apache.avro.Schema;
6+
import org.apache.avro.util.internal.JacksonUtils;
67

78
import com.fasterxml.jackson.dataformat.avro.deser.ScalarDecoder.*;
89
import com.fasterxml.jackson.dataformat.avro.schema.AvroSchemaHelper;
@@ -329,8 +330,8 @@ protected AvroStructureReader createRecordReader(Schema writerSchema, Schema rea
329330
// Any defaults to consider?
330331
if (!defaultFields.isEmpty()) {
331332
for (Schema.Field defaultField : defaultFields) {
332-
AvroFieldReader fr = AvroFieldDefaulters.createDefaulter(defaultField.name(),
333-
defaultField.defaultValue());
333+
AvroFieldReader fr =
334+
AvroFieldDefaulters.createDefaulter(defaultField.name(), JacksonUtils.toJsonNode(defaultField.defaultVal()));
334335
if (fr == null) {
335336
throw new IllegalArgumentException("Unsupported default type: "+defaultField.schema().getType());
336337
}

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

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -146,14 +146,15 @@ public static Schema numericAvroSchema(JsonParser.NumberType type) {
146146
switch (type) {
147147
case INT:
148148
return Schema.create(Schema.Type.INT);
149-
case BIG_INTEGER:
150149
case LONG:
151150
return Schema.create(Schema.Type.LONG);
152151
case FLOAT:
153152
return Schema.create(Schema.Type.FLOAT);
154-
case BIG_DECIMAL:
155153
case DOUBLE:
156154
return Schema.create(Schema.Type.DOUBLE);
155+
case BIG_INTEGER:
156+
case BIG_DECIMAL:
157+
return Schema.create(Schema.Type.STRING);
157158
default:
158159
}
159160
throw new IllegalStateException("Unrecognized number type: "+type);
@@ -212,6 +213,17 @@ public static Schema parseJsonSchema(String json) {
212213
return parser.parse(json);
213214
}
214215

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

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

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,19 @@
33
import org.apache.avro.Schema;
44

55
import com.fasterxml.jackson.core.JsonParser;
6+
import com.fasterxml.jackson.databind.JavaType;
67
import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonNumberFormatVisitor;
78

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: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,13 @@ 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/RecordVisitor.java

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -81,10 +81,6 @@ public void property(BeanProperty writer) throws JsonMappingException
8181
return;
8282
}
8383
_fields.add(schemaFieldForWriter(writer, false));
84-
/*
85-
Schema schema = schemaForWriter(writer);
86-
_fields.add(_field(writer, schema));
87-
*/
8884
}
8985

9086
@Override
@@ -274,7 +270,7 @@ protected Schema reorderUnionToMatchDefaultType(Schema schema, JsonNode defaultV
274270
}
275271
if (matchingIndex != null) {
276272
types.add(0, types.remove((int)matchingIndex));
277-
Map<String, JsonNode> jsonProps = schema.getJsonProps();
273+
Map<String, Object> jsonProps = schema.getObjectProps();
278274
schema = Schema.createUnion(types);
279275
// copy any properties over
280276
for (String property : jsonProps.keySet()) {

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

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,22 +6,26 @@
66
import org.apache.avro.Schema;
77

88
import com.fasterxml.jackson.core.JsonParser.NumberType;
9+
import com.fasterxml.jackson.databind.BeanDescription;
910
import com.fasterxml.jackson.databind.JavaType;
11+
import com.fasterxml.jackson.databind.SerializerProvider;
1012
import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonStringFormatVisitor;
1113
import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonValueFormat;
1214
import com.fasterxml.jackson.databind.type.TypeFactory;
1315

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;
4552
}
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;
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));
57+
}
58+
return schema;
5159
}
5260
}

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -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
}

0 commit comments

Comments
 (0)