AVRO-2757: Avoid dollar signs when reflecting Java. (#914)
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 96b17bb..7f15a65 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
@@ -703,7 +703,7 @@
String name = c.getSimpleName();
String space = c.getPackage() == null ? "" : c.getPackage().getName();
if (c.getEnclosingClass() != null) // nested class
- space = c.getEnclosingClass().getName();
+ space = c.getEnclosingClass().getName().replace('$', '.');
Union union = c.getAnnotation(Union.class);
if (union != null) { // union annotated
return getAnnotatedUnion(union, names);
diff --git a/lang/java/avro/src/main/java/org/apache/avro/specific/SpecificData.java b/lang/java/avro/src/main/java/org/apache/avro/specific/SpecificData.java
index 58bc660..3cbb21d 100644
--- a/lang/java/avro/src/main/java/org/apache/avro/specific/SpecificData.java
+++ b/lang/java/avro/src/main/java/org/apache/avro/specific/SpecificData.java
@@ -17,36 +17,36 @@
*/
package org.apache.avro.specific;
+import org.apache.avro.AvroRuntimeException;
+import org.apache.avro.AvroTypeException;
+import org.apache.avro.Protocol;
+import org.apache.avro.Schema;
+import org.apache.avro.Schema.Type;
+import org.apache.avro.generic.GenericData;
+import org.apache.avro.io.BinaryDecoder;
+import org.apache.avro.io.BinaryEncoder;
+import org.apache.avro.io.DatumReader;
+import org.apache.avro.io.DatumWriter;
+import org.apache.avro.io.DecoderFactory;
+import org.apache.avro.io.EncoderFactory;
+import org.apache.avro.util.ClassUtils;
+
+import java.io.ObjectInput;
+import java.io.ObjectOutput;
+import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
+import java.lang.reflect.ParameterizedType;
+import java.nio.ByteBuffer;
import java.util.Arrays;
-import java.util.HashSet;
-import java.util.Map;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
+import java.util.HashSet;
import java.util.List;
+import java.util.Map;
import java.util.Set;
import java.util.WeakHashMap;
import java.util.concurrent.ConcurrentHashMap;
-import java.nio.ByteBuffer;
-import java.lang.reflect.Constructor;
-import java.lang.reflect.ParameterizedType;
-import java.io.ObjectInput;
-import java.io.ObjectOutput;
-
-import org.apache.avro.Schema;
-import org.apache.avro.Protocol;
-import org.apache.avro.AvroRuntimeException;
-import org.apache.avro.AvroTypeException;
-import org.apache.avro.Schema.Type;
-import org.apache.avro.util.ClassUtils;
-import org.apache.avro.generic.GenericData;
-import org.apache.avro.io.DatumReader;
-import org.apache.avro.io.DatumWriter;
-import org.apache.avro.io.EncoderFactory;
-import org.apache.avro.io.BinaryEncoder;
-import org.apache.avro.io.DecoderFactory;
-import org.apache.avro.io.BinaryDecoder;
/** Utilities for generated Java classes and interfaces. */
public class SpecificData extends GenericData {
@@ -241,15 +241,24 @@
String name = schema.getFullName();
if (name == null)
return null;
- Class c = classCache.computeIfAbsent(name, n -> {
+ Class<?> c = classCache.computeIfAbsent(name, n -> {
try {
return ClassUtils.forName(getClassLoader(), getClassName(schema));
} catch (ClassNotFoundException e) {
- try { // nested class?
- return ClassUtils.forName(getClassLoader(), getNestedClassName(schema));
- } catch (ClassNotFoundException ex) {
- return NO_CLASS;
+ // This might be a nested namespace. Try using the last tokens in the
+ // namespace as an enclosing class by progressively replacing period
+ // delimiters with $
+ StringBuilder nestedName = new StringBuilder(n);
+ int lastDot = n.lastIndexOf('.');
+ while (lastDot != -1) {
+ nestedName.setCharAt(lastDot, '$');
+ try {
+ return ClassUtils.forName(getClassLoader(), nestedName.toString());
+ } catch (ClassNotFoundException ignored) {
+ }
+ lastDot = n.lastIndexOf('.', lastDot - 1);
}
+ return NO_CLASS;
}
});
return c == NO_CLASS ? null : c;
@@ -312,14 +321,6 @@
return namespace + dot + name;
}
- private String getNestedClassName(Schema schema) {
- String namespace = schema.getNamespace();
- String name = schema.getName();
- if (namespace == null || "".equals(namespace))
- return name;
- return namespace + "$" + name;
- }
-
// cache for schemas created from Class objects. Use ClassValue to avoid
// locking classloaders and is GC and thread safe.
private final ClassValue<Schema> schemaClassCache = new ClassValue<Schema>() {
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 2555e9b..700ee4c 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
@@ -18,7 +18,11 @@
package org.apache.avro.reflect;
import static java.nio.charset.StandardCharsets.UTF_8;
-import static org.junit.Assert.*;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
@@ -1211,12 +1215,22 @@
@Test
public void testDollarTerminatedNamespaceCompatibility() {
ReflectData data = ReflectData.get();
- Schema s = new Schema.Parser().parse(
+ Schema s = new Schema.Parser().setValidate(false).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\":[]}");
}
+ @Test
+ public void testDollarTerminatedNestedStaticClassNamespaceCompatibility() {
+ ReflectData data = ReflectData.get();
+ // Older versions of Avro generated this namespace on nested records.
+ Schema s = new Schema.Parser().setValidate(false).parse(
+ "{\"type\":\"record\",\"name\":\"AnotherSampleRecord\",\"namespace\":\"org.apache.avro.reflect.TestReflect$SampleRecord\",\"fields\":[]}");
+ assertThat(data.getSchema(data.getClass(s)).getFullName(),
+ is("org.apache.avro.reflect.TestReflect.SampleRecord.AnotherSampleRecord"));
+ }
+
private static class ClassWithAliasOnField {
@AvroAlias(alias = "aliasName")
int primitiveField;