diff --git a/lang/java/avro/src/main/java/org/apache/avro/reflect/ReflectData.java b/lang/java/avro/src/main/java/org/apache/avro/reflect/ReflectData.java index 7c34863e627..5a5ab8ce275 100644 --- a/lang/java/avro/src/main/java/org/apache/avro/reflect/ReflectData.java +++ b/lang/java/avro/src/main/java/org/apache/avro/reflect/ReflectData.java @@ -531,6 +531,11 @@ public Class getClass(Schema schema) { return Short.TYPE; if (Character.class.getName().equals(intClass)) return Character.TYPE; + case RECORD: + case ENUM: + Class className = getClassProp(schema, CLASS_PROP); + if (className != null) + return className; default: return super.getClass(schema); } @@ -720,10 +725,12 @@ protected Schema createSchema(Type type, Map names) { for (Enum constant : constants) symbols.add(constant.name()); schema = Schema.createEnum(name, doc, space, symbols); + schema.addProp(CLASS_PROP, c.getName()); consumeAvroAliasAnnotation(c, schema); } else if (GenericFixed.class.isAssignableFrom(c)) { // fixed int size = c.getAnnotation(FixedSize.class).value(); schema = Schema.createFixed(name, doc, space, size); + schema.addProp(CLASS_PROP, c.getName()); consumeAvroAliasAnnotation(c, schema); } else if (IndexedRecord.class.isAssignableFrom(c)) { // specific return super.createSchema(type, names); @@ -731,6 +738,7 @@ protected Schema createSchema(Type type, Map names) { List fields = new ArrayList<>(); boolean error = Throwable.class.isAssignableFrom(c); schema = Schema.createRecord(name, doc, space, error); + schema.addProp(CLASS_PROP, c.getName()); consumeAvroAliasAnnotation(c, schema); names.put(c.getName(), schema); for (Field field : getCachedFields(c)) @@ -806,12 +814,18 @@ private String simpleName(Class c) { * class */ private String getNamespace(Class c) { - AvroTypeName avroTypeName = c.getAnnotation(AvroTypeName.class); - if (avroTypeName != null) { - return avroTypeName.value(); + AvroNamespace avroNamespace = c.getAnnotation(AvroNamespace.class); + if (avroNamespace != null) { + return avroNamespace.value(); } - if (c.getEnclosingClass() != null) // nested class + if (c.getEnclosingClass() != null) { // nested class + AvroNamespace enclosingClassAvroNamespace = c.getEnclosingClass().getAnnotation(AvroNamespace.class); + if (enclosingClassAvroNamespace != null) { + return enclosingClassAvroNamespace.value(); + } return c.getEnclosingClass().getName().replace('$', '.'); + } + return c.getPackage() == null ? "" : c.getPackage().getName(); } diff --git a/lang/java/avro/src/main/java/org/apache/avro/reflect/package.html b/lang/java/avro/src/main/java/org/apache/avro/reflect/package.html index 46fa5ee11fb..bd81f075ab7 100644 --- a/lang/java/avro/src/main/java/org/apache/avro/reflect/package.html +++ b/lang/java/avro/src/main/java/org/apache/avro/reflect/package.html @@ -85,8 +85,8 @@ for a schema field with the given name, when trying to read into such an annotated java field. -

The {@link org.apache.avro.reflect.AvroTypeName AvroTypeName} annotation renames -the namespace in the schema to the given namespace. +

The {@link org.apache.avro.reflect.AvroNamespace AvroTypeName} annotation renames + the namespace in the schema to the given namespace.

The {@link org.apache.avro.reflect.AvroMeta AvroMeta} annotation adds an arbitrary key:value pair in the schema at the node of the java field. diff --git a/lang/java/avro/src/test/java/org/apache/avro/reflect/TestReflect.java b/lang/java/avro/src/test/java/org/apache/avro/reflect/TestReflect.java index 35226047ff5..52e9bf1891f 100644 --- a/lang/java/avro/src/test/java/org/apache/avro/reflect/TestReflect.java +++ b/lang/java/avro/src/test/java/org/apache/avro/reflect/TestReflect.java @@ -565,7 +565,8 @@ public void testAvroNullableDefault() { check(NullableDefaultTest.class, "{\"type\":\"record\",\"name\":\"NullableDefaultTest\"," + "\"namespace\":\"org.apache.avro.reflect.TestReflect\",\"fields\":[" - + "{\"name\":\"foo\",\"type\":[\"null\",\"int\"],\"default\":1}]}"); + + "{\"name\":\"foo\",\"type\":[\"null\",\"int\"],\"default\":1}]" + + ",\"java-class\":\"org.apache.avro.reflect.TestReflect$NullableDefaultTest\"}"); } private static class UnionDefaultTest { @@ -579,7 +580,8 @@ public void testAvroUnionDefault() { check(UnionDefaultTest.class, "{\"type\":\"record\",\"name\":\"UnionDefaultTest\"," + "\"namespace\":\"org.apache.avro.reflect.TestReflect\",\"fields\":[" - + "{\"name\":\"foo\",\"type\":[\"int\",\"string\"],\"default\":1}]}"); + + "{\"name\":\"foo\",\"type\":[\"int\",\"string\"],\"default\":1}]" + + ",\"java-class\":\"org.apache.avro.reflect.TestReflect$UnionDefaultTest\"}"); } @Test @@ -639,8 +641,10 @@ public static enum E { @Test void testEnum() throws Exception { - check(E.class, "{\"type\":\"enum\",\"name\":\"E\",\"namespace\":" - + "\"org.apache.avro.reflect.TestReflect\",\"symbols\":[\"A\",\"B\"]}"); + check(E.class, + "{\"type\":\"enum\",\"name\":\"E\",\"namespace\":" + + "\"org.apache.avro.reflect.TestReflect\",\"symbols\":[\"A\",\"B\"]" + + ",\"java-class\":\"org.apache.avro.reflect.TestReflect$E\"}"); } public static class R { @@ -652,7 +656,8 @@ public static class R { void record() throws Exception { check(R.class, "{\"type\":\"record\",\"name\":\"R\",\"namespace\":" + "\"org.apache.avro.reflect.TestReflect\",\"fields\":[" - + "{\"name\":\"a\",\"type\":\"int\"}," + "{\"name\":\"b\",\"type\":\"long\"}]}"); + + "{\"name\":\"a\",\"type\":\"int\"}," + "{\"name\":\"b\",\"type\":\"long\"}]" + + ",\"java-class\":\"org.apache.avro.reflect.TestReflect$R\"}"); } public static class RAvroIgnore { @@ -663,7 +668,7 @@ public static class RAvroIgnore { @Test void annotationAvroIgnore() throws Exception { check(RAvroIgnore.class, "{\"type\":\"record\",\"name\":\"RAvroIgnore\",\"namespace\":" - + "\"org.apache.avro.reflect.TestReflect\",\"fields\":[]}"); + + "\"org.apache.avro.reflect.TestReflect\",\"fields\":[],\"java-class\":\"org.apache.avro.reflect.TestReflect$RAvroIgnore\"}"); } @AvroMeta(key = "X", value = "Y") @@ -677,7 +682,7 @@ void annotationAvroMeta() throws Exception { check(RAvroMeta.class, "{\"type\":\"record\",\"name\":\"RAvroMeta\",\"namespace\":" + "\"org.apache.avro.reflect.TestReflect\",\"fields\":[" + "{\"name\":\"a\",\"type\":\"int\",\"K\":\"V\"}]" - + ",\"X\":\"Y\"}"); + + ",\"java-class\":\"org.apache.avro.reflect.TestReflect$RAvroMeta\"" + ",\"X\":\"Y\"}"); } @AvroMeta(key = "X", value = "Y") @@ -693,7 +698,8 @@ void annotationMultiAvroMeta() { check(RAvroMultiMeta.class, "{\"type\":\"record\",\"name\":\"RAvroMultiMeta\",\"namespace\":" + "\"org.apache.avro.reflect.TestReflect\",\"fields\":[" - + "{\"name\":\"a\",\"type\":\"int\",\"K\":\"V\",\"L\":\"W\"}]" + ",\"X\":\"Y\",\"A\":\"B\"}"); + + "{\"name\":\"a\",\"type\":\"int\",\"K\":\"V\",\"L\":\"W\"}]" + + ",\"java-class\":\"org.apache.avro.reflect.TestReflect$RAvroMultiMeta\"" + ",\"X\":\"Y\",\"A\":\"B\"}"); } public static class RAvroDuplicateFieldMeta { @@ -729,8 +735,10 @@ public static class RAvroName { @Test void annotationAvroName() throws Exception { - check(RAvroName.class, "{\"type\":\"record\",\"name\":\"RAvroName\",\"namespace\":" - + "\"org.apache.avro.reflect.TestReflect\",\"fields\":[" + "{\"name\":\"b\",\"type\":\"int\"}]}"); + check(RAvroName.class, + "{\"type\":\"record\",\"name\":\"RAvroName\",\"namespace\":" + + "\"org.apache.avro.reflect.TestReflect\",\"fields\":[" + "{\"name\":\"b\",\"type\":\"int\"}]" + + ",\"java-class\":\"org.apache.avro.reflect.TestReflect$RAvroName\"}"); } public static class RAvroNameCollide { @@ -756,8 +764,10 @@ public static class RAvroStringableField { @Test void annotationAvroStringableFields() throws Exception { - check(RAvroStringableField.class, "{\"type\":\"record\",\"name\":\"RAvroStringableField\",\"namespace\":" - + "\"org.apache.avro.reflect.TestReflect\",\"fields\":[" + "{\"name\":\"a\",\"type\":\"string\"}]}"); + check(RAvroStringableField.class, + "{\"type\":\"record\",\"name\":\"RAvroStringableField\",\"namespace\":" + + "\"org.apache.avro.reflect.TestReflect\",\"fields\":[" + "{\"name\":\"a\",\"type\":\"string\"}]" + + ",\"java-class\":\"org.apache.avro.reflect.TestReflect$RAvroStringableField\"}"); } private void check(Object o, String schemaJson) { @@ -883,7 +893,8 @@ void avroEncodeInducing() throws IOException { assertEquals(schm.toString(), "{\"type\":\"record\",\"name\":\"AvroEncRecord\",\"namespace" + "\":\"org.apache.avro.reflect.TestReflect\",\"fields\":[{\"name\":\"date\"," - + "\"type\":{\"type\":\"long\",\"CustomEncoding\":\"DateAsLongEncoding\"}}]}"); + + "\"type\":{\"type\":\"long\",\"CustomEncoding\":\"DateAsLongEncoding\"}}]" + + ",\"java-class\":\"org.apache.avro.reflect.TestReflect$AvroEncRecord\"}"); } @Test @@ -1248,11 +1259,11 @@ private static class AliasC { @Test void avroAliasOnClass() { check(AliasA.class, - "{\"type\":\"record\",\"name\":\"AliasA\",\"namespace\":\"org.apache.avro.reflect.TestReflect\",\"fields\":[],\"aliases\":[\"b.a\"]}"); + "{\"type\":\"record\",\"name\":\"AliasA\",\"namespace\":\"org.apache.avro.reflect.TestReflect\",\"fields\":[],\"java-class\":\"org.apache.avro.reflect.TestReflect$AliasA\",\"aliases\":[\"b.a\"]}"); check(AliasB.class, - "{\"type\":\"record\",\"name\":\"AliasB\",\"namespace\":\"org.apache.avro.reflect.TestReflect\",\"fields\":[],\"aliases\":[\"a\"]}"); + "{\"type\":\"record\",\"name\":\"AliasB\",\"namespace\":\"org.apache.avro.reflect.TestReflect\",\"fields\":[],\"java-class\":\"org.apache.avro.reflect.TestReflect$AliasB\",\"aliases\":[\"a\"]}"); check(AliasC.class, - "{\"type\":\"record\",\"name\":\"AliasC\",\"namespace\":\"org.apache.avro.reflect.TestReflect\",\"fields\":[],\"aliases\":[\"a\"]}"); + "{\"type\":\"record\",\"name\":\"AliasC\",\"namespace\":\"org.apache.avro.reflect.TestReflect\",\"fields\":[],\"java-class\":\"org.apache.avro.reflect.TestReflect$AliasC\",\"aliases\":[\"a\"]}"); } @AvroAlias(alias = "alias1", space = "space1") @@ -1264,7 +1275,9 @@ private static class MultipleAliasRecord { @Test void multipleAliasAnnotationsOnClass() { check(MultipleAliasRecord.class, - "{\"type\":\"record\",\"name\":\"MultipleAliasRecord\",\"namespace\":\"org.apache.avro.reflect.TestReflect\",\"fields\":[],\"aliases\":[\"space1.alias1\",\"space2.alias2\"]}"); + "{\"type\":\"record\",\"name\":\"MultipleAliasRecord\",\"namespace\":\"org.apache.avro.reflect.TestReflect\",\"fields\":[]" + + ",\"java-class\":\"org.apache.avro.reflect.TestReflect$MultipleAliasRecord\"" + + ",\"aliases\":[\"space1.alias1\",\"space2.alias2\"]}"); } @@ -1277,7 +1290,7 @@ void dollarTerminatedNamespaceCompatibility() { Schema s = new Schema.Parser(NameValidator.NO_VALIDATION).parse( "{\"type\":\"record\",\"name\":\"Z\",\"namespace\":\"org.apache.avro.reflect.TestReflect$\",\"fields\":[]}"); assertEquals(data.getSchema(data.getClass(s)).toString(), - "{\"type\":\"record\",\"name\":\"Z\",\"namespace\":\"org.apache.avro.reflect.TestReflect\",\"fields\":[]}"); + "{\"type\":\"record\",\"name\":\"Z\",\"namespace\":\"org.apache.avro.reflect.TestReflect\",\"fields\":[],\"java-class\":\"org.apache.avro.reflect.TestReflect$Z\"}"); } @Test @@ -1312,7 +1325,7 @@ void avroAliasOnField() { Schema expectedSchema = SchemaBuilder.record(ClassWithAliasOnField.class.getSimpleName()) .namespace("org.apache.avro.reflect.TestReflect").fields().name("primitiveField").aliases("aliasName") .type(Schema.create(org.apache.avro.Schema.Type.INT)).noDefault().endRecord(); - + expectedSchema.addProp("java-class", "org.apache.avro.reflect.TestReflect$ClassWithAliasOnField"); check(ClassWithAliasOnField.class, expectedSchema.toString()); } @@ -1330,7 +1343,7 @@ public void testMultipleFieldAliases() { field.addAlias("alias2"); Schema avroMultiMeta = Schema.createRecord("ClassWithMultipleAliasesOnField", null, "org.apache.avro.reflect.TestReflect", false, Arrays.asList(field)); - + avroMultiMeta.addProp("java-class", "org.apache.avro.reflect.TestReflect$ClassWithMultipleAliasesOnField"); Schema schema = ReflectData.get().getSchema(ClassWithMultipleAliasesOnField.class); assertEquals(avroMultiMeta, schema); } @@ -1344,7 +1357,8 @@ public void testOptional() { check(OptionalTest.class, "{\"type\":\"record\",\"name\":\"OptionalTest\"," + "\"namespace\":\"org.apache.avro.reflect.TestReflect\",\"fields\":[" - + "{\"name\":\"foo\",\"type\":[\"null\",\"int\"],\"default\":null}]}"); + + "{\"name\":\"foo\",\"type\":[\"null\",\"int\"],\"default\":null}]" + + ",\"java-class\":\"org.apache.avro.reflect.TestReflect$OptionalTest\"}"); } private static class DefaultTest { @@ -1357,7 +1371,8 @@ void avroDefault() { check(DefaultTest.class, "{\"type\":\"record\",\"name\":\"DefaultTest\"," + "\"namespace\":\"org.apache.avro.reflect.TestReflect\",\"fields\":[" - + "{\"name\":\"foo\",\"type\":\"int\",\"default\":1}]}"); + + "{\"name\":\"foo\",\"type\":\"int\",\"default\":1}]" + + ",\"java-class\":\"org.apache.avro.reflect.TestReflect$DefaultTest\"}"); } public static class NullableBytesTest { @@ -1407,18 +1422,17 @@ private static class DocTest { void avroDoc() { check(DocTest.class, "{\"type\":\"record\",\"name\":\"DocTest\",\"namespace\":\"org.apache.avro.reflect.TestReflect\"," - + "\"doc\":\"DocTest class docs\"," + "\"fields\":[" - + "{\"name\":\"defaultTest\",\"type\":{\"type\":\"record\",\"name\":\"DefaultTest\"," - + "\"fields\":[{\"name\":\"foo\",\"type\":\"int\",\"default\":1}]},\"doc\":\"And again\"}," - + "{\"name\":\"enums\",\"type\":{\"type\":\"enum\",\"name\":\"DocTestEnum\"," - + "\"symbols\":[\"ENUM_1\",\"ENUM_2\"]},\"doc\":\"Some other Documentation\"}," - + "{\"name\":\"foo\",\"type\":\"int\",\"doc\":\"Some Documentation\"}" + "]}"); + + "\"doc\":\"DocTest class docs\",\"fields\":[{\"name\":\"defaultTest\",\"type\":{\"type\":\"record\"," + + "\"name\":\"DefaultTest\",\"fields\":[{\"name\":\"foo\",\"type\":\"int\",\"default\":1}]," + + "\"java-class\":\"org.apache.avro.reflect.TestReflect$DefaultTest\"},\"doc\":\"And again\"}," + + "{\"name\":\"enums\",\"type\":{\"type\":\"enum\",\"name\":\"DocTestEnum\",\"symbols\":[\"ENUM_1\",\"ENUM_2\"]," + + "\"java-class\":\"org.apache.avro.reflect.TestReflect$DocTestEnum\"},\"doc\":\"Some other Documentation\"}," + + "{\"name\":\"foo\",\"type\":\"int\",\"doc\":\"Some Documentation\"}],\"java-class\":\"org.apache.avro.reflect.TestReflect$DocTest\"}"); } - @AvroTypeName("org.apache.avro.reflect.OverrideNamespace") + @AvroNamespace("org.apache.avro.reflect.OverrideNamespace") private static class NamespaceTest { - @AvroTypeName("org.apache.avro.reflect.InnerOverrideNamespace") private static class InnerNamespaceTest { } } @@ -1426,13 +1440,15 @@ private static class InnerNamespaceTest { @Test void avroOverrideNamespaceTest() { check(NamespaceTest.class, - "{\"type\":\"record\",\"name\":\"NamespaceTest\",\"namespace\":\"org.apache.avro.reflect.OverrideNamespace\",\"fields\":[]}"); + "{\"type\":\"record\",\"name\":\"NamespaceTest\",\"namespace\":\"org.apache.avro.reflect.OverrideNamespace\",\"fields\":[]" + + ",\"java-class\":\"org.apache.avro.reflect.TestReflect$NamespaceTest\"}"); } @Test - void avroOverrideInnerNamespaceTest() { + void avroOverrideEnclosingNamespaceTest() { check(NamespaceTest.InnerNamespaceTest.class, - "{\"type\":\"record\",\"name\":\"InnerNamespaceTest\",\"namespace\":\"org.apache.avro.reflect.InnerOverrideNamespace\",\"fields\":[]}"); + "{\"type\":\"record\",\"name\":\"InnerNamespaceTest\",\"namespace\":\"org.apache.avro.reflect.OverrideNamespace\",\"fields\":[]" + + ",\"java-class\":\"org.apache.avro.reflect.TestReflect$NamespaceTest$InnerNamespaceTest\"}"); } } diff --git a/lang/java/avro/src/test/java/org/apache/avro/reflect/TestReflectDatumReader.java b/lang/java/avro/src/test/java/org/apache/avro/reflect/TestReflectDatumReader.java index 52b40b87b36..1c0c2304f53 100644 --- a/lang/java/avro/src/test/java/org/apache/avro/reflect/TestReflectDatumReader.java +++ b/lang/java/avro/src/test/java/org/apache/avro/reflect/TestReflectDatumReader.java @@ -67,6 +67,43 @@ void read_PojoWithList() throws IOException { } + @Test + void testRead_PojoWithCustomNamespace() throws IOException { + PojoWithCustomNamespace pojoWithCustomNamespace = new PojoWithCustomNamespace(); + pojoWithCustomNamespace.setId(42); + + byte[] serializedBytes = serializeWithReflectDatumWriter(pojoWithCustomNamespace, PojoWithCustomNamespace.class); + + Decoder decoder = DecoderFactory.get().binaryDecoder(serializedBytes, null); + ReflectDatumReader reflectDatumReader = new ReflectDatumReader<>( + PojoWithCustomNamespace.class); + + PojoWithCustomNamespace deserialized = new PojoWithCustomNamespace(); + reflectDatumReader.read(deserialized, decoder); + + assertEquals(pojoWithCustomNamespace, deserialized); + + } + + @Test + void testRead_InnerClassPojoWithCustomNamespace() throws IOException { + PojoWithInnerClassAndWithCustomNamespace.InnerPojoClass innerPojoClass = new PojoWithInnerClassAndWithCustomNamespace.InnerPojoClass(); + innerPojoClass.setId(42); + + byte[] serializedBytes = serializeWithReflectDatumWriter(innerPojoClass, + PojoWithInnerClassAndWithCustomNamespace.InnerPojoClass.class); + + Decoder decoder = DecoderFactory.get().binaryDecoder(serializedBytes, null); + ReflectDatumReader reflectDatumReader = new ReflectDatumReader<>( + PojoWithInnerClassAndWithCustomNamespace.InnerPojoClass.class); + + PojoWithInnerClassAndWithCustomNamespace.InnerPojoClass deserialized = new PojoWithInnerClassAndWithCustomNamespace.InnerPojoClass(); + reflectDatumReader.read(deserialized, decoder); + + assertEquals(innerPojoClass, deserialized); + + } + @Test void read_PojoWithArray() throws IOException { PojoWithArray pojoWithArray = new PojoWithArray(); @@ -191,6 +228,75 @@ public void testRead_PojoWithNullableAnnotation() throws IOException { assertEquals(v2Pojo.doubleId, FieldAccess.DOUBLE_DEFAULT_VALUE); } + @AvroNamespace("com.override.sample") + public static class PojoWithCustomNamespace { + private int id; + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + id; + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + PojoWithCustomNamespace other = (PojoWithCustomNamespace) obj; + return id == other.id; + } + } + + @AvroNamespace("com.override.sample") + public static class PojoWithInnerClassAndWithCustomNamespace { + + public static class InnerPojoClass { + private int id; + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + id; + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + InnerPojoClass other = (InnerPojoClass) obj; + return id == other.id; + } + } + } + public static class PojoWithList { private int id; private List relatedIds; diff --git a/lang/java/avro/src/test/java/org/apache/avro/reflect/TestReflectLogicalTypes.java b/lang/java/avro/src/test/java/org/apache/avro/reflect/TestReflectLogicalTypes.java index 851ab95e3ea..7dbb7c18a84 100644 --- a/lang/java/avro/src/test/java/org/apache/avro/reflect/TestReflectLogicalTypes.java +++ b/lang/java/avro/src/test/java/org/apache/avro/reflect/TestReflectLogicalTypes.java @@ -73,6 +73,8 @@ void reflectedSchema() { Schema expected = SchemaBuilder.record(RecordWithUUIDList.class.getName()).fields().name("uuids").type().array() .items().stringType().noDefault().endRecord(); expected.getField("uuids").schema().addProp(SpecificData.CLASS_PROP, List.class.getName()); + expected.addProp("java-class", "org.apache.avro.reflect.RecordWithUUIDList"); + LogicalTypes.uuid().addToSchema(expected.getField("uuids").schema().getElementType()); Schema actual = REFLECT.getSchema(RecordWithUUIDList.class);