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 0476b2e2fc1..c30616e17a3 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 @@ -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 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 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 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 @@ -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 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 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("$")) { @@ -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()) { @@ -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 diff --git a/lang/java/avro/src/test/java/org/apache/avro/specific/TestSpecificData.java b/lang/java/avro/src/test/java/org/apache/avro/specific/TestSpecificData.java index 4fe9abb6bd5..5c8cad85331 100644 --- a/lang/java/avro/src/test/java/org/apache/avro/specific/TestSpecificData.java +++ b/lang/java/avro/src/test/java/org/apache/avro/specific/TestSpecificData.java @@ -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())); + } } diff --git a/lang/java/avro/src/test/java/org/apache/avro/specific/int$.java b/lang/java/avro/src/test/java/org/apache/avro/specific/int$.java new file mode 100644 index 00000000000..586d4219124 --- /dev/null +++ b/lang/java/avro/src/test/java/org/apache/avro/specific/int$.java @@ -0,0 +1,227 @@ +/** + * Autogenerated by Avro + * + * DO NOT EDIT DIRECTLY + */ +package org.apache.avro.specific; + +import org.apache.avro.message.BinaryMessageDecoder; +import org.apache.avro.message.BinaryMessageEncoder; +import org.apache.avro.message.SchemaStore; + +@AvroGenerated +public class int$ extends SpecificRecordBase implements SpecificRecord { + private static final long serialVersionUID = 3003385205621277651L; + + public static final org.apache.avro.Schema SCHEMA$ = new org.apache.avro.Schema.Parser() + .parse("{\"type\":\"record\",\"name\":\"int\",\"namespace\":\"org.apache.avro.specific\",\"fields\":[]}"); + + public static org.apache.avro.Schema getClassSchema() { + return SCHEMA$; + } + + private static final SpecificData MODEL$ = new SpecificData(); + + private static final BinaryMessageEncoder ENCODER = new BinaryMessageEncoder<>(MODEL$, SCHEMA$); + + private static final BinaryMessageDecoder DECODER = new BinaryMessageDecoder<>(MODEL$, SCHEMA$); + + /** + * Return the BinaryMessageEncoder instance used by this class. + * + * @return the message encoder used by this class + */ + public static BinaryMessageEncoder getEncoder() { + return ENCODER; + } + + /** + * Return the BinaryMessageDecoder instance used by this class. + * + * @return the message decoder used by this class + */ + public static BinaryMessageDecoder getDecoder() { + return DECODER; + } + + /** + * Create a new BinaryMessageDecoder instance for this class that uses the + * specified {@link SchemaStore}. + * + * @param resolver a {@link SchemaStore} used to find schemas by fingerprint + * @return a BinaryMessageDecoder instance for this class backed by the given + * SchemaStore + */ + public static BinaryMessageDecoder createDecoder(SchemaStore resolver) { + return new BinaryMessageDecoder<>(MODEL$, SCHEMA$, resolver); + } + + /** + * Serializes this int to a ByteBuffer. + * + * @return a buffer holding the serialized data for this instance + * @throws java.io.IOException if this instance could not be serialized + */ + public java.nio.ByteBuffer toByteBuffer() throws java.io.IOException { + return ENCODER.encode(this); + } + + /** + * Deserializes a int from a ByteBuffer. + * + * @param b a byte buffer holding serialized data for an instance of this class + * @return a int instance decoded from the given buffer + * @throws java.io.IOException if the given bytes could not be deserialized into + * an instance of this class + */ + public static int$ fromByteBuffer(java.nio.ByteBuffer b) throws java.io.IOException { + return DECODER.decode(b); + } + + public SpecificData getSpecificData() { + return MODEL$; + } + + public org.apache.avro.Schema getSchema() { + return SCHEMA$; + } + + // Used by DatumWriter. Applications should not call. + public Object get(int field$) { + switch (field$) { + default: + throw new IndexOutOfBoundsException("Invalid index: " + field$); + } + } + + // Used by DatumReader. Applications should not call. + @SuppressWarnings(value = "unchecked") + public void put(int field$, Object value$) { + switch (field$) { + default: + throw new IndexOutOfBoundsException("Invalid index: " + field$); + } + } + + /** + * Creates a new int$ RecordBuilder. + * + * @return A new int$ RecordBuilder + */ + public static Builder newBuilder() { + return new Builder(); + } + + /** + * Creates a new int$ RecordBuilder by copying an existing Builder. + * + * @param other The existing builder to copy. + * @return A new int$ RecordBuilder + */ + public static Builder newBuilder(Builder other) { + if (other == null) { + return new Builder(); + } else { + return new Builder(other); + } + } + + /** + * Creates a new int$ RecordBuilder by copying an existing int$ instance. + * + * @param other The existing instance to copy. + * @return A new int$ RecordBuilder + */ + public static Builder newBuilder(int$ other) { + if (other == null) { + return new Builder(); + } else { + return new Builder(other); + } + } + + /** + * RecordBuilder for int$ instances. + */ + @AvroGenerated + public static class Builder extends SpecificRecordBuilderBase + implements org.apache.avro.data.RecordBuilder { + + /** Creates a new Builder */ + private Builder() { + super(SCHEMA$, MODEL$); + } + + /** + * Creates a Builder by copying an existing Builder. + * + * @param other The existing Builder to copy. + */ + private Builder(Builder other) { + super(other); + } + + /** + * Creates a Builder by copying an existing int$ instance + * + * @param other The existing instance to copy. + */ + private Builder(int$ other) { + super(SCHEMA$, MODEL$); + } + + @Override + @SuppressWarnings("unchecked") + public int$ build() { + try { + int$ record = new int$(); + return record; + } catch (org.apache.avro.AvroMissingFieldException e) { + throw e; + } catch (Exception e) { + throw new org.apache.avro.AvroRuntimeException(e); + } + } + } + + @SuppressWarnings("unchecked") + private static final org.apache.avro.io.DatumWriter WRITER$ = (org.apache.avro.io.DatumWriter) MODEL$ + .createDatumWriter(SCHEMA$); + + @Override + public void writeExternal(java.io.ObjectOutput out) throws java.io.IOException { + WRITER$.write(this, SpecificData.getEncoder(out)); + } + + @SuppressWarnings("unchecked") + private static final org.apache.avro.io.DatumReader READER$ = (org.apache.avro.io.DatumReader) MODEL$ + .createDatumReader(SCHEMA$); + + @Override + public void readExternal(java.io.ObjectInput in) throws java.io.IOException { + READER$.read(this, SpecificData.getDecoder(in)); + } + + @Override + protected boolean hasCustomCoders() { + return true; + } + + @Override + public void customEncode(org.apache.avro.io.Encoder out) throws java.io.IOException { + } + + @Override + public void customDecode(org.apache.avro.io.ResolvingDecoder in) throws java.io.IOException { + org.apache.avro.Schema.Field[] fieldOrder = in.readFieldOrderIfDiff(); + if (fieldOrder == null) { + } else { + for (int i = 0; i < 0; i++) { + switch (fieldOrder[i].pos()) { + default: + throw new java.io.IOException("Corrupt ResolvingDecoder."); + } + } + } + } +} diff --git a/lang/java/compiler/src/main/java/org/apache/avro/compiler/schema/Schemas.java b/lang/java/compiler/src/main/java/org/apache/avro/compiler/schema/Schemas.java index ec8ff778983..0c0e5ab6725 100644 --- a/lang/java/compiler/src/main/java/org/apache/avro/compiler/schema/Schemas.java +++ b/lang/java/compiler/src/main/java/org/apache/avro/compiler/schema/Schemas.java @@ -29,7 +29,7 @@ import org.apache.avro.LogicalType; import org.apache.avro.Schema; import org.apache.avro.Schema.Field; -import org.apache.avro.compiler.specific.SpecificCompiler; +import org.apache.avro.specific.SpecificData; /** * Avro Schema utilities, to traverse... @@ -84,9 +84,9 @@ public static boolean hasGeneratedJavaClass(final Schema schema) { public static String getJavaClassName(final Schema schema) { String namespace = schema.getNamespace(); if (namespace == null) { - return SpecificCompiler.mangle(schema.getName()); + return SpecificData.mangle(schema.getName()); } else { - return namespace + '.' + SpecificCompiler.mangle(schema.getName()); + return namespace + '.' + SpecificData.mangle(schema.getName()); } } diff --git a/lang/java/compiler/src/main/java/org/apache/avro/compiler/specific/SpecificCompiler.java b/lang/java/compiler/src/main/java/org/apache/avro/compiler/specific/SpecificCompiler.java index c6917a25f94..e7c854c57f7 100644 --- a/lang/java/compiler/src/main/java/org/apache/avro/compiler/specific/SpecificCompiler.java +++ b/lang/java/compiler/src/main/java/org/apache/avro/compiler/specific/SpecificCompiler.java @@ -26,7 +26,6 @@ import java.lang.reflect.InvocationTargetException; import java.nio.file.Files; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashMap; @@ -52,7 +51,6 @@ import org.apache.avro.generic.GenericData; import org.apache.avro.generic.GenericData.StringType; import org.apache.avro.specific.SpecificData; -import org.apache.commons.lang3.StringUtils; import org.apache.velocity.Template; import org.apache.velocity.VelocityContext; import org.apache.velocity.app.VelocityEngine; @@ -60,8 +58,6 @@ import org.slf4j.LoggerFactory; import static java.nio.charset.StandardCharsets.UTF_8; -import static org.apache.avro.specific.SpecificData.RESERVED_WORDS; -import static org.apache.avro.specific.SpecificData.RESERVED_WORD_ESCAPE_CHAR; /** * Generate specific Java interfaces and classes for protocols and schemas. @@ -142,32 +138,6 @@ public boolean isCreateAllArgsConstructor() { return createAllArgsConstructor; } - /* Reserved words for accessor/mutator methods */ - protected static final Set 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 */ - protected static final Set 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 */ - protected static final Set 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); - } - private static final String FILE_HEADER = "/**\n" + " * Autogenerated by Avro\n" + " *\n" + " * DO NOT EDIT DIRECTLY\n" + " */\n"; @@ -834,7 +804,7 @@ private String javaType(Schema schema, boolean checkConvertedLogicalType) { case RECORD: case ENUM: case FIXED: - return mangleFullyQualified(schema.getFullName()); + return SpecificData.mangleFullyQualified(schema.getFullName()); case ARRAY: return "java.util.List<" + javaType(schema.getElementType()) + ">"; case MAP: @@ -866,19 +836,6 @@ private String javaType(Schema schema, boolean checkConvertedLogicalType) { } } - private 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); - } - } - private LogicalType getLogicalType(Schema schema) { if (enableDecimalLogicalType || !(schema.getLogicalType() instanceof LogicalTypes.Decimal)) { return schema.getLogicalType(); @@ -1103,14 +1060,14 @@ public static String nullToEmpty(String x) { * Utility for template use. Adds a dollar sign to reserved words. */ public static String mangle(String word) { - return mangle(word, false); + return SpecificData.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); + return SpecificData.mangle(word, isError); } /** @@ -1118,7 +1075,7 @@ public static String mangle(String word, boolean isError) { * identifiers. */ public static String mangleTypeIdentifier(String word) { - return mangleTypeIdentifier(word, false); + return SpecificData.mangleTypeIdentifier(word, false); } /** @@ -1126,40 +1083,21 @@ public static String mangleTypeIdentifier(String word) { * identifiers. */ public static String mangleTypeIdentifier(String word, boolean isError) { - return mangle(word, isError ? ERROR_RESERVED_WORDS : TYPE_IDENTIFIER_RESERVED_WORDS); + return SpecificData.mangle(word, isError); } /** * Utility for template use. Adds a dollar sign to reserved words. */ public static String mangle(String word, Set reservedWords) { - return mangle(word, reservedWords, false); + return SpecificData.mangle(word, reservedWords, false); } /** * Utility for template use. Adds a dollar sign to reserved words. */ public static String mangle(String word, Set reservedWords, boolean isMethod) { - if (StringUtils.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 + RESERVED_WORD_ESCAPE_CHAR; - } - return word; + return SpecificData.mangle(word, reservedWords, isMethod); } /** @@ -1293,8 +1231,7 @@ private static String generateMethodName(Schema schema, Field field, String pref int indexNameConflict = calcNameIndex(field.name(), schema); StringBuilder methodBuilder = new StringBuilder(prefix); - String fieldName = mangle(field.name(), schema.isError() ? ERROR_RESERVED_WORDS : ACCESSOR_MUTATOR_RESERVED_WORDS, - true); + String fieldName = SpecificData.mangleMethod(field.name(), schema.isError()); boolean nextCharToUpper = true; for (int ii = 0; ii < fieldName.length(); ii++) { diff --git a/lang/java/compiler/src/test/java/org/apache/avro/compiler/specific/TestSpecificCompiler.java b/lang/java/compiler/src/test/java/org/apache/avro/compiler/specific/TestSpecificCompiler.java index a55aaec31f3..94f6924035e 100644 --- a/lang/java/compiler/src/test/java/org/apache/avro/compiler/specific/TestSpecificCompiler.java +++ b/lang/java/compiler/src/test/java/org/apache/avro/compiler/specific/TestSpecificCompiler.java @@ -676,9 +676,9 @@ public void testManglingReservedIdentifiers(String schema, boolean throwsTypeExc String dstDirPrefix) throws IOException { Set reservedIdentifiers = new HashSet<>(); reservedIdentifiers.addAll(SpecificData.RESERVED_WORDS); - reservedIdentifiers.addAll(SpecificCompiler.TYPE_IDENTIFIER_RESERVED_WORDS); - reservedIdentifiers.addAll(SpecificCompiler.ACCESSOR_MUTATOR_RESERVED_WORDS); - reservedIdentifiers.addAll(SpecificCompiler.ERROR_RESERVED_WORDS); + reservedIdentifiers.addAll(SpecificData.TYPE_IDENTIFIER_RESERVED_WORDS); + reservedIdentifiers.addAll(SpecificData.ACCESSOR_MUTATOR_RESERVED_WORDS); + reservedIdentifiers.addAll(SpecificData.ERROR_RESERVED_WORDS); for (String reserved : reservedIdentifiers) { try { Schema s = new Schema.Parser().parse(schema.replace("__test__", reserved)); diff --git a/pom.xml b/pom.xml index 392e9b4d032..f7a191b99b5 100644 --- a/pom.xml +++ b/pom.xml @@ -395,6 +395,7 @@ lang/rust/.requirements-precommit.txt lang/java/avro/src/test/java/org/apache/avro/specific/TestRecordWithLogicalTypes.java lang/java/avro/src/test/java/org/apache/avro/specific/TestRecordWithoutLogicalTypes.java + lang/java/avro/src/test/java/org/apache/avro/specific/int$.java lang/java/ipc-netty/src/test/resources/**/*.txt lang/java/tools/src/test/compiler/output-string/avro/examples/baseball/NullSafeAnnotationsFieldsTest.java lang/java/tools/src/test/compiler/output-string/avro/examples/baseball/Player.java