Skip to content

Commit 8321c68

Browse files
authored
Merge pull request #51 from baharclerode/bah.AvroMetaSchema
[Avro] Add support for @AvroMeta, @AvroSchema, and Jackson class/property descriptions
2 parents f1cf48e + e8380e8 commit 8321c68

File tree

4 files changed

+286
-38
lines changed

4 files changed

+286
-38
lines changed

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

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,12 @@
88
import java.util.*;
99

1010
import org.apache.avro.Schema;
11+
import org.apache.avro.Schema.Parser;
1112
import org.apache.avro.reflect.Stringable;
1213
import org.apache.avro.specific.SpecificData;
1314

1415
import com.fasterxml.jackson.core.JsonParser;
16+
import com.fasterxml.jackson.databind.BeanDescription;
1517
import com.fasterxml.jackson.databind.JavaType;
1618
import com.fasterxml.jackson.databind.introspect.AnnotatedClass;
1719
import com.fasterxml.jackson.databind.introspect.AnnotatedConstructor;
@@ -188,6 +190,27 @@ protected static <T> T throwUnsupported() {
188190
throw new UnsupportedOperationException("Format variation not supported");
189191
}
190192

193+
/**
194+
* Initializes a record schema with metadata from the given class; this schema is returned in a non-finalized state, and still
195+
* needs to have fields added to it.
196+
*/
197+
public static Schema initializeRecordSchema(BeanDescription bean) {
198+
return Schema.createRecord(
199+
getName(bean.getType()),
200+
bean.findClassDescription(),
201+
getNamespace(bean.getType()),
202+
bean.getType().isTypeOrSubTypeOf(Throwable.class)
203+
);
204+
}
205+
206+
/**
207+
* Parses a JSON-formatted representation of a schema
208+
*/
209+
public static Schema parseJsonSchema(String json) {
210+
Schema.Parser parser = new Parser();
211+
return parser.parse(json);
212+
}
213+
191214
/**
192215
* Returns the Avro type ID for a given type
193216
*/
Lines changed: 87 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,17 @@
11
package com.fasterxml.jackson.dataformat.avro.schema;
22

3+
import java.util.ArrayList;
4+
import java.util.List;
5+
6+
import org.apache.avro.Schema;
7+
import org.apache.avro.reflect.AvroMeta;
8+
import org.apache.avro.reflect.AvroSchema;
9+
310
import com.fasterxml.jackson.databind.*;
411
import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonFormatVisitable;
512
import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonObjectFormatVisitor;
613
import com.fasterxml.jackson.databind.ser.BeanPropertyWriter;
714
import com.fasterxml.jackson.dataformat.avro.AvroFixedSize;
8-
import org.apache.avro.Schema;
9-
10-
import java.util.ArrayList;
11-
import java.util.List;
1215

1316
public class RecordVisitor
1417
extends JsonObjectFormatVisitor.Base
@@ -18,6 +21,12 @@ public class RecordVisitor
1821

1922
protected final DefinedSchemas _schemas;
2023

24+
/**
25+
* Tracks if the schema for this record has been overridden (by an annotation or other means), and calls to the {@code property} and
26+
* {@code optionalProperty} methods should be ignored.
27+
*/
28+
protected final boolean _overridden;
29+
2130
protected Schema _avroSchema;
2231

2332
protected List<Schema.Field> _fields = new ArrayList<Schema.Field>();
@@ -27,16 +36,29 @@ public RecordVisitor(SerializerProvider p, JavaType type, DefinedSchemas schemas
2736
super(p);
2837
_type = type;
2938
_schemas = schemas;
30-
_avroSchema = Schema.createRecord(AvroSchemaHelper.getName(type),
31-
"Schema for "+type.toCanonical(),
32-
AvroSchemaHelper.getNamespace(type), false);
39+
// Check if the schema for this record is overridden
40+
BeanDescription bean = getProvider().getConfig().introspectDirectClassAnnotations(_type);
41+
AvroSchema ann = bean.getClassInfo().getAnnotation(AvroSchema.class);
42+
if (ann != null) {
43+
_avroSchema = AvroSchemaHelper.parseJsonSchema(ann.value());
44+
_overridden = true;
45+
} else {
46+
_avroSchema = AvroSchemaHelper.initializeRecordSchema(bean);
47+
_overridden = false;
48+
AvroMeta meta = bean.getClassInfo().getAnnotation(AvroMeta.class);
49+
if (meta != null) {
50+
_avroSchema.addProp(meta.key(), meta.value());
51+
}
52+
}
3353
schemas.addSchema(type, _avroSchema);
3454
}
3555

3656
@Override
3757
public Schema builtAvroSchema() {
38-
// Assumption now is that we are done, so let's assign fields
39-
_avroSchema.setFields(_fields);
58+
if (!_overridden) {
59+
// Assumption now is that we are done, so let's assign fields
60+
_avroSchema.setFields(_fields);
61+
}
4062
return _avroSchema;
4163
}
4264

@@ -49,14 +71,19 @@ public Schema builtAvroSchema() {
4971
@Override
5072
public void property(BeanProperty writer) throws JsonMappingException
5173
{
52-
Schema schema = schemaForWriter(writer);
53-
_fields.add(new Schema.Field(writer.getName(), schema, null, null));
74+
if (_overridden) {
75+
return;
76+
}
77+
_fields.add(schemaFieldForWriter(writer, false));
5478
}
5579

5680
@Override
5781
public void property(String name, JsonFormatVisitable handler,
5882
JavaType type) throws JsonMappingException
5983
{
84+
if (_overridden) {
85+
return;
86+
}
6087
VisitorFormatWrapperImpl wrapper = new VisitorFormatWrapperImpl(_schemas, getProvider());
6188
handler.acceptJsonFormatVisitor(wrapper, type);
6289
Schema schema = wrapper.getAvroSchema();
@@ -65,21 +92,19 @@ public void property(String name, JsonFormatVisitable handler,
6592

6693
@Override
6794
public void optionalProperty(BeanProperty writer) throws JsonMappingException {
68-
Schema schema = schemaForWriter(writer);
69-
/* 23-Nov-2012, tatu: Actually let's also assume that primitive type values
70-
* are required, as Jackson does not distinguish whether optional has been
71-
* defined, or is merely the default setting.
72-
*/
73-
if (!writer.getType().isPrimitive()) {
74-
schema = AvroSchemaHelper.unionWithNull(schema);
95+
if (_overridden) {
96+
return;
7597
}
76-
_fields.add(new Schema.Field(writer.getName(), schema, null, null));
98+
_fields.add(schemaFieldForWriter(writer, true));
7799
}
78100

79101
@Override
80102
public void optionalProperty(String name, JsonFormatVisitable handler,
81103
JavaType type) throws JsonMappingException
82104
{
105+
if (_overridden) {
106+
return;
107+
}
83108
VisitorFormatWrapperImpl wrapper = new VisitorFormatWrapperImpl(_schemas, getProvider());
84109
handler.acceptJsonFormatVisitor(wrapper, type);
85110
Schema schema = wrapper.getAvroSchema();
@@ -95,29 +120,53 @@ public void optionalProperty(String name, JsonFormatVisitable handler,
95120
/**********************************************************************
96121
*/
97122

98-
protected Schema schemaForWriter(BeanProperty prop) throws JsonMappingException
123+
protected Schema.Field schemaFieldForWriter(BeanProperty prop, boolean optional) throws JsonMappingException
99124
{
100-
AvroFixedSize fixedSize = prop.getAnnotation(AvroFixedSize.class);
101-
if (fixedSize != null) {
102-
return Schema.createFixed(fixedSize.typeName(), null, fixedSize.typeNamespace(), fixedSize.size());
103-
}
125+
Schema writerSchema;
126+
// Check if schema for property is overridden
127+
AvroSchema schemaOverride = prop.getAnnotation(AvroSchema.class);
128+
if (schemaOverride != null) {
129+
Schema.Parser parser = new Schema.Parser();
130+
writerSchema = parser.parse(schemaOverride.value());
131+
} else {
132+
AvroFixedSize fixedSize = prop.getAnnotation(AvroFixedSize.class);
133+
if (fixedSize != null) {
134+
writerSchema = Schema.createFixed(fixedSize.typeName(), null, fixedSize.typeNamespace(), fixedSize.size());
135+
} else {
136+
JsonSerializer<?> ser = null;
104137

105-
JsonSerializer<?> ser = null;
138+
// 23-Nov-2012, tatu: Ideally shouldn't need to do this but...
139+
if (prop instanceof BeanPropertyWriter) {
140+
BeanPropertyWriter bpw = (BeanPropertyWriter) prop;
141+
ser = bpw.getSerializer();
142+
}
143+
final SerializerProvider prov = getProvider();
144+
if (ser == null) {
145+
if (prov == null) {
146+
throw JsonMappingException.from(prov, "SerializerProvider missing for RecordVisitor");
147+
}
148+
ser = prov.findValueSerializer(prop.getType(), prop);
149+
}
150+
VisitorFormatWrapperImpl visitor = new VisitorFormatWrapperImpl(_schemas, prov);
151+
ser.acceptJsonFormatVisitor(visitor, prop.getType());
152+
writerSchema = visitor.getAvroSchema();
153+
}
106154

107-
// 23-Nov-2012, tatu: Ideally shouldn't need to do this but...
108-
if (prop instanceof BeanPropertyWriter) {
109-
BeanPropertyWriter bpw = (BeanPropertyWriter) prop;
110-
ser = bpw.getSerializer();
111-
}
112-
final SerializerProvider prov = getProvider();
113-
if (ser == null) {
114-
if (prov == null) {
115-
throw JsonMappingException.from(prov, "SerializerProvider missing for RecordVisitor");
155+
/* 23-Nov-2012, tatu: Actually let's also assume that primitive type values
156+
* are required, as Jackson does not distinguish whether optional has been
157+
* defined, or is merely the default setting.
158+
*/
159+
if (optional && !prop.getType().isPrimitive()) {
160+
writerSchema = AvroSchemaHelper.unionWithNull(writerSchema);
116161
}
117-
ser = prov.findValueSerializer(prop.getType(), prop);
118162
}
119-
VisitorFormatWrapperImpl visitor = new VisitorFormatWrapperImpl(_schemas, prov);
120-
ser.acceptJsonFormatVisitor(visitor, prop.getType());
121-
return visitor.getAvroSchema();
163+
Schema.Field field = new Schema.Field(prop.getName(), writerSchema, prop.getMetadata().getDescription(), null);
164+
165+
AvroMeta meta = prop.getAnnotation(AvroMeta.class);
166+
if (meta != null) {
167+
field.addProp(meta.key(), meta.value());
168+
}
169+
170+
return field;
122171
}
123172
}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
package com.fasterxml.jackson.dataformat.avro.interop.annotations;
2+
3+
import com.fasterxml.jackson.annotation.JsonProperty;
4+
import com.fasterxml.jackson.dataformat.avro.interop.InteropTestBase;
5+
6+
import lombok.Data;
7+
import org.apache.avro.AvroRuntimeException;
8+
import org.apache.avro.Schema;
9+
import org.apache.avro.reflect.AvroMeta;
10+
import org.apache.avro.reflect.AvroSchema;
11+
import org.junit.Test;
12+
13+
import static org.assertj.core.api.Assertions.assertThat;
14+
15+
public class AvroMetaTest extends InteropTestBase {
16+
17+
@Data
18+
@AvroMeta(key = "class-meta", value = "class value")
19+
static class MetaTest {
20+
21+
@AvroMeta(key = "custom Property", value = "Some Value")
22+
private String someProperty;
23+
24+
@AvroMeta(key = "required custom Property", value = "Some required Value")
25+
@JsonProperty(required = true)
26+
private String someRequiredProperty;
27+
}
28+
29+
@Data
30+
static class BadMetaTest {
31+
32+
@AvroMeta(key = "name", value = "colliding property")
33+
private String types;
34+
}
35+
36+
@Data
37+
@AvroSchema("{\"type\":\"string\"}")
38+
@AvroMeta(key="overridden", value = "true")
39+
static class OverriddenClassSchema {
40+
41+
private int field;
42+
}
43+
44+
@Test
45+
public void testAnnotationPrecedence() {
46+
Schema schema = schemaFunctor.apply(OverriddenClassSchema.class);
47+
//
48+
assertThat(schema.getProp("overridden")).isNull();
49+
}
50+
51+
@Test
52+
public void testOptionalFieldMetaProperty() {
53+
Schema schema = schemaFunctor.apply(MetaTest.class);
54+
//
55+
assertThat(schema.getField("someProperty").getProp("custom Property")).isEqualTo("Some Value");
56+
}
57+
58+
@Test
59+
public void testRequiredFieldMetaProperty() {
60+
Schema schema = schemaFunctor.apply(MetaTest.class);
61+
//
62+
assertThat(schema.getField("someRequiredProperty").getProp("required custom Property")).isEqualTo("Some required Value");
63+
}
64+
65+
@Test
66+
public void testClassMetaProperty() {
67+
Schema schema = schemaFunctor.apply(MetaTest.class);
68+
//
69+
assertThat(schema.getProp("class-meta")).isEqualTo("class value");
70+
}
71+
72+
@Test(expected = AvroRuntimeException.class)
73+
public void testCollidingMeta() {
74+
schemaFunctor.apply(BadMetaTest.class);
75+
}
76+
77+
}

0 commit comments

Comments
 (0)