From cf77cba58c84b528e3a99ca3d7f498e29d1e09c1 Mon Sep 17 00:00:00 2001 From: Henry Fernandes Date: Wed, 1 May 2024 23:28:04 +0100 Subject: [PATCH] AVRO-3968 Add support for custom AvroTypeName annotation to allow overriding namespace when generating schema with reflection --- .../org/apache/avro/reflect/AvroTypeName.java | 33 +++++++++++++++++++ .../org/apache/avro/reflect/ReflectData.java | 19 +++++++++-- .../java/org/apache/avro/reflect/package.html | 5 ++- .../org/apache/avro/reflect/TestReflect.java | 20 +++++++++++ 4 files changed, 73 insertions(+), 4 deletions(-) create mode 100644 lang/java/avro/src/main/java/org/apache/avro/reflect/AvroTypeName.java diff --git a/lang/java/avro/src/main/java/org/apache/avro/reflect/AvroTypeName.java b/lang/java/avro/src/main/java/org/apache/avro/reflect/AvroTypeName.java new file mode 100644 index 00000000000..f1451979ada --- /dev/null +++ b/lang/java/avro/src/main/java/org/apache/avro/reflect/AvroTypeName.java @@ -0,0 +1,33 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.avro.reflect; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Sets the avrotypename for this java type. When reading into this class, a + * reflectdatumreader looks for a schema field with the avrotypename. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.TYPE }) +public @interface AvroTypeName { + String value() default ""; +} 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 347490679ee..7c34863e627 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 @@ -706,9 +706,7 @@ protected Schema createSchema(Type type, Map names) { AvroDoc annotatedDoc = c.getAnnotation(AvroDoc.class); // Docstring String doc = (annotatedDoc != null) ? annotatedDoc.value() : null; String name = c.getSimpleName(); - String space = c.getPackage() == null ? "" : c.getPackage().getName(); - if (c.getEnclosingClass() != null) // nested class - space = c.getEnclosingClass().getName().replace('$', '.'); + String space = getNamespace(c); Union union = c.getAnnotation(Union.class); if (union != null) { // union annotated return getAnnotatedUnion(union, names); @@ -802,6 +800,21 @@ private String simpleName(Class c) { return simpleName; } + /* + * Function checks if there is @AvroTypeName annotation on the class. If present + * then returns the value of the annotation else returns the package of the + * class + */ + private String getNamespace(Class c) { + AvroTypeName avroTypeName = c.getAnnotation(AvroTypeName.class); + if (avroTypeName != null) { + return avroTypeName.value(); + } + if (c.getEnclosingClass() != null) // nested class + return c.getEnclosingClass().getName().replace('$', '.'); + return c.getPackage() == null ? "" : c.getPackage().getName(); + } + private static final Schema THROWABLE_MESSAGE = makeNullable(Schema.create(Schema.Type.STRING)); // if array element type is a class with a union annotation, note it 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 3396a8c6d30..46fa5ee11fb 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 @@ -83,7 +83,10 @@

The {@link org.apache.avro.reflect.AvroName AvroName} annotation renames the field in the schema to the given name. The reflect datum reader will look for a schema field with the given name, when trying to read into such an -annotated java field. +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.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 50121b5a0dd..35226047ff5 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 @@ -1415,4 +1415,24 @@ void avroDoc() { + "{\"name\":\"foo\",\"type\":\"int\",\"doc\":\"Some Documentation\"}" + "]}"); } + @AvroTypeName("org.apache.avro.reflect.OverrideNamespace") + private static class NamespaceTest { + + @AvroTypeName("org.apache.avro.reflect.InnerOverrideNamespace") + private static class InnerNamespaceTest { + } + } + + @Test + void avroOverrideNamespaceTest() { + check(NamespaceTest.class, + "{\"type\":\"record\",\"name\":\"NamespaceTest\",\"namespace\":\"org.apache.avro.reflect.OverrideNamespace\",\"fields\":[]}"); + } + + @Test + void avroOverrideInnerNamespaceTest() { + check(NamespaceTest.InnerNamespaceTest.class, + "{\"type\":\"record\",\"name\":\"InnerNamespaceTest\",\"namespace\":\"org.apache.avro.reflect.InnerOverrideNamespace\",\"fields\":[]}"); + } + }