Skip to content

Commit

Permalink
AVRO-3377: Deserialization of record of mangled Java class throws Cla…
Browse files Browse the repository at this point in the history
…ssCastException (#1527)

* AVRO-3377: Mangle class identifier if required when initializing class

Use same mangling code that existed in the compiler (now moved to
the core library) to mangle type identifer so that the correct class
can be found more often.

* Cleaning up after merge
  • Loading branch information
kylec32 authored Dec 21, 2023
1 parent 5c85060 commit 3c466ee
Show file tree
Hide file tree
Showing 7 changed files with 373 additions and 97 deletions.
146 changes: 126 additions & 20 deletions lang/java/avro/src/main/java/org/apache/avro/specific/SpecificData.java
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,32 @@ public class SpecificData extends GenericData {
// Class names used internally by the avro code generator
"Builder"));

/* Reserved words for accessor/mutator methods */
public static final Set<String> ACCESSOR_MUTATOR_RESERVED_WORDS = new HashSet<>(
Arrays.asList("class", "schema", "classSchema"));

static {
// Add reserved words to accessor/mutator reserved words
ACCESSOR_MUTATOR_RESERVED_WORDS.addAll(RESERVED_WORDS);
}

/* Reserved words for type identifiers */
public static final Set<String> TYPE_IDENTIFIER_RESERVED_WORDS = new HashSet<>(
Arrays.asList("var", "yield", "record"));

static {
// Add reserved words to type identifier reserved words
TYPE_IDENTIFIER_RESERVED_WORDS.addAll(RESERVED_WORDS);
}

/* Reserved words for error types */
public static final Set<String> ERROR_RESERVED_WORDS = new HashSet<>(Arrays.asList("message", "cause"));

static {
// Add accessor/mutator reserved words to error reserved words
ERROR_RESERVED_WORDS.addAll(ACCESSOR_MUTATOR_RESERVED_WORDS);
}

/**
* Read/write some common builtin classes as strings. Representing these as
* strings isn't always best, as they aren't always ordered ideally, but at
Expand Down Expand Up @@ -238,6 +264,89 @@ protected Schema getEnumSchema(Object datum) {
}.getClass();
private static final Schema NULL_SCHEMA = Schema.create(Schema.Type.NULL);

/**
* Utility to mangle the fully qualified class name into a valid symbol.
*/
public static String mangleFullyQualified(String fullName) {
int lastDot = fullName.lastIndexOf('.');

if (lastDot < 0) {
return mangleTypeIdentifier(fullName);
} else {
String namespace = fullName.substring(0, lastDot);
String typeName = fullName.substring(lastDot + 1);

return mangle(namespace) + "." + mangleTypeIdentifier(typeName);
}
}

/**
* Utility for template use. Adds a dollar sign to reserved words.
*/
public static String mangle(String word) {
return mangle(word, false);
}

/**
* Utility for template use. Adds a dollar sign to reserved words.
*/
public static String mangle(String word, boolean isError) {
return mangle(word, isError ? ERROR_RESERVED_WORDS : RESERVED_WORDS);
}

/**
* Utility for template use. Adds a dollar sign to reserved words in type
* identifiers.
*/
public static String mangleTypeIdentifier(String word) {
return mangleTypeIdentifier(word, false);
}

/**
* Utility for template use. Adds a dollar sign to reserved words in type
* identifiers.
*/
public static String mangleTypeIdentifier(String word, boolean isError) {
return mangle(word, isError ? ERROR_RESERVED_WORDS : TYPE_IDENTIFIER_RESERVED_WORDS);
}

/**
* Utility for template use. Adds a dollar sign to reserved words.
*/
public static String mangle(String word, Set<String> reservedWords) {
return mangle(word, reservedWords, false);
}

public static String mangleMethod(String word, boolean isError) {
return mangle(word, isError ? ERROR_RESERVED_WORDS : ACCESSOR_MUTATOR_RESERVED_WORDS, true);
}

/**
* Utility for template use. Adds a dollar sign to reserved words.
*/
public static String mangle(String word, Set<String> reservedWords, boolean isMethod) {
if (isBlank(word)) {
return word;
}
if (word.contains(".")) {
// If the 'word' is really a full path of a class we must mangle just the
String[] packageWords = word.split("\\.");
String[] newPackageWords = new String[packageWords.length];

for (int i = 0; i < packageWords.length; i++) {
String oldName = packageWords[i];
newPackageWords[i] = mangle(oldName, reservedWords, false);
}

return String.join(".", newPackageWords);
}
if (reservedWords.contains(word) || (isMethod && reservedWords
.contains(Character.toLowerCase(word.charAt(0)) + ((word.length() > 1) ? word.substring(1) : "")))) {
return word + "$";
}
return word;
}

/** Undoes mangling for reserved words. */
protected static String unmangle(String word) {
while (word.endsWith("$")) {
Expand All @@ -246,6 +355,21 @@ protected static String unmangle(String word) {
return word;
}

private static boolean isBlank(CharSequence cs) {
int strLen = cs == null ? 0 : cs.length();
if (strLen == 0) {
return true;
} else {
for (int i = 0; i < strLen; ++i) {
if (!Character.isWhitespace(cs.charAt(i))) {
return false;
}
}

return true;
}
}

/** Return the class that implements a schema, or null if none exists. */
public Class getClass(Schema schema) {
switch (schema.getType()) {
Expand Down Expand Up @@ -331,26 +455,8 @@ public static String getClassName(Schema schema) {
String name = schema.getName();
if (namespace == null || "".equals(namespace))
return name;

StringBuilder classNameBuilder = new StringBuilder();
String[] words = namespace.split("\\.");

for (int i = 0; i < words.length; i++) {
String word = words[i];
classNameBuilder.append(word);

if (RESERVED_WORDS.contains(word)) {
classNameBuilder.append(RESERVED_WORD_ESCAPE_CHAR);
}

if (i != words.length - 1 || !word.endsWith("$")) { // back-compatibly handle $
classNameBuilder.append(".");
}
}

classNameBuilder.append(name);

return classNameBuilder.toString();
String dot = namespace.endsWith("$") ? "" : "."; // back-compatibly handle $
return mangle(namespace) + dot + mangleTypeIdentifier(name);
}

// cache for schemas created from Class objects. Use ClassValue to avoid
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -185,4 +185,9 @@ void classNameContainingReservedWords() {

assertEquals("db.public$.table.AnyName", SpecificData.getClassName(schema));
}

@Test
void testCanGetClassOfMangledType() {
assertEquals("org.apache.avro.specific.int$", SpecificData.getClassName(int$.getClassSchema()));
}
}
Loading

0 comments on commit 3c466ee

Please sign in to comment.