+ ${maven.compiler.source}
+ ${maven.compiler.target}
+ ${project.build.sourceEncoding}
${project.version}
+ ${maven.compiler.source}
+ ${maven.compiler.target}
${jackson-bom.version}
${junit5.version}
1.2.3
@@ -137,6 +142,10 @@
org.apache.maven.plugins
maven-compiler-plugin
+
+ \${maven.compiler.source}
+ \${maven.compiler.target}
+
diff --git a/lang/java/avro/pom.xml b/lang/java/avro/pom.xml
index 541a73df8be..b96673d1851 100644
--- a/lang/java/avro/pom.xml
+++ b/lang/java/avro/pom.xml
@@ -24,7 +24,7 @@
avro-parent
org.apache.avro
1.12.0-SNAPSHOT
- ../
+ ../pom.xml
avro
diff --git a/lang/java/avro/src/main/java/org/apache/avro/Conversion.java b/lang/java/avro/src/main/java/org/apache/avro/Conversion.java
index 4ae75f4a5cb..934672e7d30 100644
--- a/lang/java/avro/src/main/java/org/apache/avro/Conversion.java
+++ b/lang/java/avro/src/main/java/org/apache/avro/Conversion.java
@@ -21,6 +21,9 @@
import java.nio.ByteBuffer;
import java.util.Collection;
import java.util.Map;
+import java.util.ServiceLoader;
+
+import org.apache.avro.generic.GenericData;
import org.apache.avro.generic.GenericEnumSymbol;
import org.apache.avro.generic.GenericFixed;
import org.apache.avro.generic.IndexedRecord;
@@ -28,23 +31,33 @@
/**
* Conversion between generic and logical type instances.
*
- * Instances of this class are added to GenericData to convert a logical type to
- * a particular representation.
+ * Instances of this class can be added to GenericData to convert a logical type
+ * to a particular representation. This can be done manually, using
+ * {@link GenericData#addLogicalTypeConversion(Conversion)}, or automatically.
+ * This last option uses the Java {@link ServiceLoader}, and requires the
+ * implementation to be a public class with a public no-arg constructor, be
+ * named in a file called {@code /META-INF/services/org.apache.avro.Conversion},
+ * and both must available in the classpath.
*
- * Implementations must provide: * {@link #getConvertedType()}: get the Java
- * class used for the logical type * {@link #getLogicalTypeName()}: get the
- * logical type this implements
+ * Implementations must provide:
+ *
+ * {@link #getConvertedType()}: get the Java class used for the logical
+ * type
+ * {@link #getLogicalTypeName()}: get the logical type this implements
+ *
*
- * Subclasses must also override all of the conversion methods for Avro's base
- * types that are valid for the logical type, or else risk causing
+ * Subclasses must also override the conversion methods for Avro's base types
+ * that are valid for the logical type, or else risk causing
* {@code UnsupportedOperationException} at runtime.
*
* Optionally, use {@link #getRecommendedSchema()} to provide a Schema that will
- * be used when a Schema is generated for the class returned by
- * {@code getConvertedType}.
+ * be used when generating a Schema for the class. This is useful when using
+ * {@code ReflectData} or {@code ProtobufData}, for example.
*
- * @param a Java type that generic data is converted to
+ * @param a Java type that can represent the named logical type
+ * @see ServiceLoader
*/
+@SuppressWarnings("unused")
public abstract class Conversion {
/**
@@ -65,9 +78,9 @@ public abstract class Conversion {
* Certain logical types may require adjusting the code within the "setter"
* methods to make sure the data that is set is properly formatted. This method
* allows the Conversion to generate custom setter code if required.
- *
- * @param varName
- * @param valParamName
+ *
+ * @param varName the name of the variable holding the converted value
+ * @param valParamName the name of the parameter with the new converted value
* @return a String for the body of the setter method
*/
public String adjustAndSetValue(String varName, String valParamName) {
@@ -102,7 +115,7 @@ public T fromCharSequence(CharSequence value, Schema schema, LogicalType type) {
throw new UnsupportedOperationException("fromCharSequence is not supported for " + type.getName());
}
- public T fromEnumSymbol(GenericEnumSymbol value, Schema schema, LogicalType type) {
+ public T fromEnumSymbol(GenericEnumSymbol> value, Schema schema, LogicalType type) {
throw new UnsupportedOperationException("fromEnumSymbol is not supported for " + type.getName());
}
@@ -150,7 +163,7 @@ public CharSequence toCharSequence(T value, Schema schema, LogicalType type) {
throw new UnsupportedOperationException("toCharSequence is not supported for " + type.getName());
}
- public GenericEnumSymbol toEnumSymbol(T value, Schema schema, LogicalType type) {
+ public GenericEnumSymbol> toEnumSymbol(T value, Schema schema, LogicalType type) {
throw new UnsupportedOperationException("toEnumSymbol is not supported for " + type.getName());
}
diff --git a/lang/java/avro/src/main/java/org/apache/avro/Conversions.java b/lang/java/avro/src/main/java/org/apache/avro/Conversions.java
index 1c28c9adb81..7d01fc62a37 100644
--- a/lang/java/avro/src/main/java/org/apache/avro/Conversions.java
+++ b/lang/java/avro/src/main/java/org/apache/avro/Conversions.java
@@ -106,11 +106,12 @@ public GenericFixed toFixed(BigDecimal value, Schema schema, LogicalType type) {
byte fillByte = (byte) (value.signum() < 0 ? 0xFF : 0x00);
byte[] unscaled = value.unscaledValue().toByteArray();
byte[] bytes = new byte[schema.getFixedSize()];
- int offset = bytes.length - unscaled.length;
+ int unscaledLength = unscaled.length;
+ int offset = bytes.length - unscaledLength;
- // Fill the front of the array and copy remaining with unscaled values
+ // Fill the front with the filler and copy the unscaled value into the remainder
Arrays.fill(bytes, 0, offset, fillByte);
- System.arraycopy(unscaled, 0, bytes, offset, bytes.length - offset);
+ System.arraycopy(unscaled, 0, bytes, offset, unscaledLength);
return new GenericData.Fixed(schema, bytes);
}
@@ -147,7 +148,7 @@ private static BigDecimal validate(final LogicalTypes.Decimal decimal, BigDecima
}
/**
- * Convert a underlying representation of a logical type (such as a ByteBuffer)
+ * Convert an underlying representation of a logical type (such as a ByteBuffer)
* to a higher level object (such as a BigDecimal).
*
* @param datum The object to be converted.
@@ -157,9 +158,9 @@ private static BigDecimal validate(final LogicalTypes.Decimal decimal, BigDecima
* @param conversion The tool used to finish the conversion. Cannot be null if
* datum is not null.
* @return The result object, which is a high level object of the logical type.
- * If a null datum is passed in, a null value will be returned.
- * @throws IllegalArgumentException if a null schema, type or conversion is
- * passed in while datum is not null.
+ * The null datum always converts to a null value.
+ * @throws IllegalArgumentException if datum is not null, but schema, type or
+ * conversion is.
*/
public static Object convertToLogicalType(Object datum, Schema schema, LogicalType type, Conversion> conversion) {
if (datum == null) {
@@ -176,9 +177,9 @@ public static Object convertToLogicalType(Object datum, Schema schema, LogicalTy
case RECORD:
return conversion.fromRecord((IndexedRecord) datum, schema, type);
case ENUM:
- return conversion.fromEnumSymbol((GenericEnumSymbol) datum, schema, type);
+ return conversion.fromEnumSymbol((GenericEnumSymbol>) datum, schema, type);
case ARRAY:
- return conversion.fromArray((Collection) datum, schema, type);
+ return conversion.fromArray((Collection>) datum, schema, type);
case MAP:
return conversion.fromMap((Map, ?>) datum, schema, type);
case FIXED:
@@ -201,13 +202,13 @@ public static Object convertToLogicalType(Object datum, Schema schema, LogicalTy
return datum;
} catch (ClassCastException e) {
throw new AvroRuntimeException(
- "Cannot convert " + datum + ":" + datum.getClass().getSimpleName() + ": expected generic type", e);
+ "Cannot convert " + datum + ':' + datum.getClass().getSimpleName() + ": expected generic type", e);
}
}
/**
* Convert a high level representation of a logical type (such as a BigDecimal)
- * to the its underlying representation object (such as a ByteBuffer)
+ * to its underlying representation object (such as a ByteBuffer)
*
* @param datum The object to be converted.
* @param schema The schema of datum. Cannot be null if datum is not null.
@@ -218,8 +219,8 @@ public static Object convertToLogicalType(Object datum, Schema schema, LogicalTy
* @return The result object, which is an underlying representation object of
* the logical type. If the input param datum is null, a null value will
* be returned.
- * @throws IllegalArgumentException if a null schema, type or conversion is
- * passed in while datum is not null.
+ * @throws IllegalArgumentException if datum is not null, but schema, type or
+ * conversion is.
*/
public static Object convertToRawType(Object datum, Schema schema, LogicalType type, Conversion conversion) {
if (datum == null) {
@@ -262,7 +263,7 @@ public static Object convertToRawType(Object datum, Schema schema, LogicalTy
return datum;
} catch (ClassCastException e) {
throw new AvroRuntimeException(
- "Cannot convert " + datum + ":" + datum.getClass().getSimpleName() + ": expected logical type", e);
+ "Cannot convert " + datum + ':' + datum.getClass().getSimpleName() + ": expected logical type", e);
}
}
diff --git a/lang/java/avro/src/main/java/org/apache/avro/JsonProperties.java b/lang/java/avro/src/main/java/org/apache/avro/JsonProperties.java
index b53bc6cb2ba..300e583b40a 100644
--- a/lang/java/avro/src/main/java/org/apache/avro/JsonProperties.java
+++ b/lang/java/avro/src/main/java/org/apache/avro/JsonProperties.java
@@ -30,6 +30,7 @@
import java.util.concurrent.ConcurrentMap;
import java.io.IOException;
+import java.util.function.BiConsumer;
import org.apache.avro.util.internal.Accessor;
import org.apache.avro.util.internal.Accessor.JsonPropertiesAccessor;
@@ -241,6 +242,11 @@ public Object getObjectProp(String name) {
return JacksonUtils.toObject(props.get(name));
}
+ public Object getObjectProp(String name, Object defaultValue) {
+ final JsonNode json = props.get(name);
+ return json != null ? JacksonUtils.toObject(json) : defaultValue;
+ }
+
/**
* Adds a property with the given name name and value value .
* Neither name nor value can be null . It is illegal
@@ -307,6 +313,17 @@ public Map getObjectProps() {
return Collections.unmodifiableMap(result);
}
+ public boolean propsContainsKey(String key) {
+ return this.props.containsKey(key);
+ }
+
+ public void forEachProperty(BiConsumer consumer) {
+ for (Map.Entry entry : this.props.entrySet()) {
+ final Object value = JacksonUtils.toObject(entry.getValue());
+ consumer.accept(entry.getKey(), value);
+ }
+ }
+
void writeProps(JsonGenerator gen) throws IOException {
for (Map.Entry e : props.entrySet())
gen.writeObjectField(e.getKey(), e.getValue());
diff --git a/lang/java/avro/src/main/java/org/apache/avro/LogicalTypes.java b/lang/java/avro/src/main/java/org/apache/avro/LogicalTypes.java
index 7bb00f819b5..086c5d266a2 100644
--- a/lang/java/avro/src/main/java/org/apache/avro/LogicalTypes.java
+++ b/lang/java/avro/src/main/java/org/apache/avro/LogicalTypes.java
@@ -329,7 +329,7 @@ private long maxPrecision(Schema schema) {
}
private boolean hasProperty(Schema schema, String name) {
- return (schema.getObjectProp(name) != null);
+ return schema.propsContainsKey(name);
}
private int getInt(Schema schema, String name) {
diff --git a/lang/java/avro/src/main/java/org/apache/avro/Protocol.java b/lang/java/avro/src/main/java/org/apache/avro/Protocol.java
index ff996889517..f99df533bd7 100644
--- a/lang/java/avro/src/main/java/org/apache/avro/Protocol.java
+++ b/lang/java/avro/src/main/java/org/apache/avro/Protocol.java
@@ -174,7 +174,6 @@ public int hashCode() {
public String getDoc() {
return doc;
}
-
}
private class TwoWayMessage extends Message {
@@ -274,15 +273,29 @@ public Protocol(Protocol p) {
public Protocol(String name, String doc, String namespace) {
super(PROTOCOL_RESERVED);
- this.name = name;
+ setName(name, namespace);
this.doc = doc;
- this.namespace = namespace;
}
public Protocol(String name, String namespace) {
this(name, null, namespace);
}
+ private void setName(String name, String namespace) {
+ int lastDot = name.lastIndexOf('.');
+ if (lastDot < 0) {
+ this.name = name;
+ this.namespace = namespace;
+ } else {
+ this.name = name.substring(lastDot + 1);
+ this.namespace = name.substring(0, lastDot);
+ }
+ if (this.namespace != null && this.namespace.isEmpty()) {
+ this.namespace = null;
+ }
+ types.space(this.namespace);
+ }
+
/** The name of this protocol. */
public String getName() {
return name;
@@ -452,7 +465,9 @@ public byte[] getMD5() {
/** Read a protocol from a Json file. */
public static Protocol parse(File file) throws IOException {
- return parse(Schema.FACTORY.createParser(file));
+ try (JsonParser jsonParser = Schema.FACTORY.createParser(file)) {
+ return parse(jsonParser);
+ }
}
/** Read a protocol from a Json stream. */
@@ -488,20 +503,22 @@ private static Protocol parse(JsonParser parser) {
}
private void parse(JsonNode json) {
- parseNamespace(json);
- parseName(json);
+ parseNameAndNamespace(json);
parseTypes(json);
parseMessages(json);
parseDoc(json);
parseProps(json);
}
- private void parseNamespace(JsonNode json) {
- JsonNode nameNode = json.get("namespace");
- if (nameNode == null)
- return; // no namespace defined
- this.namespace = nameNode.textValue();
- types.space(this.namespace);
+ private void parseNameAndNamespace(JsonNode json) {
+ JsonNode nameNode = json.get("protocol");
+ if (nameNode == null) {
+ throw new SchemaParseException("No protocol name specified: " + json);
+ }
+ JsonNode namespaceNode = json.get("namespace");
+ String namespace = namespaceNode == null ? null : namespaceNode.textValue();
+
+ setName(nameNode.textValue(), namespace);
}
private void parseDoc(JsonNode json) {
@@ -515,23 +532,21 @@ private String parseDocNode(JsonNode json) {
return nameNode.textValue();
}
- private void parseName(JsonNode json) {
- JsonNode nameNode = json.get("protocol");
- if (nameNode == null)
- throw new SchemaParseException("No protocol name specified: " + json);
- this.name = nameNode.textValue();
- }
-
private void parseTypes(JsonNode json) {
JsonNode defs = json.get("types");
if (defs == null)
return; // no types defined
if (!defs.isArray())
throw new SchemaParseException("Types not an array: " + defs);
+
for (JsonNode type : defs) {
if (!type.isObject())
throw new SchemaParseException("Type not an object: " + type);
- Schema.parse(type, types);
+ Schema.parseNamesDeclared(type, types, types.space());
+
+ }
+ for (JsonNode type : defs) {
+ Schema.parseCompleteSchema(type, types, types.space());
}
}
diff --git a/lang/java/avro/src/main/java/org/apache/avro/Schema.java b/lang/java/avro/src/main/java/org/apache/avro/Schema.java
index f6c3de7684e..38a6e4a9e42 100644
--- a/lang/java/avro/src/main/java/org/apache/avro/Schema.java
+++ b/lang/java/avro/src/main/java/org/apache/avro/Schema.java
@@ -26,11 +26,13 @@
import com.fasterxml.jackson.databind.node.DoubleNode;
import com.fasterxml.jackson.databind.node.NullNode;
+import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.Serializable;
import java.io.StringWriter;
+import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
@@ -46,6 +48,9 @@
import java.util.Map;
import java.util.Objects;
import java.util.Set;
+import java.util.stream.Collectors;
+import java.util.stream.StreamSupport;
+
import org.apache.avro.util.internal.Accessor;
import org.apache.avro.util.internal.Accessor.FieldAccessor;
import org.apache.avro.util.internal.JacksonUtils;
@@ -694,6 +699,95 @@ private boolean defaultValueEquals(JsonNode thatDefaultValue) {
public String toString() {
return name + " type:" + schema.type + " pos:" + position;
}
+
+ /**
+ * Parse field.
+ *
+ * @param field : json field definition.
+ * @param names : names map.
+ * @param namespace : current working namespace.
+ * @return field.
+ */
+ static Field parse(JsonNode field, Names names, String namespace) {
+ String fieldName = getRequiredText(field, "name", "No field name");
+ String fieldDoc = getOptionalText(field, "doc");
+ JsonNode fieldTypeNode = field.get("type");
+ if (fieldTypeNode == null) {
+ throw new SchemaParseException("No field type: " + field);
+ }
+
+ Schema fieldSchema = null;
+ if (fieldTypeNode.isTextual()) {
+ Schema schemaField = names.get(fieldTypeNode.textValue());
+ if (schemaField == null) {
+ schemaField = names.get(namespace + "." + fieldTypeNode.textValue());
+ }
+ if (schemaField == null) {
+ throw new SchemaParseException(fieldTypeNode + " is not a defined name." + " The type of the \"" + fieldName
+ + "\" field must be a defined name or a {\"type\": ...} expression.");
+ }
+ fieldSchema = schemaField;
+ } else if (fieldTypeNode.isObject()) {
+ fieldSchema = resolveSchema(fieldTypeNode, names, namespace);
+ if (fieldSchema == null) {
+ fieldSchema = Schema.parseCompleteSchema(fieldTypeNode, names, namespace);
+ }
+ } else if (fieldTypeNode.isArray()) {
+ List unionTypes = new ArrayList<>();
+
+ fieldTypeNode.forEach((JsonNode node) -> {
+ Schema subSchema = null;
+ if (node.isTextual()) {
+ subSchema = names.get(node.asText());
+ if (subSchema == null) {
+ subSchema = names.get(namespace + "." + node.asText());
+ }
+ } else if (node.isObject()) {
+ subSchema = Schema.parseCompleteSchema(node, names, namespace);
+ } else {
+ throw new SchemaParseException("Illegal type in union : " + node);
+ }
+ if (subSchema == null) {
+ throw new SchemaParseException("Null element in union : " + node);
+ }
+ unionTypes.add(subSchema);
+ });
+
+ fieldSchema = Schema.createUnion(unionTypes);
+ }
+
+ if (fieldSchema == null) {
+ throw new SchemaParseException("Can't find type for field " + fieldName);
+ }
+ Field.Order order = Field.Order.ASCENDING;
+ JsonNode orderNode = field.get("order");
+ if (orderNode != null)
+ order = Field.Order.valueOf(orderNode.textValue().toUpperCase(Locale.ENGLISH));
+ JsonNode defaultValue = field.get("default");
+
+ if (defaultValue != null
+ && (Type.FLOAT.equals(fieldSchema.getType()) || Type.DOUBLE.equals(fieldSchema.getType()))
+ && defaultValue.isTextual()) {
+ try {
+ defaultValue = new DoubleNode(Double.valueOf(defaultValue.textValue()));
+ } catch (NumberFormatException ex) {
+ throw new SchemaParseException(
+ "Can't parse number '" + defaultValue.textValue() + "' for field '" + fieldName);
+ }
+ }
+
+ Field f = new Field(fieldName, fieldSchema, fieldDoc, defaultValue, true, order);
+ Iterator i = field.fieldNames();
+ while (i.hasNext()) { // add field props
+ String prop = i.next();
+ if (!FIELD_RESERVED.contains(prop))
+ f.addProp(prop, field.get(prop));
+ }
+ f.aliases = parseAliases(field);
+
+ return f;
+ }
+
}
static class Name {
@@ -896,8 +990,8 @@ public int hashCode() {
}
}
- private static final ThreadLocal SEEN_EQUALS = ThreadLocalWithInitial.of(HashSet::new);
- private static final ThreadLocal SEEN_HASHCODE = ThreadLocalWithInitial.of(IdentityHashMap::new);
+ private static final ThreadLocal> SEEN_EQUALS = ThreadLocalWithInitial.of(HashSet::new);
+ private static final ThreadLocal> SEEN_HASHCODE = ThreadLocalWithInitial.of(IdentityHashMap::new);
@SuppressWarnings(value = "unchecked")
private static class RecordSchema extends NamedSchema {
@@ -993,7 +1087,7 @@ public boolean equals(Object o) {
@Override
int computeHash() {
- Map seen = SEEN_HASHCODE.get();
+ Map seen = SEEN_HASHCODE.get();
if (seen.containsKey(this))
return 0; // prevent stack overflow
boolean first = seen.isEmpty();
@@ -1015,8 +1109,8 @@ void toJson(Names names, JsonGenerator gen) throws IOException {
gen.writeStringField("type", isError ? "error" : "record");
writeName(names, gen);
names.space = name.space; // set default namespace
- if (getDoc() != null)
- gen.writeStringField("doc", getDoc());
+ if (this.getDoc() != null)
+ gen.writeStringField("doc", this.getDoc());
if (fields != null) {
gen.writeFieldName("fields");
@@ -1238,6 +1332,16 @@ public UnionSchema(LockableArrayList types) {
}
}
+ /**
+ * Checks if a JSON value matches the schema.
+ *
+ * @param jsonValue a value to check against the schema
+ * @return true if the value is valid according to this schema
+ */
+ public boolean isValidDefault(JsonNode jsonValue) {
+ return this.types.stream().anyMatch((Schema s) -> s.isValidDefault(jsonValue));
+ }
+
@Override
public List getTypes() {
return types;
@@ -1278,6 +1382,12 @@ void toJson(Names names, JsonGenerator gen) throws IOException {
type.toJson(names, gen);
gen.writeEndArray();
}
+
+ @Override
+ public String getName() {
+ return super.getName()
+ + this.getTypes().stream().map(Schema::getName).collect(Collectors.joining(", ", "[", "]"));
+ }
}
private static class FixedSchema extends NamedSchema {
@@ -1285,8 +1395,7 @@ private static class FixedSchema extends NamedSchema {
public FixedSchema(Name name, String doc, int size) {
super(Type.FIXED, name, doc);
- if (size < 0)
- throw new IllegalArgumentException("Invalid fixed size: " + size);
+ SystemLimitException.checkMaxBytesLength(size);
this.size = size;
}
@@ -1381,15 +1490,32 @@ public NullSchema() {
*/
public static class Parser {
private Names names = new Names();
- private boolean validate = true;
+ private final Schema.NameValidator validate;
private boolean validateDefaults = true;
+ public Parser() {
+ this(NameValidator.UTF_VALIDATOR);
+ }
+
+ public Parser(final NameValidator validate) {
+ this.validate = validate;
+ }
+
/**
* Adds the provided types to the set of defined, named types known to this
- * parser.
+ * parser. deprecated: use addTypes(Iterable types)
*/
+ @Deprecated
public Parser addTypes(Map types) {
- for (Schema s : types.values())
+ return this.addTypes(types.values());
+ }
+
+ /**
+ * Adds the provided types to the set of defined, named types known to this
+ * parser.
+ */
+ public Parser addTypes(Iterable types) {
+ for (Schema s : types)
names.add(s);
return this;
}
@@ -1402,17 +1528,6 @@ public Map getTypes() {
return result;
}
- /** Enable or disable name validation. */
- public Parser setValidate(boolean validate) {
- this.validate = validate;
- return this;
- }
-
- /** True iff names are validated. True by default. */
- public boolean getValidate() {
- return this.validate;
- }
-
/** Enable or disable default value validation. */
public Parser setValidateDefaults(boolean validateDefaults) {
this.validateDefaults = validateDefaults;
@@ -1429,7 +1544,21 @@ public boolean getValidateDefaults() {
* names known to this parser.
*/
public Schema parse(File file) throws IOException {
- return parse(FACTORY.createParser(file));
+ return parse(FACTORY.createParser(file), false);
+ }
+
+ public List parse(Iterable sources) throws IOException {
+ final List schemas = new ArrayList<>();
+ for (File source : sources) {
+ final Schema emptySchema = parseNamesDeclared(FACTORY.createParser(source));
+ schemas.add(emptySchema);
+ }
+
+ for (File source : sources) {
+ parseFieldsOnly(FACTORY.createParser(source));
+ }
+
+ return schemas;
}
/**
@@ -1437,7 +1566,7 @@ public Schema parse(File file) throws IOException {
* names known to this parser. The input stream stays open after the parsing.
*/
public Schema parse(InputStream in) throws IOException {
- return parse(FACTORY.createParser(in).disable(JsonParser.Feature.AUTO_CLOSE_SOURCE));
+ return parse(FACTORY.createParser(in).disable(JsonParser.Feature.AUTO_CLOSE_SOURCE), true);
}
/** Read a schema from one or more json strings */
@@ -1454,19 +1583,24 @@ public Schema parse(String s, String... more) {
*/
public Schema parse(String s) {
try {
- return parse(FACTORY.createParser(s));
+ return parse(FACTORY.createParser(s), false);
} catch (IOException e) {
throw new SchemaParseException(e);
}
}
- private Schema parse(JsonParser parser) throws IOException {
- boolean saved = validateNames.get();
+ private static interface ParseFunction {
+ Schema parse(JsonNode node) throws IOException;
+ }
+
+ private Schema runParser(JsonParser parser, ParseFunction f) throws IOException {
+ NameValidator saved = validateNames.get();
boolean savedValidateDefaults = VALIDATE_DEFAULTS.get();
try {
validateNames.set(validate);
VALIDATE_DEFAULTS.set(validateDefaults);
- return Schema.parse(MAPPER.readTree(parser), names);
+ JsonNode jsonNode = MAPPER.readTree(parser);
+ return f.parse(jsonNode);
} catch (JsonParseException e) {
throw new SchemaParseException(e);
} finally {
@@ -1475,6 +1609,37 @@ private Schema parse(JsonParser parser) throws IOException {
VALIDATE_DEFAULTS.set(savedValidateDefaults);
}
}
+
+ private Schema parse(JsonParser parser, final boolean allowDanglingContent) throws IOException {
+ return this.runParser(parser, (JsonNode jsonNode) -> {
+ Schema schema = Schema.parse(jsonNode, names);
+ if (!allowDanglingContent) {
+ String dangling;
+ StringWriter danglingWriter = new StringWriter();
+ int numCharsReleased = parser.releaseBuffered(danglingWriter);
+ if (numCharsReleased == -1) {
+ ByteArrayOutputStream danglingOutputStream = new ByteArrayOutputStream();
+ parser.releaseBuffered(danglingOutputStream); // if input isnt chars above it must be bytes
+ dangling = new String(danglingOutputStream.toByteArray(), StandardCharsets.UTF_8).trim();
+ } else {
+ dangling = danglingWriter.toString().trim();
+ }
+ if (!dangling.isEmpty()) {
+ throw new SchemaParseException("dangling content after end of schema: " + dangling);
+ }
+ }
+ return schema;
+ });
+ }
+
+ private Schema parseNamesDeclared(JsonParser parser) throws IOException {
+ return this.runParser(parser, (JsonNode jsonNode) -> Schema.parseNamesDeclared(jsonNode, names, names.space));
+ }
+
+ private Schema parseFieldsOnly(JsonParser parser) throws IOException {
+ return this.runParser(parser, (JsonNode jsonNode) -> Schema.parseCompleteSchema(jsonNode, names, names.space));
+ }
+
}
/**
@@ -1525,7 +1690,8 @@ public static Schema parse(String jsonSchema) {
*/
@Deprecated
public static Schema parse(String jsonSchema, boolean validate) {
- return new Parser().setValidate(validate).parse(jsonSchema);
+ final NameValidator validator = validate ? NameValidator.UTF_VALIDATOR : NameValidator.NO_VALIDATION;
+ return new Parser(validator).parse(jsonSchema);
}
static final Map PRIMITIVES = new HashMap<>();
@@ -1582,43 +1748,53 @@ public void add(Schema schema) {
@Override
public Schema put(Name name, Schema schema) {
- if (containsKey(name))
- throw new SchemaParseException("Can't redefine: " + name);
+ if (containsKey(name)) {
+ final Schema other = super.get(name);
+ if (!Objects.equals(other, schema)) {
+ throw new SchemaParseException("Can't redefine: " + name);
+ } else {
+ return schema;
+ }
+ }
return super.put(name, schema);
}
}
- private static ThreadLocal validateNames = ThreadLocalWithInitial.of(() -> true);
+ private static ThreadLocal validateNames = ThreadLocalWithInitial
+ .of(() -> NameValidator.UTF_VALIDATOR);
private static String validateName(String name) {
- if (!validateNames.get())
- return name; // not validating names
- if (name == null)
- throw new SchemaParseException("Null name");
- int length = name.length();
- if (length == 0)
- throw new SchemaParseException("Empty name");
- char first = name.charAt(0);
- if (!(Character.isLetter(first) || first == '_'))
- throw new SchemaParseException("Illegal initial character: " + name);
- for (int i = 1; i < length; i++) {
- char c = name.charAt(i);
- if (!(Character.isLetterOrDigit(c) || c == '_'))
- throw new SchemaParseException("Illegal character in: " + name);
+ NameValidator.Result result = validateNames.get().validate(name);
+ if (!result.isOK()) {
+ throw new SchemaParseException(result.errors);
}
return name;
}
+ public static void setNameValidator(final Schema.NameValidator validator) {
+ Schema.validateNames.set(validator);
+ }
+
private static final ThreadLocal VALIDATE_DEFAULTS = ThreadLocalWithInitial.of(() -> true);
private static JsonNode validateDefault(String fieldName, Schema schema, JsonNode defaultValue) {
- if (VALIDATE_DEFAULTS.get() && (defaultValue != null) && !isValidDefault(schema, defaultValue)) { // invalid default
+ if (VALIDATE_DEFAULTS.get() && (defaultValue != null) && !schema.isValidDefault(defaultValue)) { // invalid default
String message = "Invalid default for field " + fieldName + ": " + defaultValue + " not a " + schema;
throw new AvroTypeException(message); // throw exception
}
return defaultValue;
}
+ /**
+ * Checks if a JSON value matches the schema.
+ *
+ * @param jsonValue a value to check against the schema
+ * @return true if the value is valid according to this schema
+ */
+ public boolean isValidDefault(JsonNode jsonValue) {
+ return isValidDefault(this, jsonValue);
+ }
+
private static boolean isValidDefault(Schema schema, JsonNode defaultValue) {
if (defaultValue == null)
return false;
@@ -1653,13 +1829,13 @@ private static boolean isValidDefault(Schema schema, JsonNode defaultValue) {
if (!isValidDefault(schema.getValueType(), value))
return false;
return true;
- case UNION: // union default: first branch
- return isValidDefault(schema.getTypes().get(0), defaultValue);
+ case UNION: // union default: any branch
+ return schema.getTypes().stream().anyMatch((Schema s) -> isValidValue(s, defaultValue));
case RECORD:
if (!defaultValue.isObject())
return false;
for (Field field : schema.getFields())
- if (!isValidDefault(field.schema(),
+ if (!isValidValue(field.schema(),
defaultValue.has(field.name()) ? defaultValue.get(field.name()) : field.defaultValue()))
return false;
return true;
@@ -1668,78 +1844,71 @@ private static boolean isValidDefault(Schema schema, JsonNode defaultValue) {
}
}
- /** @see #parse(String) */
- static Schema parse(JsonNode schema, Names names) {
+ /**
+ * Validate a value against the schema.
+ *
+ * @param schema : schema for value.
+ * @param value : value to validate.
+ * @return true if ok.
+ */
+ private static boolean isValidValue(Schema schema, JsonNode value) {
+ if (value == null)
+ return false;
+ if (schema.isUnion()) {
+ // For Union, only need that one sub schema is ok.
+ for (Schema sub : schema.getTypes()) {
+ if (Schema.isValidDefault(sub, value)) {
+ return true;
+ }
+ }
+ return false;
+ } else {
+ // for other types, same as validate default.
+ return Schema.isValidDefault(schema, value);
+ }
+ }
+
+ /**
+ * Parse named schema in order to fill names map. This method does not parse
+ * field of record/error schema.
+ *
+ * @param schema : json schema representation.
+ * @param names : map of named schema.
+ * @param currentNameSpace : current working name space.
+ * @return schema.
+ */
+ static Schema parseNamesDeclared(JsonNode schema, Names names, String currentNameSpace) {
if (schema == null) {
- throw new SchemaParseException("Cannot parse schema");
+ return null;
}
- if (schema.isTextual()) { // name
- Schema result = names.get(schema.textValue());
- if (result == null)
- throw new SchemaParseException("Undefined name: " + schema);
- return result;
- } else if (schema.isObject()) {
- Schema result;
- String type = getRequiredText(schema, "type", "No type");
+ if (schema.isObject()) {
+
+ String type = Schema.getOptionalText(schema, "type");
Name name = null;
- String savedSpace = names.space();
+
String doc = null;
+ Schema result = null;
final boolean isTypeError = "error".equals(type);
final boolean isTypeRecord = "record".equals(type);
final boolean isTypeEnum = "enum".equals(type);
final boolean isTypeFixed = "fixed".equals(type);
+
if (isTypeRecord || isTypeError || isTypeEnum || isTypeFixed) {
String space = getOptionalText(schema, "namespace");
doc = getOptionalText(schema, "doc");
if (space == null)
- space = savedSpace;
+ space = currentNameSpace;
name = new Name(getRequiredText(schema, "name", "No name in schema"), space);
- names.space(name.space); // set default namespace
}
- if (PRIMITIVES.containsKey(type)) { // primitive
- result = create(PRIMITIVES.get(type));
- } else if (isTypeRecord || isTypeError) { // record
- List fields = new ArrayList<>();
+ if (isTypeRecord || isTypeError) { // record
result = new RecordSchema(name, doc, isTypeError);
- if (name != null)
- names.add(result);
+ names.add(result);
JsonNode fieldsNode = schema.get("fields");
+
if (fieldsNode == null || !fieldsNode.isArray())
throw new SchemaParseException("Record has no fields: " + schema);
- for (JsonNode field : fieldsNode) {
- String fieldName = getRequiredText(field, "name", "No field name");
- String fieldDoc = getOptionalText(field, "doc");
- JsonNode fieldTypeNode = field.get("type");
- if (fieldTypeNode == null)
- throw new SchemaParseException("No field type: " + field);
- if (fieldTypeNode.isTextual() && names.get(fieldTypeNode.textValue()) == null)
- throw new SchemaParseException(fieldTypeNode + " is not a defined name." + " The type of the \"" + fieldName
- + "\" field must be a defined name or a {\"type\": ...} expression.");
- Schema fieldSchema = parse(fieldTypeNode, names);
- Field.Order order = Field.Order.ASCENDING;
- JsonNode orderNode = field.get("order");
- if (orderNode != null)
- order = Field.Order.valueOf(orderNode.textValue().toUpperCase(Locale.ENGLISH));
- JsonNode defaultValue = field.get("default");
- if (defaultValue != null
- && (Type.FLOAT.equals(fieldSchema.getType()) || Type.DOUBLE.equals(fieldSchema.getType()))
- && defaultValue.isTextual())
- defaultValue = new DoubleNode(Double.valueOf(defaultValue.textValue()));
- Field f = new Field(fieldName, fieldSchema, fieldDoc, defaultValue, true, order);
- Iterator i = field.fieldNames();
- while (i.hasNext()) { // add field props
- String prop = i.next();
- if (!FIELD_RESERVED.contains(prop))
- f.addProp(prop, field.get(prop));
- }
- f.aliases = parseAliases(field);
- fields.add(f);
- if (fieldSchema.getLogicalType() == null && getOptionalText(field, LOGICAL_TYPE_PROP) != null)
- LOG.warn(
- "Ignored the {}.{}.logicalType property (\"{}\"). It should probably be nested inside the \"type\" for the field.",
- name, fieldName, getOptionalText(field, "logicalType"));
- }
- result.setFields(fields);
+ exploreFields(fieldsNode, names, name != null ? name.space : null);
+
} else if (isTypeEnum) { // enum
JsonNode symbolsNode = schema.get("symbols");
if (symbolsNode == null || !symbolsNode.isArray())
@@ -1752,18 +1921,19 @@ static Schema parse(JsonNode schema, Names names) {
if (enumDefault != null)
defaultSymbol = enumDefault.textValue();
result = new EnumSchema(name, doc, symbols, defaultSymbol);
- if (name != null)
- names.add(result);
+ names.add(result);
} else if (type.equals("array")) { // array
JsonNode itemsNode = schema.get("items");
if (itemsNode == null)
throw new SchemaParseException("Array has no items type: " + schema);
- result = new ArraySchema(parse(itemsNode, names));
+ final Schema items = Schema.parseNamesDeclared(itemsNode, names, currentNameSpace);
+ result = Schema.createArray(items);
} else if (type.equals("map")) { // map
JsonNode valuesNode = schema.get("values");
if (valuesNode == null)
throw new SchemaParseException("Map has no values type: " + schema);
- result = new MapSchema(parse(valuesNode, names));
+ final Schema values = Schema.parseNamesDeclared(valuesNode, names, currentNameSpace);
+ result = Schema.createMap(values);
} else if (isTypeFixed) { // fixed
JsonNode sizeNode = schema.get("size");
if (sizeNode == null || !sizeNode.isInt())
@@ -1771,42 +1941,194 @@ static Schema parse(JsonNode schema, Names names) {
result = new FixedSchema(name, doc, sizeNode.intValue());
if (name != null)
names.add(result);
- } else { // For unions with self reference
- Name nameFromType = new Name(type, names.space);
- if (names.containsKey(nameFromType)) {
- return names.get(nameFromType);
+ } else if (PRIMITIVES.containsKey(type)) {
+ result = Schema.create(PRIMITIVES.get(type));
+ }
+ if (result != null) {
+ Set reserved = SCHEMA_RESERVED;
+ if (isTypeEnum) {
+ reserved = ENUM_RESERVED;
}
- throw new SchemaParseException("Type not supported: " + type);
+ Schema.addProperties(schema, reserved, result);
}
- Iterator i = schema.fieldNames();
+ return result;
+ } else if (schema.isArray()) {
+ List subs = new ArrayList<>(schema.size());
+ schema.forEach((JsonNode item) -> {
+ Schema sub = Schema.parseNamesDeclared(item, names, currentNameSpace);
+ if (sub != null) {
+ subs.add(sub);
+ }
+ });
+ return Schema.createUnion(subs);
+ } else if (schema.isTextual()) {
+ String value = schema.asText();
+ return names.get(value);
+ }
+ return null;
+ }
- Set reserved = SCHEMA_RESERVED;
- if (isTypeEnum) {
- reserved = ENUM_RESERVED;
+ private static void addProperties(JsonNode schema, Set reserved, Schema avroSchema) {
+ Iterator i = schema.fieldNames();
+ while (i.hasNext()) { // add properties
+ String prop = i.next();
+ if (!reserved.contains(prop)) // ignore reserved
+ avroSchema.addProp(prop, schema.get(prop));
+ }
+ // parse logical type if present
+ avroSchema.logicalType = LogicalTypes.fromSchemaIgnoreInvalid(avroSchema);
+ // names.space(savedSpace); // restore space
+ if (avroSchema instanceof NamedSchema) {
+ Set aliases = parseAliases(schema);
+ if (aliases != null) // add aliases
+ for (String alias : aliases)
+ avroSchema.addAlias(alias);
+ }
+ }
+
+ /**
+ * Explore record fields in order to fill names map with inner defined named
+ * types.
+ *
+ * @param fieldsNode : json node for field.
+ * @param names : names map.
+ * @param nameSpace : current working namespace.
+ */
+ private static void exploreFields(JsonNode fieldsNode, Names names, String nameSpace) {
+ for (JsonNode field : fieldsNode) {
+ final JsonNode fieldType = field.get("type");
+ if (fieldType != null) {
+ if (fieldType.isObject()) {
+ parseNamesDeclared(fieldType, names, nameSpace);
+ } else if (fieldType.isArray()) {
+ exploreFields(fieldType, names, nameSpace);
+ } else if (fieldType.isTextual() && field.isObject()) {
+ parseNamesDeclared(field, names, nameSpace);
+ }
}
- while (i.hasNext()) { // add properties
- String prop = i.next();
- if (!reserved.contains(prop)) // ignore reserved
- result.addProp(prop, schema.get(prop));
+ }
+ }
+
+ /**
+ * in complement of parseNamesDeclared, this method parse schema in details.
+ *
+ * @param schema : json schema.
+ * @param names : names map.
+ * @param currentSpace : current working name space.
+ * @return complete schema.
+ */
+ static Schema parseCompleteSchema(JsonNode schema, Names names, String currentSpace) {
+ if (schema == null) {
+ throw new SchemaParseException("Cannot parse schema");
+ }
+ if (schema.isTextual()) {
+ String type = schema.asText();
+ Schema avroSchema = names.get(type);
+ if (avroSchema == null) {
+ avroSchema = names.get(currentSpace + "." + type);
+ }
+ return avroSchema;
+ }
+ if (schema.isArray()) {
+ List schemas = StreamSupport.stream(schema.spliterator(), false)
+ .map((JsonNode sub) -> parseCompleteSchema(sub, names, currentSpace)).collect(Collectors.toList());
+ return Schema.createUnion(schemas);
+ }
+ if (schema.isObject()) {
+ Schema result = null;
+ String type = getRequiredText(schema, "type", "No type");
+ Name name = null;
+
+ final boolean isTypeError = "error".equals(type);
+ final boolean isTypeRecord = "record".equals(type);
+ final boolean isTypeArray = "array".equals(type);
+
+ if (isTypeRecord || isTypeError || "enum".equals(type) || "fixed".equals(type)) {
+ // named schema
+ String space = getOptionalText(schema, "namespace");
+
+ if (space == null)
+ space = currentSpace;
+ name = new Name(getRequiredText(schema, "name", "No name in schema"), space);
+
+ result = names.get(name);
+ if (result == null) {
+ throw new SchemaParseException("Unparsed field type " + name);
+ }
+ }
+ if (isTypeRecord || isTypeError) {
+ if (result != null && !result.hasFields()) {
+ final List fields = new ArrayList<>();
+ JsonNode fieldsNode = schema.get("fields");
+ if (fieldsNode == null || !fieldsNode.isArray())
+ throw new SchemaParseException("Record has no fields: " + schema);
+
+ for (JsonNode field : fieldsNode) {
+ Field f = Field.parse(field, names, name.space);
+
+ fields.add(f);
+ if (f.schema.getLogicalType() == null && getOptionalText(field, LOGICAL_TYPE_PROP) != null)
+ LOG.warn(
+ "Ignored the {}.{}.logicalType property (\"{}\"). It should probably be nested inside the \"type\" for the field.",
+ name, f.name, getOptionalText(field, "logicalType"));
+ }
+ result.setFields(fields);
+ }
+ } else if (isTypeArray) {
+ JsonNode items = schema.get("items");
+ Schema schemaItems = parseCompleteSchema(items, names, currentSpace);
+ result = Schema.createArray(schemaItems);
+ } else if ("map".equals(type)) {
+ JsonNode values = schema.get("values");
+ Schema mapItems = parseCompleteSchema(values, names, currentSpace);
+ result = Schema.createMap(mapItems);
+ } else if (result == null) {
+ result = names.get(currentSpace + "." + type);
+ if (result == null) {
+ result = names.get(type);
+ }
}
- // parse logical type if present
- result.logicalType = LogicalTypes.fromSchemaIgnoreInvalid(result);
- names.space(savedSpace); // restore space
- if (result instanceof NamedSchema) {
- Set aliases = parseAliases(schema);
- if (aliases != null) // add aliases
- for (String alias : aliases)
- result.addAlias(alias);
+
+ Set reserved = SCHEMA_RESERVED;
+ if ("enum".equals(type)) {
+ reserved = ENUM_RESERVED;
}
+ Schema.addProperties(schema, reserved, result);
return result;
- } else if (schema.isArray()) { // union
- LockableArrayList types = new LockableArrayList<>(schema.size());
- for (JsonNode typeNode : schema)
- types.add(parse(typeNode, names));
- return new UnionSchema(types);
- } else {
- throw new SchemaParseException("Schema not yet supported: " + schema);
}
+ return null;
+ }
+
+ static Schema parse(JsonNode schema, Names names) {
+ if (schema == null) {
+ throw new SchemaParseException("Cannot parse schema");
+ }
+
+ Schema result = Schema.parseNamesDeclared(schema, names, names.space);
+ Schema.parseCompleteSchema(schema, names, names.space);
+
+ return result;
+ }
+
+ static Schema resolveSchema(JsonNode schema, Names names, String currentNameSpace) {
+ String np = currentNameSpace;
+ String nodeName = getOptionalText(schema, "name");
+ if (nodeName != null) {
+ final JsonNode nameSpace = schema.get("namespace");
+ StringBuilder fullName = new StringBuilder();
+ if (nameSpace != null && nameSpace.isTextual()) {
+ fullName.append(nameSpace.asText()).append(".");
+ np = nameSpace.asText();
+ }
+ fullName.append(nodeName);
+ Schema schema1 = names.get(fullName.toString());
+
+ if (schema1 != null && schema1.getType() == Type.RECORD && !schema1.hasFields()) {
+ Schema.parseCompleteSchema(schema, names, np);
+ }
+ return schema1;
+ }
+ return null;
}
static Set parseAliases(JsonNode node) {
@@ -1989,6 +2311,84 @@ private static String getFieldAlias(Name record, String field, Map= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z');
+ }
+
+ private boolean isDigit(char c) {
+ return c >= '0' && c <= '9';
+ }
+
+ };
+
+ }
+
/**
* No change is permitted on LockableArrayList once lock() has been called on
* it.
diff --git a/lang/java/avro/src/main/java/org/apache/avro/SchemaCompatibility.java b/lang/java/avro/src/main/java/org/apache/avro/SchemaCompatibility.java
index 3e5628d9b3b..8b6a2839ad6 100644
--- a/lang/java/avro/src/main/java/org/apache/avro/SchemaCompatibility.java
+++ b/lang/java/avro/src/main/java/org/apache/avro/SchemaCompatibility.java
@@ -324,8 +324,10 @@ private SchemaCompatibilityResult calculateCompatibility(final Schema reader, fi
// Reader compatible with all branches of a writer union is compatible
if (writer.getType() == Schema.Type.UNION) {
+ int index = 0;
for (Schema s : writer.getTypes()) {
- result = result.mergedWith(getCompatibility(reader, s));
+ result = result.mergedWith(getCompatibility(Integer.toString(index), reader, s, location));
+ index++;
}
return result;
}
diff --git a/lang/java/avro/src/main/java/org/apache/avro/SystemLimitException.java b/lang/java/avro/src/main/java/org/apache/avro/SystemLimitException.java
new file mode 100644
index 00000000000..a96f812d84d
--- /dev/null
+++ b/lang/java/avro/src/main/java/org/apache/avro/SystemLimitException.java
@@ -0,0 +1,190 @@
+/*
+ * 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;
+
+import org.slf4j.LoggerFactory;
+
+/**
+ * Thrown to prevent making large allocations when reading potentially
+ * pathological input data from an untrusted source.
+ *
+ * The following system properties can be set to limit the size of bytes,
+ * strings and collection types to be allocated:
+ *
+ * org.apache.avro.limits.byte.maxLength limits the maximum
+ * size of byte types.
+ * org.apache.avro.limits.collectionItems.maxLength limits the
+ * maximum number of map and list items that can be read at
+ * once single sequence.
+ * org.apache.avro.limits.string.maxLength limits the maximum
+ * size of string types.
+ *
+ *
+ * The default is to permit sizes up to {@link #MAX_ARRAY_VM_LIMIT}.
+ */
+public class SystemLimitException extends AvroRuntimeException {
+
+ /**
+ * The maximum length of array to allocate (unless necessary). Some VMs reserve
+ * some header words in an array. Attempts to allocate larger arrays may result
+ * in {@code OutOfMemoryError: Requested array size exceeds VM limit}
+ *
+ * @see JDK-8246725
+ */
+ // VisibleForTesting
+ static final int MAX_ARRAY_VM_LIMIT = Integer.MAX_VALUE - 8;
+
+ public static final String MAX_BYTES_LENGTH_PROPERTY = "org.apache.avro.limits.bytes.maxLength";
+ public static final String MAX_COLLECTION_LENGTH_PROPERTY = "org.apache.avro.limits.collectionItems.maxLength";
+ public static final String MAX_STRING_LENGTH_PROPERTY = "org.apache.avro.limits.string.maxLength";
+
+ private static int maxBytesLength = MAX_ARRAY_VM_LIMIT;
+ private static int maxCollectionLength = MAX_ARRAY_VM_LIMIT;
+ private static int maxStringLength = MAX_ARRAY_VM_LIMIT;
+
+ static {
+ resetLimits();
+ }
+
+ public SystemLimitException(String message) {
+ super(message);
+ }
+
+ /**
+ * Get an integer value stored in a system property, used to configure the
+ * system behaviour of decoders
+ *
+ * @param property The system property to fetch
+ * @param defaultValue The value to use if the system property is not present or
+ * parsable as an int
+ * @return The value from the system property
+ */
+ private static int getLimitFromProperty(String property, int defaultValue) {
+ String o = System.getProperty(property);
+ int i = defaultValue;
+ if (o != null) {
+ try {
+ i = Integer.parseUnsignedInt(o);
+ } catch (NumberFormatException nfe) {
+ LoggerFactory.getLogger(SystemLimitException.class).warn("Could not parse property " + property + ": " + o,
+ nfe);
+ }
+ }
+ return i;
+ }
+
+ /**
+ * Check to ensure that reading the bytes is within the specified limits.
+ *
+ * @param length The proposed size of the bytes to read
+ * @return The size of the bytes if and only if it is within the limit and
+ * non-negative.
+ * @throws UnsupportedOperationException if reading the datum would allocate a
+ * collection that the Java VM would be
+ * unable to handle
+ * @throws SystemLimitException if the decoding should fail because it
+ * would otherwise result in an allocation
+ * exceeding the set limit
+ * @throws AvroRuntimeException if the length is negative
+ */
+ public static int checkMaxBytesLength(long length) {
+ if (length < 0) {
+ throw new AvroRuntimeException("Malformed data. Length is negative: " + length);
+ }
+ if (length > MAX_ARRAY_VM_LIMIT) {
+ throw new UnsupportedOperationException(
+ "Cannot read arrays longer than " + MAX_ARRAY_VM_LIMIT + " bytes in Java library");
+ }
+ if (length > maxBytesLength) {
+ throw new SystemLimitException("Bytes length " + length + " exceeds maximum allowed");
+ }
+ return (int) length;
+ }
+
+ /**
+ * Check to ensure that reading the specified number of items remains within the
+ * specified limits.
+ *
+ * @param existing The number of elements items read in the collection
+ * @param items The next number of items to read. In normal usage, this is
+ * always a positive, permitted value. Negative and zero values
+ * have a special meaning in Avro decoding.
+ * @return The total number of items in the collection if and only if it is
+ * within the limit and non-negative.
+ * @throws UnsupportedOperationException if reading the items would allocate a
+ * collection that the Java VM would be
+ * unable to handle
+ * @throws SystemLimitException if the decoding should fail because it
+ * would otherwise result in an allocation
+ * exceeding the set limit
+ * @throws AvroRuntimeException if the length is negative
+ */
+ public static int checkMaxCollectionLength(long existing, long items) {
+ long length = existing + items;
+ if (existing < 0) {
+ throw new AvroRuntimeException("Malformed data. Length is negative: " + existing);
+ }
+ if (items < 0) {
+ throw new AvroRuntimeException("Malformed data. Length is negative: " + items);
+ }
+ if (length > MAX_ARRAY_VM_LIMIT || length < existing) {
+ throw new UnsupportedOperationException(
+ "Cannot read collections larger than " + MAX_ARRAY_VM_LIMIT + " items in Java library");
+ }
+ if (length > maxCollectionLength) {
+ throw new SystemLimitException("Collection length " + length + " exceeds maximum allowed");
+ }
+ return (int) length;
+ }
+
+ /**
+ * Check to ensure that reading the string size is within the specified limits.
+ *
+ * @param length The proposed size of the string to read
+ * @return The size of the string if and only if it is within the limit and
+ * non-negative.
+ * @throws UnsupportedOperationException if reading the items would allocate a
+ * collection that the Java VM would be
+ * unable to handle
+ * @throws SystemLimitException if the decoding should fail because it
+ * would otherwise result in an allocation
+ * exceeding the set limit
+ * @throws AvroRuntimeException if the length is negative
+ */
+ public static int checkMaxStringLength(long length) {
+ if (length < 0) {
+ throw new AvroRuntimeException("Malformed data. Length is negative: " + length);
+ }
+ if (length > MAX_ARRAY_VM_LIMIT) {
+ throw new UnsupportedOperationException("Cannot read strings longer than " + MAX_ARRAY_VM_LIMIT + " bytes");
+ }
+ if (length > maxStringLength) {
+ throw new SystemLimitException("String length " + length + " exceeds maximum allowed");
+ }
+ return (int) length;
+ }
+
+ /** Reread the limits from the system properties. */
+ // VisibleForTesting
+ static void resetLimits() {
+ maxBytesLength = getLimitFromProperty(MAX_BYTES_LENGTH_PROPERTY, MAX_ARRAY_VM_LIMIT);
+ maxCollectionLength = getLimitFromProperty(MAX_COLLECTION_LENGTH_PROPERTY, MAX_ARRAY_VM_LIMIT);
+ maxStringLength = getLimitFromProperty(MAX_STRING_LENGTH_PROPERTY, MAX_ARRAY_VM_LIMIT);
+ }
+}
diff --git a/lang/java/avro/src/main/java/org/apache/avro/file/CodecFactory.java b/lang/java/avro/src/main/java/org/apache/avro/file/CodecFactory.java
index 351c036b861..1cfed238f7e 100644
--- a/lang/java/avro/src/main/java/org/apache/avro/file/CodecFactory.java
+++ b/lang/java/avro/src/main/java/org/apache/avro/file/CodecFactory.java
@@ -28,12 +28,14 @@
/**
* Encapsulates the ability to specify and configure a compression codec.
*
- * Currently there are three codecs registered by default:
+ * Currently there are five codecs registered by default:
*
* {@code null}
* {@code deflate}
* {@code snappy}
* {@code bzip2}
+ * {@code xz}
+ * {@code zstandard}
*
*
* New and custom codecs can be registered using
diff --git a/lang/java/avro/src/main/java/org/apache/avro/file/DataFileStream.java b/lang/java/avro/src/main/java/org/apache/avro/file/DataFileStream.java
index a2b5172251d..150d2ace9ba 100644
--- a/lang/java/avro/src/main/java/org/apache/avro/file/DataFileStream.java
+++ b/lang/java/avro/src/main/java/org/apache/avro/file/DataFileStream.java
@@ -139,7 +139,7 @@ void initialize(InputStream in, byte[] magic) throws IOException {
// finalize the header
header.metaKeyList = Collections.unmodifiableList(header.metaKeyList);
- header.schema = new Schema.Parser().setValidate(false).setValidateDefaults(false)
+ header.schema = new Schema.Parser(Schema.NameValidator.NO_VALIDATION).setValidateDefaults(false)
.parse(getMetaString(DataFileConstants.SCHEMA));
this.codec = resolveCodec();
reader.setSchema(header.schema);
diff --git a/lang/java/avro/src/main/java/org/apache/avro/file/DataFileWriter.java b/lang/java/avro/src/main/java/org/apache/avro/file/DataFileWriter.java
index 05e5006acbf..65a305f34fc 100644
--- a/lang/java/avro/src/main/java/org/apache/avro/file/DataFileWriter.java
+++ b/lang/java/avro/src/main/java/org/apache/avro/file/DataFileWriter.java
@@ -22,6 +22,7 @@
import java.io.BufferedOutputStream;
import java.io.Closeable;
import java.io.File;
+import java.io.FileOutputStream;
import java.io.FilterOutputStream;
import java.io.Flushable;
import java.io.IOException;
@@ -51,7 +52,7 @@
* blocks . A synchronization marker is written between blocks, so that
* files may be split. Blocks may be compressed. Extensible metadata is stored
* at the end of the file. Files may be appended to.
- *
+ *
* @see DataFileReader
*/
public class DataFileWriter implements Closeable, Flushable {
@@ -181,7 +182,7 @@ public DataFileWriter create(Schema schema, OutputStream outs, byte[] sync) t
* sync marker is written. By default, the writer will flush the buffer each
* time a sync marker is written (if the block size limit is reached or the
* {@linkplain #sync()} is called.
- *
+ *
* @param flushOnEveryBlock - If set to false, this writer will not flush the
* block to the stream until {@linkplain #flush()} is
* explicitly called.
@@ -211,7 +212,7 @@ public DataFileWriter appendTo(File file) throws IOException {
/**
* Open a writer appending to an existing file. Since 1.9.0 this method
* does not close in.
- *
+ *
* @param in reading the existing file.
* @param out positioned at the end of the existing file.
*/
@@ -304,7 +305,7 @@ public AppendWriteException(Exception e) {
/**
* Append a datum to the file.
- *
+ *
* @see AppendWriteException
*/
public void append(D datum) throws IOException {
@@ -365,7 +366,7 @@ private void writeIfBlockFull() throws IOException {
* at compression level 7. If recompress is false, blocks will be copied
* without changing the compression level. If true, they will be converted to
* the new compression level.
- *
+ *
* @param otherFile
* @param recompress
* @throws IOException
@@ -439,10 +440,10 @@ public void flush() throws IOException {
}
/**
- * If this writer was instantiated using a File or using an
- * {@linkplain Syncable} instance, this method flushes all buffers for this
- * writer to disk. In other cases, this method behaves exactly like
- * {@linkplain #flush()}.
+ * If this writer was instantiated using a {@linkplain File},
+ * {@linkplain FileOutputStream} or {@linkplain Syncable} instance, this method
+ * flushes all buffers for this writer to disk. In other cases, this method
+ * behaves exactly like {@linkplain #flush()}.
*
* @throws IOException
*/
@@ -450,6 +451,8 @@ public void fSync() throws IOException {
flush();
if (underlyingStream instanceof Syncable) {
((Syncable) underlyingStream).sync();
+ } else if (underlyingStream instanceof FileOutputStream) {
+ ((FileOutputStream) underlyingStream).getFD().sync();
}
}
diff --git a/lang/java/avro/src/main/java/org/apache/avro/generic/GenericData.java b/lang/java/avro/src/main/java/org/apache/avro/generic/GenericData.java
index 71861ef9728..6db0a40eee6 100644
--- a/lang/java/avro/src/main/java/org/apache/avro/generic/GenericData.java
+++ b/lang/java/avro/src/main/java/org/apache/avro/generic/GenericData.java
@@ -32,6 +32,7 @@
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
+import java.util.ServiceLoader;
import java.util.UUID;
import java.util.concurrent.ConcurrentMap;
@@ -117,6 +118,7 @@ public GenericData() {
/** For subclasses. GenericData does not use a ClassLoader. */
public GenericData(ClassLoader classLoader) {
this.classLoader = (classLoader != null) ? classLoader : getClass().getClassLoader();
+ loadConversions();
}
/** Return the class loader that's used (by subclasses). */
@@ -124,6 +126,17 @@ public ClassLoader getClassLoader() {
return classLoader;
}
+ /**
+ * Use the Java 6 ServiceLoader to load conversions.
+ *
+ * @see #addLogicalTypeConversion(Conversion)
+ */
+ private void loadConversions() {
+ for (Conversion> conversion : ServiceLoader.load(Conversion.class, classLoader)) {
+ addLogicalTypeConversion(conversion);
+ }
+ }
+
private Map> conversions = new HashMap<>();
private Map, Map>> conversionsByClass = new IdentityHashMap<>();
@@ -134,19 +147,17 @@ public Collection> getConversions() {
/**
* Registers the given conversion to be used when reading and writing with this
- * data model.
+ * data model. Conversions can also be registered automatically, as documented
+ * on the class {@link Conversion Conversion<T>}.
*
* @param conversion a logical type Conversion.
*/
public void addLogicalTypeConversion(Conversion> conversion) {
conversions.put(conversion.getLogicalTypeName(), conversion);
Class> type = conversion.getConvertedType();
- Map> conversions = conversionsByClass.get(type);
- if (conversions == null) {
- conversions = new LinkedHashMap<>();
- conversionsByClass.put(type, conversions);
- }
- conversions.put(conversion.getLogicalTypeName(), conversion);
+ Map> conversionsForClass = conversionsByClass.computeIfAbsent(type,
+ k -> new LinkedHashMap<>());
+ conversionsForClass.put(conversion.getLogicalTypeName(), conversion);
}
/**
@@ -187,11 +198,11 @@ public Conversion getConversionByClass(Class datumClass, LogicalType l
* @return the conversion for the logical type, or null
*/
@SuppressWarnings("unchecked")
- public Conversion getConversionFor(LogicalType logicalType) {
+ public Conversion getConversionFor(LogicalType logicalType) {
if (logicalType == null) {
return null;
}
- return (Conversion) conversions.get(logicalType.getName());
+ return (Conversion) conversions.get(logicalType.getName());
}
public static final String FAST_READER_PROP = "org.apache.avro.fastread";
@@ -306,30 +317,16 @@ public String toString() {
}
}
- /** Default implementation of an array. */
- @SuppressWarnings(value = "unchecked")
- public static class Array extends AbstractList implements GenericArray, Comparable> {
- private static final Object[] EMPTY = new Object[0];
+ public static abstract class AbstractArray extends AbstractList
+ implements GenericArray, Comparable> {
private final Schema schema;
- private int size;
- private Object[] elements = EMPTY;
- public Array(int capacity, Schema schema) {
- if (schema == null || !Type.ARRAY.equals(schema.getType()))
- throw new AvroRuntimeException("Not an array schema: " + schema);
- this.schema = schema;
- if (capacity != 0)
- elements = new Object[capacity];
- }
+ protected int size = 0;
- public Array(Schema schema, Collection c) {
+ public AbstractArray(Schema schema) {
if (schema == null || !Type.ARRAY.equals(schema.getType()))
throw new AvroRuntimeException("Not an array schema: " + schema);
this.schema = schema;
- if (c != null) {
- elements = new Object[c.size()];
- addAll(c);
- }
}
@Override
@@ -343,22 +340,26 @@ public int size() {
}
@Override
- public void clear() {
- // Let GC do its work
- Arrays.fill(elements, 0, size, null);
+ public void reset() {
size = 0;
}
@Override
- public void reset() {
- size = 0;
+ public int compareTo(GenericArray that) {
+ return GenericData.get().compare(this, that, this.getSchema());
}
@Override
- public void prune() {
- if (size < elements.length) {
- Arrays.fill(elements, size, elements.length, null);
+ public boolean equals(final Object o) {
+ if (!(o instanceof Collection)) {
+ return false;
}
+ return GenericData.get().compare(this, o, this.getSchema()) == 0;
+ }
+
+ @Override
+ public int hashCode() {
+ return super.hashCode();
}
@Override
@@ -373,7 +374,7 @@ public boolean hasNext() {
@Override
public T next() {
- return (T) elements[position++];
+ return AbstractArray.this.get(position++);
}
@Override
@@ -383,6 +384,57 @@ public void remove() {
};
}
+ @Override
+ public void reverse() {
+ int left = 0;
+ int right = size - 1;
+
+ while (left < right) {
+ this.swap(left, right);
+
+ left++;
+ right--;
+ }
+ }
+
+ protected abstract void swap(int index1, int index2);
+ }
+
+ /** Default implementation of an array. */
+ @SuppressWarnings(value = "unchecked")
+ public static class Array extends AbstractArray {
+ private static final Object[] EMPTY = new Object[0];
+
+ private Object[] elements = EMPTY;
+
+ public Array(int capacity, Schema schema) {
+ super(schema);
+ if (capacity != 0)
+ elements = new Object[capacity];
+ }
+
+ public Array(Schema schema, Collection c) {
+ super(schema);
+ if (c != null) {
+ elements = new Object[c.size()];
+ addAll(c);
+ }
+ }
+
+ @Override
+ public void clear() {
+ // Let GC do its work
+ Arrays.fill(elements, 0, size, null);
+ size = 0;
+ }
+
+ @Override
+ public void prune() {
+ if (size < elements.length) {
+ Arrays.fill(elements, size, elements.length, null);
+ }
+ }
+
@Override
public T get(int i) {
if (i >= size)
@@ -431,23 +483,10 @@ public T peek() {
}
@Override
- public int compareTo(GenericArray that) {
- return GenericData.get().compare(this, that, this.getSchema());
- }
-
- @Override
- public void reverse() {
- int left = 0;
- int right = elements.length - 1;
-
- while (left < right) {
- Object tmp = elements[left];
- elements[left] = elements[right];
- elements[right] = tmp;
-
- left++;
- right--;
- }
+ protected void swap(final int index1, final int index2) {
+ Object tmp = elements[index1];
+ elements[index1] = elements[index2];
+ elements[index2] = tmp;
}
}
@@ -1167,7 +1206,11 @@ protected int compareMaps(final Map, ?> m1, final Map, ?> m2) {
return 0;
}
- if (m2.size() != m2.size()) {
+ if (m1.isEmpty() && m2.isEmpty()) {
+ return 0;
+ }
+
+ if (m1.size() != m2.size()) {
return 1;
}
@@ -1484,8 +1527,24 @@ public Object newArray(Object old, int size, Schema schema) {
} else if (old instanceof Collection) {
((Collection>) old).clear();
return old;
- } else
+ } else {
+ if (schema.getElementType().getType() == Type.INT) {
+ return new PrimitivesArrays.IntArray(size, schema);
+ }
+ if (schema.getElementType().getType() == Type.BOOLEAN) {
+ return new PrimitivesArrays.BooleanArray(size, schema);
+ }
+ if (schema.getElementType().getType() == Type.LONG) {
+ return new PrimitivesArrays.LongArray(size, schema);
+ }
+ if (schema.getElementType().getType() == Type.FLOAT) {
+ return new PrimitivesArrays.FloatArray(size, schema);
+ }
+ if (schema.getElementType().getType() == Type.DOUBLE) {
+ return new PrimitivesArrays.DoubleArray(size, schema);
+ }
return new GenericData.Array(size, schema);
+ }
}
/**
diff --git a/lang/java/avro/src/main/java/org/apache/avro/generic/PrimitivesArrays.java b/lang/java/avro/src/main/java/org/apache/avro/generic/PrimitivesArrays.java
new file mode 100644
index 00000000000..d34ce0f5bcb
--- /dev/null
+++ b/lang/java/avro/src/main/java/org/apache/avro/generic/PrimitivesArrays.java
@@ -0,0 +1,609 @@
+/*
+ * 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.generic;
+
+import org.apache.avro.AvroRuntimeException;
+import org.apache.avro.Schema;
+
+import java.util.Arrays;
+import java.util.Collection;
+
+public class PrimitivesArrays {
+
+ public static class IntArray extends GenericData.AbstractArray {
+ private static final int[] EMPTY = new int[0];
+
+ private int[] elements = EMPTY;
+
+ public IntArray(int capacity, Schema schema) {
+ super(schema);
+ if (!Schema.Type.INT.equals(schema.getElementType().getType()))
+ throw new AvroRuntimeException("Not a int array schema: " + schema);
+ if (capacity != 0)
+ elements = new int[capacity];
+ }
+
+ public IntArray(Schema schema, Collection c) {
+ super(schema);
+ if (c != null) {
+ elements = new int[c.size()];
+ addAll(c);
+ }
+ }
+
+ @Override
+ public void clear() {
+ size = 0;
+ }
+
+ @Override
+ public Integer get(int i) {
+ return this.getInt(i);
+ }
+
+ /**
+ * Direct primitive int access.
+ *
+ * @param i : index.
+ * @return value at index.
+ */
+ public int getInt(int i) {
+ if (i >= size)
+ throw new IndexOutOfBoundsException("Index " + i + " out of bounds.");
+ return elements[i];
+ }
+
+ @Override
+ public void add(int location, Integer o) {
+ if (o == null) {
+ return;
+ }
+ this.add(location, o.intValue());
+ }
+
+ public void add(int location, int o) {
+ if (location > size || location < 0) {
+ throw new IndexOutOfBoundsException("Index " + location + " out of bounds.");
+ }
+ if (size == elements.length) {
+ // Increase size by 1.5x + 1
+ final int newSize = size + (size >> 1) + 1;
+ elements = Arrays.copyOf(elements, newSize);
+ }
+ System.arraycopy(elements, location, elements, location + 1, size - location);
+ elements[location] = o;
+ size++;
+ }
+
+ @Override
+ public Integer set(int i, Integer o) {
+ if (o == null) {
+ return null;
+ }
+ return this.set(i, o.intValue());
+ }
+
+ public int set(int i, int o) {
+ if (i >= size)
+ throw new IndexOutOfBoundsException("Index " + i + " out of bounds.");
+ int response = elements[i];
+ elements[i] = o;
+ return response;
+ }
+
+ @Override
+ public Integer remove(int i) {
+ if (i >= size)
+ throw new IndexOutOfBoundsException("Index " + i + " out of bounds.");
+ int result = elements[i];
+ --size;
+ System.arraycopy(elements, i + 1, elements, i, (size - i));
+ return result;
+ }
+
+ @Override
+ public Integer peek() {
+ return (size < elements.length) ? elements[size] : null;
+ }
+
+ @Override
+ protected void swap(final int index1, final int index2) {
+ int tmp = elements[index1];
+ elements[index1] = elements[index2];
+ elements[index2] = tmp;
+ }
+ }
+
+ public static class LongArray extends GenericData.AbstractArray {
+ private static final long[] EMPTY = new long[0];
+
+ private long[] elements = EMPTY;
+
+ public LongArray(int capacity, Schema schema) {
+ super(schema);
+ if (!Schema.Type.LONG.equals(schema.getElementType().getType()))
+ throw new AvroRuntimeException("Not a long array schema: " + schema);
+ if (capacity != 0)
+ elements = new long[capacity];
+ }
+
+ public LongArray(Schema schema, Collection c) {
+ super(schema);
+ if (c != null) {
+ elements = new long[c.size()];
+ addAll(c);
+ }
+ }
+
+ @Override
+ public void clear() {
+ size = 0;
+ }
+
+ @Override
+ public Long get(int i) {
+ return getLong(i);
+ }
+
+ /**
+ * Direct primitive int access.
+ *
+ * @param i : index.
+ * @return value at index.
+ */
+ public long getLong(int i) {
+ if (i >= size)
+ throw new IndexOutOfBoundsException("Index " + i + " out of bounds.");
+ return elements[i];
+ }
+
+ @Override
+ public void add(int location, Long o) {
+ if (o == null) {
+ return;
+ }
+ this.add(location, o.longValue());
+ }
+
+ public void add(int location, long o) {
+ if (location > size || location < 0) {
+ throw new IndexOutOfBoundsException("Index " + location + " out of bounds.");
+ }
+ if (size == elements.length) {
+ // Increase size by 1.5x + 1
+ final int newSize = size + (size >> 1) + 1;
+ elements = Arrays.copyOf(elements, newSize);
+ }
+ System.arraycopy(elements, location, elements, location + 1, size - location);
+ elements[location] = o;
+ size++;
+ }
+
+ @Override
+ public Long set(int i, Long o) {
+ if (o == null) {
+ return null;
+ }
+ return this.set(i, o.longValue());
+ }
+
+ public long set(int i, long o) {
+ if (i >= size)
+ throw new IndexOutOfBoundsException("Index " + i + " out of bounds.");
+ long response = elements[i];
+ elements[i] = o;
+ return response;
+ }
+
+ @Override
+ public Long remove(int i) {
+ if (i >= size)
+ throw new IndexOutOfBoundsException("Index " + i + " out of bounds.");
+ long result = elements[i];
+ --size;
+ System.arraycopy(elements, i + 1, elements, i, (size - i));
+ return result;
+ }
+
+ @Override
+ public Long peek() {
+ return (size < elements.length) ? elements[size] : null;
+ }
+
+ @Override
+ protected void swap(final int index1, final int index2) {
+ long tmp = elements[index1];
+ elements[index1] = elements[index2];
+ elements[index2] = tmp;
+ }
+ }
+
+ public static class BooleanArray extends GenericData.AbstractArray {
+ private static final byte[] EMPTY = new byte[0];
+
+ private byte[] elements = EMPTY;
+
+ public BooleanArray(int capacity, Schema schema) {
+ super(schema);
+ if (!Schema.Type.BOOLEAN.equals(schema.getElementType().getType()))
+ throw new AvroRuntimeException("Not a boolean array schema: " + schema);
+ if (capacity != 0)
+ elements = new byte[1 + (capacity / Byte.SIZE)];
+ }
+
+ public BooleanArray(Schema schema, Collection c) {
+ super(schema);
+
+ if (c != null) {
+ elements = new byte[1 + (c.size() / 8)];
+ if (c instanceof BooleanArray) {
+ BooleanArray other = (BooleanArray) c;
+ this.size = other.size;
+ System.arraycopy(other.elements, 0, this.elements, 0, other.elements.length);
+ } else {
+ addAll(c);
+ }
+ }
+ }
+
+ @Override
+ public void clear() {
+ size = 0;
+ }
+
+ @Override
+ public Boolean get(int i) {
+ return this.getBoolean(i);
+ }
+
+ /**
+ * Direct primitive int access.
+ *
+ * @param i : index.
+ * @return value at index.
+ */
+ public boolean getBoolean(int i) {
+ if (i >= size)
+ throw new IndexOutOfBoundsException("Index " + i + " out of bounds.");
+ return (elements[i / 8] & (1 << (i % 8))) > 0;
+ }
+
+ @Override
+ public boolean add(final Boolean o) {
+ if (o == null) {
+ return false;
+ }
+ return this.add(o.booleanValue());
+ }
+
+ public boolean add(final boolean o) {
+ if (this.size == elements.length * 8) {
+ final int newLength = elements.length + (elements.length >> 1) + 1;
+ elements = Arrays.copyOf(elements, newLength);
+ }
+ this.size++;
+ this.set(this.size - 1, o);
+ return true;
+ }
+
+ @Override
+ public void add(int location, Boolean o) {
+ if (o == null) {
+ return;
+ }
+ this.add(location, o.booleanValue());
+ }
+
+ public void add(int location, boolean o) {
+ if (location > size || location < 0) {
+ throw new IndexOutOfBoundsException("Index " + location + " out of bounds.");
+ }
+ if (size == elements.length * 8) {
+ // Increase size by 1.5x + 1
+ final int newLength = elements.length + (elements.length >> 1) + 1;
+ elements = Arrays.copyOf(elements, newLength);
+ }
+ size++;
+ for (int index = this.size / 8; index > (location / 8); index--) {
+ elements[index] <<= 1;
+ if (index > 0 && (elements[index - 1] & (1 << Byte.SIZE)) > 0) {
+ elements[index] |= 1;
+ }
+ }
+ byte pos = (byte) (1 << (location % Byte.SIZE));
+ byte highbits = (byte) ~(pos + (pos - 1));
+ byte lowbits = (byte) (pos - 1);
+ byte currentHigh = (byte) ((elements[location / 8] & highbits) << 1);
+ byte currentLow = (byte) (elements[location / 8] & lowbits);
+ if (o) {
+ elements[location / 8] = (byte) (currentHigh | currentLow | pos);
+ } else {
+ elements[location / 8] = (byte) (currentHigh | currentLow);
+ }
+
+ }
+
+ @Override
+ public Boolean set(int i, Boolean o) {
+ if (o == null) {
+ return null;
+ }
+ return this.set(i, o.booleanValue());
+ }
+
+ public boolean set(int i, boolean o) {
+ if (i >= size)
+ throw new IndexOutOfBoundsException("Index " + i + " out of bounds.");
+ boolean response = (elements[i / 8] & (1 << (i % 8))) > 0;
+ if (o) {
+ elements[i / 8] |= 1 << (i % 8);
+ } else {
+ elements[i / 8] &= 0xFF - (1 << (i % 8));
+ }
+ return response;
+ }
+
+ @Override
+ public Boolean remove(int i) {
+ if (i >= size)
+ throw new IndexOutOfBoundsException("Index " + i + " out of bounds.");
+ boolean result = (elements[(i / 8)] & (1 << (i % 8))) > 0;
+ --size;
+
+ byte memo = 0;
+ if ((i / 8) + 1 < elements.length) {
+ memo = (byte) ((1 & (elements[(i / 8) + 1])) << 7);
+ }
+ for (int index = (i / 8) + 1; index <= (size / 8); index++) {
+ elements[index] = (byte) ((elements[index] & 0xff) >>> 1);
+ if (index + 1 < elements.length && (elements[index + 1] & 1) == 1) {
+ elements[index] |= 1 << (Byte.SIZE - 1);
+ }
+ }
+ // 87654321 => 8764321
+ byte start = (byte) ((1 << ((i + 1) % 8)) - 1);
+ byte end = (byte) ~start;
+ elements[i / 8] = (byte) ((((start & 0xff) >>> 1) & elements[i / 8]) // 1234
+ | (end & (elements[i / 8] >> 1)) // 876
+ | memo);
+
+ return result;
+ }
+
+ @Override
+ public Boolean peek() {
+ return (size < elements.length * Byte.SIZE) ? (elements[(size / 8)] & (1 << (size % 8))) > 0 : null;
+ }
+
+ @Override
+ protected void swap(final int index1, final int index2) {
+ boolean tmp = this.get(index1);
+ this.set(index1, this.get(index2));
+ this.set(index2, tmp);
+ }
+ }
+
+ public static class FloatArray extends GenericData.AbstractArray {
+ private static final float[] EMPTY = new float[0];
+
+ private float[] elements = EMPTY;
+
+ public FloatArray(int capacity, Schema schema) {
+ super(schema);
+ if (!Schema.Type.FLOAT.equals(schema.getElementType().getType()))
+ throw new AvroRuntimeException("Not a float array schema: " + schema);
+ if (capacity != 0)
+ elements = new float[capacity];
+ }
+
+ public FloatArray(Schema schema, Collection c) {
+ super(schema);
+ if (c != null) {
+ elements = new float[c.size()];
+ addAll(c);
+ }
+ }
+
+ @Override
+ public void clear() {
+ size = 0;
+ }
+
+ @Override
+ public Float get(int i) {
+ return this.getFloat(i);
+ }
+
+ /**
+ * Direct primitive int access.
+ *
+ * @param i : index.
+ * @return value at index.
+ */
+ public float getFloat(int i) {
+ if (i >= size)
+ throw new IndexOutOfBoundsException("Index " + i + " out of bounds.");
+ return elements[i];
+ }
+
+ @Override
+ public void add(int location, Float o) {
+ if (o == null) {
+ return;
+ }
+ this.add(location, o.floatValue());
+ }
+
+ public void add(int location, float o) {
+ if (location > size || location < 0) {
+ throw new IndexOutOfBoundsException("Index " + location + " out of bounds.");
+ }
+ if (size == elements.length) {
+ // Increase size by 1.5x + 1
+ final int newSize = size + (size >> 1) + 1;
+ elements = Arrays.copyOf(elements, newSize);
+ }
+ System.arraycopy(elements, location, elements, location + 1, size - location);
+ elements[location] = o;
+ size++;
+ }
+
+ @Override
+ public Float set(int i, Float o) {
+ if (o == null) {
+ return null;
+ }
+ return this.set(i, o.floatValue());
+ }
+
+ public float set(int i, float o) {
+ if (i >= size)
+ throw new IndexOutOfBoundsException("Index " + i + " out of bounds.");
+ float response = elements[i];
+ elements[i] = o;
+ return response;
+ }
+
+ @Override
+ public Float remove(int i) {
+ if (i >= size)
+ throw new IndexOutOfBoundsException("Index " + i + " out of bounds.");
+ float result = elements[i];
+ --size;
+ System.arraycopy(elements, i + 1, elements, i, (size - i));
+ return result;
+ }
+
+ @Override
+ public Float peek() {
+ return (size < elements.length) ? elements[size] : null;
+ }
+
+ @Override
+ protected void swap(final int index1, final int index2) {
+ float tmp = this.get(index1);
+ this.set(index1, this.get(index2));
+ this.set(index2, tmp);
+ }
+ }
+
+ public static class DoubleArray extends GenericData.AbstractArray {
+ private static final double[] EMPTY = new double[0];
+
+ private double[] elements = EMPTY;
+
+ public DoubleArray(int capacity, Schema schema) {
+ super(schema);
+ if (!Schema.Type.DOUBLE.equals(schema.getElementType().getType()))
+ throw new AvroRuntimeException("Not a double array schema: " + schema);
+ if (capacity != 0)
+ elements = new double[capacity];
+ }
+
+ public DoubleArray(Schema schema, Collection c) {
+ super(schema);
+ if (c != null) {
+ elements = new double[c.size()];
+ addAll(c);
+ }
+ }
+
+ @Override
+ public void clear() {
+ size = 0;
+ }
+
+ @Override
+ public Double get(int i) {
+ return this.getDouble(i);
+ }
+
+ /**
+ * Direct primitive int access.
+ *
+ * @param i : index.
+ * @return value at index.
+ */
+ public double getDouble(int i) {
+ if (i >= size)
+ throw new IndexOutOfBoundsException("Index " + i + " out of bounds.");
+ return elements[i];
+ }
+
+ @Override
+ public void add(int location, Double o) {
+ if (o == null) {
+ return;
+ }
+ this.add(location, o.floatValue());
+ }
+
+ public void add(int location, double o) {
+ if (location > size || location < 0) {
+ throw new IndexOutOfBoundsException("Index " + location + " out of bounds.");
+ }
+ if (size == elements.length) {
+ // Increase size by 1.5x + 1
+ final int newSize = size + (size >> 1) + 1;
+ elements = Arrays.copyOf(elements, newSize);
+ }
+ System.arraycopy(elements, location, elements, location + 1, size - location);
+ elements[location] = o;
+ size++;
+ }
+
+ @Override
+ public Double set(int i, Double o) {
+ if (o == null) {
+ return null;
+ }
+ return this.set(i, o.floatValue());
+ }
+
+ public double set(int i, double o) {
+ if (i >= size)
+ throw new IndexOutOfBoundsException("Index " + i + " out of bounds.");
+ double response = elements[i];
+ elements[i] = o;
+ return response;
+ }
+
+ @Override
+ public Double remove(int i) {
+ if (i >= size)
+ throw new IndexOutOfBoundsException("Index " + i + " out of bounds.");
+ double result = elements[i];
+ --size;
+ System.arraycopy(elements, i + 1, elements, i, (size - i));
+ return result;
+ }
+
+ @Override
+ public Double peek() {
+ return (size < elements.length) ? elements[size] : null;
+ }
+
+ @Override
+ protected void swap(final int index1, final int index2) {
+ double tmp = this.get(index1);
+ this.set(index1, this.get(index2));
+ this.set(index2, tmp);
+ }
+ }
+
+}
diff --git a/lang/java/avro/src/main/java/org/apache/avro/io/BinaryDecoder.java b/lang/java/avro/src/main/java/org/apache/avro/io/BinaryDecoder.java
index 051563abaef..3fa675d793a 100644
--- a/lang/java/avro/src/main/java/org/apache/avro/io/BinaryDecoder.java
+++ b/lang/java/avro/src/main/java/org/apache/avro/io/BinaryDecoder.java
@@ -26,8 +26,8 @@
import org.apache.avro.AvroRuntimeException;
import org.apache.avro.InvalidNumberEncodingException;
+import org.apache.avro.SystemLimitException;
import org.apache.avro.util.Utf8;
-import org.slf4j.LoggerFactory;
/**
* An {@link Decoder} for binary-format data.
@@ -39,27 +39,20 @@
* can be accessed by inputStream().remaining(), if the BinaryDecoder is not
* 'direct'.
*
- * To prevent this class from making large allocations when handling potentially
- * pathological input data, set Java properties
- * org.apache.avro.limits.string.maxLength and
- * org.apache.avro.limits.bytes.maxLength before instantiating this
- * class to limit the maximum sizes of string and bytes types
- * handled. The default is to permit sizes up to Java's maximum array length.
*
* @see Encoder
+ * @see SystemLimitException
*/
public class BinaryDecoder extends Decoder {
/**
- * The maximum size of array to allocate. Some VMs reserve some header words in
- * an array. Attempts to allocate larger arrays may result in OutOfMemoryError:
- * Requested array size exceeds VM limit
+ * When reading a collection (MAP or ARRAY), this keeps track of the number of
+ * elements to ensure that the
+ * {@link SystemLimitException#checkMaxCollectionLength} constraint is
+ * respected.
*/
- static final long MAX_ARRAY_SIZE = (long) Integer.MAX_VALUE - 8L;
-
- private static final String MAX_BYTES_LENGTH_PROPERTY = "org.apache.avro.limits.bytes.maxLength";
- protected final int maxBytesLength;
+ private long collectionCount = 0L;
private ByteSource source = null;
// we keep the buffer and its state variables in this class and not in a
@@ -99,17 +92,6 @@ void clearBuf() {
/** protected constructor for child classes */
protected BinaryDecoder() {
super();
- String o = System.getProperty(MAX_BYTES_LENGTH_PROPERTY);
- int i = Integer.MAX_VALUE;
- if (o != null) {
- try {
- i = Integer.parseUnsignedInt(o);
- } catch (NumberFormatException nfe) {
- LoggerFactory.getLogger(BinaryDecoder.class)
- .warn("Could not parse property " + MAX_BYTES_LENGTH_PROPERTY + ": " + o, nfe);
- }
- }
- maxBytesLength = i;
}
BinaryDecoder(InputStream in, int bufferSize) {
@@ -300,17 +282,11 @@ public double readDouble() throws IOException {
@Override
public Utf8 readString(Utf8 old) throws IOException {
- long length = readLong();
- if (length > MAX_ARRAY_SIZE) {
- throw new UnsupportedOperationException("Cannot read strings longer than " + MAX_ARRAY_SIZE + " bytes");
- }
- if (length < 0L) {
- throw new AvroRuntimeException("Malformed data. Length is negative: " + length);
- }
+ int length = SystemLimitException.checkMaxStringLength(readLong());
Utf8 result = (old != null ? old : new Utf8());
- result.setByteLength((int) length);
- if (0L != length) {
- doReadBytes(result.getBytes(), 0, (int) length);
+ result.setByteLength(length);
+ if (0 != length) {
+ doReadBytes(result.getBytes(), 0, length);
}
return result;
}
@@ -329,25 +305,16 @@ public void skipString() throws IOException {
@Override
public ByteBuffer readBytes(ByteBuffer old) throws IOException {
- int length = readInt();
- if (length > MAX_ARRAY_SIZE) {
- throw new UnsupportedOperationException("Cannot read arrays longer than " + MAX_ARRAY_SIZE + " bytes");
- }
- if (length > maxBytesLength) {
- throw new AvroRuntimeException("Bytes length " + length + " exceeds maximum allowed");
- }
- if (length < 0L) {
- throw new AvroRuntimeException("Malformed data. Length is negative: " + length);
- }
+ int length = SystemLimitException.checkMaxBytesLength(readLong());
final ByteBuffer result;
if (old != null && length <= old.capacity()) {
result = old;
((Buffer) result).clear();
} else {
- result = ByteBuffer.allocate(length);
+ result = ByteBuffer.allocate((int) length);
}
- doReadBytes(result.array(), result.position(), length);
- ((Buffer) result).limit(length);
+ doReadBytes(result.array(), result.position(), (int) length);
+ ((Buffer) result).limit((int) length);
return result;
}
@@ -443,7 +410,6 @@ protected long doReadItemCount() throws IOException {
* @return Zero if there are no more items to skip and end of array/map is
* reached. Positive number if some items are found that cannot be
* skipped and the client needs to skip them individually.
- *
* @throws IOException If the first byte cannot be read for any reason other
* than the end of the file, if the input stream has been
* closed, or if some other I/O error occurs.
@@ -460,12 +426,15 @@ private long doSkipItems() throws IOException {
@Override
public long readArrayStart() throws IOException {
- return doReadItemCount();
+ collectionCount = SystemLimitException.checkMaxCollectionLength(0L, doReadItemCount());
+ return collectionCount;
}
@Override
public long arrayNext() throws IOException {
- return doReadItemCount();
+ long length = doReadItemCount();
+ collectionCount = SystemLimitException.checkMaxCollectionLength(collectionCount, length);
+ return length;
}
@Override
@@ -475,12 +444,15 @@ public long skipArray() throws IOException {
@Override
public long readMapStart() throws IOException {
- return doReadItemCount();
+ collectionCount = SystemLimitException.checkMaxCollectionLength(0L, doReadItemCount());
+ return collectionCount;
}
@Override
public long mapNext() throws IOException {
- return doReadItemCount();
+ long length = doReadItemCount();
+ collectionCount = SystemLimitException.checkMaxCollectionLength(collectionCount, length);
+ return length;
}
@Override
@@ -932,7 +904,6 @@ public void close() throws IOException {
/**
* This byte source is special. It will avoid copying data by using the source's
* byte[] as a buffer in the decoder.
- *
*/
private static class ByteArrayByteSource extends ByteSource {
private static final int MIN_SIZE = 16;
diff --git a/lang/java/avro/src/main/java/org/apache/avro/io/DirectBinaryDecoder.java b/lang/java/avro/src/main/java/org/apache/avro/io/DirectBinaryDecoder.java
index d9bbe93534c..6f07b13eee2 100644
--- a/lang/java/avro/src/main/java/org/apache/avro/io/DirectBinaryDecoder.java
+++ b/lang/java/avro/src/main/java/org/apache/avro/io/DirectBinaryDecoder.java
@@ -22,8 +22,8 @@
import java.io.InputStream;
import java.nio.ByteBuffer;
-import org.apache.avro.AvroRuntimeException;
import org.apache.avro.InvalidNumberEncodingException;
+import org.apache.avro.SystemLimitException;
import org.apache.avro.util.ByteBufferInputStream;
/**
@@ -40,30 +40,17 @@ class DirectBinaryDecoder extends BinaryDecoder {
private class ByteReader {
public ByteBuffer read(ByteBuffer old, int length) throws IOException {
- this.checkLength(length);
final ByteBuffer result;
if (old != null && length <= old.capacity()) {
result = old;
result.clear();
} else {
- result = ByteBuffer.allocate(length);
+ result = ByteBuffer.allocate((int) length);
}
- doReadBytes(result.array(), result.position(), length);
- result.limit(length);
+ doReadBytes(result.array(), result.position(), (int) length);
+ result.limit((int) length);
return result;
}
-
- protected final void checkLength(int length) {
- if (length < 0L) {
- throw new AvroRuntimeException("Malformed data. Length is negative: " + length);
- }
- if (length > MAX_ARRAY_SIZE) {
- throw new UnsupportedOperationException("Cannot read arrays longer than " + MAX_ARRAY_SIZE + " bytes");
- }
- if (length > maxBytesLength) {
- throw new AvroRuntimeException("Bytes length " + length + " exceeds maximum allowed");
- }
- }
}
private class ReuseByteReader extends ByteReader {
@@ -75,14 +62,12 @@ public ReuseByteReader(ByteBufferInputStream bbi) {
@Override
public ByteBuffer read(ByteBuffer old, int length) throws IOException {
- this.checkLength(length);
if (old != null) {
return super.read(old, length);
} else {
- return bbi.readBuffer(length);
+ return bbi.readBuffer((int) length);
}
}
-
}
private ByteReader byteReader;
@@ -170,8 +155,8 @@ public double readDouble() throws IOException {
@Override
public ByteBuffer readBytes(ByteBuffer old) throws IOException {
- int length = readInt();
- return byteReader.read(old, length);
+ long length = readLong();
+ return byteReader.read(old, SystemLimitException.checkMaxBytesLength(length));
}
@Override
diff --git a/lang/java/avro/src/main/java/org/apache/avro/io/EncoderFactory.java b/lang/java/avro/src/main/java/org/apache/avro/io/EncoderFactory.java
index 0188a29637d..055ef9541d9 100644
--- a/lang/java/avro/src/main/java/org/apache/avro/io/EncoderFactory.java
+++ b/lang/java/avro/src/main/java/org/apache/avro/io/EncoderFactory.java
@@ -19,6 +19,7 @@
import java.io.IOException;
import java.io.OutputStream;
+import java.util.EnumSet;
import org.apache.avro.AvroRuntimeException;
import org.apache.avro.Schema;
@@ -71,11 +72,11 @@ public static EncoderFactory get() {
* likely to improve performance but may be useful for the
* downstream OutputStream.
* @return This factory, to enable method chaining:
- *
+ *
*
* EncoderFactory factory = new EncoderFactory().configureBufferSize(4096);
*
- *
+ *
* @see #binaryEncoder(OutputStream, BinaryEncoder)
*/
public EncoderFactory configureBufferSize(int size) {
@@ -90,7 +91,7 @@ public EncoderFactory configureBufferSize(int size) {
/**
* Returns this factory's configured default buffer size. Used when creating
* Encoder instances that buffer writes.
- *
+ *
* @see #configureBufferSize(int)
* @see #binaryEncoder(OutputStream, BinaryEncoder)
* @return The preferred buffer size, in bytes.
@@ -109,11 +110,11 @@ public int getBufferSize() {
* outside this range are set to the nearest value in the range. The
* encoder will require at least this amount of memory.
* @return This factory, to enable method chaining:
- *
+ *
*
* EncoderFactory factory = new EncoderFactory().configureBlockSize(8000);
*
- *
+ *
* @see #blockingBinaryEncoder(OutputStream, BinaryEncoder)
*/
public EncoderFactory configureBlockSize(int size) {
@@ -131,7 +132,7 @@ public EncoderFactory configureBlockSize(int size) {
* #blockingBinaryEncoder(OutputStream, BinaryEncoder) will have block buffers
* of this size.
*
- *
+ *
* @see #configureBlockSize(int)
* @see #blockingBinaryEncoder(OutputStream, BinaryEncoder)
* @return The preferred block size, in bytes.
@@ -297,6 +298,38 @@ public JsonEncoder jsonEncoder(Schema schema, OutputStream out, boolean pretty)
return new JsonEncoder(schema, out, pretty);
}
+ /**
+ * Creates a {@link JsonEncoder} using the OutputStream provided for writing
+ * data conforming to the Schema provided with optional pretty printing.
+ *
+ * {@link JsonEncoder} buffers its output. Data may not appear on the underlying
+ * OutputStream until {@link Encoder#flush()} is called.
+ *
+ * {@link JsonEncoder} is not thread-safe.
+ *
+ * @param schema The Schema for data written to this JsonEncoder. Cannot be
+ * null.
+ * @param out The OutputStream to write to. Cannot be null.
+ * @param pretty Pretty print encoding.
+ * @param autoflush Whether to Automatically flush the data to storage, default
+ * is true controls the underlying FLUSH_PASSED_TO_STREAM
+ * feature of JsonGenerator
+ * @return A JsonEncoder configured with out , schema and
+ * pretty
+ * @throws IOException
+ */
+ public JsonEncoder jsonEncoder(Schema schema, OutputStream out, boolean pretty, boolean autoflush)
+ throws IOException {
+ EnumSet options = EnumSet.noneOf(JsonEncoder.JsonOptions.class);
+ if (pretty) {
+ options.add(JsonEncoder.JsonOptions.Pretty);
+ }
+ if (!autoflush) {
+ options.add(JsonEncoder.JsonOptions.NoFlushStream);
+ }
+ return new JsonEncoder(schema, out, options);
+ }
+
/**
* Creates a {@link JsonEncoder} using the {@link JsonGenerator} provided for
* output of data conforming to the Schema provided.
diff --git a/lang/java/avro/src/main/java/org/apache/avro/io/JsonDecoder.java b/lang/java/avro/src/main/java/org/apache/avro/io/JsonDecoder.java
index c1c38511ab4..2ad496a5b87 100644
--- a/lang/java/avro/src/main/java/org/apache/avro/io/JsonDecoder.java
+++ b/lang/java/avro/src/main/java/org/apache/avro/io/JsonDecoder.java
@@ -86,7 +86,7 @@ private static Symbol getSymbol(Schema schema) {
*
* Otherwise, this JsonDecoder will reset its state and then reconfigure its
* input.
- *
+ *
* @param in The InputStream to read from. Cannot be null.
* @throws IOException
* @throws NullPointerException if {@code in} is {@code null}
@@ -109,7 +109,7 @@ public JsonDecoder configure(InputStream in) throws IOException {
*
* Otherwise, this JsonDecoder will reset its state and then reconfigure its
* input.
- *
+ *
* @param in The String to read from. Cannot be null.
* @throws IOException
* @throws NullPointerException if {@code in} is {@code null}
@@ -157,25 +157,39 @@ public boolean readBoolean() throws IOException {
@Override
public int readInt() throws IOException {
advance(Symbol.INT);
- if (in.getCurrentToken().isNumeric()) {
+ if (in.getCurrentToken() == JsonToken.VALUE_NUMBER_INT) {
int result = in.getIntValue();
in.nextToken();
return result;
- } else {
- throw error("int");
}
+ if (in.getCurrentToken() == JsonToken.VALUE_NUMBER_FLOAT) {
+ float value = in.getFloatValue();
+ if (Math.abs(value - Math.round(value)) <= Float.MIN_VALUE) {
+ int result = Math.round(value);
+ in.nextToken();
+ return result;
+ }
+ }
+ throw error("int");
}
@Override
public long readLong() throws IOException {
advance(Symbol.LONG);
- if (in.getCurrentToken().isNumeric()) {
+ if (in.getCurrentToken() == JsonToken.VALUE_NUMBER_INT) {
long result = in.getLongValue();
in.nextToken();
return result;
- } else {
- throw error("long");
}
+ if (in.getCurrentToken() == JsonToken.VALUE_NUMBER_FLOAT) {
+ double value = in.getDoubleValue();
+ if (Math.abs(value - Math.round(value)) <= Double.MIN_VALUE) {
+ long result = Math.round(value);
+ in.nextToken();
+ return result;
+ }
+ }
+ throw error("long");
}
@Override
diff --git a/lang/java/avro/src/main/java/org/apache/avro/io/JsonEncoder.java b/lang/java/avro/src/main/java/org/apache/avro/io/JsonEncoder.java
index 71cc690b8a4..7e3a67eb6db 100644
--- a/lang/java/avro/src/main/java/org/apache/avro/io/JsonEncoder.java
+++ b/lang/java/avro/src/main/java/org/apache/avro/io/JsonEncoder.java
@@ -22,7 +22,9 @@
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.BitSet;
+import java.util.EnumSet;
import java.util.Objects;
+import java.util.Set;
import org.apache.avro.AvroTypeException;
import org.apache.avro.Schema;
@@ -33,6 +35,7 @@
import com.fasterxml.jackson.core.JsonEncoding;
import com.fasterxml.jackson.core.JsonFactory;
import com.fasterxml.jackson.core.JsonGenerator;
+import com.fasterxml.jackson.core.PrettyPrinter;
import com.fasterxml.jackson.core.util.DefaultPrettyPrinter;
import com.fasterxml.jackson.core.util.MinimalPrettyPrinter;
@@ -58,11 +61,15 @@ public class JsonEncoder extends ParsingEncoder implements Parser.ActionHandler
protected BitSet isEmpty = new BitSet();
JsonEncoder(Schema sc, OutputStream out) throws IOException {
- this(sc, getJsonGenerator(out, false));
+ this(sc, getJsonGenerator(out, EnumSet.noneOf(JsonOptions.class)));
}
JsonEncoder(Schema sc, OutputStream out, boolean pretty) throws IOException {
- this(sc, getJsonGenerator(out, pretty));
+ this(sc, getJsonGenerator(out, pretty ? EnumSet.of(JsonOptions.Pretty) : EnumSet.noneOf(JsonOptions.class)));
+ }
+
+ JsonEncoder(Schema sc, OutputStream out, Set options) throws IOException {
+ this(sc, getJsonGenerator(out, options));
}
JsonEncoder(Schema sc, JsonGenerator out) throws IOException {
@@ -78,24 +85,28 @@ public void flush() throws IOException {
}
}
+ enum JsonOptions {
+ Pretty,
+
+ // Prevent underlying outputstream to be flush for optimisation purpose.
+ NoFlushStream
+ }
+
// by default, one object per line.
// with pretty option use default pretty printer with root line separator.
- private static JsonGenerator getJsonGenerator(OutputStream out, boolean pretty) throws IOException {
+ private static JsonGenerator getJsonGenerator(OutputStream out, Set options) throws IOException {
Objects.requireNonNull(out, "OutputStream cannot be null");
JsonGenerator g = new JsonFactory().createGenerator(out, JsonEncoding.UTF8);
- if (pretty) {
- DefaultPrettyPrinter pp = new DefaultPrettyPrinter() {
- @Override
- public void writeRootValueSeparator(JsonGenerator jg) throws IOException {
- jg.writeRaw(LINE_SEPARATOR);
- }
- };
- g.setPrettyPrinter(pp);
+ if (options.contains(JsonOptions.NoFlushStream)) {
+ g = g.configure(JsonGenerator.Feature.FLUSH_PASSED_TO_STREAM, false);
+ }
+ final PrettyPrinter pp;
+ if (options.contains(JsonOptions.Pretty)) {
+ pp = new DefaultPrettyPrinter(LINE_SEPARATOR);
} else {
- MinimalPrettyPrinter pp = new MinimalPrettyPrinter();
- pp.setRootValueSeparator(LINE_SEPARATOR);
- g.setPrettyPrinter(pp);
+ pp = new MinimalPrettyPrinter(LINE_SEPARATOR);
}
+ g.setPrettyPrinter(pp);
return g;
}
@@ -122,7 +133,29 @@ public void setIncludeNamespace(final boolean includeNamespace) {
* @return this JsonEncoder
*/
public JsonEncoder configure(OutputStream out) throws IOException {
- this.configure(getJsonGenerator(out, false));
+ return this.configure(out, true);
+ }
+
+ /**
+ * Reconfigures this JsonEncoder to use the output stream provided.
+ *
+ * If the OutputStream provided is null, a NullPointerException is thrown.
+ *
+ * Otherwise, this JsonEncoder will flush its current output and then
+ * reconfigure its output to use a default UTF8 JsonGenerator that writes to the
+ * provided OutputStream.
+ *
+ * @param out The OutputStream to direct output to. Cannot be null.
+ * @throws IOException
+ * @throws NullPointerException if {@code out} is {@code null}
+ * @return this JsonEncoder
+ */
+ public JsonEncoder configure(OutputStream out, boolean autoflush) throws IOException {
+ EnumSet jsonOptions = EnumSet.noneOf(JsonOptions.class);
+ if (!autoflush) {
+ jsonOptions.add(JsonOptions.NoFlushStream);
+ }
+ this.configure(getJsonGenerator(out, jsonOptions));
return this;
}
diff --git a/lang/java/avro/src/main/java/org/apache/avro/io/parsing/ResolvingGrammarGenerator.java b/lang/java/avro/src/main/java/org/apache/avro/io/parsing/ResolvingGrammarGenerator.java
index 77fbe1c7ad0..f1c9d139e7d 100644
--- a/lang/java/avro/src/main/java/org/apache/avro/io/parsing/ResolvingGrammarGenerator.java
+++ b/lang/java/avro/src/main/java/org/apache/avro/io/parsing/ResolvingGrammarGenerator.java
@@ -292,8 +292,16 @@ public static void encode(Encoder e, Schema s, JsonNode n) throws IOException {
e.writeMapEnd();
break;
case UNION:
- e.writeIndex(0);
- encode(e, s.getTypes().get(0), n);
+ int correctIndex = 0;
+ List innerTypes = s.getTypes();
+ while (correctIndex < innerTypes.size() && !isCompatible(innerTypes.get(correctIndex).getType(), n)) {
+ correctIndex++;
+ }
+ if (correctIndex >= innerTypes.size()) {
+ throw new AvroTypeException("Not compatible default value for union: " + n);
+ }
+ e.writeIndex(correctIndex);
+ encode(e, innerTypes.get(correctIndex), n);
break;
case FIXED:
if (!n.isTextual())
@@ -346,4 +354,29 @@ public static void encode(Encoder e, Schema s, JsonNode n) throws IOException {
break;
}
}
+
+ private static boolean isCompatible(Schema.Type stype, JsonNode value) {
+ switch (stype) {
+ case RECORD:
+ case ENUM:
+ case ARRAY:
+ case MAP:
+ case UNION:
+ return true;
+ case FIXED:
+ case STRING:
+ case BYTES:
+ return value.isTextual();
+ case INT:
+ case LONG:
+ case FLOAT:
+ case DOUBLE:
+ return value.isNumber();
+ case BOOLEAN:
+ return value.isBoolean();
+ case NULL:
+ return value.isNull();
+ }
+ return true;
+ }
}
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 ec490979477..347490679ee 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
@@ -617,11 +617,8 @@ protected Object createSchemaDefaultValue(Type type, Field field, Schema fieldSc
AvroDefault defaultAnnotation = field.getAnnotation(AvroDefault.class);
defaultValue = (defaultAnnotation == null) ? null : Schema.parseJsonToObject(defaultAnnotation.value());
- if (defaultValue == null && fieldSchema.getType() == Schema.Type.UNION) {
- Schema defaultType = fieldSchema.getTypes().get(0);
- if (defaultType.getType() == Schema.Type.NULL) {
- defaultValue = JsonProperties.NULL_VALUE;
- }
+ if (defaultValue == null && fieldSchema.isNullable()) {
+ defaultValue = JsonProperties.NULL_VALUE;
}
return defaultValue;
}
@@ -756,7 +753,7 @@ protected Schema createSchema(Type type, Map names) {
AvroMeta[] metadata = field.getAnnotationsByType(AvroMeta.class); // add metadata
for (AvroMeta meta : metadata) {
- if (recordField.getObjectProps().containsKey(meta.key())) {
+ if (recordField.propsContainsKey(meta.key())) {
throw new AvroTypeException("Duplicate field prop key: " + meta.key());
}
recordField.addProp(meta.key(), meta.value());
@@ -775,7 +772,7 @@ protected Schema createSchema(Type type, Map names) {
schema.setFields(fields);
AvroMeta[] metadata = c.getAnnotationsByType(AvroMeta.class);
for (AvroMeta meta : metadata) {
- if (schema.getObjectProps().containsKey(meta.key())) {
+ if (schema.propsContainsKey(meta.key())) {
throw new AvroTypeException("Duplicate type prop key: " + meta.key());
}
schema.addProp(meta.key(), meta.value());
diff --git a/lang/java/avro/src/main/java/org/apache/avro/util/Utf8.java b/lang/java/avro/src/main/java/org/apache/avro/util/Utf8.java
index f54b6e2062b..9238fd78c65 100644
--- a/lang/java/avro/src/main/java/org/apache/avro/util/Utf8.java
+++ b/lang/java/avro/src/main/java/org/apache/avro/util/Utf8.java
@@ -24,9 +24,8 @@
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
-import org.apache.avro.AvroRuntimeException;
+import org.apache.avro.SystemLimitException;
import org.apache.avro.io.BinaryData;
-import org.slf4j.LoggerFactory;
/**
* A Utf8 string. Unlike {@link String}, instances are mutable. This is more
@@ -34,22 +33,8 @@
* as a single instance may be reused.
*/
public class Utf8 implements Comparable, CharSequence, Externalizable {
- private static final String MAX_LENGTH_PROPERTY = "org.apache.avro.limits.string.maxLength";
- private static final int MAX_LENGTH;
- private static final byte[] EMPTY = new byte[0];
- static {
- String o = System.getProperty(MAX_LENGTH_PROPERTY);
- int i = Integer.MAX_VALUE;
- if (o != null) {
- try {
- i = Integer.parseUnsignedInt(o);
- } catch (NumberFormatException nfe) {
- LoggerFactory.getLogger(Utf8.class).warn("Could not parse property " + MAX_LENGTH_PROPERTY + ": " + o, nfe);
- }
- }
- MAX_LENGTH = i;
- }
+ private static final byte[] EMPTY = new byte[0];
private byte[] bytes;
private int hash;
@@ -63,7 +48,7 @@ public Utf8() {
public Utf8(String string) {
byte[] bytes = getBytesFor(string);
int length = bytes.length;
- checkLength(length);
+ SystemLimitException.checkMaxStringLength(length);
this.bytes = bytes;
this.length = length;
this.string = string;
@@ -78,7 +63,7 @@ public Utf8(Utf8 other) {
public Utf8(byte[] bytes) {
int length = bytes.length;
- checkLength(length);
+ SystemLimitException.checkMaxStringLength(length);
this.bytes = bytes;
this.length = length;
}
@@ -121,7 +106,7 @@ public Utf8 setLength(int newLength) {
* length does not change, as this also clears the cached String.
*/
public Utf8 setByteLength(int newLength) {
- checkLength(newLength);
+ SystemLimitException.checkMaxStringLength(newLength);
if (this.bytes.length < newLength) {
this.bytes = Arrays.copyOf(this.bytes, newLength);
}
@@ -135,7 +120,7 @@ public Utf8 setByteLength(int newLength) {
public Utf8 set(String string) {
byte[] bytes = getBytesFor(string);
int length = bytes.length;
- checkLength(length);
+ SystemLimitException.checkMaxStringLength(length);
this.bytes = bytes;
this.length = length;
this.string = string;
@@ -215,12 +200,6 @@ public CharSequence subSequence(int start, int end) {
return toString().subSequence(start, end);
}
- private static void checkLength(int length) {
- if (length > MAX_LENGTH) {
- throw new AvroRuntimeException("String length " + length + " exceeds maximum allowed");
- }
- }
-
/** Gets the UTF-8 bytes for a String */
public static byte[] getBytesFor(String str) {
return str.getBytes(StandardCharsets.UTF_8);
diff --git a/lang/java/avro/src/main/java/org/apache/avro/util/WeakIdentityHashMap.java b/lang/java/avro/src/main/java/org/apache/avro/util/WeakIdentityHashMap.java
index a57cb49ac13..565d8e7ed36 100644
--- a/lang/java/avro/src/main/java/org/apache/avro/util/WeakIdentityHashMap.java
+++ b/lang/java/avro/src/main/java/org/apache/avro/util/WeakIdentityHashMap.java
@@ -22,10 +22,10 @@
import java.lang.ref.WeakReference;
import java.util.Collection;
import java.util.Collections;
-import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
/**
* Implements a combination of WeakHashMap and IdentityHashMap. Useful for
@@ -41,7 +41,7 @@
*/
public class WeakIdentityHashMap implements Map {
private final ReferenceQueue queue = new ReferenceQueue<>();
- private Map backingStore = new HashMap<>();
+ private Map backingStore = new ConcurrentHashMap<>();
public WeakIdentityHashMap() {
}
diff --git a/lang/java/avro/src/test/java/org/apache/avro/CustomType.java b/lang/java/avro/src/test/java/org/apache/avro/CustomType.java
new file mode 100644
index 00000000000..140ac901b0b
--- /dev/null
+++ b/lang/java/avro/src/test/java/org/apache/avro/CustomType.java
@@ -0,0 +1,48 @@
+/*
+ * 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;
+
+import java.util.Objects;
+
+public final class CustomType {
+ private final String name;
+
+ public CustomType(CharSequence name) {
+ this.name = name.toString();
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hashCode(name);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ return obj instanceof CustomType && name.equals(((CustomType) obj).name);
+ }
+
+ @Override
+ public String toString() {
+ return "CustomType{name='" + name + "'}";
+ }
+}
diff --git a/lang/java/avro/src/test/java/org/apache/avro/CustomTypeConverter.java b/lang/java/avro/src/test/java/org/apache/avro/CustomTypeConverter.java
new file mode 100644
index 00000000000..de8fea02ca4
--- /dev/null
+++ b/lang/java/avro/src/test/java/org/apache/avro/CustomTypeConverter.java
@@ -0,0 +1,48 @@
+/*
+ * 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;
+
+public class CustomTypeConverter extends Conversion {
+ private static final CustomTypeLogicalTypeFactory logicalTypeFactory = new CustomTypeLogicalTypeFactory();
+
+ @Override
+ public Class getConvertedType() {
+ return CustomType.class;
+ }
+
+ @Override
+ public String getLogicalTypeName() {
+ return logicalTypeFactory.getTypeName();
+ }
+
+ @Override
+ public Schema getRecommendedSchema() {
+ return Schema.create(Schema.Type.STRING);
+ }
+
+ @Override
+ public CustomType fromCharSequence(CharSequence value, Schema schema, LogicalType type) {
+ return new CustomType(value);
+ }
+
+ @Override
+ public CharSequence toCharSequence(CustomType value, Schema schema, LogicalType type) {
+ return value.getName();
+ }
+}
diff --git a/lang/java/avro/src/test/java/org/apache/avro/DummyLogicalTypeFactory.java b/lang/java/avro/src/test/java/org/apache/avro/CustomTypeLogicalTypeFactory.java
similarity index 86%
rename from lang/java/avro/src/test/java/org/apache/avro/DummyLogicalTypeFactory.java
rename to lang/java/avro/src/test/java/org/apache/avro/CustomTypeLogicalTypeFactory.java
index 4957e376521..3e121e0242c 100644
--- a/lang/java/avro/src/test/java/org/apache/avro/DummyLogicalTypeFactory.java
+++ b/lang/java/avro/src/test/java/org/apache/avro/CustomTypeLogicalTypeFactory.java
@@ -17,14 +17,14 @@
*/
package org.apache.avro;
-public class DummyLogicalTypeFactory implements LogicalTypes.LogicalTypeFactory {
+public class CustomTypeLogicalTypeFactory implements LogicalTypes.LogicalTypeFactory {
@Override
public LogicalType fromSchema(Schema schema) {
- return LogicalTypes.date();
+ return new LogicalType(getTypeName());
}
@Override
public String getTypeName() {
- return "service-example";
+ return "custom";
}
}
diff --git a/lang/java/avro/src/test/java/org/apache/avro/SchemaNameValidatorTest.java b/lang/java/avro/src/test/java/org/apache/avro/SchemaNameValidatorTest.java
new file mode 100644
index 00000000000..6846c4434cf
--- /dev/null
+++ b/lang/java/avro/src/test/java/org/apache/avro/SchemaNameValidatorTest.java
@@ -0,0 +1,59 @@
+/*
+ * 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;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+import java.util.stream.Stream;
+
+class SchemaNameValidatorTest {
+
+ @ParameterizedTest
+ @MethodSource("data")
+ void validator(Schema.NameValidator validator, String input, boolean expectedResult) {
+ Schema.NameValidator.Result result = validator.validate(input);
+ Assertions.assertEquals(expectedResult, result.isOK(), result.getErrors());
+ }
+
+ static Stream data() {
+ return Stream.of(Arguments.of(Schema.NameValidator.UTF_VALIDATOR, null, false), // null not accepted
+ Arguments.of(Schema.NameValidator.STRICT_VALIDATOR, null, false), // null not accepted
+ Arguments.of(Schema.NameValidator.UTF_VALIDATOR, "", false), // empty not accepted
+ Arguments.of(Schema.NameValidator.STRICT_VALIDATOR, "", false), // empty not accepted
+ Arguments.of(Schema.NameValidator.UTF_VALIDATOR, "Hello world", false), // space not accepted
+ Arguments.of(Schema.NameValidator.STRICT_VALIDATOR, "Hello world", false), // space not accepted
+ Arguments.of(Schema.NameValidator.UTF_VALIDATOR, "H&", false), // non letter or digit not accepted
+ Arguments.of(Schema.NameValidator.STRICT_VALIDATOR, "H&", false), // non letter or digit not accepted
+ Arguments.of(Schema.NameValidator.UTF_VALIDATOR, "H=", false), // non letter or digit not accepted
+ Arguments.of(Schema.NameValidator.STRICT_VALIDATOR, "H=", false), // non letter or digit not accepted
+ Arguments.of(Schema.NameValidator.UTF_VALIDATOR, "H]", false), // non letter or digit not accepted
+ Arguments.of(Schema.NameValidator.STRICT_VALIDATOR, "H]", false), // non letter or digit not accepted
+ Arguments.of(Schema.NameValidator.UTF_VALIDATOR, "Hello_world", true),
+ Arguments.of(Schema.NameValidator.STRICT_VALIDATOR, "Hello_world", true),
+ Arguments.of(Schema.NameValidator.UTF_VALIDATOR, "éàçô", true), // Accept accent
+ Arguments.of(Schema.NameValidator.STRICT_VALIDATOR, "éàçô", false), // Not Accept accent
+ Arguments.of(Schema.NameValidator.UTF_VALIDATOR, "5éàçô", false), // can't start with number
+ Arguments.of(Schema.NameValidator.STRICT_VALIDATOR, "5éàçô", false), // can't start with number
+ Arguments.of(Schema.NameValidator.UTF_VALIDATOR, "_Hello_world", true),
+ Arguments.of(Schema.NameValidator.STRICT_VALIDATOR, "_Hello_world", true));
+ }
+
+}
diff --git a/lang/java/avro/src/test/java/org/apache/avro/TestDataFileConcat.java b/lang/java/avro/src/test/java/org/apache/avro/TestDataFileConcat.java
index f1267ab9788..1aeebcddad5 100644
--- a/lang/java/avro/src/test/java/org/apache/avro/TestDataFileConcat.java
+++ b/lang/java/avro/src/test/java/org/apache/avro/TestDataFileConcat.java
@@ -17,60 +17,42 @@
*/
package org.apache.avro;
-import static org.junit.Assert.assertEquals;
-
-import java.io.File;
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.List;
-
import org.apache.avro.file.CodecFactory;
import org.apache.avro.file.DataFileReader;
import org.apache.avro.file.DataFileWriter;
import org.apache.avro.generic.GenericDatumReader;
import org.apache.avro.generic.GenericDatumWriter;
import org.apache.avro.util.RandomData;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.rules.TemporaryFolder;
-import org.junit.runner.RunWith;
-import org.junit.runners.Parameterized;
-import org.junit.runners.Parameterized.Parameters;
+
+import org.junit.jupiter.api.io.TempDir;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-@RunWith(Parameterized.class)
-public class TestDataFileConcat {
- private static final Logger LOG = LoggerFactory.getLogger(TestDataFileConcat.class);
-
- @Rule
- public TemporaryFolder DIR = new TemporaryFolder();
+import java.io.File;
+import java.io.IOException;
+import java.util.stream.Stream;
- CodecFactory codec;
- CodecFactory codec2;
- boolean recompress;
+import static org.junit.Assert.assertEquals;
- public TestDataFileConcat(CodecFactory codec, CodecFactory codec2, Boolean recompress) {
- this.codec = codec;
- this.codec2 = codec2;
- this.recompress = recompress;
- LOG.info("Testing concatenating files, " + codec2 + " into " + codec + " with recompress=" + recompress);
- }
+public class TestDataFileConcat {
+ private static final Logger LOG = LoggerFactory.getLogger(TestDataFileConcat.class);
- @Parameters
- public static List codecs() {
- List r = new ArrayList<>();
- r.add(new Object[] { null, null, false });
- r.add(new Object[] { null, null, true });
- r.add(new Object[] { CodecFactory.deflateCodec(1), CodecFactory.deflateCodec(6), false });
- r.add(new Object[] { CodecFactory.deflateCodec(1), CodecFactory.deflateCodec(6), true });
- r.add(new Object[] { CodecFactory.deflateCodec(3), CodecFactory.nullCodec(), false });
- r.add(new Object[] { CodecFactory.nullCodec(), CodecFactory.deflateCodec(6), false });
- r.add(new Object[] { CodecFactory.xzCodec(1), CodecFactory.xzCodec(2), false });
- r.add(new Object[] { CodecFactory.xzCodec(1), CodecFactory.xzCodec(2), true });
- r.add(new Object[] { CodecFactory.xzCodec(2), CodecFactory.nullCodec(), false });
- r.add(new Object[] { CodecFactory.nullCodec(), CodecFactory.xzCodec(2), false });
- return r;
+ @TempDir
+ public File DIR;
+
+ public static Stream codecs() {
+ return Stream.of(Arguments.of(null, null, false), Arguments.of(null, null, true),
+ Arguments.of(CodecFactory.deflateCodec(1), CodecFactory.deflateCodec(6), false),
+ Arguments.of(CodecFactory.deflateCodec(1), CodecFactory.deflateCodec(6), true),
+ Arguments.of(CodecFactory.deflateCodec(3), CodecFactory.nullCodec(), false),
+ Arguments.of(CodecFactory.nullCodec(), CodecFactory.deflateCodec(6), false),
+ Arguments.of(CodecFactory.xzCodec(1), CodecFactory.xzCodec(2), false),
+ Arguments.of(CodecFactory.xzCodec(1), CodecFactory.xzCodec(2), true),
+ Arguments.of(CodecFactory.xzCodec(2), CodecFactory.nullCodec(), false),
+ Arguments.of(CodecFactory.nullCodec(), CodecFactory.xzCodec(2), false));
}
private static final int COUNT = Integer.parseInt(System.getProperty("test.count", "200"));
@@ -83,11 +65,12 @@ public static List codecs() {
private static final Schema SCHEMA = new Schema.Parser().parse(SCHEMA_JSON);
private File makeFile(String name) {
- return new File(DIR.getRoot().getPath(), "test-" + name + ".avro");
+ return new File(DIR, "test-" + name + ".avro");
}
- @Test
- public void testConcatenateFiles() throws IOException {
+ @ParameterizedTest
+ @MethodSource("codecs")
+ void concatenateFiles(CodecFactory codec, CodecFactory codec2, boolean recompress) throws IOException {
System.out.println("SEED = " + SEED);
System.out.println("COUNT = " + COUNT);
for (int k = 0; k < 5; k++) {
diff --git a/lang/java/avro/src/test/java/org/apache/avro/TestDataFileReader.java b/lang/java/avro/src/test/java/org/apache/avro/TestDataFileReader.java
index fb42d639ecb..a85b966409b 100644
--- a/lang/java/avro/src/test/java/org/apache/avro/TestDataFileReader.java
+++ b/lang/java/avro/src/test/java/org/apache/avro/TestDataFileReader.java
@@ -87,7 +87,7 @@ void throttledInputStream() throws IOException {
// magic header check. This happens with throttled input stream,
// where we read into buffer less bytes than requested.
- Schema legacySchema = new Schema.Parser().setValidate(false).setValidateDefaults(false)
+ Schema legacySchema = new Schema.Parser(Schema.NameValidator.NO_VALIDATION).setValidateDefaults(false)
.parse("{\"type\": \"record\", \"name\": \"TestSchema\", \"fields\": "
+ "[ {\"name\": \"id\", \"type\": [\"long\", \"null\"], \"default\": null}]}");
File f = Files.createTempFile("testThrottledInputStream", ".avro").toFile();
@@ -146,7 +146,7 @@ void inputStreamEOF() throws IOException {
// AVRO-2944 describes hanging/failure in reading Avro file with performing
// magic header check. This potentially happens with a defective input stream
// where a -1 value is unexpectedly returned from a read.
- Schema legacySchema = new Schema.Parser().setValidate(false).setValidateDefaults(false)
+ Schema legacySchema = new Schema.Parser(Schema.NameValidator.NO_VALIDATION).setValidateDefaults(false)
.parse("{\"type\": \"record\", \"name\": \"TestSchema\", \"fields\": "
+ "[ {\"name\": \"id\", \"type\": [\"long\", \"null\"], \"default\": null}]}");
File f = Files.createTempFile("testInputStreamEOF", ".avro").toFile();
@@ -195,7 +195,7 @@ void ignoreSchemaValidationOnRead() throws IOException {
// This schema has an accent in the name and the default for the field doesn't
// match the first type in the union. A Java SDK in the past could create a file
// containing this schema.
- Schema legacySchema = new Schema.Parser().setValidate(false).setValidateDefaults(false)
+ Schema legacySchema = new Schema.Parser(Schema.NameValidator.NO_VALIDATION).setValidateDefaults(false)
.parse("{\"type\": \"record\", \"name\": \"InvalidAccëntWithInvalidNull\", \"fields\": "
+ "[ {\"name\": \"id\", \"type\": [\"long\", \"null\"], \"default\": null}]}");
diff --git a/lang/java/avro/src/test/java/org/apache/avro/TestFixed.java b/lang/java/avro/src/test/java/org/apache/avro/TestFixed.java
index 4741e4c08c5..f35c62d7a2e 100644
--- a/lang/java/avro/src/test/java/org/apache/avro/TestFixed.java
+++ b/lang/java/avro/src/test/java/org/apache/avro/TestFixed.java
@@ -18,11 +18,10 @@
package org.apache.avro;
-import static org.junit.jupiter.api.Assertions.assertArrayEquals;
-import static org.junit.jupiter.api.Assertions.assertNotNull;
-
import org.junit.jupiter.api.Test;
+import static org.junit.jupiter.api.Assertions.*;
+
public class TestFixed {
@Test
@@ -35,4 +34,16 @@ void fixedDefaultValueDrop() {
assertArrayEquals(new byte[16], (byte[]) field.defaultVal());
}
+ @Test
+ void fixedLengthOutOfLimit() {
+ Exception ex = assertThrows(UnsupportedOperationException.class,
+ () -> Schema.createFixed("oversize", "doc", "space", Integer.MAX_VALUE));
+ assertEquals(TestSystemLimitException.ERROR_VM_LIMIT_BYTES, ex.getMessage());
+ }
+
+ @Test
+ void fixedNegativeLength() {
+ Exception ex = assertThrows(AvroRuntimeException.class, () -> Schema.createFixed("negative", "doc", "space", -1));
+ assertEquals(TestSystemLimitException.ERROR_NEGATIVE, ex.getMessage());
+ }
}
diff --git a/lang/java/avro/src/test/java/org/apache/avro/TestLogicalType.java b/lang/java/avro/src/test/java/org/apache/avro/TestLogicalType.java
index 800772e7f1d..acc8899b21c 100644
--- a/lang/java/avro/src/test/java/org/apache/avro/TestLogicalType.java
+++ b/lang/java/avro/src/test/java/org/apache/avro/TestLogicalType.java
@@ -288,9 +288,9 @@ void registerLogicalTypeWithFactoryNameNotProvided() {
}
@Test
- void registerLogicalTypeFactoryByServiceLoader() {
+ public void testRegisterLogicalTypeFactoryByServiceLoader() {
assertThat(LogicalTypes.getCustomRegisteredTypes(),
- IsMapContaining.hasEntry(equalTo("service-example"), instanceOf(LogicalTypes.LogicalTypeFactory.class)));
+ IsMapContaining.hasEntry(equalTo("custom"), instanceOf(LogicalTypes.LogicalTypeFactory.class)));
}
public static void assertEqualsTrue(String message, Object o1, Object o2) {
diff --git a/lang/java/avro/src/test/java/org/apache/avro/TestProtocol.java b/lang/java/avro/src/test/java/org/apache/avro/TestProtocol.java
index af7c6a0f6c3..f7859e1c8e3 100644
--- a/lang/java/avro/src/test/java/org/apache/avro/TestProtocol.java
+++ b/lang/java/avro/src/test/java/org/apache/avro/TestProtocol.java
@@ -17,16 +17,104 @@
*/
package org.apache.avro;
+import org.apache.avro.generic.GenericData;
+import org.apache.avro.generic.GenericDatumWriter;
+import org.apache.avro.generic.IndexedRecord;
+import org.apache.avro.io.EncoderFactory;
+import org.apache.avro.io.JsonEncoder;
+
+import com.fasterxml.jackson.databind.JsonNode;
+
import static java.util.Collections.emptyList;
import static java.util.Collections.emptyMap;
import static java.util.Collections.singletonList;
import static java.util.Collections.singletonMap;
import static org.junit.jupiter.api.Assertions.*;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.IOException;
+import java.util.Collections;
+
import org.junit.jupiter.api.Test;
public class TestProtocol {
+ @Test
+ public void parse() throws IOException {
+ File fic = new File("../../../share/test/schemas/namespace.avpr");
+ Protocol protocol = Protocol.parse(fic);
+ assertNotNull(protocol);
+ assertEquals("TestNamespace", protocol.getName());
+ }
+
+ /**
+ * record type 'User' contains a field of type 'Status', which contains a field
+ * of type 'User'.
+ */
+ @Test
+ public void crossProtocol() {
+ String userStatus = "{ \"protocol\" : \"p1\", " + "\"types\": ["
+ + "{\"name\": \"User\", \"type\": \"record\", \"fields\": [{\"name\": \"current_status\", \"type\": \"Status\"}]},\n"
+ + "\n"
+ + "{\"name\": \"Status\", \"type\": \"record\", \"fields\": [{\"name\": \"author\", \"type\": \"User\"}]}"
+ + "]}";
+
+ Protocol protocol = Protocol.parse(userStatus);
+ Schema userSchema = protocol.getType("User");
+ Schema statusSchema = protocol.getType("Status");
+ assertSame(statusSchema, userSchema.getField("current_status").schema());
+ assertSame(userSchema, statusSchema.getField("author").schema());
+
+ String parsingFormUser = SchemaNormalization.toParsingForm(userSchema);
+ assertEquals(
+ "{\"name\":\"User\",\"type\":\"record\",\"fields\":[{\"name\":\"current_status\",\"type\":{\"name\":\"Status\",\"type\":\"record\",\"fields\":[{\"name\":\"author\",\"type\":\"User\"}]}}]}",
+ parsingFormUser);
+
+ String parsingFormStatus = SchemaNormalization.toParsingForm(statusSchema);
+ assertEquals(
+ "{\"name\":\"Status\",\"type\":\"record\",\"fields\":[{\"name\":\"author\",\"type\":{\"name\":\"User\",\"type\":\"record\",\"fields\":[{\"name\":\"current_status\",\"type\":\"Status\"}]}}]}",
+ parsingFormStatus);
+ }
+
+ /**
+ * When one schema with a type used before it is defined, test normalization
+ * defined schema before it is used.
+ */
+ @Test
+ void normalization() {
+ final String schema = "{\n" + " \"type\":\"record\", \"name\": \"Main\", " + " \"fields\":[\n"
+ + " { \"name\":\"f1\", \"type\":\"Sub\" },\n" // use Sub
+ + " { \"name\":\"f2\", " + " \"type\":{\n" + " \"type\":\"enum\", \"name\":\"Sub\",\n" // define
+ // Sub
+ + " \"symbols\":[\"OPEN\",\"CLOSE\"]\n" + " }\n" + " }\n" + " ]\n" + "}";
+ Schema s = new Schema.Parser().parse(schema);
+ assertNotNull(s);
+
+ String parsingForm = SchemaNormalization.toParsingForm(s);
+ assertEquals(
+ "{\"name\":\"Main\",\"type\":\"record\",\"fields\":[{\"name\":\"f1\",\"type\":{\"name\":\"Sub\",\"type\":\"enum\",\"symbols\":[\"OPEN\",\"CLOSE\"]}},{\"name\":\"f2\",\"type\":\"Sub\"}]}",
+ parsingForm);
+ }
+
+ @Test
+ void namespaceAndNameRules() {
+ Protocol p1 = new Protocol("P", null, "foo");
+ Protocol p2 = new Protocol("foo.P", null, null);
+ Protocol p3 = new Protocol("foo.P", null, "bar");
+ assertEquals(p1.getName(), p2.getName());
+ assertEquals(p1.getNamespace(), p2.getNamespace());
+ assertEquals(p1.getName(), p3.getName());
+ assertEquals(p1.getNamespace(), p3.getNamespace());
+
+ // The following situation is allowed, even if confusing, because the
+ // specification describes this algorithm without specifying that the resulting
+ // namespace mst be non-empty.
+ Protocol invalidName = new Protocol(".P", null, "ignored");
+ assertNull(invalidName.getNamespace());
+ assertEquals("P", invalidName.getName());
+ }
+
@Test
void propEquals() {
Protocol p1 = new Protocol("P", null, "foo");
diff --git a/lang/java/avro/src/test/java/org/apache/avro/TestReadingWritingDataInEvolvedSchemas.java b/lang/java/avro/src/test/java/org/apache/avro/TestReadingWritingDataInEvolvedSchemas.java
index 47cafcec189..89fedc75ca7 100644
--- a/lang/java/avro/src/test/java/org/apache/avro/TestReadingWritingDataInEvolvedSchemas.java
+++ b/lang/java/avro/src/test/java/org/apache/avro/TestReadingWritingDataInEvolvedSchemas.java
@@ -27,6 +27,8 @@
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Collection;
+import java.util.stream.Stream;
+
import org.apache.avro.generic.GenericData;
import org.apache.avro.generic.GenericData.EnumSymbol;
import org.apache.avro.generic.GenericData.Record;
@@ -38,24 +40,17 @@
import org.apache.avro.io.DecoderFactory;
import org.apache.avro.io.Encoder;
import org.apache.avro.io.EncoderFactory;
-import org.junit.Assert;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.rules.ExpectedException;
-import org.junit.runner.RunWith;
-import org.junit.runners.Parameterized;
-import org.junit.runners.Parameterized.Parameters;
-
-@RunWith(Parameterized.class)
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.EnumSource;
+
public class TestReadingWritingDataInEvolvedSchemas {
private static final String RECORD_A = "RecordA";
private static final String FIELD_A = "fieldA";
private static final char LATIN_SMALL_LETTER_O_WITH_DIARESIS = '\u00F6';
- @Rule
- public ExpectedException expectedException = ExpectedException.none();
-
private static final Schema DOUBLE_RECORD = SchemaBuilder.record(RECORD_A) //
.fields() //
.name(FIELD_A).type().doubleType().noDefault() //
@@ -89,13 +84,18 @@ public class TestReadingWritingDataInEvolvedSchemas {
.fields() //
.name(FIELD_A).type().unionOf().stringType().and().bytesType().endUnion().noDefault() //
.endRecord();
+
+ private static final Schema ENUM_AB = SchemaBuilder.enumeration("Enum1").symbols("A", "B");
+
private static final Schema ENUM_AB_RECORD = SchemaBuilder.record(RECORD_A) //
.fields() //
- .name(FIELD_A).type().enumeration("Enum1").symbols("A", "B").noDefault() //
+ .name(FIELD_A).type(ENUM_AB).noDefault() //
.endRecord();
+
+ private static final Schema ENUM_ABC = SchemaBuilder.enumeration("Enum1").symbols("A", "B", "C");
private static final Schema ENUM_ABC_RECORD = SchemaBuilder.record(RECORD_A) //
.fields() //
- .name(FIELD_A).type().enumeration("Enum1").symbols("A", "B", "C").noDefault() //
+ .name(FIELD_A).type(ENUM_ABC).noDefault() //
.endRecord();
private static final Schema UNION_INT_RECORD = SchemaBuilder.record(RECORD_A) //
.fields() //
@@ -122,221 +122,235 @@ public class TestReadingWritingDataInEvolvedSchemas {
.name(FIELD_A).type().unionOf().floatType().and().doubleType().endUnion().noDefault() //
.endRecord();
- @Parameters(name = "encoder = {0}")
- public static Collection data() {
- return Arrays.asList(new EncoderType[][] { { EncoderType.BINARY }, { EncoderType.JSON } });
- }
-
- public TestReadingWritingDataInEvolvedSchemas(EncoderType encoderType) {
- this.encoderType = encoderType;
- }
-
- private final EncoderType encoderType;
-
enum EncoderType {
BINARY, JSON
}
- @Test
- public void doubleWrittenWithUnionSchemaIsConvertedToDoubleSchema() throws Exception {
+ @ParameterizedTest
+ @EnumSource(EncoderType.class)
+ void doubleWrittenWithUnionSchemaIsConvertedToDoubleSchema(EncoderType encoderType) throws Exception {
Schema writer = UNION_INT_LONG_FLOAT_DOUBLE_RECORD;
Record record = defaultRecordWithSchema(writer, FIELD_A, 42.0);
- byte[] encoded = encodeGenericBlob(record);
- Record decoded = decodeGenericBlob(DOUBLE_RECORD, writer, encoded);
+ byte[] encoded = encodeGenericBlob(record, encoderType);
+ Record decoded = decodeGenericBlob(DOUBLE_RECORD, writer, encoded, encoderType);
assertEquals(42.0, decoded.get(FIELD_A));
}
- @Test
- public void longWrittenWithUnionSchemaIsConvertedToUnionLongFloatSchema() throws Exception {
+ @ParameterizedTest
+ @EnumSource(EncoderType.class)
+ void longWrittenWithUnionSchemaIsConvertedToUnionLongFloatSchema(EncoderType encoderType) throws Exception {
Schema writer = UNION_LONG_RECORD;
Record record = defaultRecordWithSchema(writer, FIELD_A, 42L);
- byte[] encoded = encodeGenericBlob(record);
- Record decoded = decodeGenericBlob(UNION_LONG_FLOAT_RECORD, writer, encoded);
+ byte[] encoded = encodeGenericBlob(record, encoderType);
+ Record decoded = decodeGenericBlob(UNION_LONG_FLOAT_RECORD, writer, encoded, encoderType);
assertEquals(42L, decoded.get(FIELD_A));
}
- @Test
- public void longWrittenWithUnionSchemaIsConvertedToDoubleSchema() throws Exception {
+ @ParameterizedTest
+ @EnumSource(EncoderType.class)
+ void longWrittenWithUnionSchemaIsConvertedToDoubleSchema(EncoderType encoderType) throws Exception {
Schema writer = UNION_LONG_RECORD;
Record record = defaultRecordWithSchema(writer, FIELD_A, 42L);
- byte[] encoded = encodeGenericBlob(record);
- Record decoded = decodeGenericBlob(UNION_DOUBLE_RECORD, writer, encoded);
+ byte[] encoded = encodeGenericBlob(record, encoderType);
+ Record decoded = decodeGenericBlob(UNION_DOUBLE_RECORD, writer, encoded, encoderType);
assertEquals(42.0, decoded.get(FIELD_A));
}
- @Test
- public void intWrittenWithUnionSchemaIsConvertedToDoubleSchema() throws Exception {
+ @ParameterizedTest
+ @EnumSource(EncoderType.class)
+ void intWrittenWithUnionSchemaIsConvertedToDoubleSchema(EncoderType encoderType) throws Exception {
Schema writer = UNION_INT_RECORD;
Record record = defaultRecordWithSchema(writer, FIELD_A, 42);
- byte[] encoded = encodeGenericBlob(record);
- Record decoded = decodeGenericBlob(UNION_DOUBLE_RECORD, writer, encoded);
+ byte[] encoded = encodeGenericBlob(record, encoderType);
+ Record decoded = decodeGenericBlob(UNION_DOUBLE_RECORD, writer, encoded, encoderType);
assertEquals(42.0, decoded.get(FIELD_A));
}
- @Test
- public void intWrittenWithUnionSchemaIsReadableByFloatSchema() throws Exception {
+ @ParameterizedTest
+ @EnumSource(EncoderType.class)
+ void intWrittenWithUnionSchemaIsReadableByFloatSchema(EncoderType encoderType) throws Exception {
Schema writer = UNION_INT_RECORD;
Record record = defaultRecordWithSchema(writer, FIELD_A, 42);
- byte[] encoded = encodeGenericBlob(record);
- Record decoded = decodeGenericBlob(FLOAT_RECORD, writer, encoded);
+ byte[] encoded = encodeGenericBlob(record, encoderType);
+ Record decoded = decodeGenericBlob(FLOAT_RECORD, writer, encoded, encoderType);
assertEquals(42.0f, decoded.get(FIELD_A));
}
- @Test
- public void intWrittenWithUnionSchemaIsReadableByFloatUnionSchema() throws Exception {
+ @ParameterizedTest
+ @EnumSource(EncoderType.class)
+ void intWrittenWithUnionSchemaIsReadableByFloatUnionSchema(EncoderType encoderType) throws Exception {
Schema writer = UNION_INT_RECORD;
Record record = defaultRecordWithSchema(writer, FIELD_A, 42);
- byte[] encoded = encodeGenericBlob(record);
- Record decoded = decodeGenericBlob(UNION_FLOAT_RECORD, writer, encoded);
+ byte[] encoded = encodeGenericBlob(record, encoderType);
+ Record decoded = decodeGenericBlob(UNION_FLOAT_RECORD, writer, encoded, encoderType);
assertEquals(42.0f, decoded.get(FIELD_A));
}
- @Test
- public void longWrittenWithUnionSchemaIsReadableByFloatSchema() throws Exception {
+ @ParameterizedTest
+ @EnumSource(EncoderType.class)
+ void longWrittenWithUnionSchemaIsReadableByFloatSchema(EncoderType encoderType) throws Exception {
Schema writer = UNION_LONG_RECORD;
Record record = defaultRecordWithSchema(writer, FIELD_A, 42L);
- byte[] encoded = encodeGenericBlob(record);
- Record decoded = decodeGenericBlob(FLOAT_RECORD, writer, encoded);
+ byte[] encoded = encodeGenericBlob(record, encoderType);
+ Record decoded = decodeGenericBlob(FLOAT_RECORD, writer, encoded, encoderType);
assertEquals(42.0f, decoded.get(FIELD_A));
}
- @Test
- public void longWrittenWithUnionSchemaIsReadableByFloatUnionSchema() throws Exception {
+ @ParameterizedTest
+ @EnumSource(EncoderType.class)
+ void longWrittenWithUnionSchemaIsReadableByFloatUnionSchema(EncoderType encoderType) throws Exception {
Schema writer = UNION_LONG_RECORD;
Record record = defaultRecordWithSchema(writer, FIELD_A, 42L);
- byte[] encoded = encodeGenericBlob(record);
- Record decoded = decodeGenericBlob(UNION_FLOAT_RECORD, writer, encoded);
+ byte[] encoded = encodeGenericBlob(record, encoderType);
+ Record decoded = decodeGenericBlob(UNION_FLOAT_RECORD, writer, encoded, encoderType);
assertEquals(42.0f, decoded.get(FIELD_A));
}
- @Test
- public void longWrittenWithUnionSchemaIsConvertedToLongFloatUnionSchema() throws Exception {
+ @ParameterizedTest
+ @EnumSource(EncoderType.class)
+ void longWrittenWithUnionSchemaIsConvertedToLongFloatUnionSchema(EncoderType encoderType) throws Exception {
Schema writer = UNION_LONG_RECORD;
Record record = defaultRecordWithSchema(writer, FIELD_A, 42L);
- byte[] encoded = encodeGenericBlob(record);
- Record decoded = decodeGenericBlob(UNION_LONG_FLOAT_RECORD, writer, encoded);
+ byte[] encoded = encodeGenericBlob(record, encoderType);
+ Record decoded = decodeGenericBlob(UNION_LONG_FLOAT_RECORD, writer, encoded, encoderType);
assertEquals(42L, decoded.get(FIELD_A));
}
- @Test
- public void longWrittenWithUnionSchemaIsConvertedToFloatDoubleUnionSchema() throws Exception {
+ @ParameterizedTest
+ @EnumSource(EncoderType.class)
+ void longWrittenWithUnionSchemaIsConvertedToFloatDoubleUnionSchema(EncoderType encoderType) throws Exception {
Schema writer = UNION_LONG_RECORD;
Record record = defaultRecordWithSchema(writer, FIELD_A, 42L);
- byte[] encoded = encodeGenericBlob(record);
- Record decoded = decodeGenericBlob(UNION_FLOAT_DOUBLE_RECORD, writer, encoded);
+ byte[] encoded = encodeGenericBlob(record, encoderType);
+ Record decoded = decodeGenericBlob(UNION_FLOAT_DOUBLE_RECORD, writer, encoded, encoderType);
assertEquals(42.0F, decoded.get(FIELD_A));
}
- @Test
- public void doubleWrittenWithUnionSchemaIsNotConvertedToFloatSchema() throws Exception {
- expectedException.expect(AvroTypeException.class);
- expectedException.expectMessage("Found double, expecting float");
+ @ParameterizedTest
+ @EnumSource(EncoderType.class)
+ void doubleWrittenWithUnionSchemaIsNotConvertedToFloatSchema(EncoderType encoderType) throws Exception {
Schema writer = UNION_INT_LONG_FLOAT_DOUBLE_RECORD;
Record record = defaultRecordWithSchema(writer, FIELD_A, 42.0);
- byte[] encoded = encodeGenericBlob(record);
- decodeGenericBlob(FLOAT_RECORD, writer, encoded);
+ byte[] encoded = encodeGenericBlob(record, encoderType);
+ AvroTypeException exception = Assertions.assertThrows(AvroTypeException.class,
+ () -> decodeGenericBlob(FLOAT_RECORD, writer, encoded, encoderType));
+ Assertions.assertEquals("Found double, expecting float", exception.getMessage());
}
- @Test
- public void floatWrittenWithUnionSchemaIsNotConvertedToLongSchema() throws Exception {
- expectedException.expect(AvroTypeException.class);
- expectedException.expectMessage("Found float, expecting long");
+ @ParameterizedTest
+ @EnumSource(EncoderType.class)
+ void floatWrittenWithUnionSchemaIsNotConvertedToLongSchema(EncoderType encoderType) throws Exception {
Schema writer = UNION_INT_LONG_FLOAT_DOUBLE_RECORD;
Record record = defaultRecordWithSchema(writer, FIELD_A, 42.0f);
- byte[] encoded = encodeGenericBlob(record);
- decodeGenericBlob(LONG_RECORD, writer, encoded);
+ byte[] encoded = encodeGenericBlob(record, encoderType);
+ AvroTypeException exception = Assertions.assertThrows(AvroTypeException.class,
+ () -> decodeGenericBlob(LONG_RECORD, writer, encoded, encoderType));
+ Assertions.assertEquals("Found float, expecting long", exception.getMessage());
}
- @Test
- public void longWrittenWithUnionSchemaIsNotConvertedToIntSchema() throws Exception {
- expectedException.expect(AvroTypeException.class);
- expectedException.expectMessage("Found long, expecting int");
+ @ParameterizedTest
+ @EnumSource(EncoderType.class)
+ void longWrittenWithUnionSchemaIsNotConvertedToIntSchema(EncoderType encoderType) throws Exception {
Schema writer = UNION_INT_LONG_FLOAT_DOUBLE_RECORD;
Record record = defaultRecordWithSchema(writer, FIELD_A, 42L);
- byte[] encoded = encodeGenericBlob(record);
- decodeGenericBlob(INT_RECORD, writer, encoded);
+ byte[] encoded = encodeGenericBlob(record, encoderType);
+ AvroTypeException exception = Assertions.assertThrows(AvroTypeException.class,
+ () -> decodeGenericBlob(INT_RECORD, writer, encoded, encoderType));
+ Assertions.assertEquals("Found long, expecting int", exception.getMessage());
}
- @Test
- public void intWrittenWithUnionSchemaIsConvertedToAllNumberSchemas() throws Exception {
+ @ParameterizedTest
+ @EnumSource(EncoderType.class)
+ void intWrittenWithUnionSchemaIsConvertedToAllNumberSchemas(EncoderType encoderType) throws Exception {
Schema writer = UNION_INT_LONG_FLOAT_DOUBLE_RECORD;
Record record = defaultRecordWithSchema(writer, FIELD_A, 42);
- byte[] encoded = encodeGenericBlob(record);
- assertEquals(42.0, decodeGenericBlob(DOUBLE_RECORD, writer, encoded).get(FIELD_A));
- assertEquals(42.0f, decodeGenericBlob(FLOAT_RECORD, writer, encoded).get(FIELD_A));
- assertEquals(42L, decodeGenericBlob(LONG_RECORD, writer, encoded).get(FIELD_A));
- assertEquals(42, decodeGenericBlob(INT_RECORD, writer, encoded).get(FIELD_A));
+ byte[] encoded = encodeGenericBlob(record, encoderType);
+ assertEquals(42.0, decodeGenericBlob(DOUBLE_RECORD, writer, encoded, encoderType).get(FIELD_A));
+ assertEquals(42.0f, decodeGenericBlob(FLOAT_RECORD, writer, encoded, encoderType).get(FIELD_A));
+ assertEquals(42L, decodeGenericBlob(LONG_RECORD, writer, encoded, encoderType).get(FIELD_A));
+ assertEquals(42, decodeGenericBlob(INT_RECORD, writer, encoded, encoderType).get(FIELD_A));
}
- @Test
- public void asciiStringWrittenWithUnionSchemaIsConvertedToBytesSchema() throws Exception {
+ @ParameterizedTest
+ @EnumSource(EncoderType.class)
+ void asciiStringWrittenWithUnionSchemaIsConvertedToBytesSchema(EncoderType encoderType) throws Exception {
Schema writer = UNION_STRING_BYTES_RECORD;
Record record = defaultRecordWithSchema(writer, FIELD_A, "42");
- byte[] encoded = encodeGenericBlob(record);
- ByteBuffer actual = (ByteBuffer) decodeGenericBlob(BYTES_RECORD, writer, encoded).get(FIELD_A);
+ byte[] encoded = encodeGenericBlob(record, encoderType);
+ ByteBuffer actual = (ByteBuffer) decodeGenericBlob(BYTES_RECORD, writer, encoded, encoderType).get(FIELD_A);
assertArrayEquals("42".getBytes(StandardCharsets.UTF_8), actual.array());
}
- @Test
- public void utf8StringWrittenWithUnionSchemaIsConvertedToBytesSchema() throws Exception {
+ @ParameterizedTest
+ @EnumSource(EncoderType.class)
+ void utf8StringWrittenWithUnionSchemaIsConvertedToBytesSchema(EncoderType encoderType) throws Exception {
String goeran = String.format("G%sran", LATIN_SMALL_LETTER_O_WITH_DIARESIS);
Schema writer = UNION_STRING_BYTES_RECORD;
Record record = defaultRecordWithSchema(writer, FIELD_A, goeran);
- byte[] encoded = encodeGenericBlob(record);
- ByteBuffer actual = (ByteBuffer) decodeGenericBlob(BYTES_RECORD, writer, encoded).get(FIELD_A);
+ byte[] encoded = encodeGenericBlob(record, encoderType);
+ ByteBuffer actual = (ByteBuffer) decodeGenericBlob(BYTES_RECORD, writer, encoded, encoderType).get(FIELD_A);
assertArrayEquals(goeran.getBytes(StandardCharsets.UTF_8), actual.array());
}
- @Test
- public void asciiBytesWrittenWithUnionSchemaIsConvertedToStringSchema() throws Exception {
+ @ParameterizedTest
+ @EnumSource(EncoderType.class)
+ void asciiBytesWrittenWithUnionSchemaIsConvertedToStringSchema(EncoderType encoderType) throws Exception {
Schema writer = UNION_STRING_BYTES_RECORD;
ByteBuffer buf = ByteBuffer.wrap("42".getBytes(StandardCharsets.UTF_8));
Record record = defaultRecordWithSchema(writer, FIELD_A, buf);
- byte[] encoded = encodeGenericBlob(record);
- CharSequence read = (CharSequence) decodeGenericBlob(STRING_RECORD, writer, encoded).get(FIELD_A);
+ byte[] encoded = encodeGenericBlob(record, encoderType);
+ CharSequence read = (CharSequence) decodeGenericBlob(STRING_RECORD, writer, encoded, encoderType).get(FIELD_A);
assertEquals("42", read.toString());
}
- @Test
- public void utf8BytesWrittenWithUnionSchemaIsConvertedToStringSchema() throws Exception {
+ @ParameterizedTest
+ @EnumSource(EncoderType.class)
+ void utf8BytesWrittenWithUnionSchemaIsConvertedToStringSchema(EncoderType encoderType) throws Exception {
String goeran = String.format("G%sran", LATIN_SMALL_LETTER_O_WITH_DIARESIS);
Schema writer = UNION_STRING_BYTES_RECORD;
Record record = defaultRecordWithSchema(writer, FIELD_A, goeran);
- byte[] encoded = encodeGenericBlob(record);
- CharSequence read = (CharSequence) decodeGenericBlob(STRING_RECORD, writer, encoded).get(FIELD_A);
+ byte[] encoded = encodeGenericBlob(record, encoderType);
+ CharSequence read = (CharSequence) decodeGenericBlob(STRING_RECORD, writer, encoded, encoderType).get(FIELD_A);
assertEquals(goeran, read.toString());
}
- @Test
- public void enumRecordCanBeReadWithExtendedEnumSchema() throws Exception {
+ @ParameterizedTest
+ @EnumSource(EncoderType.class)
+ void enumRecordCanBeReadWithExtendedEnumSchema(EncoderType encoderType) throws Exception {
Schema writer = ENUM_AB_RECORD;
- Record record = defaultRecordWithSchema(writer, FIELD_A, new EnumSymbol(writer, "A"));
- byte[] encoded = encodeGenericBlob(record);
- Record decoded = decodeGenericBlob(ENUM_ABC_RECORD, writer, encoded);
+ Record record = defaultRecordWithSchema(writer, FIELD_A, new EnumSymbol(ENUM_AB, "A"));
+ byte[] encoded = encodeGenericBlob(record, encoderType);
+ Record decoded = decodeGenericBlob(ENUM_ABC_RECORD, writer, encoded, encoderType);
assertEquals("A", decoded.get(FIELD_A).toString());
}
- @Test
- public void enumRecordWithExtendedSchemaCanBeReadWithOriginalEnumSchemaIfOnlyOldValues() throws Exception {
+ @ParameterizedTest
+ @EnumSource(EncoderType.class)
+ void enumRecordWithExtendedSchemaCanBeReadWithOriginalEnumSchemaIfOnlyOldValues(EncoderType encoderType)
+ throws Exception {
Schema writer = ENUM_ABC_RECORD;
- Record record = defaultRecordWithSchema(writer, FIELD_A, new EnumSymbol(writer, "A"));
- byte[] encoded = encodeGenericBlob(record);
- Record decoded = decodeGenericBlob(ENUM_AB_RECORD, writer, encoded);
+ Record record = defaultRecordWithSchema(writer, FIELD_A, new EnumSymbol(ENUM_ABC, "A"));
+ byte[] encoded = encodeGenericBlob(record, encoderType);
+ Record decoded = decodeGenericBlob(ENUM_AB_RECORD, writer, encoded, encoderType);
assertEquals("A", decoded.get(FIELD_A).toString());
}
- @Test
- public void enumRecordWithExtendedSchemaCanNotBeReadIfNewValuesAreUsed() throws Exception {
- expectedException.expect(AvroTypeException.class);
- expectedException.expectMessage("No match for C");
+ @ParameterizedTest
+ @EnumSource(EncoderType.class)
+ void enumRecordWithExtendedSchemaCanNotBeReadIfNewValuesAreUsed(EncoderType encoderType) throws Exception {
Schema writer = ENUM_ABC_RECORD;
- Record record = defaultRecordWithSchema(writer, FIELD_A, new EnumSymbol(writer, "C"));
- byte[] encoded = encodeGenericBlob(record);
- decodeGenericBlob(ENUM_AB_RECORD, writer, encoded);
+ Record record = defaultRecordWithSchema(writer, FIELD_A, new EnumSymbol(ENUM_ABC, "C"));
+ byte[] encoded = encodeGenericBlob(record, encoderType);
+
+ AvroTypeException exception = Assertions.assertThrows(AvroTypeException.class,
+ () -> decodeGenericBlob(ENUM_AB_RECORD, writer, encoded, encoderType));
+ Assertions.assertEquals("No match for C", exception.getMessage());
}
- @Test
- public void recordWrittenWithExtendedSchemaCanBeReadWithOriginalSchemaButLossOfData() throws Exception {
+ @ParameterizedTest
+ @EnumSource(EncoderType.class)
+ void recordWrittenWithExtendedSchemaCanBeReadWithOriginalSchemaButLossOfData(EncoderType encoderType)
+ throws Exception {
Schema writer = SchemaBuilder.record(RECORD_A) //
.fields() //
.name("newTopField").type().stringType().noDefault() //
@@ -344,47 +358,50 @@ public void recordWrittenWithExtendedSchemaCanBeReadWithOriginalSchemaButLossOfD
.endRecord();
Record record = defaultRecordWithSchema(writer, FIELD_A, 42);
record.put("newTopField", "not decoded");
- byte[] encoded = encodeGenericBlob(record);
- Record decoded = decodeGenericBlob(INT_RECORD, writer, encoded);
+ byte[] encoded = encodeGenericBlob(record, encoderType);
+ Record decoded = decodeGenericBlob(INT_RECORD, writer, encoded, encoderType);
assertEquals(42, decoded.get(FIELD_A));
try {
decoded.get("newTopField");
- Assert.fail("get should throw a exception");
+ Assertions.fail("get should throw a exception");
} catch (AvroRuntimeException ex) {
- Assert.assertEquals("Not a valid schema field: newTopField", ex.getMessage());
+ Assertions.assertEquals("Not a valid schema field: newTopField", ex.getMessage());
}
}
- @Test
- public void readerWithoutDefaultValueThrowsException() throws Exception {
- expectedException.expect(AvroTypeException.class);
- expectedException.expectMessage("missing required field newField");
+ @ParameterizedTest
+ @EnumSource(EncoderType.class)
+ void readerWithoutDefaultValueThrowsException(EncoderType encoderType) throws Exception {
Schema reader = SchemaBuilder.record(RECORD_A) //
.fields() //
.name("newField").type().intType().noDefault() //
.name(FIELD_A).type().intType().noDefault() //
.endRecord();
Record record = defaultRecordWithSchema(INT_RECORD, FIELD_A, 42);
- byte[] encoded = encodeGenericBlob(record);
- decodeGenericBlob(reader, INT_RECORD, encoded);
+ byte[] encoded = encodeGenericBlob(record, encoderType);
+ AvroTypeException exception = Assertions.assertThrows(AvroTypeException.class,
+ () -> decodeGenericBlob(reader, INT_RECORD, encoded, encoderType));
+ Assertions.assertTrue(exception.getMessage().contains("missing required field newField"), exception.getMessage());
}
- @Test
- public void readerWithDefaultValueIsApplied() throws Exception {
+ @ParameterizedTest
+ @EnumSource(EncoderType.class)
+ void readerWithDefaultValueIsApplied(EncoderType encoderType) throws Exception {
Schema reader = SchemaBuilder.record(RECORD_A) //
.fields() //
.name("newFieldWithDefault").type().intType().intDefault(314) //
.name(FIELD_A).type().intType().noDefault() //
.endRecord();
Record record = defaultRecordWithSchema(INT_RECORD, FIELD_A, 42);
- byte[] encoded = encodeGenericBlob(record);
- Record decoded = decodeGenericBlob(reader, INT_RECORD, encoded);
+ byte[] encoded = encodeGenericBlob(record, encoderType);
+ Record decoded = decodeGenericBlob(reader, INT_RECORD, encoded, encoderType);
assertEquals(42, decoded.get(FIELD_A));
assertEquals(314, decoded.get("newFieldWithDefault"));
}
- @Test
- public void aliasesInSchema() throws Exception {
+ @ParameterizedTest
+ @EnumSource(EncoderType.class)
+ void aliasesInSchema(EncoderType encoderType) throws Exception {
Schema writer = new Schema.Parser()
.parse("{\"namespace\": \"example.avro\", \"type\": \"record\", \"name\": \"User\", \"fields\": ["
+ "{\"name\": \"name\", \"type\": \"int\"}\n" + "]}\n");
@@ -393,8 +410,8 @@ public void aliasesInSchema() throws Exception {
+ "{\"name\": \"fname\", \"type\": \"int\", \"aliases\" : [ \"name\" ]}\n" + "]}\n");
GenericData.Record record = defaultRecordWithSchema(writer, "name", 1);
- byte[] encoded = encodeGenericBlob(record);
- GenericData.Record decoded = decodeGenericBlob(reader, reader, encoded);
+ byte[] encoded = encodeGenericBlob(record, encoderType);
+ GenericData.Record decoded = decodeGenericBlob(reader, reader, encoded, encoderType);
assertEquals(1, decoded.get("fname"));
}
@@ -405,7 +422,7 @@ private Record defaultRecordWithSchema(Schema schema, String key, T value) {
return data;
}
- private byte[] encodeGenericBlob(GenericRecord data) throws IOException {
+ private byte[] encodeGenericBlob(GenericRecord data, EncoderType encoderType) throws IOException {
DatumWriter writer = new GenericDatumWriter<>(data.getSchema());
ByteArrayOutputStream outStream = new ByteArrayOutputStream();
Encoder encoder = encoderType == EncoderType.BINARY ? EncoderFactory.get().binaryEncoder(outStream, null)
@@ -416,7 +433,8 @@ private byte[] encodeGenericBlob(GenericRecord data) throws IOException {
return outStream.toByteArray();
}
- private Record decodeGenericBlob(Schema expectedSchema, Schema schemaOfBlob, byte[] blob) throws IOException {
+ private Record decodeGenericBlob(Schema expectedSchema, Schema schemaOfBlob, byte[] blob, EncoderType encoderType)
+ throws IOException {
if (blob == null) {
return null;
}
diff --git a/lang/java/avro/src/test/java/org/apache/avro/TestSchema.java b/lang/java/avro/src/test/java/org/apache/avro/TestSchema.java
index 77adac28e8a..64748da1364 100644
--- a/lang/java/avro/src/test/java/org/apache/avro/TestSchema.java
+++ b/lang/java/avro/src/test/java/org/apache/avro/TestSchema.java
@@ -21,18 +21,35 @@
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
+import java.util.Set;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.IntNode;
+import com.fasterxml.jackson.databind.node.JsonNodeFactory;
+import com.fasterxml.jackson.databind.node.NullNode;
+import com.fasterxml.jackson.databind.node.TextNode;
import org.apache.avro.Schema.Field;
import org.apache.avro.Schema.Type;
import org.apache.avro.generic.GenericData;
+
+import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
public class TestSchema {
@@ -409,4 +426,199 @@ void qualifiedName() {
assertEquals("Int", nameInt.getQualified("space"));
}
+ @Test
+ void validValue() {
+ // Valid null value
+ final Schema nullSchema = Schema.create(Type.NULL);
+ assertTrue(nullSchema.isValidDefault(JsonNodeFactory.instance.nullNode()));
+
+ // Valid int value
+ final Schema intSchema = Schema.create(Type.INT);
+ assertTrue(intSchema.isValidDefault(JsonNodeFactory.instance.numberNode(12)));
+
+ // Valid Text value
+ final Schema strSchema = Schema.create(Type.STRING);
+ assertTrue(strSchema.isValidDefault(new TextNode("textNode")));
+
+ // Valid Array value
+ final Schema arraySchema = Schema.createArray(Schema.create(Type.STRING));
+ final ArrayNode arrayValue = JsonNodeFactory.instance.arrayNode();
+ assertTrue(arraySchema.isValidDefault(arrayValue)); // empty array
+
+ arrayValue.add("Hello");
+ arrayValue.add("World");
+ assertTrue(arraySchema.isValidDefault(arrayValue));
+
+ arrayValue.add(5);
+ assertFalse(arraySchema.isValidDefault(arrayValue));
+
+ // Valid Union type
+ final Schema unionSchema = Schema.createUnion(strSchema, intSchema, nullSchema);
+ assertTrue(unionSchema.isValidDefault(JsonNodeFactory.instance.textNode("Hello")));
+ assertTrue(unionSchema.isValidDefault(new IntNode(23)));
+ assertTrue(unionSchema.isValidDefault(JsonNodeFactory.instance.nullNode()));
+
+ assertFalse(unionSchema.isValidDefault(arrayValue));
+
+ // Array of union
+ final Schema arrayUnion = Schema.createArray(unionSchema);
+ final ArrayNode arrayUnionValue = JsonNodeFactory.instance.arrayNode();
+ arrayUnionValue.add("Hello");
+ arrayUnionValue.add(NullNode.getInstance());
+ assertTrue(arrayUnion.isValidDefault(arrayUnionValue));
+
+ // Union String, bytes
+ final Schema unionStrBytes = Schema.createUnion(strSchema, Schema.create(Type.BYTES));
+ assertTrue(unionStrBytes.isValidDefault(JsonNodeFactory.instance.textNode("Hello")));
+ assertFalse(unionStrBytes.isValidDefault(JsonNodeFactory.instance.numberNode(123)));
+ }
+
+ @Test
+ void enumLateDefine() {
+ String schemaString = "{\n" + " \"type\":\"record\",\n" + " \"name\": \"Main\",\n" + " \"fields\":[\n"
+ + " {\n" + " \"name\":\"f1\",\n" + " \"type\":\"Sub\"\n" + " },\n"
+ + " {\n" + " \"name\":\"f2\",\n" + " \"type\":{\n"
+ + " \"type\":\"enum\",\n" + " \"name\":\"Sub\",\n"
+ + " \"symbols\":[\"OPEN\",\"CLOSE\"]\n" + " }\n" + " }\n" + " ]\n" + "}";
+
+ final Schema schema = new Schema.Parser().parse(schemaString);
+ Schema f1Schema = schema.getField("f1").schema();
+ Schema f2Schema = schema.getField("f2").schema();
+ assertSame(f1Schema, f2Schema);
+ assertEquals(Type.ENUM, f1Schema.getType());
+ String stringSchema = schema.toString();
+ int definitionIndex = stringSchema.indexOf("\"symbols\":[\"OPEN\",\"CLOSE\"]");
+ int usageIndex = stringSchema.indexOf("\"type\":\"Sub\"");
+ assertTrue(definitionIndex < usageIndex, "usage is before definition");
+ }
+
+ @Test
+ public void testRecordInArray() {
+ String schemaString = "{\n" + " \"type\": \"record\",\n" + " \"name\": \"TestRecord\",\n" + " \"fields\": [\n"
+ + " {\n" + " \"name\": \"value\",\n" + " \"type\": {\n" + " \"type\": \"record\",\n"
+ + " \"name\": \"Container\",\n" + " \"fields\": [\n" + " {\n"
+ + " \"name\": \"Optional\",\n" + " \"type\": {\n" + " \"type\": \"array\",\n"
+ + " \"items\": [\n" + " {\n" + " \"type\": \"record\",\n"
+ + " \"name\": \"optional_field_0\",\n" + " \"namespace\": \"\",\n"
+ + " \"doc\": \"\",\n" + " \"fields\": [\n" + " {\n"
+ + " \"name\": \"optional_field_1\",\n" + " \"type\": \"long\",\n"
+ + " \"doc\": \"\",\n" + " \"default\": 0\n"
+ + " }\n" + " ]\n" + " }\n" + " ]\n"
+ + " }\n" + " }\n" + " ]\n" + " }\n" + " }\n" + " ]\n" + "}";
+ final Schema schema = new Schema.Parser().parse(schemaString);
+ assertNotNull(schema);
+ }
+
+ /*
+ * @Test public void testRec() { String schemaString =
+ * "[{\"name\":\"employees\",\"type\":[\"null\",{\"type\":\"array\",\"items\":{\"type\":\"record\",\"name\":\"Pair1081149ea1d6eb80\",\"fields\":[{\"name\":\"key\",\"type\":\"int\"},{\"name\":\"value\",\"type\":{\"type\":\"record\",\"name\":\"EmployeeInfo2\",\"fields\":[{\"name\":\"companyMap\",\"type\":[\"null\",{\"type\":\"array\",\"items\":{\"type\":\"record\",\"name\":\"PairIntegerString\",\"fields\":[{\"name\":\"key\",\"type\":\"int\"},{\"name\":\"value\",\"type\":\"string\"}]},\"java-class\":\"java.util.HashMap\"}],\"default\":null},{\"name\":\"name\",\"type\":[\"null\",\"string\"],\"default\":null}]}}]},\"java-class\":\"java.util.HashMap\"}],\"default\":null}]";
+ * final Schema schema = new Schema.Parser().parse(schemaString);
+ * Assert.assertNotNull(schema);
+ *
+ * }
+ */
+
+ @Test
+ public void testUnionFieldType() {
+ String schemaString = "{\"type\": \"record\", \"name\": \"Lisp\", \"fields\": [{\"name\":\"value\", \"type\":[\"null\", \"string\",{\"type\": \"record\", \"name\": \"Cons\", \"fields\": [{\"name\":\"car\", \"type\":\"Lisp\"},{\"name\":\"cdr\", \"type\":\"Lisp\"}]}]}]}";
+ final Schema schema = new Schema.Parser().parse(schemaString);
+ Field value = schema.getField("value");
+ Schema fieldSchema = value.schema();
+ Schema subSchema = fieldSchema.getTypes().stream().filter((Schema s) -> s.getType() == Type.RECORD).findFirst()
+ .get();
+ assertTrue(subSchema.hasFields());
+ }
+
+ @Test
+ public void parseAliases() throws JsonProcessingException {
+ String s1 = "{ \"aliases\" : [\"a1\", \"b1\"]}";
+ ObjectMapper mapper = new ObjectMapper();
+ JsonNode j1 = mapper.readTree(s1);
+ Set aliases = Schema.parseAliases(j1);
+ assertEquals(2, aliases.size());
+ assertTrue(aliases.contains("a1"));
+ assertTrue(aliases.contains("b1"));
+
+ String s2 = "{ \"aliases\" : {\"a1\": \"b1\"}}";
+ JsonNode j2 = mapper.readTree(s2);
+
+ SchemaParseException ex = assertThrows(SchemaParseException.class, () -> Schema.parseAliases(j2));
+ assertTrue(ex.getMessage().contains("aliases not an array"));
+
+ String s3 = "{ \"aliases\" : [11, \"b1\"]}";
+ JsonNode j3 = mapper.readTree(s3);
+ SchemaParseException ex3 = assertThrows(SchemaParseException.class, () -> Schema.parseAliases(j3));
+ assertTrue(ex3.getMessage().contains("alias not a string"));
+ }
+
+ @Test
+ void testContentAfterAvsc() {
+ Schema.Parser parser = new Schema.Parser(Schema.NameValidator.UTF_VALIDATOR);
+ parser.setValidateDefaults(true);
+ assertThrows(SchemaParseException.class, () -> parser.parse("{\"type\": \"string\"}; DROP TABLE STUDENTS"));
+ }
+
+ @Test
+ void testContentAfterAvscInInputStream() throws Exception {
+ Schema.Parser parser = new Schema.Parser(Schema.NameValidator.UTF_VALIDATOR);
+ parser.setValidateDefaults(true);
+ String avsc = "{\"type\": \"string\"}; DROP TABLE STUDENTS";
+ ByteArrayInputStream is = new ByteArrayInputStream(avsc.getBytes(StandardCharsets.UTF_8));
+ Schema schema = parser.parse(is);
+ assertNotNull(schema);
+ }
+
+ @Test
+ void testContentAfterAvscInFile() throws Exception {
+ File avscFile = Files.createTempFile("testContentAfterAvscInFile", null).toFile();
+ try (FileWriter writer = new FileWriter(avscFile)) {
+ writer.write("{\"type\": \"string\"}; DROP TABLE STUDENTS");
+ writer.flush();
+ }
+
+ Schema.Parser parser = new Schema.Parser(Schema.NameValidator.UTF_VALIDATOR);
+ parser.setValidateDefaults(true);
+ assertThrows(SchemaParseException.class, () -> parser.parse(avscFile));
+ }
+
+ @Test
+ void testParseMultipleFile() throws IOException {
+ URL directory = Thread.currentThread().getContextClassLoader().getResource("multipleFile");
+ File f1 = new File(directory.getPath(), "ApplicationEvent.avsc");
+ File f2 = new File(directory.getPath(), "DocumentInfo.avsc");
+ File f3 = new File(directory.getPath(), "MyResponse.avsc");
+ Assertions.assertTrue(f1.exists(), "File not exist for test " + f1.getPath());
+ Assertions.assertTrue(f2.exists(), "File not exist for test " + f2.getPath());
+ Assertions.assertTrue(f3.exists(), "File not exist for test " + f3.getPath());
+
+ final List schemas = new Schema.Parser().parse(Arrays.asList(f1, f2, f3));
+ Assertions.assertEquals(3, schemas.size());
+ Schema schemaAppEvent = schemas.get(0);
+ Schema schemaDocInfo = schemas.get(1);
+ Schema schemaResponse = schemas.get(2);
+
+ Assertions.assertNotNull(schemaAppEvent);
+ Assertions.assertEquals(3, schemaAppEvent.getFields().size());
+ Field documents = schemaAppEvent.getField("documents");
+ Schema docSchema = documents.schema().getTypes().get(1).getElementType();
+ Assertions.assertEquals(docSchema, schemaDocInfo);
+
+ Assertions.assertNotNull(schemaDocInfo);
+ Assertions.assertNotNull(schemaResponse);
+ }
+
+ @Test
+ void add_types() {
+ String schemaRecord2 = "{\"type\":\"record\", \"name\":\"record2\", \"fields\": ["
+ + " {\"name\":\"f1\", \"type\":\"record1\" }" + "]}";
+ // register schema1 in schema.
+ Schema schemaRecord1 = Schema.createRecord("record1", "doc", "", false);
+ Schema.Parser parser = new Schema.Parser().addTypes(Collections.singleton(schemaRecord1));
+
+ // parse schema for record2 that contains field for schema1.
+ final Schema schema = parser.parse(schemaRecord2);
+ final Field f1 = schema.getField("f1");
+ assertNotNull(f1);
+ assertEquals(schemaRecord1, f1.schema());
+ }
}
diff --git a/lang/java/avro/src/test/java/org/apache/avro/TestSchemaBuilder.java b/lang/java/avro/src/test/java/org/apache/avro/TestSchemaBuilder.java
index ddfbe7229c9..fcbaae65570 100644
--- a/lang/java/avro/src/test/java/org/apache/avro/TestSchemaBuilder.java
+++ b/lang/java/avro/src/test/java/org/apache/avro/TestSchemaBuilder.java
@@ -38,6 +38,8 @@
import org.apache.avro.generic.GenericDatumReader;
import org.apache.avro.generic.GenericDatumWriter;
import org.apache.avro.generic.GenericRecordBuilder;
+
+import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
@@ -149,6 +151,9 @@ void fieldObjectProps() {
assertEquals("ABC", f.getObjectProp("byteProp"));
assertTrue(f.getObjectProp("stringProp") instanceof String);
assertEquals("abc", f.getObjectProp("stringProp"));
+
+ assertEquals("abc", f.getObjectProp("stringProp", "default"));
+ assertEquals("default", f.getObjectProp("unknwon", "default"));
}
@Test
@@ -871,4 +876,25 @@ void namespaceDefaulting() {
assertEquals(a2, a1);
}
+
+ @Test
+ void namesAcceptAll() throws InterruptedException {
+ // Ensure that Schema.setNameValidator won't interfere with others unit tests.
+ Runnable r = () -> {
+ Schema.setNameValidator(Schema.NameValidator.NO_VALIDATION);
+ final Schema schema = SchemaBuilder.record("7name").fields().name("123").type(Schema.create(Schema.Type.INT))
+ .noDefault().endRecord();
+ Assertions.assertNotNull(schema);
+ Assertions.assertEquals("7name", schema.getName());
+ final Schema.Field field = schema.getField("123");
+ Assertions.assertEquals("123", field.name());
+ };
+
+ final Throwable[] exception = new Throwable[] { null };
+ Thread t = new Thread(r);
+ t.setUncaughtExceptionHandler((Thread th, Throwable e) -> exception[0] = e);
+ t.start();
+ t.join();
+ Assertions.assertNull(exception[0], () -> exception[0].getMessage());
+ }
}
diff --git a/lang/java/avro/src/test/java/org/apache/avro/TestSchemaCompatibility.java b/lang/java/avro/src/test/java/org/apache/avro/TestSchemaCompatibility.java
index cc5ada76e90..275bcfafede 100644
--- a/lang/java/avro/src/test/java/org/apache/avro/TestSchemaCompatibility.java
+++ b/lang/java/avro/src/test/java/org/apache/avro/TestSchemaCompatibility.java
@@ -120,7 +120,7 @@ public class TestSchemaCompatibility {
@Test
void validateSchemaPairMissingField() {
final List readerFields = list(new Schema.Field("oldfield1", INT_SCHEMA, null, null));
- final Schema reader = Schema.createRecord(readerFields);
+ final Schema reader = Schema.createRecord(null, null, null, false, readerFields);
final SchemaCompatibility.SchemaPairCompatibility expectedResult = new SchemaCompatibility.SchemaPairCompatibility(
SchemaCompatibility.SchemaCompatibilityResult.compatible(), reader, WRITER_SCHEMA,
SchemaCompatibility.READER_WRITER_COMPATIBLE_MESSAGE);
@@ -132,7 +132,7 @@ void validateSchemaPairMissingField() {
@Test
void validateSchemaPairMissingSecondField() {
final List readerFields = list(new Schema.Field("oldfield2", STRING_SCHEMA, null, null));
- final Schema reader = Schema.createRecord(readerFields);
+ final Schema reader = Schema.createRecord(null, null, null, false, readerFields);
final SchemaCompatibility.SchemaPairCompatibility expectedResult = new SchemaCompatibility.SchemaPairCompatibility(
SchemaCompatibility.SchemaCompatibilityResult.compatible(), reader, WRITER_SCHEMA,
SchemaCompatibility.READER_WRITER_COMPATIBLE_MESSAGE);
@@ -145,7 +145,7 @@ void validateSchemaPairMissingSecondField() {
void validateSchemaPairAllFields() {
final List readerFields = list(new Schema.Field("oldfield1", INT_SCHEMA, null, null),
new Schema.Field("oldfield2", STRING_SCHEMA, null, null));
- final Schema reader = Schema.createRecord(readerFields);
+ final Schema reader = Schema.createRecord(null, null, null, false, readerFields);
final SchemaCompatibility.SchemaPairCompatibility expectedResult = new SchemaCompatibility.SchemaPairCompatibility(
SchemaCompatibility.SchemaCompatibilityResult.compatible(), reader, WRITER_SCHEMA,
SchemaCompatibility.READER_WRITER_COMPATIBLE_MESSAGE);
@@ -158,7 +158,7 @@ void validateSchemaPairAllFields() {
void validateSchemaNewFieldWithDefault() {
final List readerFields = list(new Schema.Field("oldfield1", INT_SCHEMA, null, null),
new Schema.Field("newfield1", INT_SCHEMA, null, 42));
- final Schema reader = Schema.createRecord(readerFields);
+ final Schema reader = Schema.createRecord(null, null, null, false, readerFields);
final SchemaCompatibility.SchemaPairCompatibility expectedResult = new SchemaCompatibility.SchemaPairCompatibility(
SchemaCompatibility.SchemaCompatibilityResult.compatible(), reader, WRITER_SCHEMA,
SchemaCompatibility.READER_WRITER_COMPATIBLE_MESSAGE);
@@ -171,7 +171,7 @@ void validateSchemaNewFieldWithDefault() {
void validateSchemaNewField() {
final List readerFields = list(new Schema.Field("oldfield1", INT_SCHEMA, null, null),
new Schema.Field("newfield1", INT_SCHEMA, null, null));
- final Schema reader = Schema.createRecord(readerFields);
+ final Schema reader = Schema.createRecord(null, null, null, false, readerFields);
SchemaPairCompatibility compatibility = checkReaderWriterCompatibility(reader, WRITER_SCHEMA);
// Test new field without default value.
@@ -233,6 +233,22 @@ void unionReaderWriterSubsetIncompatibility() {
final Schema unionReader = Schema.createUnion(list(INT_SCHEMA, STRING_SCHEMA));
final SchemaPairCompatibility result = checkReaderWriterCompatibility(unionReader, unionWriter);
assertEquals(SchemaCompatibilityType.INCOMPATIBLE, result.getType());
+ assertEquals("/2", result.getResult().getIncompatibilities().get(0).getLocation());
+ }
+
+ @Test
+ void unionWriterSimpleReaderIncompatibility() {
+ Schema mandatorySchema = SchemaBuilder.record("Account").fields().name("age").type().intType().noDefault()
+ .endRecord();
+ Schema optionalSchema = SchemaBuilder.record("Account").fields().optionalInt("age").endRecord();
+
+ SchemaPairCompatibility compatibility = checkReaderWriterCompatibility(mandatorySchema, optionalSchema);
+
+ assertEquals(SchemaCompatibilityType.INCOMPATIBLE, compatibility.getType());
+
+ Incompatibility incompatibility = compatibility.getResult().getIncompatibilities().get(0);
+ assertEquals("reader type: INT not compatible with writer type: NULL", incompatibility.getMessage());
+ assertEquals("/fields/0/type/0", incompatibility.getLocation());
}
// -----------------------------------------------------------------------------------------------
diff --git a/lang/java/avro/src/test/java/org/apache/avro/TestSchemaCompatibilityFixedSizeMismatch.java b/lang/java/avro/src/test/java/org/apache/avro/TestSchemaCompatibilityFixedSizeMismatch.java
index 6ac3c68dc03..05321527cb4 100644
--- a/lang/java/avro/src/test/java/org/apache/avro/TestSchemaCompatibilityFixedSizeMismatch.java
+++ b/lang/java/avro/src/test/java/org/apache/avro/TestSchemaCompatibilityFixedSizeMismatch.java
@@ -17,44 +17,34 @@
*/
package org.apache.avro;
-import static org.apache.avro.TestSchemaCompatibility.validateIncompatibleSchemas;
-import static org.apache.avro.TestSchemas.*;
+import org.apache.avro.SchemaCompatibility.SchemaIncompatibilityType;
-import java.util.Arrays;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
-import org.apache.avro.SchemaCompatibility.SchemaIncompatibilityType;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.Parameterized;
-import org.junit.runners.Parameterized.Parameter;
-import org.junit.runners.Parameterized.Parameters;
+import java.util.stream.Stream;
+
+import static org.apache.avro.TestSchemaCompatibility.validateIncompatibleSchemas;
+import static org.apache.avro.TestSchemas.A_DINT_B_DFIXED_4_BYTES_RECORD1;
+import static org.apache.avro.TestSchemas.A_DINT_B_DFIXED_8_BYTES_RECORD1;
+import static org.apache.avro.TestSchemas.FIXED_4_BYTES;
+import static org.apache.avro.TestSchemas.FIXED_8_BYTES;
-@RunWith(Parameterized.class)
public class TestSchemaCompatibilityFixedSizeMismatch {
- @Parameters(name = "r: {0} | w: {1}")
- public static Iterable data() {
- Object[][] fields = { //
- { FIXED_4_BYTES, FIXED_8_BYTES, "expected: 8, found: 4", "/size" },
- { FIXED_8_BYTES, FIXED_4_BYTES, "expected: 4, found: 8", "/size" },
- { A_DINT_B_DFIXED_8_BYTES_RECORD1, A_DINT_B_DFIXED_4_BYTES_RECORD1, "expected: 4, found: 8",
- "/fields/1/type/size" },
- { A_DINT_B_DFIXED_4_BYTES_RECORD1, A_DINT_B_DFIXED_8_BYTES_RECORD1, "expected: 8, found: 4",
- "/fields/1/type/size" }, };
- return Arrays.asList(fields);
+ public static Stream data() {
+ return Stream.of(Arguments.of(FIXED_4_BYTES, FIXED_8_BYTES, "expected: 8, found: 4", "/size"),
+ Arguments.of(FIXED_8_BYTES, FIXED_4_BYTES, "expected: 4, found: 8", "/size"),
+ Arguments.of(A_DINT_B_DFIXED_8_BYTES_RECORD1, A_DINT_B_DFIXED_4_BYTES_RECORD1, "expected: 4, found: 8",
+ "/fields/1/type/size"),
+ Arguments.of(A_DINT_B_DFIXED_4_BYTES_RECORD1, A_DINT_B_DFIXED_8_BYTES_RECORD1, "expected: 8, found: 4",
+ "/fields/1/type/size"));
}
- @Parameter(0)
- public Schema reader;
- @Parameter(1)
- public Schema writer;
- @Parameter(2)
- public String details;
- @Parameter(3)
- public String location;
-
- @Test
- public void testFixedSizeMismatchSchemas() throws Exception {
+ @ParameterizedTest
+ @MethodSource("data")
+ void fixedSizeMismatchSchemas(Schema reader, Schema writer, String details, String location) {
validateIncompatibleSchemas(reader, writer, SchemaIncompatibilityType.FIXED_SIZE_MISMATCH, details, location);
}
}
diff --git a/lang/java/avro/src/test/java/org/apache/avro/TestSchemaCompatibilityMissingEnumSymbols.java b/lang/java/avro/src/test/java/org/apache/avro/TestSchemaCompatibilityMissingEnumSymbols.java
index 82b70fe2443..63d607cd596 100644
--- a/lang/java/avro/src/test/java/org/apache/avro/TestSchemaCompatibilityMissingEnumSymbols.java
+++ b/lang/java/avro/src/test/java/org/apache/avro/TestSchemaCompatibilityMissingEnumSymbols.java
@@ -17,19 +17,19 @@
*/
package org.apache.avro;
-import static org.apache.avro.TestSchemaCompatibility.validateIncompatibleSchemas;
-import static org.apache.avro.TestSchemas.*;
+import org.apache.avro.SchemaCompatibility.SchemaIncompatibilityType;
-import java.util.Arrays;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
-import org.apache.avro.SchemaCompatibility.SchemaIncompatibilityType;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.Parameterized;
-import org.junit.runners.Parameterized.Parameter;
-import org.junit.runners.Parameterized.Parameters;
+import java.util.stream.Stream;
+
+import static org.apache.avro.TestSchemaCompatibility.validateIncompatibleSchemas;
+import static org.apache.avro.TestSchemas.ENUM1_ABC_SCHEMA;
+import static org.apache.avro.TestSchemas.ENUM1_AB_SCHEMA;
+import static org.apache.avro.TestSchemas.ENUM1_BC_SCHEMA;
-@RunWith(Parameterized.class)
public class TestSchemaCompatibilityMissingEnumSymbols {
private static final Schema RECORD1_WITH_ENUM_AB = SchemaBuilder.record("Record1").fields() //
@@ -39,26 +39,15 @@ public class TestSchemaCompatibilityMissingEnumSymbols {
.name("field1").type(ENUM1_ABC_SCHEMA).noDefault() //
.endRecord();
- @Parameters(name = "r: {0} | w: {1}")
- public static Iterable data() {
- Object[][] fields = { //
- { ENUM1_AB_SCHEMA, ENUM1_ABC_SCHEMA, "[C]", "/symbols" },
- { ENUM1_BC_SCHEMA, ENUM1_ABC_SCHEMA, "[A]", "/symbols" },
- { RECORD1_WITH_ENUM_AB, RECORD1_WITH_ENUM_ABC, "[C]", "/fields/0/type/symbols" } };
- return Arrays.asList(fields);
+ public static Stream data() {
+ return Stream.of(Arguments.of(ENUM1_AB_SCHEMA, ENUM1_ABC_SCHEMA, "[C]", "/symbols"),
+ Arguments.of(ENUM1_BC_SCHEMA, ENUM1_ABC_SCHEMA, "[A]", "/symbols"),
+ Arguments.of(RECORD1_WITH_ENUM_AB, RECORD1_WITH_ENUM_ABC, "[C]", "/fields/0/type/symbols"));
}
- @Parameter(0)
- public Schema reader;
- @Parameter(1)
- public Schema writer;
- @Parameter(2)
- public String details;
- @Parameter(3)
- public String location;
-
- @Test
- public void testTypeMismatchSchemas() throws Exception {
+ @ParameterizedTest
+ @MethodSource("data")
+ public void testTypeMismatchSchemas(Schema reader, Schema writer, String details, String location) {
validateIncompatibleSchemas(reader, writer, SchemaIncompatibilityType.MISSING_ENUM_SYMBOLS, details, location);
}
}
diff --git a/lang/java/avro/src/test/java/org/apache/avro/TestSchemaCompatibilityMissingUnionBranch.java b/lang/java/avro/src/test/java/org/apache/avro/TestSchemaCompatibilityMissingUnionBranch.java
index 4f947690009..3e84a5337c9 100644
--- a/lang/java/avro/src/test/java/org/apache/avro/TestSchemaCompatibilityMissingUnionBranch.java
+++ b/lang/java/avro/src/test/java/org/apache/avro/TestSchemaCompatibilityMissingUnionBranch.java
@@ -17,22 +17,40 @@
*/
package org.apache.avro;
-import static java.util.Arrays.asList;
-import static org.apache.avro.TestSchemaCompatibility.validateIncompatibleSchemas;
-import static org.apache.avro.TestSchemas.*;
-
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.List;
-
import org.apache.avro.SchemaCompatibility.SchemaIncompatibilityType;
-import org.junit.Test;
+
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
-import org.junit.runners.Parameterized.Parameter;
import org.junit.runners.Parameterized.Parameters;
-@RunWith(Parameterized.class)
+import java.util.Collections;
+import java.util.List;
+import java.util.stream.Stream;
+
+import static java.util.Arrays.asList;
+import static org.apache.avro.TestSchemaCompatibility.validateIncompatibleSchemas;
+import static org.apache.avro.TestSchemas.A_DINT_B_DINT_STRING_UNION_RECORD1;
+import static org.apache.avro.TestSchemas.A_DINT_B_DINT_UNION_RECORD1;
+import static org.apache.avro.TestSchemas.BOOLEAN_SCHEMA;
+import static org.apache.avro.TestSchemas.BYTES_UNION_SCHEMA;
+import static org.apache.avro.TestSchemas.DOUBLE_UNION_SCHEMA;
+import static org.apache.avro.TestSchemas.ENUM1_AB_SCHEMA;
+import static org.apache.avro.TestSchemas.FIXED_4_BYTES;
+import static org.apache.avro.TestSchemas.FLOAT_UNION_SCHEMA;
+import static org.apache.avro.TestSchemas.INT_ARRAY_SCHEMA;
+import static org.apache.avro.TestSchemas.INT_LONG_FLOAT_DOUBLE_UNION_SCHEMA;
+import static org.apache.avro.TestSchemas.INT_MAP_SCHEMA;
+import static org.apache.avro.TestSchemas.INT_SCHEMA;
+import static org.apache.avro.TestSchemas.INT_STRING_UNION_SCHEMA;
+import static org.apache.avro.TestSchemas.INT_UNION_SCHEMA;
+import static org.apache.avro.TestSchemas.LONG_UNION_SCHEMA;
+import static org.apache.avro.TestSchemas.NULL_SCHEMA;
+import static org.apache.avro.TestSchemas.STRING_UNION_SCHEMA;
+import static org.apache.avro.TestSchemas.list;
+
public class TestSchemaCompatibilityMissingUnionBranch {
private static final Schema RECORD1_WITH_INT = SchemaBuilder.record("Record1").fields() //
@@ -50,61 +68,52 @@ public class TestSchemaCompatibilityMissingUnionBranch {
private static final Schema UNION_INT_MAP_INT = Schema.createUnion(list(INT_SCHEMA, INT_MAP_SCHEMA));
private static final Schema UNION_INT_NULL = Schema.createUnion(list(INT_SCHEMA, NULL_SCHEMA));
- @Parameters(name = "r: {0} | w: {1}")
- public static Iterable data() {
- Object[][] fields = { //
- { INT_UNION_SCHEMA, INT_STRING_UNION_SCHEMA,
- Collections.singletonList("reader union lacking writer type: STRING"), Collections.singletonList("/1") },
- { STRING_UNION_SCHEMA, INT_STRING_UNION_SCHEMA,
- Collections.singletonList("reader union lacking writer type: INT"), Collections.singletonList("/0") },
- { INT_UNION_SCHEMA, UNION_INT_RECORD1, Collections.singletonList("reader union lacking writer type: RECORD"),
- Collections.singletonList("/1") },
- { INT_UNION_SCHEMA, UNION_INT_RECORD2, Collections.singletonList("reader union lacking writer type: RECORD"),
- Collections.singletonList("/1") },
+ public static Stream data() {
+ return Stream.of( //
+ Arguments.of(INT_UNION_SCHEMA, INT_STRING_UNION_SCHEMA,
+ Collections.singletonList("reader union lacking writer type: STRING"), Collections.singletonList("/1")),
+ Arguments.of(STRING_UNION_SCHEMA, INT_STRING_UNION_SCHEMA,
+ Collections.singletonList("reader union lacking writer type: INT"), Collections.singletonList("/0")),
+ Arguments.of(INT_UNION_SCHEMA, UNION_INT_RECORD1,
+ Collections.singletonList("reader union lacking writer type: RECORD"), Collections.singletonList("/1")),
+ Arguments.of(INT_UNION_SCHEMA, UNION_INT_RECORD2,
+ Collections.singletonList("reader union lacking writer type: RECORD"), Collections.singletonList("/1")),
// more info in the subset schemas
- { UNION_INT_RECORD1, UNION_INT_RECORD2, Collections.singletonList("reader union lacking writer type: RECORD"),
- Collections.singletonList("/1") },
- { INT_UNION_SCHEMA, UNION_INT_ENUM1_AB, Collections.singletonList("reader union lacking writer type: ENUM"),
- Collections.singletonList("/1") },
- { INT_UNION_SCHEMA, UNION_INT_FIXED_4_BYTES,
- Collections.singletonList("reader union lacking writer type: FIXED"), Collections.singletonList("/1") },
- { INT_UNION_SCHEMA, UNION_INT_BOOLEAN, Collections.singletonList("reader union lacking writer type: BOOLEAN"),
- Collections.singletonList("/1") },
- { INT_UNION_SCHEMA, LONG_UNION_SCHEMA, Collections.singletonList("reader union lacking writer type: LONG"),
- Collections.singletonList("/0") },
- { INT_UNION_SCHEMA, FLOAT_UNION_SCHEMA, Collections.singletonList("reader union lacking writer type: FLOAT"),
- Collections.singletonList("/0") },
- { INT_UNION_SCHEMA, DOUBLE_UNION_SCHEMA, Collections.singletonList("reader union lacking writer type: DOUBLE"),
- Collections.singletonList("/0") },
- { INT_UNION_SCHEMA, BYTES_UNION_SCHEMA, Collections.singletonList("reader union lacking writer type: BYTES"),
- Collections.singletonList("/0") },
- { INT_UNION_SCHEMA, UNION_INT_ARRAY_INT, Collections.singletonList("reader union lacking writer type: ARRAY"),
- Collections.singletonList("/1") },
- { INT_UNION_SCHEMA, UNION_INT_MAP_INT, Collections.singletonList("reader union lacking writer type: MAP"),
- Collections.singletonList("/1") },
- { INT_UNION_SCHEMA, UNION_INT_NULL, Collections.singletonList("reader union lacking writer type: NULL"),
- Collections.singletonList("/1") },
- { INT_UNION_SCHEMA, INT_LONG_FLOAT_DOUBLE_UNION_SCHEMA,
+ Arguments.of(UNION_INT_RECORD1, UNION_INT_RECORD2,
+ Collections.singletonList("reader union lacking writer type: RECORD"), Collections.singletonList("/1")),
+ Arguments.of(INT_UNION_SCHEMA, UNION_INT_ENUM1_AB,
+ Collections.singletonList("reader union lacking writer type: ENUM"), Collections.singletonList("/1")),
+ Arguments.of(INT_UNION_SCHEMA, UNION_INT_FIXED_4_BYTES,
+ Collections.singletonList("reader union lacking writer type: FIXED"), Collections.singletonList("/1")),
+ Arguments.of(INT_UNION_SCHEMA, UNION_INT_BOOLEAN,
+ Collections.singletonList("reader union lacking writer type: BOOLEAN"), Collections.singletonList("/1")),
+ Arguments.of(INT_UNION_SCHEMA, LONG_UNION_SCHEMA,
+ Collections.singletonList("reader union lacking writer type: LONG"), Collections.singletonList("/0")),
+ Arguments.of(INT_UNION_SCHEMA, FLOAT_UNION_SCHEMA,
+ Collections.singletonList("reader union lacking writer type: FLOAT"), Collections.singletonList("/0")),
+ Arguments.of(INT_UNION_SCHEMA, DOUBLE_UNION_SCHEMA,
+ Collections.singletonList("reader union lacking writer type: DOUBLE"), Collections.singletonList("/0")),
+ Arguments.of(INT_UNION_SCHEMA, BYTES_UNION_SCHEMA,
+ Collections.singletonList("reader union lacking writer type: BYTES"), Collections.singletonList("/0")),
+ Arguments.of(INT_UNION_SCHEMA, UNION_INT_ARRAY_INT,
+ Collections.singletonList("reader union lacking writer type: ARRAY"), Collections.singletonList("/1")),
+ Arguments.of(INT_UNION_SCHEMA, UNION_INT_MAP_INT,
+ Collections.singletonList("reader union lacking writer type: MAP"), Collections.singletonList("/1")),
+ Arguments.of(INT_UNION_SCHEMA, UNION_INT_NULL,
+ Collections.singletonList("reader union lacking writer type: NULL"), Collections.singletonList("/1")),
+ Arguments.of(INT_UNION_SCHEMA, INT_LONG_FLOAT_DOUBLE_UNION_SCHEMA,
asList("reader union lacking writer type: LONG", "reader union lacking writer type: FLOAT",
"reader union lacking writer type: DOUBLE"),
- asList("/1", "/2", "/3") },
- { A_DINT_B_DINT_UNION_RECORD1, A_DINT_B_DINT_STRING_UNION_RECORD1,
+ asList("/1", "/2", "/3")),
+ Arguments.of(A_DINT_B_DINT_UNION_RECORD1, A_DINT_B_DINT_STRING_UNION_RECORD1,
Collections.singletonList("reader union lacking writer type: STRING"),
- Collections.singletonList("/fields/1/type/1") } };
- return Arrays.asList(fields);
+ Collections.singletonList("/fields/1/type/1")));
}
- @Parameter(0)
- public Schema reader;
- @Parameter(1)
- public Schema writer;
- @Parameter(2)
- public List details;
- @Parameter(3)
- public List location;
-
- @Test
- public void testMissingUnionBranch() throws Exception {
+ @ParameterizedTest
+ @MethodSource("data")
+ public void testMissingUnionBranch(Schema reader, Schema writer, List details, List location)
+ throws Exception {
List types = Collections.nCopies(details.size(),
SchemaIncompatibilityType.MISSING_UNION_BRANCH);
validateIncompatibleSchemas(reader, writer, types, details, location);
diff --git a/lang/java/avro/src/test/java/org/apache/avro/TestSchemaCompatibilityNameMismatch.java b/lang/java/avro/src/test/java/org/apache/avro/TestSchemaCompatibilityNameMismatch.java
index 83c89ab7b76..d20561faae8 100644
--- a/lang/java/avro/src/test/java/org/apache/avro/TestSchemaCompatibilityNameMismatch.java
+++ b/lang/java/avro/src/test/java/org/apache/avro/TestSchemaCompatibilityNameMismatch.java
@@ -17,44 +17,37 @@
*/
package org.apache.avro;
-import static org.apache.avro.TestSchemaCompatibility.validateIncompatibleSchemas;
-import static org.apache.avro.TestSchemas.*;
+import org.apache.avro.SchemaCompatibility.SchemaIncompatibilityType;
-import java.util.Arrays;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
-import org.apache.avro.SchemaCompatibility.SchemaIncompatibilityType;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.Parameterized;
-import org.junit.runners.Parameterized.Parameter;
-import org.junit.runners.Parameterized.Parameters;
+import java.util.stream.Stream;
+
+import static org.apache.avro.TestSchemaCompatibility.validateIncompatibleSchemas;
+import static org.apache.avro.TestSchemas.A_DINT_B_DENUM_1_RECORD1;
+import static org.apache.avro.TestSchemas.A_DINT_B_DENUM_2_RECORD1;
+import static org.apache.avro.TestSchemas.EMPTY_RECORD1;
+import static org.apache.avro.TestSchemas.EMPTY_RECORD2;
+import static org.apache.avro.TestSchemas.ENUM1_AB_SCHEMA;
+import static org.apache.avro.TestSchemas.ENUM2_AB_SCHEMA;
+import static org.apache.avro.TestSchemas.FIXED_4_BYTES;
-@RunWith(Parameterized.class)
public class TestSchemaCompatibilityNameMismatch {
private static final Schema FIXED_4_ANOTHER_NAME = Schema.createFixed("AnotherName", null, null, 4);
- @Parameters(name = "r: {0} | w: {1}")
- public static Iterable data() {
- Object[][] fields = { //
- { ENUM1_AB_SCHEMA, ENUM2_AB_SCHEMA, "expected: Enum2", "/name" },
- { EMPTY_RECORD2, EMPTY_RECORD1, "expected: Record1", "/name" },
- { FIXED_4_BYTES, FIXED_4_ANOTHER_NAME, "expected: AnotherName", "/name" },
- { A_DINT_B_DENUM_1_RECORD1, A_DINT_B_DENUM_2_RECORD1, "expected: Enum2", "/fields/1/type/name" } };
- return Arrays.asList(fields);
+ public static Stream data() {
+ return Stream.of(Arguments.of(ENUM1_AB_SCHEMA, ENUM2_AB_SCHEMA, "expected: Enum2", "/name"),
+ Arguments.of(EMPTY_RECORD2, EMPTY_RECORD1, "expected: Record1", "/name"),
+ Arguments.of(FIXED_4_BYTES, FIXED_4_ANOTHER_NAME, "expected: AnotherName", "/name"),
+ Arguments.of(A_DINT_B_DENUM_1_RECORD1, A_DINT_B_DENUM_2_RECORD1, "expected: Enum2", "/fields/1/type/name"));
}
- @Parameter(0)
- public Schema reader;
- @Parameter(1)
- public Schema writer;
- @Parameter(2)
- public String details;
- @Parameter(3)
- public String location;
-
- @Test
- public void testNameMismatchSchemas() throws Exception {
+ @ParameterizedTest
+ @MethodSource("data")
+ public void testNameMismatchSchemas(Schema reader, Schema writer, String details, String location) throws Exception {
validateIncompatibleSchemas(reader, writer, SchemaIncompatibilityType.NAME_MISMATCH, details, location);
}
}
diff --git a/lang/java/avro/src/test/java/org/apache/avro/TestSchemaCompatibilityReaderFieldMissingDefaultValue.java b/lang/java/avro/src/test/java/org/apache/avro/TestSchemaCompatibilityReaderFieldMissingDefaultValue.java
index d367caed941..7a21c1a5fcd 100644
--- a/lang/java/avro/src/test/java/org/apache/avro/TestSchemaCompatibilityReaderFieldMissingDefaultValue.java
+++ b/lang/java/avro/src/test/java/org/apache/avro/TestSchemaCompatibilityReaderFieldMissingDefaultValue.java
@@ -17,38 +17,29 @@
*/
package org.apache.avro;
-import static org.apache.avro.TestSchemaCompatibility.validateIncompatibleSchemas;
-import static org.apache.avro.TestSchemas.*;
+import org.apache.avro.SchemaCompatibility.SchemaIncompatibilityType;
-import java.util.Arrays;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
-import org.apache.avro.SchemaCompatibility.SchemaIncompatibilityType;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.Parameterized;
-import org.junit.runners.Parameterized.Parameter;
-import org.junit.runners.Parameterized.Parameters;
+import java.util.stream.Stream;
+
+import static org.apache.avro.TestSchemaCompatibility.validateIncompatibleSchemas;
+import static org.apache.avro.TestSchemas.A_INT_B_DINT_RECORD1;
+import static org.apache.avro.TestSchemas.A_INT_RECORD1;
+import static org.apache.avro.TestSchemas.EMPTY_RECORD1;
-@RunWith(Parameterized.class)
public class TestSchemaCompatibilityReaderFieldMissingDefaultValue {
- @Parameters(name = "r: {0} | w: {1}")
- public static Iterable data() {
- Object[][] fields = { //
- { A_INT_RECORD1, EMPTY_RECORD1, "a", "/fields/0" }, { A_INT_B_DINT_RECORD1, EMPTY_RECORD1, "a", "/fields/0" } };
- return Arrays.asList(fields);
- }
- @Parameter(0)
- public Schema reader;
- @Parameter(1)
- public Schema writer;
- @Parameter(2)
- public String details;
- @Parameter(3)
- public String location;
+ public static Stream data() {
+ return Stream.of(Arguments.of(A_INT_RECORD1, EMPTY_RECORD1, "a", "/fields/0"),
+ Arguments.of(A_INT_B_DINT_RECORD1, EMPTY_RECORD1, "a", "/fields/0"));
+ }
- @Test
- public void testReaderFieldMissingDefaultValueSchemas() throws Exception {
+ @ParameterizedTest
+ @MethodSource("data")
+ public void testReaderFieldMissingDefaultValueSchemas(Schema reader, Schema writer, String details, String location) {
validateIncompatibleSchemas(reader, writer, SchemaIncompatibilityType.READER_FIELD_MISSING_DEFAULT_VALUE, details,
location);
}
diff --git a/lang/java/avro/src/test/java/org/apache/avro/TestSchemaCompatibilityTypeMismatch.java b/lang/java/avro/src/test/java/org/apache/avro/TestSchemaCompatibilityTypeMismatch.java
index 63dd3ac11a7..247e40404ba 100644
--- a/lang/java/avro/src/test/java/org/apache/avro/TestSchemaCompatibilityTypeMismatch.java
+++ b/lang/java/avro/src/test/java/org/apache/avro/TestSchemaCompatibilityTypeMismatch.java
@@ -17,82 +17,94 @@
*/
package org.apache.avro;
-import static org.apache.avro.TestSchemaCompatibility.validateIncompatibleSchemas;
-import static org.apache.avro.TestSchemas.*;
+import org.apache.avro.SchemaCompatibility.SchemaIncompatibilityType;
-import java.util.Arrays;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
-import org.apache.avro.SchemaCompatibility.SchemaIncompatibilityType;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.Parameterized;
-import org.junit.runners.Parameterized.Parameter;
-import org.junit.runners.Parameterized.Parameters;
+import java.util.stream.Stream;
+
+import static org.apache.avro.TestSchemaCompatibility.validateIncompatibleSchemas;
+import static org.apache.avro.TestSchemas.A_INT_RECORD1;
+import static org.apache.avro.TestSchemas.BOOLEAN_SCHEMA;
+import static org.apache.avro.TestSchemas.BYTES_SCHEMA;
+import static org.apache.avro.TestSchemas.DOUBLE_SCHEMA;
+import static org.apache.avro.TestSchemas.ENUM2_AB_SCHEMA;
+import static org.apache.avro.TestSchemas.FIXED_4_BYTES;
+import static org.apache.avro.TestSchemas.FLOAT_SCHEMA;
+import static org.apache.avro.TestSchemas.INT_ARRAY_SCHEMA;
+import static org.apache.avro.TestSchemas.INT_FLOAT_UNION_SCHEMA;
+import static org.apache.avro.TestSchemas.INT_LIST_RECORD;
+import static org.apache.avro.TestSchemas.INT_LONG_FLOAT_DOUBLE_UNION_SCHEMA;
+import static org.apache.avro.TestSchemas.INT_MAP_SCHEMA;
+import static org.apache.avro.TestSchemas.INT_SCHEMA;
+import static org.apache.avro.TestSchemas.LONG_ARRAY_SCHEMA;
+import static org.apache.avro.TestSchemas.LONG_LIST_RECORD;
+import static org.apache.avro.TestSchemas.LONG_MAP_SCHEMA;
+import static org.apache.avro.TestSchemas.LONG_SCHEMA;
+import static org.apache.avro.TestSchemas.NULL_SCHEMA;
+import static org.apache.avro.TestSchemas.STRING_SCHEMA;
-@RunWith(Parameterized.class)
public class TestSchemaCompatibilityTypeMismatch {
- @Parameters(name = "r: {0} | w: {1}")
- public static Iterable data() {
- Object[][] fields = { //
- { NULL_SCHEMA, INT_SCHEMA, "reader type: NULL not compatible with writer type: INT", "/" },
- { NULL_SCHEMA, LONG_SCHEMA, "reader type: NULL not compatible with writer type: LONG", "/" },
- { BOOLEAN_SCHEMA, INT_SCHEMA, "reader type: BOOLEAN not compatible with writer type: INT", "/" },
+ public static Stream data() {
+ return Stream.of(
+ Arguments.of(NULL_SCHEMA, INT_SCHEMA, "reader type: NULL not compatible with writer type: INT", "/"),
+ Arguments.of(NULL_SCHEMA, LONG_SCHEMA, "reader type: NULL not compatible with writer type: LONG", "/"),
+
+ Arguments.of(BOOLEAN_SCHEMA, INT_SCHEMA, "reader type: BOOLEAN not compatible with writer type: INT", "/"),
- { INT_SCHEMA, NULL_SCHEMA, "reader type: INT not compatible with writer type: NULL", "/" },
- { INT_SCHEMA, BOOLEAN_SCHEMA, "reader type: INT not compatible with writer type: BOOLEAN", "/" },
- { INT_SCHEMA, LONG_SCHEMA, "reader type: INT not compatible with writer type: LONG", "/" },
- { INT_SCHEMA, FLOAT_SCHEMA, "reader type: INT not compatible with writer type: FLOAT", "/" },
- { INT_SCHEMA, DOUBLE_SCHEMA, "reader type: INT not compatible with writer type: DOUBLE", "/" },
+ Arguments.of(INT_SCHEMA, NULL_SCHEMA, "reader type: INT not compatible with writer type: NULL", "/"),
+ Arguments.of(INT_SCHEMA, BOOLEAN_SCHEMA, "reader type: INT not compatible with writer type: BOOLEAN", "/"),
+ Arguments.of(INT_SCHEMA, LONG_SCHEMA, "reader type: INT not compatible with writer type: LONG", "/"),
+ Arguments.of(INT_SCHEMA, FLOAT_SCHEMA, "reader type: INT not compatible with writer type: FLOAT", "/"),
+ Arguments.of(INT_SCHEMA, DOUBLE_SCHEMA, "reader type: INT not compatible with writer type: DOUBLE", "/"),
- { LONG_SCHEMA, FLOAT_SCHEMA, "reader type: LONG not compatible with writer type: FLOAT", "/" },
- { LONG_SCHEMA, DOUBLE_SCHEMA, "reader type: LONG not compatible with writer type: DOUBLE", "/" },
+ Arguments.of(LONG_SCHEMA, FLOAT_SCHEMA, "reader type: LONG not compatible with writer type: FLOAT", "/"),
+ Arguments.of(LONG_SCHEMA, DOUBLE_SCHEMA, "reader type: LONG not compatible with writer type: DOUBLE", "/"),
- { FLOAT_SCHEMA, DOUBLE_SCHEMA, "reader type: FLOAT not compatible with writer type: DOUBLE", "/" },
+ Arguments.of(FLOAT_SCHEMA, DOUBLE_SCHEMA, "reader type: FLOAT not compatible with writer type: DOUBLE", "/"),
- { DOUBLE_SCHEMA, STRING_SCHEMA, "reader type: DOUBLE not compatible with writer type: STRING", "/" },
+ Arguments.of(DOUBLE_SCHEMA, STRING_SCHEMA, "reader type: DOUBLE not compatible with writer type: STRING", "/"),
- { FIXED_4_BYTES, STRING_SCHEMA, "reader type: FIXED not compatible with writer type: STRING", "/" },
+ Arguments.of(FIXED_4_BYTES, STRING_SCHEMA, "reader type: FIXED not compatible with writer type: STRING", "/"),
- { STRING_SCHEMA, BOOLEAN_SCHEMA, "reader type: STRING not compatible with writer type: BOOLEAN", "/" },
- { STRING_SCHEMA, INT_SCHEMA, "reader type: STRING not compatible with writer type: INT", "/" },
+ Arguments.of(STRING_SCHEMA, BOOLEAN_SCHEMA, "reader type: STRING not compatible with writer type: BOOLEAN",
+ "/"),
+ Arguments.of(STRING_SCHEMA, INT_SCHEMA, "reader type: STRING not compatible with writer type: INT", "/"),
- { BYTES_SCHEMA, NULL_SCHEMA, "reader type: BYTES not compatible with writer type: NULL", "/" },
- { BYTES_SCHEMA, INT_SCHEMA, "reader type: BYTES not compatible with writer type: INT", "/" },
+ Arguments.of(BYTES_SCHEMA, NULL_SCHEMA, "reader type: BYTES not compatible with writer type: NULL", "/"),
+ Arguments.of(BYTES_SCHEMA, INT_SCHEMA, "reader type: BYTES not compatible with writer type: INT", "/"),
- { A_INT_RECORD1, INT_SCHEMA, "reader type: RECORD not compatible with writer type: INT", "/" },
+ Arguments.of(A_INT_RECORD1, INT_SCHEMA, "reader type: RECORD not compatible with writer type: INT", "/"),
- { INT_ARRAY_SCHEMA, LONG_ARRAY_SCHEMA, "reader type: INT not compatible with writer type: LONG", "/items" },
- { INT_MAP_SCHEMA, INT_ARRAY_SCHEMA, "reader type: MAP not compatible with writer type: ARRAY", "/" },
- { INT_ARRAY_SCHEMA, INT_MAP_SCHEMA, "reader type: ARRAY not compatible with writer type: MAP", "/" },
- { INT_MAP_SCHEMA, LONG_MAP_SCHEMA, "reader type: INT not compatible with writer type: LONG", "/values" },
+ Arguments.of(INT_ARRAY_SCHEMA, LONG_ARRAY_SCHEMA, "reader type: INT not compatible with writer type: LONG",
+ "/items"),
+ Arguments.of(INT_MAP_SCHEMA, INT_ARRAY_SCHEMA, "reader type: MAP not compatible with writer type: ARRAY", "/"),
+ Arguments.of(INT_ARRAY_SCHEMA, INT_MAP_SCHEMA, "reader type: ARRAY not compatible with writer type: MAP", "/"),
+ Arguments.of(INT_MAP_SCHEMA, LONG_MAP_SCHEMA, "reader type: INT not compatible with writer type: LONG",
+ "/values"),
- { INT_SCHEMA, ENUM2_AB_SCHEMA, "reader type: INT not compatible with writer type: ENUM", "/" },
- { ENUM2_AB_SCHEMA, INT_SCHEMA, "reader type: ENUM not compatible with writer type: INT", "/" },
+ Arguments.of(INT_SCHEMA, ENUM2_AB_SCHEMA, "reader type: INT not compatible with writer type: ENUM", "/"),
+ Arguments.of(ENUM2_AB_SCHEMA, INT_SCHEMA, "reader type: ENUM not compatible with writer type: INT", "/"),
- { FLOAT_SCHEMA, INT_LONG_FLOAT_DOUBLE_UNION_SCHEMA,
- "reader type: FLOAT not compatible with writer type: DOUBLE", "/" },
- { LONG_SCHEMA, INT_FLOAT_UNION_SCHEMA, "reader type: LONG not compatible with writer type: FLOAT", "/" },
- { INT_SCHEMA, INT_FLOAT_UNION_SCHEMA, "reader type: INT not compatible with writer type: FLOAT", "/" },
+ Arguments.of(FLOAT_SCHEMA, INT_LONG_FLOAT_DOUBLE_UNION_SCHEMA,
+ "reader type: FLOAT not compatible with writer type: DOUBLE", "/3"),
+ Arguments.of(LONG_SCHEMA, INT_FLOAT_UNION_SCHEMA, "reader type: LONG not compatible with writer type: FLOAT",
+ "/1"),
+ Arguments.of(INT_SCHEMA, INT_FLOAT_UNION_SCHEMA, "reader type: INT not compatible with writer type: FLOAT",
+ "/1"),
- { INT_LIST_RECORD, LONG_LIST_RECORD, "reader type: INT not compatible with writer type: LONG",
- "/fields/0/type" },
+ Arguments.of(INT_LIST_RECORD, LONG_LIST_RECORD, "reader type: INT not compatible with writer type: LONG",
+ "/fields/0/type"),
- { NULL_SCHEMA, INT_SCHEMA, "reader type: NULL not compatible with writer type: INT", "/" } };
- return Arrays.asList(fields);
+ Arguments.of(NULL_SCHEMA, INT_SCHEMA, "reader type: NULL not compatible with writer type: INT", "/"));
}
- @Parameter(0)
- public Schema reader;
- @Parameter(1)
- public Schema writer;
- @Parameter(2)
- public String details;
- @Parameter(3)
- public String location;
-
- @Test
- public void testTypeMismatchSchemas() throws Exception {
+ @ParameterizedTest
+ @MethodSource("data")
+ public void testTypeMismatchSchemas(Schema reader, Schema writer, String details, String location) throws Exception {
validateIncompatibleSchemas(reader, writer, SchemaIncompatibilityType.TYPE_MISMATCH, details, location);
}
}
diff --git a/lang/java/avro/src/test/java/org/apache/avro/TestSchemaValidateDefault.java b/lang/java/avro/src/test/java/org/apache/avro/TestSchemaValidateDefault.java
new file mode 100644
index 00000000000..a86519c7560
--- /dev/null
+++ b/lang/java/avro/src/test/java/org/apache/avro/TestSchemaValidateDefault.java
@@ -0,0 +1,152 @@
+/*
+ * 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;
+
+import org.apache.avro.generic.GenericData;
+import org.apache.avro.io.Decoder;
+import org.apache.avro.io.DecoderFactory;
+import org.apache.avro.io.Encoder;
+import org.apache.avro.io.EncoderFactory;
+import org.apache.avro.reflect.ReflectData;
+import org.apache.avro.reflect.ReflectDatumReader;
+import org.apache.avro.reflect.ReflectDatumWriter;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.util.Objects;
+import java.util.function.Function;
+
+public class TestSchemaValidateDefault {
+
+ @Test
+ public void valueReadWithCorrectDefaultValue() throws IOException {
+
+ ExampleRecord writtenValue = new ExampleRecord(new ComplexValue(42L), new ComplexValue(666L));
+ byte[] bytes = getSerializer(ExampleRecord.SCHEMA_WITH_ONE_FIELD).apply(writtenValue);
+
+ ReflectDatumReader reader = new ReflectDatumReader<>(ExampleRecord.SCHEMA_WITH_ONE_FIELD,
+ ExampleRecord.SCHEMA_WITH_TWO_FIELDS, ReflectData.get());
+ Decoder decoder = DecoderFactory.get().jsonDecoder(ExampleRecord.SCHEMA_WITH_ONE_FIELD,
+ new ByteArrayInputStream(bytes));
+ ExampleRecord deserializedValue = reader.read(null, decoder);
+
+ Assertions.assertNotNull(deserializedValue.getValue2(), "Null get value2");
+ Assertions.assertEquals(15L, deserializedValue.getValue2().getValue());
+ }
+
+ public static Function getSerializer(Schema writerSchema) {
+ Objects.requireNonNull(writerSchema, "writerSchema must not be null");
+
+ ReflectDatumWriter writer = new ReflectDatumWriter<>(writerSchema, new ReflectData());
+ return object -> {
+ try {
+ ByteArrayOutputStream stream = new ByteArrayOutputStream();
+ Encoder encoder = EncoderFactory.get().jsonEncoder(writerSchema, stream);
+ writer.write(object, encoder);
+ encoder.flush();
+ return stream.toByteArray();
+ } catch (IOException e) {
+ throw new IllegalStateException(String.format("Avro failed to encode %s to schema %s", object, writerSchema),
+ e);
+ }
+ };
+ }
+
+ public static Function getDeserializer(Class readClass, Schema readerSchema, Schema writerSchema) {
+ Objects.requireNonNull(readClass, "readClass must not be null");
+ Objects.requireNonNull(readerSchema, "readerSchema must not be null");
+ Objects.requireNonNull(writerSchema, "writerSchema must not be null");
+
+ ReflectDatumReader reader = new ReflectDatumReader<>(writerSchema, readerSchema, new ReflectData());
+ return (byte[] bytes) -> {
+ try {
+ Decoder decoder = DecoderFactory.get().jsonDecoder(writerSchema, new ByteArrayInputStream(bytes));
+ T readValue = reader.read(null, decoder);
+ return readValue;
+ } catch (IOException e) {
+ throw new IllegalStateException(String.format("Avro failed to decode %s to %s", new String(bytes), readClass),
+ e);
+ }
+ };
+ }
+
+ static final Schema SCHEMA = SchemaBuilder.record("org.apache.avro.TestSchemaValidateDefault.ComplexValue").fields()
+ .optionalLong("value").endRecord();
+
+ public static class ComplexValue {
+
+ private Long value;
+
+ public ComplexValue() {
+ }
+
+ public ComplexValue(Long value) {
+ this.value = value;
+ }
+
+ public Long getValue() {
+ return this.value;
+ }
+
+ @Override
+ public String toString() {
+ return "{" + "\"value\": { \"long\": " + this.value + "}}";
+ }
+ }
+
+ public static class ExampleRecord {
+ public static final Schema SCHEMA_WITH_ONE_FIELD;
+ public static final Schema SCHEMA_WITH_TWO_FIELDS;
+
+ static {
+ SCHEMA_WITH_ONE_FIELD = SchemaBuilder.record("org.apache.avro.TestSchemaValidateDefault.ExampleRecord").fields()
+ .name("value1").type(TestSchemaValidateDefault.SCHEMA).noDefault().endRecord();
+
+ GenericData.Record record = new GenericData.Record(TestSchemaValidateDefault.SCHEMA);
+ record.put("value", 15L);
+
+ SCHEMA_WITH_TWO_FIELDS = SchemaBuilder.record("org.apache.avro.TestSchemaValidateDefault.ExampleRecord").fields()
+ .name("value1").type(TestSchemaValidateDefault.SCHEMA).noDefault().name("value2")
+ .type(TestSchemaValidateDefault.SCHEMA).withDefault(record).endRecord();
+ }
+
+ private ComplexValue value1;
+ private ComplexValue value2;
+
+ public ExampleRecord() {
+ }
+
+ public ExampleRecord(ComplexValue value1, ComplexValue value2) {
+ this.value1 = value1;
+ this.value2 = value2;
+ }
+
+ public ComplexValue getValue1() {
+ return this.value1;
+ }
+
+ public ComplexValue getValue2() {
+ return this.value2;
+ }
+ }
+
+}
diff --git a/lang/java/avro/src/test/java/org/apache/avro/TestSystemLimitException.java b/lang/java/avro/src/test/java/org/apache/avro/TestSystemLimitException.java
new file mode 100644
index 00000000000..0da39179506
--- /dev/null
+++ b/lang/java/avro/src/test/java/org/apache/avro/TestSystemLimitException.java
@@ -0,0 +1,164 @@
+/*
+ * Copyright 2017 The Apache Software Foundation.
+ *
+ * Licensed 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;
+
+import static org.apache.avro.SystemLimitException.*;
+import static org.junit.jupiter.api.Assertions.*;
+
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Test;
+
+import java.util.function.Function;
+
+public class TestSystemLimitException {
+
+ /** Delegated here for package visibility. */
+ public static final int MAX_ARRAY_VM_LIMIT = SystemLimitException.MAX_ARRAY_VM_LIMIT;
+
+ public static final String ERROR_NEGATIVE = "Malformed data. Length is negative: -1";
+ public static final String ERROR_VM_LIMIT_BYTES = "Cannot read arrays longer than " + MAX_ARRAY_VM_LIMIT
+ + " bytes in Java library";
+ public static final String ERROR_VM_LIMIT_COLLECTION = "Cannot read collections larger than " + MAX_ARRAY_VM_LIMIT
+ + " items in Java library";
+ public static final String ERROR_VM_LIMIT_STRING = "Cannot read strings longer than " + MAX_ARRAY_VM_LIMIT + " bytes";
+
+ /** Delegated here for package visibility. */
+ public static void resetLimits() {
+ SystemLimitException.resetLimits();
+ }
+
+ @AfterEach
+ void reset() {
+ System.clearProperty(MAX_BYTES_LENGTH_PROPERTY);
+ System.clearProperty(MAX_COLLECTION_LENGTH_PROPERTY);
+ System.clearProperty(MAX_STRING_LENGTH_PROPERTY);
+ resetLimits();
+ }
+
+ /**
+ * A helper method that tests the consistent limit handling from system
+ * properties.
+ *
+ * @param f The function to be tested.
+ * @param sysProperty The system property used to control the custom limit.
+ * @param errorVmLimit The error message used when the property would be
+ * over the VM limit.
+ * @param errorCustomLimit The error message used when the property would be
+ * over the custom limit of 1000.
+ */
+ void helpCheckSystemLimits(Function f, String sysProperty, String errorVmLimit,
+ String errorCustomLimit) {
+ // Correct values pass through
+ assertEquals(0, f.apply(0L));
+ assertEquals(1024, f.apply(1024L));
+ assertEquals(MAX_ARRAY_VM_LIMIT, f.apply((long) MAX_ARRAY_VM_LIMIT));
+
+ // Values that exceed the default system limits throw exceptions
+ Exception ex = assertThrows(UnsupportedOperationException.class, () -> f.apply(Long.MAX_VALUE));
+ assertEquals(errorVmLimit, ex.getMessage());
+ ex = assertThrows(UnsupportedOperationException.class, () -> f.apply((long) MAX_ARRAY_VM_LIMIT + 1));
+ assertEquals(errorVmLimit, ex.getMessage());
+ ex = assertThrows(AvroRuntimeException.class, () -> f.apply(-1L));
+ assertEquals(ERROR_NEGATIVE, ex.getMessage());
+
+ // Setting the system property to provide a custom limit.
+ System.setProperty(sysProperty, Long.toString(1000L));
+ resetLimits();
+
+ // Correct values pass through
+ assertEquals(0, f.apply(0L));
+ assertEquals(102, f.apply(102L));
+
+ // Values that exceed the custom system limits throw exceptions
+ ex = assertThrows(UnsupportedOperationException.class, () -> f.apply((long) MAX_ARRAY_VM_LIMIT + 1));
+ assertEquals(errorVmLimit, ex.getMessage());
+ ex = assertThrows(SystemLimitException.class, () -> f.apply(1024L));
+ assertEquals(errorCustomLimit, ex.getMessage());
+ ex = assertThrows(AvroRuntimeException.class, () -> f.apply(-1L));
+ assertEquals(ERROR_NEGATIVE, ex.getMessage());
+ }
+
+ @Test
+ void testCheckMaxBytesLength() {
+ helpCheckSystemLimits(SystemLimitException::checkMaxBytesLength, MAX_BYTES_LENGTH_PROPERTY, ERROR_VM_LIMIT_BYTES,
+ "Bytes length 1024 exceeds maximum allowed");
+ }
+
+ @Test
+ void testCheckMaxCollectionLengthFromZero() {
+ helpCheckSystemLimits(l -> checkMaxCollectionLength(0L, l), MAX_COLLECTION_LENGTH_PROPERTY,
+ ERROR_VM_LIMIT_COLLECTION, "Collection length 1024 exceeds maximum allowed");
+ }
+
+ @Test
+ void testCheckMaxStringLength() {
+ helpCheckSystemLimits(SystemLimitException::checkMaxStringLength, MAX_STRING_LENGTH_PROPERTY, ERROR_VM_LIMIT_STRING,
+ "String length 1024 exceeds maximum allowed");
+ }
+
+ @Test
+ void testCheckMaxCollectionLengthFromNonZero() {
+ // Correct values pass through
+ assertEquals(10, checkMaxCollectionLength(10L, 0L));
+ assertEquals(MAX_ARRAY_VM_LIMIT, checkMaxCollectionLength(10L, MAX_ARRAY_VM_LIMIT - 10L));
+ assertEquals(MAX_ARRAY_VM_LIMIT, checkMaxCollectionLength(MAX_ARRAY_VM_LIMIT - 10L, 10L));
+
+ // Values that exceed the default system limits throw exceptions
+ Exception ex = assertThrows(UnsupportedOperationException.class,
+ () -> checkMaxCollectionLength(10L, MAX_ARRAY_VM_LIMIT - 9L));
+ assertEquals(ERROR_VM_LIMIT_COLLECTION, ex.getMessage());
+ ex = assertThrows(UnsupportedOperationException.class,
+ () -> checkMaxCollectionLength(SystemLimitException.MAX_ARRAY_VM_LIMIT - 9L, 10L));
+ assertEquals(ERROR_VM_LIMIT_COLLECTION, ex.getMessage());
+
+ ex = assertThrows(UnsupportedOperationException.class, () -> checkMaxCollectionLength(10L, Long.MAX_VALUE - 10L));
+ assertEquals(ERROR_VM_LIMIT_COLLECTION, ex.getMessage());
+ ex = assertThrows(UnsupportedOperationException.class, () -> checkMaxCollectionLength(Long.MAX_VALUE - 10L, 10L));
+ assertEquals(ERROR_VM_LIMIT_COLLECTION, ex.getMessage());
+
+ // Overflow that adds to negative
+ ex = assertThrows(UnsupportedOperationException.class, () -> checkMaxCollectionLength(10L, Long.MAX_VALUE));
+ assertEquals(ERROR_VM_LIMIT_COLLECTION, ex.getMessage());
+ ex = assertThrows(UnsupportedOperationException.class, () -> checkMaxCollectionLength(Long.MAX_VALUE, 10L));
+ assertEquals(ERROR_VM_LIMIT_COLLECTION, ex.getMessage());
+
+ ex = assertThrows(AvroRuntimeException.class, () -> checkMaxCollectionLength(10L, -1L));
+ assertEquals(ERROR_NEGATIVE, ex.getMessage());
+ ex = assertThrows(AvroRuntimeException.class, () -> checkMaxCollectionLength(-1L, 10L));
+ assertEquals(ERROR_NEGATIVE, ex.getMessage());
+
+ // Setting the system property to provide a custom limit.
+ System.setProperty(MAX_COLLECTION_LENGTH_PROPERTY, Long.toString(1000L));
+ resetLimits();
+
+ // Correct values pass through
+ assertEquals(10, checkMaxCollectionLength(10L, 0L));
+ assertEquals(102, checkMaxCollectionLength(10L, 92L));
+ assertEquals(102, checkMaxCollectionLength(92L, 10L));
+
+ // Values that exceed the custom system limits throw exceptions
+ ex = assertThrows(UnsupportedOperationException.class, () -> checkMaxCollectionLength(MAX_ARRAY_VM_LIMIT, 1));
+ assertEquals(ERROR_VM_LIMIT_COLLECTION, ex.getMessage());
+ ex = assertThrows(UnsupportedOperationException.class, () -> checkMaxCollectionLength(1, MAX_ARRAY_VM_LIMIT));
+ assertEquals(ERROR_VM_LIMIT_COLLECTION, ex.getMessage());
+
+ ex = assertThrows(SystemLimitException.class, () -> checkMaxCollectionLength(999, 25));
+ assertEquals("Collection length 1024 exceeds maximum allowed", ex.getMessage());
+ ex = assertThrows(SystemLimitException.class, () -> checkMaxCollectionLength(25, 999));
+ assertEquals("Collection length 1024 exceeds maximum allowed", ex.getMessage());
+ }
+}
diff --git a/lang/java/avro/src/test/java/org/apache/avro/TestUnionError.java b/lang/java/avro/src/test/java/org/apache/avro/TestUnionError.java
new file mode 100644
index 00000000000..7f5e48fb962
--- /dev/null
+++ b/lang/java/avro/src/test/java/org/apache/avro/TestUnionError.java
@@ -0,0 +1,84 @@
+/*
+ * 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;
+
+import org.apache.avro.generic.GenericData;
+import org.apache.avro.generic.GenericDatumReader;
+import org.apache.avro.generic.GenericDatumWriter;
+import org.apache.avro.generic.GenericRecord;
+import org.apache.avro.io.BinaryDecoder;
+import org.apache.avro.io.BinaryEncoder;
+import org.apache.avro.io.DecoderFactory;
+import org.apache.avro.io.EncoderFactory;
+
+import org.junit.jupiter.api.Test;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+public class TestUnionError {
+
+ @Test
+ void unionErrorMessage() throws IOException {
+ String writerSchemaJson = " {\n" + " \"type\" : \"record\",\n"
+ + " \"name\" : \"C\",\n" + " \"fields\" : [ {\n"
+ + " \"name\" : \"c\",\n" + " \"type\" : [ {\n"
+ + " \"type\" : \"record\",\n" + " \"name\" : \"A\",\n"
+ + " \"fields\" : [ {\n" + " \"name\" : \"amount\",\n"
+ + " \"type\" : \"int\"\n" + " } ]\n" + " }, {\n"
+ + " \"type\" : \"record\",\n" + " \"name\" : \"B\",\n"
+ + " \"fields\" : [ {\n" + " \"name\" : \"amount1\",\n"
+ + " \"type\" : \"int\"\n" + " } ]\n" + " } ]\n"
+ + " } ]\n" + " }";
+ Schema writerSchema = new Schema.Parser().parse(writerSchemaJson);
+
+ String readerSchemaJson = " {\n" + " \"type\" : \"record\",\n" + " \"name\" : \"C1\",\n"
+ + " \"fields\" : [ {\n" + " \"name\" : \"c\",\n"
+ + " \"type\" : [ {\n" + " \"type\" : \"record\",\n"
+ + " \"name\" : \"A\",\n" + " \"fields\" : [ {\n"
+ + " \"name\" : \"amount\",\n" + " \"type\" : \"int\"\n"
+ + " } ]\n" + " }, \"float\" ]\n" + " } ]\n" + " }";
+ Schema readerSchema = new Schema.Parser().parse(readerSchemaJson);
+
+ List unionSchemas = writerSchema.getField("c").schema().getTypes();
+
+ GenericRecord r = new GenericData.Record(writerSchema);
+ GenericRecord b = new GenericData.Record(unionSchemas.get(1));
+ b.put("amount1", 12);
+ r.put("c", b);
+
+ ByteArrayOutputStream outs = new ByteArrayOutputStream();
+ GenericDatumWriter datumWriter = new GenericDatumWriter<>(writerSchema);
+ BinaryEncoder encoder = EncoderFactory.get().binaryEncoder(outs, null);
+ datumWriter.write(r, encoder);
+ encoder.flush();
+
+ InputStream ins = new ByteArrayInputStream(outs.toByteArray());
+ BinaryDecoder decoder = DecoderFactory.get().binaryDecoder(ins, null);
+
+ GenericDatumReader datumReader = new GenericDatumReader<>(writerSchema, readerSchema);
+ AvroTypeException avroException = assertThrows(AvroTypeException.class, () -> datumReader.read(null, decoder));
+ assertEquals("Found B, expecting union[A, float]", avroException.getMessage());
+ }
+}
diff --git a/lang/java/avro/src/test/java/org/apache/avro/file/TestAllCodecs.java b/lang/java/avro/src/test/java/org/apache/avro/file/TestAllCodecs.java
index 491a7e3f713..ef928db6f47 100644
--- a/lang/java/avro/src/test/java/org/apache/avro/file/TestAllCodecs.java
+++ b/lang/java/avro/src/test/java/org/apache/avro/file/TestAllCodecs.java
@@ -18,43 +18,27 @@
package org.apache.avro.file;
-import org.junit.Assert;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.Parameterized;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
import java.io.IOException;
import java.nio.ByteBuffer;
-import java.util.Arrays;
-import java.util.Collection;
+import java.util.stream.Stream;
-import static org.junit.Assert.assertTrue;
-
-@RunWith(Parameterized.class)
public class TestAllCodecs {
- @Parameterized.Parameters(name = "{index}: codec={0}")
- public static Collection data() {
- return Arrays.asList(new Object[][] { { "bzip2", BZip2Codec.class }, { "zstandard", ZstandardCodec.class },
- { "null", NullCodec.class }, { "xz", XZCodec.class }, { "snappy", SnappyCodec.class },
- { "deflate", DeflateCodec.class }, });
- }
-
- @Parameterized.Parameter(0)
- public String codec;
-
- @Parameterized.Parameter(1)
- public Class extends Codec> codecClass;
-
- @Test
- public void testCodec() throws IOException {
+ @ParameterizedTest
+ @MethodSource("codecTypes")
+ void codec(String codec, Class extends Codec> codecClass) throws IOException {
int inputSize = 500_000;
byte[] input = generateTestData(inputSize);
Codec codecInstance = CodecFactory.fromString(codec).createInstance();
- assertTrue(codecClass.isInstance(codecInstance));
- assertTrue(codecInstance.getName().equals(codec));
+ Assertions.assertTrue(codecClass.isInstance(codecInstance));
+ Assertions.assertTrue(codecInstance.getName().equals(codec));
ByteBuffer inputByteBuffer = ByteBuffer.wrap(input);
ByteBuffer compressedBuffer = codecInstance.compress(inputByteBuffer);
@@ -62,28 +46,30 @@ public void testCodec() throws IOException {
int compressedSize = compressedBuffer.remaining();
// Make sure something returned
- assertTrue(compressedSize > 0);
+ Assertions.assertTrue(compressedSize > 0);
// While the compressed size could in many real cases
// *increase* compared to the input size, our input data
// is extremely easy to compress and all Avro's compression algorithms
// should have a compression ratio greater than 1 (except 'null').
- assertTrue(compressedSize < inputSize || codec.equals("null"));
+ Assertions.assertTrue(compressedSize < inputSize || codec.equals("null"));
// Decompress the data
ByteBuffer decompressedBuffer = codecInstance.decompress(compressedBuffer);
// Validate the the input and output are equal.
inputByteBuffer.rewind();
- Assert.assertEquals(decompressedBuffer, inputByteBuffer);
+ Assertions.assertEquals(inputByteBuffer, decompressedBuffer);
}
- @Test
- public void testCodecSlice() throws IOException {
+ @ParameterizedTest
+ @MethodSource("codecTypes")
+ void codecSlice(String codec, Class extends Codec> codecClass) throws IOException {
int inputSize = 500_000;
byte[] input = generateTestData(inputSize);
Codec codecInstance = CodecFactory.fromString(codec).createInstance();
+ Assertions.assertTrue(codecClass.isInstance(codecInstance));
ByteBuffer partialBuffer = ByteBuffer.wrap(input);
partialBuffer.position(17);
@@ -94,7 +80,7 @@ public void testCodecSlice() throws IOException {
int compressedSize = compressedBuffer.remaining();
// Make sure something returned
- assertTrue(compressedSize > 0);
+ Assertions.assertTrue(compressedSize > 0);
// Create a slice from the compressed buffer
ByteBuffer sliceBuffer = ByteBuffer.allocate(compressedSize + 100);
@@ -108,7 +94,13 @@ public void testCodecSlice() throws IOException {
// Validate the the input and output are equal.
inputByteBuffer.rewind();
- Assert.assertEquals(decompressedBuffer, inputByteBuffer);
+ Assertions.assertEquals(inputByteBuffer, decompressedBuffer);
+ }
+
+ public static Stream codecTypes() {
+ return Stream.of(Arguments.of("bzip2", BZip2Codec.class), Arguments.of("zstandard", ZstandardCodec.class),
+ Arguments.of("null", NullCodec.class), Arguments.of("xz", XZCodec.class),
+ Arguments.of("snappy", SnappyCodec.class), Arguments.of("deflate", DeflateCodec.class));
}
// Generate some test data that will compress easily
diff --git a/lang/java/avro/src/test/java/org/apache/avro/generic/GenericDataArrayTest.java b/lang/java/avro/src/test/java/org/apache/avro/generic/GenericDataArrayTest.java
new file mode 100644
index 00000000000..a4ffebac02d
--- /dev/null
+++ b/lang/java/avro/src/test/java/org/apache/avro/generic/GenericDataArrayTest.java
@@ -0,0 +1,44 @@
+/*
+ * 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.generic;
+
+import org.apache.avro.Schema;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+class GenericDataArrayTest {
+
+ @Test
+ void test() {
+ GenericData.Array array = new GenericData.Array<>(10,
+ Schema.createArray(Schema.create(Schema.Type.STRING)));
+ array.add("One");
+ array.add("Two");
+ array.add("Two");
+ array.add("Three");
+ array.add(4, "Four");
+ array.remove(1);
+ Assertions.assertEquals(4, array.size());
+ Assertions.assertEquals("One", array.get(0));
+ Assertions.assertEquals("Two", array.get(1));
+ Assertions.assertEquals("Three", array.get(2));
+ Assertions.assertEquals("Four", array.get(3));
+ }
+
+}
diff --git a/lang/java/avro/src/test/java/org/apache/avro/generic/PrimitivesArraysTest.java b/lang/java/avro/src/test/java/org/apache/avro/generic/PrimitivesArraysTest.java
new file mode 100644
index 00000000000..7d199bf92c8
--- /dev/null
+++ b/lang/java/avro/src/test/java/org/apache/avro/generic/PrimitivesArraysTest.java
@@ -0,0 +1,280 @@
+/*
+ * 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.generic;
+
+import org.apache.avro.Schema;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+class PrimitivesArraysTest {
+
+ @Test
+ void booleanArray() {
+ PrimitivesArrays.BooleanArray ba = new PrimitivesArrays.BooleanArray(4,
+ Schema.createArray(Schema.create(Schema.Type.BOOLEAN)));
+
+ Assertions.assertEquals(0, ba.size());
+ for (int i = 1; i < 100; i++) {
+ if (i % 3 == 0 || i % 5 == 0) {
+ ba.add(true);
+ } else {
+ ba.add(false);
+ }
+ }
+ Assertions.assertEquals(99, ba.size());
+ for (int i = 1; i < 100; i++) {
+ if (i % 3 == 0 || i % 5 == 0) {
+ Assertions.assertTrue(ba.get(i - 1), "Error for " + i);
+ } else {
+ Assertions.assertFalse(ba.get(i - 1), "Error for " + i);
+ }
+ }
+ Assertions.assertFalse(ba.remove(12));
+ Assertions.assertEquals(98, ba.size());
+ for (int i = 13; i < 99; i++) {
+ if ((i + 1) % 3 == 0 || (i + 1) % 5 == 0) {
+ Assertions.assertTrue(ba.get(i - 1), "After delete, Error for " + i);
+ } else {
+ Assertions.assertFalse(ba.get(i - 1), "After delete, Error for " + i);
+ }
+ }
+
+ ba.add(12, false);
+ Assertions.assertEquals(99, ba.size());
+ for (int i = 1; i < 100; i++) {
+ if (i % 3 == 0 || i % 5 == 0) {
+ Assertions.assertTrue(ba.get(i - 1), "Error for " + i);
+ } else {
+ Assertions.assertFalse(ba.get(i - 1), "Error for " + i);
+ }
+ }
+ Assertions.assertFalse(ba.remove(12));
+ ba.add(12, true);
+ for (int i = 1; i < 100; i++) {
+ if (i % 3 == 0 || i % 5 == 0 || i == 13) {
+ Assertions.assertTrue(ba.get(i - 1), "Error for " + i);
+ } else {
+ Assertions.assertFalse(ba.get(i - 1), "Error for " + i);
+ }
+ }
+ ba.add(99, true);
+ Assertions.assertTrue(ba.get(99), "Error for 99");
+ ba.remove(99);
+ ba.reverse();
+ for (int i = 1; i < 100; i++) {
+ if (i % 3 == 0 || i % 5 == 0 || i == 13) {
+ Assertions.assertTrue(ba.get(99 - i), "Error for " + i);
+ } else {
+ Assertions.assertFalse(ba.get(99 - i), "Error for " + i);
+ }
+ }
+ }
+
+ @Test
+ void booleanArrayIterator() {
+ PrimitivesArrays.BooleanArray ba = new PrimitivesArrays.BooleanArray(4,
+ Schema.createArray(Schema.create(Schema.Type.BOOLEAN)));
+ boolean[] model = new boolean[] { true, false, false, true, true, true, false, false, true, false, false };
+ for (boolean x : model) {
+ ba.add(x);
+ }
+ Assertions.assertEquals(model.length, ba.size());
+ int index = 0;
+ for (Boolean b : ba) {
+ Assertions.assertEquals(model[index], b);
+ index++;
+ }
+ }
+
+ @Test
+ void intArray() {
+ final PrimitivesArrays.IntArray intArray = new PrimitivesArrays.IntArray(4,
+ Schema.createArray(Schema.create(Schema.Type.INT)));
+ for (int i = 1; i <= 100; i++) {
+ intArray.add(i);
+ }
+ Assertions.assertEquals(100, intArray.size());
+ for (int i = 1; i <= 100; i++) {
+ Assertions.assertEquals(i, intArray.get(i - 1));
+ }
+
+ int expectedValue = 1;
+ for (Integer value : intArray) {
+ Assertions.assertEquals(expectedValue, value);
+ expectedValue++;
+ }
+
+ intArray.remove(40);
+ Assertions.assertEquals(99, intArray.size());
+ for (int i = 1; i <= 99; i++) {
+ if (i <= 40) {
+ Assertions.assertEquals(i, intArray.get(i - 1));
+ } else {
+ Assertions.assertEquals(i + 1, intArray.get(i - 1));
+ }
+ }
+ intArray.add(40, 41);
+ Assertions.assertEquals(100, intArray.size());
+ for (int i = 1; i <= 100; i++) {
+ Assertions.assertEquals(i, intArray.get(i - 1));
+ }
+ intArray.set(40, 25);
+ Assertions.assertEquals(25, intArray.get(40));
+
+ Assertions.assertEquals(0, intArray.peek());
+ intArray.set(40, 41);
+ intArray.reverse();
+ Assertions.assertEquals(100, intArray.size());
+ for (int i = 1; i <= 100; i++) {
+ Assertions.assertEquals(101 - i, intArray.get(i - 1));
+ }
+ }
+
+ @Test
+ void longArray() {
+ final PrimitivesArrays.LongArray longArray = new PrimitivesArrays.LongArray(4,
+ Schema.createArray(Schema.create(Schema.Type.LONG)));
+ for (long i = 1; i <= 100; i++) {
+ longArray.add(i);
+ }
+ Assertions.assertEquals(100l, longArray.size());
+ for (int i = 1; i <= 100; i++) {
+ Assertions.assertEquals(i, longArray.get(i - 1));
+ }
+
+ int expectedValue = 1;
+ for (Long value : longArray) {
+ Assertions.assertEquals(expectedValue, value);
+ expectedValue++;
+ }
+
+ longArray.remove(40);
+ Assertions.assertEquals(99, longArray.size());
+ for (int i = 1; i <= 99; i++) {
+ if (i <= 40) {
+ Assertions.assertEquals(i, longArray.get(i - 1));
+ } else {
+ Assertions.assertEquals(i + 1, longArray.get(i - 1));
+ }
+ }
+ longArray.add(40, 41);
+ Assertions.assertEquals(100, longArray.size());
+ for (int i = 1; i <= 100; i++) {
+ Assertions.assertEquals(i, longArray.get(i - 1));
+ }
+ longArray.set(40, 25);
+ Assertions.assertEquals(25, longArray.get(40));
+
+ Assertions.assertEquals(0, longArray.peek());
+ longArray.set(40, 41);
+ longArray.reverse();
+ Assertions.assertEquals(100, longArray.size());
+ for (int i = 1; i <= 100; i++) {
+ Assertions.assertEquals(101 - i, longArray.get(i - 1));
+ }
+ }
+
+ @Test
+ void floatArray() {
+ final PrimitivesArrays.FloatArray floatArray = new PrimitivesArrays.FloatArray(4,
+ Schema.createArray(Schema.create(Schema.Type.FLOAT)));
+ for (int i = 1; i <= 100; i++) {
+ floatArray.add(i * 3.3f);
+ }
+ Assertions.assertEquals(100, floatArray.size());
+ for (int i = 1; i <= 100; i++) {
+ Assertions.assertEquals(i * 3.3f, floatArray.get(i - 1));
+ }
+
+ float expectedValue = 1.0f;
+ for (Float value : floatArray) {
+ Assertions.assertEquals(expectedValue * 3.3f, value);
+ expectedValue++;
+ }
+
+ floatArray.remove(40);
+ Assertions.assertEquals(99, floatArray.size());
+ for (int i = 1; i <= 99; i++) {
+ if (i <= 40) {
+ Assertions.assertEquals(i * 3.3f, floatArray.get(i - 1));
+ } else {
+ Assertions.assertEquals((i + 1) * 3.3f, floatArray.get(i - 1));
+ }
+ }
+ floatArray.add(40, 41 * 3.3f);
+ Assertions.assertEquals(100, floatArray.size());
+ for (int i = 1; i <= 100; i++) {
+ Assertions.assertEquals(i * 3.3f, floatArray.get(i - 1));
+ }
+ floatArray.set(40, 25.2f);
+ Assertions.assertEquals(25.2f, floatArray.get(40));
+
+ Assertions.assertEquals(0.0f, floatArray.peek());
+ floatArray.set(40, 41 * 3.3f);
+ floatArray.reverse();
+ Assertions.assertEquals(100, floatArray.size());
+ for (int i = 1; i <= 100; i++) {
+ Assertions.assertEquals((101 - i) * 3.3f, floatArray.get(i - 1));
+ }
+ }
+
+ @Test
+ void doubleArray() {
+ final PrimitivesArrays.DoubleArray doubleArray = new PrimitivesArrays.DoubleArray(4,
+ Schema.createArray(Schema.create(Schema.Type.DOUBLE)));
+ for (int i = 1; i <= 100; i++) {
+ doubleArray.add(i * 3.0d);
+ }
+ Assertions.assertEquals(100, doubleArray.size());
+ for (int i = 1; i <= 100; i++) {
+ Assertions.assertEquals(i * 3.0d, doubleArray.get(i - 1));
+ }
+
+ double expectedValue = 1.0f;
+ for (Double value : doubleArray) {
+ Assertions.assertEquals(expectedValue * 3.0d, value);
+ expectedValue++;
+ }
+
+ doubleArray.remove(40);
+ Assertions.assertEquals(99, doubleArray.size());
+ for (int i = 1; i <= 99; i++) {
+ if (i <= 40) {
+ Assertions.assertEquals(i * 3.0d, doubleArray.get(i - 1));
+ } else {
+ Assertions.assertEquals((i + 1) * 3.0d, doubleArray.get(i - 1));
+ }
+ }
+ doubleArray.add(40, 41 * 3.0d);
+ Assertions.assertEquals(100, doubleArray.size());
+ for (int i = 1; i <= 100; i++) {
+ Assertions.assertEquals(i * 3.0d, doubleArray.get(i - 1));
+ }
+ doubleArray.set(40, 25.2d);
+ Assertions.assertEquals(25.2d, doubleArray.get(40));
+
+ Assertions.assertEquals(0.0d, doubleArray.peek());
+ doubleArray.set(40, 41 * 3.0d);
+ doubleArray.reverse();
+ Assertions.assertEquals(100, doubleArray.size());
+ for (int i = 1; i <= 100; i++) {
+ Assertions.assertEquals((101 - i) * 3.0d, doubleArray.get(i - 1));
+ }
+ }
+}
diff --git a/lang/java/avro/src/test/java/org/apache/avro/generic/TestGenericData.java b/lang/java/avro/src/test/java/org/apache/avro/generic/TestGenericData.java
index bca151c15df..20c82179561 100644
--- a/lang/java/avro/src/test/java/org/apache/avro/generic/TestGenericData.java
+++ b/lang/java/avro/src/test/java/org/apache/avro/generic/TestGenericData.java
@@ -157,9 +157,8 @@ void testEquals() {
}
@Test
- public void testMapKeyEquals() {
- Schema mapSchema = new Schema.Parser().parse("{\"type\": \"map\", \"values\": \"string\"}");
- Field myMapField = new Field("my_map", Schema.createMap(mapSchema), null, null);
+ public void testMapKeyEqualsStringAndUtf8Compatibility() {
+ Field myMapField = new Field("my_map", Schema.createMap(Schema.create(Schema.Type.STRING)), null, null);
Schema schema = Schema.createRecord("my_record", "doc", "mytest", false);
schema.setFields(Arrays.asList(myMapField));
GenericRecord r0 = new GenericData.Record(schema);
@@ -178,9 +177,8 @@ public void testMapKeyEquals() {
}
@Test
- public void testMapValuesEquals() {
- Schema mapSchema = new Schema.Parser().parse("{\"type\": \"map\", \"values\": \"string\"}");
- Field myMapField = new Field("my_map", Schema.createMap(mapSchema), null, null);
+ public void testMapValuesEqualsStringAndUtf8Compatibility() {
+ Field myMapField = new Field("my_map", Schema.createMap(Schema.create(Schema.Type.STRING)), null, null);
Schema schema = Schema.createRecord("my_record", "doc", "mytest", false);
schema.setFields(Arrays.asList(myMapField));
GenericRecord r0 = new GenericData.Record(schema);
@@ -198,6 +196,117 @@ public void testMapValuesEquals() {
assertEquals(r1, r0);
}
+ @Test
+ public void testEqualsEmptyMaps() {
+ Field myMapField = new Field("my_map", Schema.createMap(Schema.create(Schema.Type.STRING)), null, null);
+ Schema schema = Schema.createRecord("my_record", "doc", "mytest", false);
+ schema.setFields(Arrays.asList(myMapField));
+
+ GenericRecord r0 = new GenericData.Record(schema);
+ r0.put("my_map", new HashMap<>());
+ GenericRecord r1 = new GenericData.Record(schema);
+ r1.put("my_map", new HashMap<>());
+
+ assertEquals(r0, r1);
+ assertEquals(r1, r0);
+ }
+
+ @Test
+ public void testEqualsEmptyMapAndNonEmptyMap() {
+ Field myMapField = new Field("my_map", Schema.createMap(Schema.create(Schema.Type.STRING)), null, null);
+ Schema schema = Schema.createRecord("my_record", "doc", "mytest", false);
+ schema.setFields(Arrays.asList(myMapField));
+
+ GenericRecord r0 = new GenericData.Record(schema);
+ r0.put("my_map", new HashMap<>());
+ GenericRecord r1 = new GenericData.Record(schema);
+ HashMap pair1 = new HashMap<>();
+ pair1.put("keyOne", "valueOne");
+ r1.put("my_map", pair1);
+
+ assertNotEquals(r0, r1);
+ assertNotEquals(r1, r0);
+ }
+
+ @Test
+ public void testEqualsMapAndSubset() {
+ Field myMapField = new Field("my_map", Schema.createMap(Schema.create(Schema.Type.STRING)), null, null);
+ Schema schema = Schema.createRecord("my_record", "doc", "mytest", false);
+ schema.setFields(Arrays.asList(myMapField));
+
+ GenericRecord r0 = new GenericData.Record(schema);
+ HashMap m1 = new HashMap<>();
+ m1.put("keyOne", "valueOne");
+ m1.put("keyTwo", "valueTwo");
+ r0.put("my_map", m1);
+
+ GenericRecord r1 = new GenericData.Record(schema);
+ HashMap m2 = new HashMap<>();
+ m2.put("keyOne", "valueOne");
+ r1.put("my_map", m2);
+
+ assertNotEquals(r0, r1);
+ assertNotEquals(r1, r0);
+ }
+
+ @Test
+ public void testEqualsMapAndSameSizeMapWithDifferentKeys() {
+ Field myMapField = new Field("my_map", Schema.createMap(Schema.create(Schema.Type.STRING)), null, null);
+ Schema schema = Schema.createRecord("my_record", "doc", "mytest", false);
+ schema.setFields(Arrays.asList(myMapField));
+
+ GenericRecord r0 = new GenericData.Record(schema);
+ HashMap m1 = new HashMap<>();
+ m1.put("keyOne", "valueOne");
+ r0.put("my_map", m1);
+
+ GenericRecord r1 = new GenericData.Record(schema);
+ HashMap m2 = new HashMap<>();
+ m2.put("keyTwo", "valueTwo");
+ r1.put("my_map", m2);
+
+ assertNotEquals(r0, r1);
+ assertNotEquals(r1, r0);
+ }
+
+ @Test
+ public void testEqualsMapAndSameSizeMapWithDifferentValues() {
+ Field myMapField = new Field("my_map", Schema.createMap(Schema.create(Schema.Type.STRING)), null, null);
+ Schema schema = Schema.createRecord("my_record", "doc", "mytest", false);
+ schema.setFields(Arrays.asList(myMapField));
+
+ GenericRecord r0 = new GenericData.Record(schema);
+ HashMap m1 = new HashMap<>();
+ m1.put("keyOne", "valueOne");
+ r0.put("my_map", m1);
+
+ GenericRecord r1 = new GenericData.Record(schema);
+ HashMap m2 = new HashMap<>();
+ m2.put("keyOne", "valueTwo");
+ r1.put("my_map", m2);
+
+ assertNotEquals(r0, r1);
+ assertNotEquals(r1, r0);
+ }
+
+ @Test
+ public void testArrayValuesEqualsStringAndUtf8Compatibility() {
+ Field myArrayField = new Field("my_array", Schema.createArray(Schema.create(Schema.Type.STRING)), null, null);
+ Schema schema = Schema.createRecord("my_record", "doc", "mytest", false);
+ schema.setFields(Arrays.asList(myArrayField));
+ GenericRecord r0 = new GenericData.Record(schema);
+ GenericRecord r1 = new GenericData.Record(schema);
+
+ List array1 = Arrays.asList("valueOne");
+ r0.put("my_array", array1);
+
+ List array2 = Arrays.asList(new Utf8("valueOne"));
+ r1.put("my_array", array2);
+
+ assertEquals(r0, r1);
+ assertEquals(r1, r0);
+ }
+
private Schema recordSchema() {
List fields = new ArrayList<>();
fields.add(new Field("anArray", Schema.createArray(Schema.create(Type.STRING)), null, null));
diff --git a/lang/java/avro/src/test/java/org/apache/avro/generic/TestGenericLogicalTypes.java b/lang/java/avro/src/test/java/org/apache/avro/generic/TestGenericLogicalTypes.java
index 3d5e2300d9d..25a838db335 100644
--- a/lang/java/avro/src/test/java/org/apache/avro/generic/TestGenericLogicalTypes.java
+++ b/lang/java/avro/src/test/java/org/apache/avro/generic/TestGenericLogicalTypes.java
@@ -18,11 +18,6 @@
package org.apache.avro.generic;
-import static org.hamcrest.Matchers.is;
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertNotSame;
-import static org.hamcrest.MatcherAssert.assertThat;
-
import java.io.File;
import java.io.IOException;
import java.math.BigDecimal;
@@ -35,8 +30,10 @@
import java.util.Collections;
import java.util.List;
import java.util.UUID;
+
import org.apache.avro.Conversion;
import org.apache.avro.Conversions;
+import org.apache.avro.CustomType;
import org.apache.avro.LogicalType;
import org.apache.avro.LogicalTypes;
import org.apache.avro.Schema;
@@ -51,6 +48,11 @@
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotSame;
+
public class TestGenericLogicalTypes {
@TempDir
@@ -402,4 +404,53 @@ public void writeLocalTimestampMicros() throws IOException {
assertEquals(expected, read(GenericData.get().createDatumReader(timestampSchema), test),
"Should read LocalDateTime as longs");
}
+
+ @Test
+ public void testReadAutomaticallyRegisteredUri() throws IOException {
+ Schema stringSchema = Schema.create(Schema.Type.STRING);
+ GenericData.setStringType(stringSchema, GenericData.StringType.String);
+ LogicalType customType = LogicalTypes.getCustomRegisteredTypes().get("custom").fromSchema(stringSchema);
+ Schema customTypeSchema = customType.addToSchema(Schema.create(Schema.Type.STRING));
+
+ CustomType ct1 = new CustomType("foo");
+ CustomType ct2 = new CustomType("bar");
+ List expected = Arrays.asList(ct1, ct2);
+
+ Conversion conversion = GENERIC.getConversionFor(customType);
+
+ // use the conversion directly instead of relying on the write side
+ CharSequence ct1String = conversion.toCharSequence(ct1, stringSchema, customType);
+ CharSequence ct2String = conversion.toCharSequence(ct2, stringSchema, customType);
+
+ File test = write(stringSchema, ct1String, ct2String);
+ assertEquals(expected, read(GENERIC.createDatumReader(customTypeSchema), test),
+ "Should convert string to CustomType");
+ }
+
+ @Test
+ public void testWriteAutomaticallyRegisteredUri() throws IOException {
+ Schema stringSchema = Schema.create(Schema.Type.STRING);
+ GenericData.setStringType(stringSchema, GenericData.StringType.String);
+ LogicalType customType = LogicalTypes.getCustomRegisteredTypes().get("custom").fromSchema(stringSchema);
+ Schema customTypeSchema = customType.addToSchema(Schema.create(Schema.Type.STRING));
+
+ CustomType ct1 = new CustomType("foo");
+ CustomType ct2 = new CustomType("bar");
+
+ Conversion conversion = GENERIC.getConversionFor(customType);
+
+ // use the conversion directly instead of relying on the write side
+ CharSequence ct1String = conversion.toCharSequence(ct1, stringSchema, customType);
+ CharSequence ct2String = conversion.toCharSequence(ct2, stringSchema, customType);
+ List expected = Arrays.asList(ct1String, ct2String);
+
+ File test = write(GENERIC, customTypeSchema, ct1, ct2);
+
+ // Note that this test still cannot read strings using the logical type
+ // schema, as all GenericData instances have the logical type and the
+ // conversions loaded. That's why this final assert is slightly different.
+
+ assertEquals(expected, read(GenericData.get().createDatumReader(stringSchema), test),
+ "Should read CustomType as strings");
+ }
}
diff --git a/lang/java/avro/src/test/java/org/apache/avro/io/TestBinaryData.java b/lang/java/avro/src/test/java/org/apache/avro/io/TestBinaryData.java
index ccd627cb944..167cd724630 100644
--- a/lang/java/avro/src/test/java/org/apache/avro/io/TestBinaryData.java
+++ b/lang/java/avro/src/test/java/org/apache/avro/io/TestBinaryData.java
@@ -18,6 +18,7 @@
package org.apache.avro.io;
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
import org.junit.jupiter.api.Test;
@@ -39,4 +40,24 @@ void skipLong() {
assertEquals(nextIndex, 10);
}
+ @Test
+ void testIntLongVleEquality() {
+ byte[] intResult = new byte[9];
+ byte[] longResult = new byte[9];
+ BinaryData.encodeInt(0, intResult, 0);
+ BinaryData.encodeLong(0, longResult, 0);
+ assertArrayEquals(intResult, longResult);
+ BinaryData.encodeInt(42, intResult, 0);
+ BinaryData.encodeLong(42, longResult, 0);
+ assertArrayEquals(intResult, longResult);
+ BinaryData.encodeInt(-24, intResult, 0);
+ BinaryData.encodeLong(-24, longResult, 0);
+ assertArrayEquals(intResult, longResult);
+ BinaryData.encodeInt(Integer.MAX_VALUE, intResult, 0);
+ BinaryData.encodeLong(Integer.MAX_VALUE, longResult, 0);
+ assertArrayEquals(intResult, longResult);
+ BinaryData.encodeInt(Integer.MIN_VALUE, intResult, 0);
+ BinaryData.encodeLong(Integer.MIN_VALUE, longResult, 0);
+ assertArrayEquals(intResult, longResult);
+ }
}
diff --git a/lang/java/avro/src/test/java/org/apache/avro/io/TestBinaryDecoder.java b/lang/java/avro/src/test/java/org/apache/avro/io/TestBinaryDecoder.java
index fe405cfb9d2..6010fc9c69f 100644
--- a/lang/java/avro/src/test/java/org/apache/avro/io/TestBinaryDecoder.java
+++ b/lang/java/avro/src/test/java/org/apache/avro/io/TestBinaryDecoder.java
@@ -17,56 +17,49 @@
*/
package org.apache.avro.io;
-import java.io.ByteArrayInputStream;
-import java.io.ByteArrayOutputStream;
-import java.io.EOFException;
-import java.io.IOException;
-import java.io.InputStream;
-import java.nio.ByteBuffer;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collection;
import org.apache.avro.AvroRuntimeException;
import org.apache.avro.Schema;
+import org.apache.avro.SystemLimitException;
import org.apache.avro.generic.GenericDatumReader;
import org.apache.avro.generic.GenericDatumWriter;
import org.apache.avro.util.ByteBufferInputStream;
import org.apache.avro.util.ByteBufferOutputStream;
import org.apache.avro.util.RandomData;
import org.apache.avro.util.Utf8;
-import org.junit.Assert;
-import org.junit.BeforeClass;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.Parameterized;
-import org.junit.runners.Parameterized.Parameters;
-
-@RunWith(Parameterized.class)
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.EOFException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.Arrays;
+
+import static org.apache.avro.TestSystemLimitException.*;
+
public class TestBinaryDecoder {
// prime number buffer size so that looping tests hit the buffer edge
// at different points in the loop.
DecoderFactory factory = new DecoderFactory().configureDecoderBufferSize(521);
- private boolean useDirect = false;
- static EncoderFactory e_factory = EncoderFactory.get();
- public TestBinaryDecoder(boolean useDirect) {
- this.useDirect = useDirect;
- }
-
- @Parameters
- public static Collection data() {
- return Arrays.asList(new Object[][] { { true }, { false }, });
- }
+ static EncoderFactory e_factory = EncoderFactory.get();
- private Decoder newDecoderWithNoData() {
- return newDecoder(new byte[0]);
+ private Decoder newDecoderWithNoData(boolean useDirect) {
+ return newDecoder(new byte[0], useDirect);
}
- private BinaryDecoder newDecoder(byte[] bytes, int start, int len) {
- return this.newDecoder(bytes, start, len, null);
+ private BinaryDecoder newDecoder(byte[] bytes, int start, int len, boolean useDirect) {
+ return this.newDecoder(bytes, start, len, null, useDirect);
}
- private BinaryDecoder newDecoder(byte[] bytes, int start, int len, BinaryDecoder reuse) {
+ private BinaryDecoder newDecoder(byte[] bytes, int start, int len, BinaryDecoder reuse, boolean useDirect) {
if (useDirect) {
final ByteArrayInputStream input = new ByteArrayInputStream(bytes, start, len);
return factory.directBinaryDecoder(input, reuse);
@@ -75,11 +68,11 @@ private BinaryDecoder newDecoder(byte[] bytes, int start, int len, BinaryDecoder
}
}
- private BinaryDecoder newDecoder(InputStream in) {
- return this.newDecoder(in, null);
+ private BinaryDecoder newDecoder(InputStream in, boolean useDirect) {
+ return this.newDecoder(in, null, useDirect);
}
- private BinaryDecoder newDecoder(InputStream in, BinaryDecoder reuse) {
+ private BinaryDecoder newDecoder(InputStream in, BinaryDecoder reuse, boolean useDirect) {
if (useDirect) {
return factory.directBinaryDecoder(in, reuse);
} else {
@@ -87,67 +80,93 @@ private BinaryDecoder newDecoder(InputStream in, BinaryDecoder reuse) {
}
}
- private BinaryDecoder newDecoder(byte[] bytes, BinaryDecoder reuse) {
- if (this.useDirect) {
+ private BinaryDecoder newDecoder(byte[] bytes, BinaryDecoder reuse, boolean useDirect) {
+ if (useDirect) {
return this.factory.directBinaryDecoder(new ByteArrayInputStream(bytes), reuse);
} else {
return factory.binaryDecoder(bytes, reuse);
}
}
- private BinaryDecoder newDecoder(byte[] bytes) {
- return this.newDecoder(bytes, null);
+ private BinaryDecoder newDecoder(byte[] bytes, boolean useDirect) {
+ return this.newDecoder(bytes, null, useDirect);
+ }
+
+ /**
+ * Create a decoder for simulating reading corrupt, unexpected or out-of-bounds
+ * data.
+ *
+ * @return a {@link org.apache.avro.io.BinaryDecoder that has been initialized
+ * on a byte array containing the sequence of encoded longs in order.
+ */
+ private BinaryDecoder newDecoder(boolean useDirect, long... values) throws IOException {
+ try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
+ BinaryEncoder encoder = EncoderFactory.get().binaryEncoder(baos, null);
+ for (long v : values)
+ encoder.writeLong(v);
+ encoder.flush();
+ return newDecoder(baos.toByteArray(), useDirect);
+ }
}
/** Verify EOFException throw at EOF */
- @Test(expected = EOFException.class)
- public void testEOFBoolean() throws IOException {
- newDecoderWithNoData().readBoolean();
+ @ParameterizedTest
+ @ValueSource(booleans = { true, false })
+ void eofBoolean(boolean useDirect) {
+ Assertions.assertThrows(EOFException.class, () -> newDecoderWithNoData(useDirect).readBoolean());
}
- @Test(expected = EOFException.class)
- public void testEOFInt() throws IOException {
- newDecoderWithNoData().readInt();
+ @ParameterizedTest
+ @ValueSource(booleans = { true, false })
+ void eofInt(boolean useDirect) {
+ Assertions.assertThrows(EOFException.class, () -> newDecoderWithNoData(useDirect).readInt());
}
- @Test(expected = EOFException.class)
- public void testEOFLong() throws IOException {
- newDecoderWithNoData().readLong();
+ @ParameterizedTest
+ @ValueSource(booleans = { true, false })
+ void eofLong(boolean useDirect) {
+ Assertions.assertThrows(EOFException.class, () -> newDecoderWithNoData(useDirect).readLong());
}
- @Test(expected = EOFException.class)
- public void testEOFFloat() throws IOException {
- newDecoderWithNoData().readFloat();
+ @ParameterizedTest
+ @ValueSource(booleans = { true, false })
+ void eofFloat(boolean useDirect) {
+ Assertions.assertThrows(EOFException.class, () -> newDecoderWithNoData(useDirect).readFloat());
}
- @Test(expected = EOFException.class)
- public void testEOFDouble() throws IOException {
- newDecoderWithNoData().readDouble();
+ @ParameterizedTest
+ @ValueSource(booleans = { true, false })
+ void eofDouble(boolean useDirect) {
+ Assertions.assertThrows(EOFException.class, () -> newDecoderWithNoData(useDirect).readDouble());
}
- @Test(expected = EOFException.class)
- public void testEOFBytes() throws IOException {
- newDecoderWithNoData().readBytes(null);
+ @ParameterizedTest
+ @ValueSource(booleans = { true, false })
+ void eofBytes(boolean useDirect) {
+ Assertions.assertThrows(EOFException.class, () -> newDecoderWithNoData(useDirect).readBytes(null));
}
- @Test(expected = EOFException.class)
- public void testEOFString() throws IOException {
- newDecoderWithNoData().readString(new Utf8("a"));
+ @ParameterizedTest
+ @ValueSource(booleans = { true, false })
+ void eofString(boolean useDirect) {
+ Assertions.assertThrows(EOFException.class, () -> newDecoderWithNoData(useDirect).readString(new Utf8("a")));
}
- @Test(expected = EOFException.class)
- public void testEOFFixed() throws IOException {
- newDecoderWithNoData().readFixed(new byte[1]);
+ @ParameterizedTest
+ @ValueSource(booleans = { true, false })
+ void eofFixed(boolean useDirect) {
+ Assertions.assertThrows(EOFException.class, () -> newDecoderWithNoData(useDirect).readFixed(new byte[1]));
}
- @Test(expected = EOFException.class)
- public void testEOFEnum() throws IOException {
- newDecoderWithNoData().readEnum();
+ @ParameterizedTest
+ @ValueSource(booleans = { true, false })
+ void eofEnum(boolean useDirect) {
+ Assertions.assertThrows(EOFException.class, () -> newDecoderWithNoData(useDirect).readEnum());
}
@Test
- public void testReuse() throws IOException {
+ void reuse() throws IOException {
ByteBufferOutputStream bbo1 = new ByteBufferOutputStream();
ByteBufferOutputStream bbo2 = new ByteBufferOutputStream();
byte[] b1 = new byte[] { 1, 2 };
@@ -162,11 +181,11 @@ public void testReuse() throws IOException {
DirectBinaryDecoder d = new DirectBinaryDecoder(new ByteBufferInputStream(bbo1.getBufferList()));
ByteBuffer bb1 = d.readBytes(null);
- Assert.assertEquals(b1.length, bb1.limit() - bb1.position());
+ Assertions.assertEquals(b1.length, bb1.limit() - bb1.position());
d.configure(new ByteBufferInputStream(bbo2.getBufferList()));
ByteBuffer bb2 = d.readBytes(null);
- Assert.assertEquals(b1.length, bb2.limit() - bb2.position());
+ Assertions.assertEquals(b1.length, bb2.limit() - bb2.position());
}
@@ -175,7 +194,7 @@ public void testReuse() throws IOException {
private static final int count = 200;
private static final ArrayList records = new ArrayList<>(count);
- @BeforeClass
+ @BeforeAll
public static void generateData() throws IOException {
int seed = (int) System.currentTimeMillis();
// note some tests (testSkipping) rely on this explicitly
@@ -199,8 +218,9 @@ public static void generateData() throws IOException {
data = baos.toByteArray();
}
- @Test
- public void testDecodeFromSources() throws IOException {
+ @ParameterizedTest
+ @ValueSource(booleans = { true, false })
+ void decodeFromSources(boolean useDirect) throws IOException {
GenericDatumReader reader = new GenericDatumReader<>();
reader.setSchema(schema);
@@ -208,81 +228,82 @@ public void testDecodeFromSources() throws IOException {
ByteArrayInputStream is2 = new ByteArrayInputStream(data);
ByteArrayInputStream is3 = new ByteArrayInputStream(data);
- Decoder fromInputStream = newDecoder(is);
- Decoder fromArray = newDecoder(data);
+ Decoder fromInputStream = newDecoder(is, useDirect);
+ Decoder fromArray = newDecoder(data, useDirect);
byte[] data2 = new byte[data.length + 30];
Arrays.fill(data2, (byte) 0xff);
System.arraycopy(data, 0, data2, 15, data.length);
- Decoder fromOffsetArray = newDecoder(data2, 15, data.length);
+ Decoder fromOffsetArray = newDecoder(data2, 15, data.length, useDirect);
- BinaryDecoder initOnInputStream = newDecoder(new byte[50], 0, 30);
- initOnInputStream = newDecoder(is2, initOnInputStream);
- BinaryDecoder initOnArray = this.newDecoder(is3, null);
- initOnArray = this.newDecoder(data, initOnArray);
+ BinaryDecoder initOnInputStream = newDecoder(new byte[50], 0, 30, useDirect);
+ initOnInputStream = newDecoder(is2, initOnInputStream, useDirect);
+ BinaryDecoder initOnArray = this.newDecoder(is3, null, useDirect);
+ initOnArray = this.newDecoder(data, initOnArray, useDirect);
for (Object datum : records) {
- Assert.assertEquals("InputStream based BinaryDecoder result does not match", datum,
- reader.read(null, fromInputStream));
- Assert.assertEquals("Array based BinaryDecoder result does not match", datum, reader.read(null, fromArray));
- Assert.assertEquals("offset Array based BinaryDecoder result does not match", datum,
- reader.read(null, fromOffsetArray));
- Assert.assertEquals("InputStream initialized BinaryDecoder result does not match", datum,
- reader.read(null, initOnInputStream));
- Assert.assertEquals("Array initialized BinaryDecoder result does not match", datum,
- reader.read(null, initOnArray));
+ Assertions.assertEquals(datum, reader.read(null, fromInputStream),
+ "InputStream based BinaryDecoder result does not match");
+ Assertions.assertEquals(datum, reader.read(null, fromArray), "Array based BinaryDecoder result does not match");
+ Assertions.assertEquals(datum, reader.read(null, fromOffsetArray),
+ "offset Array based BinaryDecoder result does not match");
+ Assertions.assertEquals(datum, reader.read(null, initOnInputStream),
+ "InputStream initialized BinaryDecoder result does not match");
+ Assertions.assertEquals(datum, reader.read(null, initOnArray),
+ "Array initialized BinaryDecoder result does not match");
}
}
- @Test
- public void testInputStreamProxy() throws IOException {
- BinaryDecoder d = newDecoder(data);
+ @ParameterizedTest
+ @ValueSource(booleans = { true, false })
+ void inputStreamProxy(boolean useDirect) throws IOException {
+ BinaryDecoder d = newDecoder(data, useDirect);
if (d != null) {
BinaryDecoder bd = d;
InputStream test = bd.inputStream();
InputStream check = new ByteArrayInputStream(data);
validateInputStreamReads(test, check);
- bd = this.newDecoder(data, bd);
+ bd = this.newDecoder(data, bd, useDirect);
test = bd.inputStream();
check = new ByteArrayInputStream(data);
validateInputStreamSkips(test, check);
// with input stream sources
- bd = newDecoder(new ByteArrayInputStream(data), bd);
+ bd = newDecoder(new ByteArrayInputStream(data), bd, useDirect);
test = bd.inputStream();
check = new ByteArrayInputStream(data);
validateInputStreamReads(test, check);
- bd = newDecoder(new ByteArrayInputStream(data), bd);
+ bd = newDecoder(new ByteArrayInputStream(data), bd, useDirect);
test = bd.inputStream();
check = new ByteArrayInputStream(data);
validateInputStreamSkips(test, check);
}
}
- @Test
- public void testInputStreamProxyDetached() throws IOException {
- Decoder d = newDecoder(data);
- if (d instanceof BinaryDecoder) {
- BinaryDecoder bd = (BinaryDecoder) d;
- InputStream test = bd.inputStream();
- InputStream check = new ByteArrayInputStream(data);
- // detach input stream and decoder from old source
- this.newDecoder(new byte[56]);
- try (InputStream bad = bd.inputStream(); InputStream check2 = new ByteArrayInputStream(data)) {
- validateInputStreamReads(test, check);
- Assert.assertNotEquals(bad.read(), check2.read());
- }
+ @ParameterizedTest
+ @ValueSource(booleans = { true, false })
+ void inputStreamProxyDetached(boolean useDirect) throws IOException {
+ BinaryDecoder bd = newDecoder(data, useDirect);
+
+ InputStream test = bd.inputStream();
+ InputStream check = new ByteArrayInputStream(data);
+ // detach input stream and decoder from old source
+ this.newDecoder(new byte[56], useDirect);
+ try (InputStream bad = bd.inputStream(); InputStream check2 = new ByteArrayInputStream(data)) {
+ validateInputStreamReads(test, check);
+ Assertions.assertNotEquals(bad.read(), check2.read());
}
}
- @Test
- public void testInputStreamPartiallyUsed() throws IOException {
- BinaryDecoder bd = this.newDecoder(new ByteArrayInputStream(data));
+ @ParameterizedTest
+ @ValueSource(booleans = { true, false })
+ void inputStreamPartiallyUsed(boolean useDirect) throws IOException {
+ BinaryDecoder bd = this.newDecoder(new ByteArrayInputStream(data), useDirect);
InputStream test = bd.inputStream();
InputStream check = new ByteArrayInputStream(data);
// triggers buffer fill if unused and tests isEnd()
try {
- Assert.assertFalse(bd.isEnd());
+ Assertions.assertFalse(bd.isEnd());
} catch (UnsupportedOperationException e) {
// this is ok if its a DirectBinaryDecoder.
if (bd.getClass() != DirectBinaryDecoder.class) {
@@ -300,25 +321,28 @@ private void validateInputStreamReads(InputStream test, InputStream check) throw
while (true) {
int t = test.read();
int c = check.read();
- Assert.assertEquals(c, t);
- if (-1 == t)
+ Assertions.assertEquals(c, t);
+ if (-1 == t) {
break;
+ }
t = test.read(bt);
c = check.read(bc);
- Assert.assertEquals(c, t);
- Assert.assertArrayEquals(bt, bc);
- if (-1 == t)
+ Assertions.assertEquals(c, t);
+ Assertions.assertArrayEquals(bt, bc);
+ if (-1 == t) {
break;
+ }
t = test.read(bt, 1, 4);
c = check.read(bc, 1, 4);
- Assert.assertEquals(c, t);
- Assert.assertArrayEquals(bt, bc);
- if (-1 == t)
+ Assertions.assertEquals(c, t);
+ Assertions.assertArrayEquals(bt, bc);
+ if (-1 == t) {
break;
+ }
}
- Assert.assertEquals(0, test.skip(5));
- Assert.assertEquals(0, test.available());
- Assert.assertFalse(test.getClass() != ByteArrayInputStream.class && test.markSupported());
+ Assertions.assertEquals(0, test.skip(5));
+ Assertions.assertEquals(0, test.available());
+ Assertions.assertFalse(test.getClass() != ByteArrayInputStream.class && test.markSupported());
test.close();
}
@@ -326,154 +350,300 @@ private void validateInputStreamSkips(InputStream test, InputStream check) throw
while (true) {
long t2 = test.skip(19);
long c2 = check.skip(19);
- Assert.assertEquals(c2, t2);
- if (0 == t2)
+ Assertions.assertEquals(c2, t2);
+ if (0 == t2) {
break;
+ }
}
- Assert.assertEquals(-1, test.read());
+ Assertions.assertEquals(-1, test.read());
}
- @Test
- public void testBadIntEncoding() throws IOException {
+ @ParameterizedTest
+ @ValueSource(booleans = { true, false })
+ void badIntEncoding(boolean useDirect) throws IOException {
byte[] badint = new byte[5];
Arrays.fill(badint, (byte) 0xff);
- Decoder bd = this.newDecoder(badint);
+ Decoder bd = this.newDecoder(badint, useDirect);
String message = "";
try {
bd.readInt();
} catch (IOException ioe) {
message = ioe.getMessage();
}
- Assert.assertEquals("Invalid int encoding", message);
+ Assertions.assertEquals("Invalid int encoding", message);
}
- @Test
- public void testBadLongEncoding() throws IOException {
+ @ParameterizedTest
+ @ValueSource(booleans = { true, false })
+ void badLongEncoding(boolean useDirect) throws IOException {
byte[] badint = new byte[10];
Arrays.fill(badint, (byte) 0xff);
- Decoder bd = this.newDecoder(badint);
+ Decoder bd = this.newDecoder(badint, useDirect);
String message = "";
try {
bd.readLong();
} catch (IOException ioe) {
message = ioe.getMessage();
}
- Assert.assertEquals("Invalid long encoding", message);
+ Assertions.assertEquals("Invalid long encoding", message);
}
- @Test
- public void testNegativeStringLength() throws IOException {
- byte[] bad = new byte[] { (byte) 1 };
- Decoder bd = this.newDecoder(bad);
+ @ParameterizedTest
+ @ValueSource(booleans = { true, false })
+ public void testStringNegativeLength(boolean useDirect) throws IOException {
+ Exception ex = Assertions.assertThrows(AvroRuntimeException.class, this.newDecoder(useDirect, -1L)::readString);
+ Assertions.assertEquals(ERROR_NEGATIVE, ex.getMessage());
+ }
- Assert.assertThrows("Malformed data. Length is negative: -1", AvroRuntimeException.class, bd::readString);
+ @ParameterizedTest
+ @ValueSource(booleans = { true, false })
+ public void testStringVmMaxSize(boolean useDirect) throws IOException {
+ Exception ex = Assertions.assertThrows(UnsupportedOperationException.class,
+ newDecoder(useDirect, MAX_ARRAY_VM_LIMIT + 1L)::readString);
+ Assertions.assertEquals(ERROR_VM_LIMIT_STRING, ex.getMessage());
}
- @Test
- public void testStringMaxArraySize() throws IOException {
- byte[] bad = new byte[10];
- BinaryData.encodeLong(BinaryDecoder.MAX_ARRAY_SIZE + 1, bad, 0);
- Decoder bd = this.newDecoder(bad);
+ @ParameterizedTest
+ @ValueSource(booleans = { true, false })
+ public void testStringMaxCustom(boolean useDirect) throws IOException {
+ try {
+ System.setProperty(SystemLimitException.MAX_STRING_LENGTH_PROPERTY, Long.toString(128));
+ resetLimits();
+ Exception ex = Assertions.assertThrows(SystemLimitException.class, newDecoder(useDirect, 129)::readString);
+ Assertions.assertEquals("String length 129 exceeds maximum allowed", ex.getMessage());
+ } finally {
+ System.clearProperty(SystemLimitException.MAX_STRING_LENGTH_PROPERTY);
+ resetLimits();
+ }
+ }
- Assert.assertThrows("Cannot read strings longer than " + BinaryDecoder.MAX_ARRAY_SIZE + " bytes",
- UnsupportedOperationException.class, bd::readString);
+ @ParameterizedTest
+ @ValueSource(booleans = { true, false })
+ public void testBytesNegativeLength(boolean useDirect) throws IOException {
+ Exception ex = Assertions.assertThrows(AvroRuntimeException.class,
+ () -> this.newDecoder(useDirect, -1).readBytes(null));
+ Assertions.assertEquals(ERROR_NEGATIVE, ex.getMessage());
}
- @Test
- public void testNegativeBytesLength() throws IOException {
- byte[] bad = new byte[] { (byte) 1 };
- Decoder bd = this.newDecoder(bad);
+ @ParameterizedTest
+ @ValueSource(booleans = { true, false })
+ public void testBytesVmMaxSize(boolean useDirect) throws IOException {
+ Exception ex = Assertions.assertThrows(UnsupportedOperationException.class,
+ () -> this.newDecoder(useDirect, MAX_ARRAY_VM_LIMIT + 1).readBytes(null));
+ Assertions.assertEquals(ERROR_VM_LIMIT_BYTES, ex.getMessage());
+ }
- Assert.assertThrows("Malformed data. Length is negative: -1", AvroRuntimeException.class, () -> bd.readBytes(null));
+ @ParameterizedTest
+ @ValueSource(booleans = { true, false })
+ public void testBytesMaxCustom(boolean useDirect) throws IOException {
+ try {
+ System.setProperty(SystemLimitException.MAX_BYTES_LENGTH_PROPERTY, Long.toString(128));
+ resetLimits();
+ Exception ex = Assertions.assertThrows(SystemLimitException.class,
+ () -> newDecoder(useDirect, 129).readBytes(null));
+ Assertions.assertEquals("Bytes length 129 exceeds maximum allowed", ex.getMessage());
+ } finally {
+ System.clearProperty(SystemLimitException.MAX_BYTES_LENGTH_PROPERTY);
+ resetLimits();
+ }
}
- @Test
- public void testBytesMaxArraySize() {
- byte[] bad = new byte[10];
- BinaryData.encodeLong(BinaryDecoder.MAX_ARRAY_SIZE + 1, bad, 0);
- Decoder bd = this.newDecoder(bad);
+ @ParameterizedTest
+ @ValueSource(booleans = { true, false })
+ public void testArrayVmMaxSize(boolean useDirect) throws IOException {
+ // At start
+ Exception ex = Assertions.assertThrows(UnsupportedOperationException.class,
+ () -> this.newDecoder(useDirect, MAX_ARRAY_VM_LIMIT + 1).readArrayStart());
+ Assertions.assertEquals(ERROR_VM_LIMIT_COLLECTION, ex.getMessage());
+
+ // Next
+ ex = Assertions.assertThrows(UnsupportedOperationException.class,
+ () -> this.newDecoder(useDirect, MAX_ARRAY_VM_LIMIT + 1).arrayNext());
+ Assertions.assertEquals(ERROR_VM_LIMIT_COLLECTION, ex.getMessage());
+
+ // An OK reads followed by an overflow
+ Decoder bd = newDecoder(useDirect, MAX_ARRAY_VM_LIMIT - 100, Long.MAX_VALUE);
+ Assertions.assertEquals(MAX_ARRAY_VM_LIMIT - 100, bd.readArrayStart());
+ ex = Assertions.assertThrows(UnsupportedOperationException.class, bd::arrayNext);
+ Assertions.assertEquals(ERROR_VM_LIMIT_COLLECTION, ex.getMessage());
+
+ // Two OK reads followed by going over the VM limit.
+ bd = newDecoder(useDirect, MAX_ARRAY_VM_LIMIT - 100, 100, 1);
+ Assertions.assertEquals(MAX_ARRAY_VM_LIMIT - 100, bd.readArrayStart());
+ Assertions.assertEquals(100, bd.arrayNext());
+ ex = Assertions.assertThrows(UnsupportedOperationException.class, bd::arrayNext);
+ Assertions.assertEquals(ERROR_VM_LIMIT_COLLECTION, ex.getMessage());
+
+ // Two OK reads followed by going over the VM limit, where negative numbers are
+ // followed by the byte length of the items. For testing, the 999 values are
+ // read but ignored.
+ bd = newDecoder(useDirect, 100 - MAX_ARRAY_VM_LIMIT, 999, -100, 999, 1);
+ Assertions.assertEquals(MAX_ARRAY_VM_LIMIT - 100, bd.readArrayStart());
+ Assertions.assertEquals(100, bd.arrayNext());
+ ex = Assertions.assertThrows(UnsupportedOperationException.class, bd::arrayNext);
+ Assertions.assertEquals(ERROR_VM_LIMIT_COLLECTION, ex.getMessage());
+ }
+
+ @ParameterizedTest
+ @ValueSource(booleans = { true, false })
+ public void testArrayMaxCustom(boolean useDirect) throws IOException {
+ try {
+ System.setProperty(SystemLimitException.MAX_COLLECTION_LENGTH_PROPERTY, Long.toString(128));
+ resetLimits();
+ Exception ex = Assertions.assertThrows(UnsupportedOperationException.class,
+ () -> newDecoder(useDirect, MAX_ARRAY_VM_LIMIT + 1).readArrayStart());
+ Assertions.assertEquals(ERROR_VM_LIMIT_COLLECTION, ex.getMessage());
+
+ // Two OK reads followed by going over the custom limit.
+ Decoder bd = newDecoder(useDirect, 118, 10, 1);
+ Assertions.assertEquals(118, bd.readArrayStart());
+ Assertions.assertEquals(10, bd.arrayNext());
+ ex = Assertions.assertThrows(SystemLimitException.class, bd::arrayNext);
+ Assertions.assertEquals("Collection length 129 exceeds maximum allowed", ex.getMessage());
+
+ // Two OK reads followed by going over the VM limit, where negative numbers are
+ // followed by the byte length of the items. For testing, the 999 values are
+ // read but ignored.
+ bd = newDecoder(useDirect, -118, 999, -10, 999, 1);
+ Assertions.assertEquals(118, bd.readArrayStart());
+ Assertions.assertEquals(10, bd.arrayNext());
+ ex = Assertions.assertThrows(SystemLimitException.class, bd::arrayNext);
+ Assertions.assertEquals("Collection length 129 exceeds maximum allowed", ex.getMessage());
- Assert.assertThrows("Cannot read arrays longer than " + BinaryDecoder.MAX_ARRAY_SIZE + " bytes",
- UnsupportedOperationException.class, () -> bd.readBytes(null));
+ } finally {
+ System.clearProperty(SystemLimitException.MAX_COLLECTION_LENGTH_PROPERTY);
+ resetLimits();
+ }
}
- @Test
- public void testBytesMaxLengthProperty() {
- int maxLength = 128;
- byte[] bad = new byte[10];
- BinaryData.encodeLong(maxLength + 1, bad, 0);
+ @ParameterizedTest
+ @ValueSource(booleans = { true, false })
+ public void testMapVmMaxSize(boolean useDirect) throws IOException {
+ // At start
+ Exception ex = Assertions.assertThrows(UnsupportedOperationException.class,
+ () -> this.newDecoder(useDirect, MAX_ARRAY_VM_LIMIT + 1).readMapStart());
+ Assertions.assertEquals(ERROR_VM_LIMIT_COLLECTION, ex.getMessage());
+
+ // Next
+ ex = Assertions.assertThrows(UnsupportedOperationException.class,
+ () -> this.newDecoder(useDirect, MAX_ARRAY_VM_LIMIT + 1).mapNext());
+ Assertions.assertEquals(ERROR_VM_LIMIT_COLLECTION, ex.getMessage());
+
+ // Two OK reads followed by going over the VM limit.
+ Decoder bd = newDecoder(useDirect, MAX_ARRAY_VM_LIMIT - 100, 100, 1);
+ Assertions.assertEquals(MAX_ARRAY_VM_LIMIT - 100, bd.readMapStart());
+ Assertions.assertEquals(100, bd.mapNext());
+ ex = Assertions.assertThrows(UnsupportedOperationException.class, bd::mapNext);
+ Assertions.assertEquals(ERROR_VM_LIMIT_COLLECTION, ex.getMessage());
+
+ // Two OK reads followed by going over the VM limit, where negative numbers are
+ // followed by the byte length of the items. For testing, the 999 values are
+ // read but ignored.
+ bd = newDecoder(useDirect, 100 - MAX_ARRAY_VM_LIMIT, 999, -100, 999, 1);
+ Assertions.assertEquals(MAX_ARRAY_VM_LIMIT - 100, bd.readMapStart());
+ Assertions.assertEquals(100, bd.mapNext());
+ ex = Assertions.assertThrows(UnsupportedOperationException.class, bd::mapNext);
+ Assertions.assertEquals(ERROR_VM_LIMIT_COLLECTION, ex.getMessage());
+ }
+
+ @ParameterizedTest
+ @ValueSource(booleans = { true, false })
+ public void testMapMaxCustom(boolean useDirect) throws IOException {
try {
- System.setProperty("org.apache.avro.limits.bytes.maxLength", Long.toString(maxLength));
- Decoder bd = this.newDecoder(bad);
+ System.setProperty(SystemLimitException.MAX_COLLECTION_LENGTH_PROPERTY, Long.toString(128));
+ resetLimits();
+ Exception ex = Assertions.assertThrows(UnsupportedOperationException.class,
+ () -> newDecoder(useDirect, MAX_ARRAY_VM_LIMIT + 1).readMapStart());
+ Assertions.assertEquals(ERROR_VM_LIMIT_COLLECTION, ex.getMessage());
+
+ // Two OK reads followed by going over the custom limit.
+ Decoder bd = newDecoder(useDirect, 118, 10, 1);
+ Assertions.assertEquals(118, bd.readMapStart());
+ Assertions.assertEquals(10, bd.mapNext());
+ ex = Assertions.assertThrows(SystemLimitException.class, bd::mapNext);
+ Assertions.assertEquals("Collection length 129 exceeds maximum allowed", ex.getMessage());
+
+ // Two OK reads followed by going over the VM limit, where negative numbers are
+ // followed by the byte length of the items. For testing, the 999 values are
+ // read but ignored.
+ bd = newDecoder(useDirect, -118, 999, -10, 999, 1);
+ Assertions.assertEquals(118, bd.readMapStart());
+ Assertions.assertEquals(10, bd.mapNext());
+ ex = Assertions.assertThrows(SystemLimitException.class, bd::mapNext);
+ Assertions.assertEquals("Collection length 129 exceeds maximum allowed", ex.getMessage());
- Assert.assertThrows("Bytes length " + (maxLength + 1) + " exceeds maximum allowed", AvroRuntimeException.class,
- () -> bd.readBytes(null));
} finally {
- System.clearProperty("org.apache.avro.limits.bytes.maxLength");
+ System.clearProperty(SystemLimitException.MAX_COLLECTION_LENGTH_PROPERTY);
+ resetLimits();
}
}
- @Test(expected = UnsupportedOperationException.class)
- public void testLongLengthEncoding() throws IOException {
+ @ParameterizedTest
+ @ValueSource(booleans = { true, false })
+ void longLengthEncoding(boolean useDirect) {
// Size equivalent to Integer.MAX_VALUE + 1
byte[] bad = new byte[] { (byte) -128, (byte) -128, (byte) -128, (byte) -128, (byte) 16 };
- Decoder bd = this.newDecoder(bad);
- bd.readString();
+ Decoder bd = this.newDecoder(bad, useDirect);
+ Assertions.assertThrows(UnsupportedOperationException.class, bd::readString);
}
- @Test(expected = EOFException.class)
- public void testIntTooShort() throws IOException {
+ @ParameterizedTest
+ @ValueSource(booleans = { true, false })
+ void intTooShort(boolean useDirect) {
byte[] badint = new byte[4];
Arrays.fill(badint, (byte) 0xff);
- newDecoder(badint).readInt();
+ Assertions.assertThrows(EOFException.class, () -> newDecoder(badint, useDirect).readInt());
}
- @Test(expected = EOFException.class)
- public void testLongTooShort() throws IOException {
+ @ParameterizedTest
+ @ValueSource(booleans = { true, false })
+ void longTooShort(boolean useDirect) {
byte[] badint = new byte[9];
Arrays.fill(badint, (byte) 0xff);
- newDecoder(badint).readLong();
+ Assertions.assertThrows(EOFException.class, () -> newDecoder(badint, useDirect).readLong());
}
- @Test(expected = EOFException.class)
- public void testFloatTooShort() throws IOException {
+ @ParameterizedTest
+ @ValueSource(booleans = { true, false })
+ void floatTooShort(boolean useDirect) {
byte[] badint = new byte[3];
Arrays.fill(badint, (byte) 0xff);
- newDecoder(badint).readInt();
+ Assertions.assertThrows(EOFException.class, () -> newDecoder(badint, useDirect).readInt());
}
- @Test(expected = EOFException.class)
- public void testDoubleTooShort() throws IOException {
+ @ParameterizedTest
+ @ValueSource(booleans = { true, false })
+ void doubleTooShort(boolean useDirect) {
byte[] badint = new byte[7];
Arrays.fill(badint, (byte) 0xff);
- newDecoder(badint).readLong();
+ Assertions.assertThrows(EOFException.class, () -> newDecoder(badint, useDirect).readLong());
}
- @Test
- public void testSkipping() throws IOException {
- Decoder d = newDecoder(data);
- skipGenerated(d);
- if (d instanceof BinaryDecoder) {
- BinaryDecoder bd = (BinaryDecoder) d;
- try {
- Assert.assertTrue(bd.isEnd());
- } catch (UnsupportedOperationException e) {
- // this is ok if its a DirectBinaryDecoder.
- if (bd.getClass() != DirectBinaryDecoder.class) {
- throw e;
- }
+ @ParameterizedTest
+ @ValueSource(booleans = { true, false })
+ void skipping(boolean useDirect) throws IOException {
+ BinaryDecoder bd = newDecoder(data, useDirect);
+ skipGenerated(bd);
+
+ try {
+ Assertions.assertTrue(bd.isEnd());
+ } catch (UnsupportedOperationException e) {
+ // this is ok if its a DirectBinaryDecoder.
+ if (bd.getClass() != DirectBinaryDecoder.class) {
+ throw e;
}
- bd = this.newDecoder(new ByteArrayInputStream(data), bd);
- skipGenerated(bd);
- try {
- Assert.assertTrue(bd.isEnd());
- } catch (UnsupportedOperationException e) {
- // this is ok if its a DirectBinaryDecoder.
- if (bd.getClass() != DirectBinaryDecoder.class) {
- throw e;
- }
+ }
+ bd = this.newDecoder(new ByteArrayInputStream(data), bd, useDirect);
+ skipGenerated(bd);
+ try {
+ Assertions.assertTrue(bd.isEnd());
+ } catch (UnsupportedOperationException e) {
+ // this is ok if its a DirectBinaryDecoder.
+ if (bd.getClass() != DirectBinaryDecoder.class) {
+ throw e;
}
}
+
}
private void skipGenerated(Decoder bd) throws IOException {
@@ -496,19 +666,20 @@ private void skipGenerated(Decoder bd) throws IOException {
} catch (EOFException e) {
eof = e;
}
- Assert.assertNotNull(eof);
+ Assertions.assertNotNull(eof);
}
- @Test(expected = EOFException.class)
- public void testEOF() throws IOException {
+ @ParameterizedTest
+ @ValueSource(booleans = { true, false })
+ void eof(boolean useDirect) throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
Encoder e = EncoderFactory.get().binaryEncoder(baos, null);
e.writeLong(0x10000000000000L);
e.flush();
- Decoder d = newDecoder(new ByteArrayInputStream(baos.toByteArray()));
- Assert.assertEquals(0x10000000000000L, d.readLong());
- d.readInt();
+ Decoder d = newDecoder(new ByteArrayInputStream(baos.toByteArray()), useDirect);
+ Assertions.assertEquals(0x10000000000000L, d.readLong());
+ Assertions.assertThrows(EOFException.class, () -> d.readInt());
}
}
diff --git a/lang/java/avro/src/test/java/org/apache/avro/io/TestBlockingIO.java b/lang/java/avro/src/test/java/org/apache/avro/io/TestBlockingIO.java
index 6beda2ae66e..d107b9d82d7 100644
--- a/lang/java/avro/src/test/java/org/apache/avro/io/TestBlockingIO.java
+++ b/lang/java/avro/src/test/java/org/apache/avro/io/TestBlockingIO.java
@@ -17,7 +17,12 @@
*/
package org.apache.avro.io;
-import static org.junit.Assert.*;
+import com.fasterxml.jackson.core.JsonFactory;
+import com.fasterxml.jackson.core.JsonParser;
+
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
@@ -25,28 +30,14 @@
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.ArrayDeque;
-import java.util.Arrays;
-import java.util.Collection;
+import java.util.stream.Stream;
-import com.fasterxml.jackson.core.JsonFactory;
-import com.fasterxml.jackson.core.JsonParser;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.Parameterized;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
-@RunWith(Parameterized.class)
public class TestBlockingIO {
- private final int iSize;
- private final int iDepth;
- private final String sInput;
-
- public TestBlockingIO(int sz, int dp, String inp) {
- this.iSize = sz;
- this.iDepth = dp;
- this.sInput = inp;
- }
-
private static class Tests {
private final JsonParser parser;
private final Decoder input;
@@ -206,25 +197,29 @@ public S(long count, boolean isArray) {
}
}
- @Test
- public void testScan() throws IOException {
- Tests t = new Tests(iSize, iDepth, sInput);
+ @ParameterizedTest
+ @MethodSource("data")
+ public void testScan(int size, int depth, String input) throws IOException {
+ Tests t = new Tests(size, depth, input);
t.scan();
}
- @Test
- public void testSkip1() throws IOException {
- testSkip(iSize, iDepth, sInput, 0);
+ @ParameterizedTest
+ @MethodSource("data")
+ public void testSkip1(int size, int depth, String input) throws IOException {
+ testSkip(size, depth, input, 0);
}
- @Test
- public void testSkip2() throws IOException {
- testSkip(iSize, iDepth, sInput, 1);
+ @ParameterizedTest
+ @MethodSource("data")
+ public void testSkip2(int size, int depth, String input) throws IOException {
+ testSkip(size, depth, input, 1);
}
- @Test
- public void testSkip3() throws IOException {
- testSkip(iSize, iDepth, sInput, 2);
+ @ParameterizedTest
+ @MethodSource("data")
+ public void testSkip3(int size, int depth, String input) throws IOException {
+ testSkip(size, depth, input, 2);
}
private void testSkip(int bufferSize, int depth, String input, int skipLevel) throws IOException {
@@ -323,9 +318,8 @@ private static void serialize(Encoder cos, JsonParser p, ByteArrayOutputStream o
}
}
- @Parameterized.Parameters
- public static Collection data() {
- return Arrays.asList(new Object[][] { { 64, 0, "" }, { 64, 0, jss(0, 'a') }, { 64, 0, jss(3, 'a') },
+ public static Stream data() {
+ return Stream.of(new Object[][] { { 64, 0, "" }, { 64, 0, jss(0, 'a') }, { 64, 0, jss(3, 'a') },
{ 64, 0, jss(64, 'a') }, { 64, 0, jss(65, 'a') }, { 64, 0, jss(100, 'a') }, { 64, 1, "[]" },
{ 64, 1, "[" + jss(0, 'a') + "]" }, { 64, 1, "[" + jss(3, 'a') + "]" }, { 64, 1, "[" + jss(61, 'a') + "]" },
{ 64, 1, "[" + jss(62, 'a') + "]" }, { 64, 1, "[" + jss(64, 'a') + "]" }, { 64, 1, "[" + jss(65, 'a') + "]" },
@@ -387,7 +381,8 @@ public static Collection data() {
{ 100, 2, "[[\"pqr\", \"ab\", \"mnopqrstuvwx\"]]" }, { 64, 2, "[[[\"pqr\"]], [[\"ab\"], [\"mnopqrstuvwx\"]]]" },
{ 64, 1, "{}" }, { 64, 1, "{\"n\": \"v\"}" }, { 64, 1, "{\"n1\": \"v\", \"n2\": []}" },
- { 100, 1, "{\"n1\": \"v\", \"n2\": []}" }, { 100, 1, "{\"n1\": \"v\", \"n2\": [\"abc\"]}" }, });
+ { 100, 1, "{\"n1\": \"v\", \"n2\": []}" }, { 100, 1, "{\"n1\": \"v\", \"n2\": [\"abc\"]}" }, })
+ .map(Arguments::of);
}
/**
diff --git a/lang/java/avro/src/test/java/org/apache/avro/io/TestBlockingIO2.java b/lang/java/avro/src/test/java/org/apache/avro/io/TestBlockingIO2.java
index 3a91bb96dea..378e17ee613 100644
--- a/lang/java/avro/src/test/java/org/apache/avro/io/TestBlockingIO2.java
+++ b/lang/java/avro/src/test/java/org/apache/avro/io/TestBlockingIO2.java
@@ -17,14 +17,13 @@
*/
package org.apache.avro.io;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
import java.io.ByteArrayOutputStream;
import java.io.IOException;
-import java.util.Arrays;
-import java.util.Collection;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.Parameterized;
+import java.util.stream.Stream;
/**
* This class has more exhaustive tests for Blocking IO. The reason we have both
@@ -32,38 +31,29 @@
* TestBlockingIO2, it is hard to test skip() operations. and with the test
* infrastructure of TestBlockingIO, it is hard to test enums, unions etc.
*/
-@RunWith(Parameterized.class)
public class TestBlockingIO2 {
- private final Decoder decoder;
- private final String calls;
- private Object[] values;
- private String msg;
-
- public TestBlockingIO2(int bufferSize, int skipLevel, String calls) throws IOException {
+ @ParameterizedTest
+ @MethodSource("data")
+ public void testScan(int bufferSize, int skipLevel, String calls) throws IOException {
ByteArrayOutputStream os = new ByteArrayOutputStream();
EncoderFactory factory = new EncoderFactory().configureBlockSize(bufferSize);
Encoder encoder = factory.blockingBinaryEncoder(os, null);
- this.values = TestValidatingIO.randomValues(calls);
+ Object[] values = TestValidatingIO.randomValues(calls);
TestValidatingIO.generate(encoder, calls, values);
encoder.flush();
byte[] bb = os.toByteArray();
- decoder = DecoderFactory.get().binaryDecoder(bb, null);
- this.calls = calls;
- this.msg = "Case: { " + bufferSize + ", " + skipLevel + ", \"" + calls + "\" }";
- }
+ Decoder decoder = DecoderFactory.get().binaryDecoder(bb, null);
+ String msg = "Case: { " + bufferSize + ", " + skipLevel + ", \"" + calls + "\" }";
- @Test
- public void testScan() throws IOException {
TestValidatingIO.check(msg, decoder, calls, values, -1);
}
- @Parameterized.Parameters
- public static Collection data() {
- return Arrays.asList(new Object[][] { { 64, 0, "" }, { 64, 0, "S0" }, { 64, 0, "S3" }, { 64, 0, "S64" },
+ public static Stream data() {
+ return Stream.of(new Object[][] { { 64, 0, "" }, { 64, 0, "S0" }, { 64, 0, "S3" }, { 64, 0, "S64" },
{ 64, 0, "S65" }, { 64, 0, "S100" }, { 64, 1, "[]" }, { 64, 1, "[c1sS0]" }, { 64, 1, "[c1sS3]" },
{ 64, 1, "[c1sS61]" }, { 64, 1, "[c1sS62]" }, { 64, 1, "[c1sS64]" }, { 64, 1, "[c1sS65]" },
{ 64, 1, "[c2sS0sS0]" }, { 64, 1, "[c2sS0sS10]" }, { 64, 1, "[c2sS0sS63]" }, { 64, 1, "[c2sS0sS64]" },
@@ -99,6 +89,6 @@ public static Collection data() {
{ 100, 1, "{c1sK5e10}" }, { 100, 1, "{c1sK5U1S10}" }, { 100, 1, "{c1sK5f10S10}" }, { 100, 1, "{c1sK5NS10}" },
{ 100, 1, "{c1sK5BS10}" }, { 100, 1, "{c1sK5IS10}" }, { 100, 1, "{c1sK5LS10}" }, { 100, 1, "{c1sK5FS10}" },
- { 100, 1, "{c1sK5DS10}" }, });
+ { 100, 1, "{c1sK5DS10}" }, }).map(Arguments::of);
}
}
diff --git a/lang/java/avro/src/test/java/org/apache/avro/io/TestEncoders.java b/lang/java/avro/src/test/java/org/apache/avro/io/TestEncoders.java
index 665a0e7b6f9..dbed64d6a18 100644
--- a/lang/java/avro/src/test/java/org/apache/avro/io/TestEncoders.java
+++ b/lang/java/avro/src/test/java/org/apache/avro/io/TestEncoders.java
@@ -29,6 +29,8 @@
import org.apache.avro.generic.GenericDatumWriter;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
+
+import java.io.BufferedOutputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
@@ -332,4 +334,35 @@ private String fromAvroToJson(byte[] avroBytes, Schema schema, boolean includeNa
return new String(output.toByteArray(), StandardCharsets.UTF_8.name());
}
+
+ @Test
+ public void testJsonEncoderInitAutoFlush() throws IOException {
+ Schema s = new Schema.Parser().parse("\"int\"");
+ OutputStream baos = new ByteArrayOutputStream();
+ OutputStream out = new BufferedOutputStream(baos);
+ JsonEncoder enc = factory.jsonEncoder(s, out, false);
+ enc.configure(out, false);
+ enc.writeInt(24);
+ enc.flush();
+ assertEquals("", baos.toString());
+ out.flush();
+ assertEquals("24", baos.toString());
+ }
+
+ @Test
+ public void testJsonEncoderInitAutoFlushDisabled() throws IOException {
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ OutputStream out = new BufferedOutputStream(baos);
+ Schema ints = Schema.create(Type.INT);
+ Encoder e = factory.jsonEncoder(ints, out, false, false);
+ String separator = System.getProperty("line.separator");
+ GenericDatumWriter writer = new GenericDatumWriter(ints);
+ writer.write(1, e);
+ writer.write(2, e);
+ e.flush();
+ assertEquals("", baos.toString());
+ out.flush();
+ assertEquals("1" + separator + "2", baos.toString());
+ out.close();
+ }
}
diff --git a/lang/java/avro/src/test/java/org/apache/avro/io/TestJsonDecoder.java b/lang/java/avro/src/test/java/org/apache/avro/io/TestJsonDecoder.java
index 693fbd421e8..05057139600 100644
--- a/lang/java/avro/src/test/java/org/apache/avro/io/TestJsonDecoder.java
+++ b/lang/java/avro/src/test/java/org/apache/avro/io/TestJsonDecoder.java
@@ -19,11 +19,17 @@
import static org.junit.jupiter.api.Assertions.assertEquals;
+import org.apache.avro.AvroTypeException;
import org.apache.avro.Schema;
+import org.apache.avro.SchemaBuilder;
import org.apache.avro.generic.GenericDatumReader;
import org.apache.avro.generic.GenericRecord;
+
+import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
+import java.io.IOException;
+
public class TestJsonDecoder {
@Test
@@ -76,4 +82,14 @@ void reorderFields() throws Exception {
assertEquals(200, in.readLong());
in.skipArray();
}
+
+ @Test
+ void testIntWithError() throws IOException {
+ Schema schema = SchemaBuilder.builder("test").record("example").fields().requiredInt("id").endRecord();
+ String record = "{ \"id\": -1.2 }";
+
+ GenericDatumReader reader = new GenericDatumReader<>(schema, schema);
+ JsonDecoder decoder = DecoderFactory.get().jsonDecoder(schema, record);
+ Assertions.assertThrows(AvroTypeException.class, () -> reader.read(null, decoder));
+ }
}
diff --git a/lang/java/avro/src/test/java/org/apache/avro/io/TestResolvingIO.java b/lang/java/avro/src/test/java/org/apache/avro/io/TestResolvingIO.java
index c880d9fd55a..8a960427922 100644
--- a/lang/java/avro/src/test/java/org/apache/avro/io/TestResolvingIO.java
+++ b/lang/java/avro/src/test/java/org/apache/avro/io/TestResolvingIO.java
@@ -17,48 +17,34 @@
*/
package org.apache.avro.io;
+import org.apache.avro.Schema;
+import org.apache.avro.io.TestValidatingIO.Encoding;
+
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
-import java.util.Arrays;
-import java.util.Collection;
+import java.util.stream.Stream;
-import org.apache.avro.Schema;
-import org.apache.avro.io.TestValidatingIO.Encoding;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.Parameterized;
-
-@RunWith(Parameterized.class)
public class TestResolvingIO {
- protected final Encoding eEnc;
- protected final int iSkipL;
- protected final String sJsWrtSchm;
- protected final String sWrtCls;
- protected final String sJsRdrSchm;
- protected final String sRdrCls;
-
- public TestResolvingIO(Encoding encoding, int skipLevel, String jsonWriterSchema, String writerCalls,
- String jsonReaderSchema, String readerCalls) {
- this.eEnc = encoding;
- this.iSkipL = skipLevel;
- this.sJsWrtSchm = jsonWriterSchema;
- this.sWrtCls = writerCalls;
- this.sJsRdrSchm = jsonReaderSchema;
- this.sRdrCls = readerCalls;
- }
-
- @Test
- public void testIdentical() throws IOException {
- performTest(eEnc, iSkipL, sJsWrtSchm, sWrtCls, sJsWrtSchm, sWrtCls);
+ @ParameterizedTest
+ @MethodSource("data2")
+ public void testIdentical(Encoding encoding, int skip, String jsonWriterSchema, String writerCalls,
+ String jsonReaderSchema, String readerCalls) throws IOException {
+ performTest(encoding, skip, jsonWriterSchema, writerCalls, jsonWriterSchema, writerCalls);
}
private static final int COUNT = 10;
- @Test
- public void testCompatible() throws IOException {
- performTest(eEnc, iSkipL, sJsWrtSchm, sWrtCls, sJsRdrSchm, sRdrCls);
+ @ParameterizedTest
+ @MethodSource("data2")
+ public void testCompatible(Encoding encoding, int skip, String jsonWriterSchema, String writerCalls,
+ String jsonReaderSchema, String readerCalls) throws IOException {
+ performTest(encoding, skip, jsonWriterSchema, writerCalls, jsonReaderSchema, readerCalls);
}
private void performTest(Encoding encoding, int skipLevel, String jsonWriterSchema, String writerCalls,
@@ -100,9 +86,8 @@ static void check(Schema wsc, Schema rsc, byte[] bytes, String calls, Object[] v
TestValidatingIO.check(msg, vi, calls, values, skipLevel);
}
- @Parameterized.Parameters
- public static Collection data2() {
- return Arrays.asList(TestValidatingIO.convertTo2dArray(encodings, skipLevels, testSchemas()));
+ public static Stream data2() {
+ return TestValidatingIO.convertTo2dStream(encodings, skipLevels, testSchemas());
}
static Object[][] encodings = new Object[][] { { Encoding.BINARY }, { Encoding.BLOCKING_BINARY }, { Encoding.JSON } };
diff --git a/lang/java/avro/src/test/java/org/apache/avro/io/TestResolvingIOResolving.java b/lang/java/avro/src/test/java/org/apache/avro/io/TestResolvingIOResolving.java
index 8e3dc8e53d7..0a55d18a742 100644
--- a/lang/java/avro/src/test/java/org/apache/avro/io/TestResolvingIOResolving.java
+++ b/lang/java/avro/src/test/java/org/apache/avro/io/TestResolvingIOResolving.java
@@ -17,53 +17,32 @@
*/
package org.apache.avro.io;
-import java.io.IOException;
-import java.util.Arrays;
-import java.util.Collection;
-
import org.apache.avro.Schema;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.Parameterized;
-@RunWith(Parameterized.class)
-public class TestResolvingIOResolving {
- protected TestValidatingIO.Encoding eEnc;
- protected final int iSkipL;
- protected final String sJsWrtSchm;
- protected final String sWrtCls;
- protected final String sJsRdrSchm;
- protected final String sRdrCls;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
- protected final Object[] oaWrtVals;
- protected final Object[] oaRdrVals;
+import java.io.IOException;
+import java.util.stream.Stream;
- public TestResolvingIOResolving(TestValidatingIO.Encoding encoding, int skipLevel, String jsonWriterSchema,
- String writerCalls, Object[] writerValues, String jsonReaderSchema, String readerCalls, Object[] readerValues) {
- this.eEnc = encoding;
- this.iSkipL = skipLevel;
- this.sJsWrtSchm = jsonWriterSchema;
- this.sWrtCls = writerCalls;
- this.oaWrtVals = writerValues;
- this.sJsRdrSchm = jsonReaderSchema;
- this.sRdrCls = readerCalls;
- this.oaRdrVals = readerValues;
- }
+public class TestResolvingIOResolving {
- @Test
- public void testResolving() throws IOException {
- Schema writerSchema = new Schema.Parser().parse(sJsWrtSchm);
- byte[] bytes = TestValidatingIO.make(writerSchema, sWrtCls, oaWrtVals, eEnc);
- Schema readerSchema = new Schema.Parser().parse(sJsRdrSchm);
- TestValidatingIO.print(eEnc, iSkipL, writerSchema, readerSchema, oaWrtVals, oaRdrVals);
- TestResolvingIO.check(writerSchema, readerSchema, bytes, sRdrCls, oaRdrVals, eEnc, iSkipL);
+ @ParameterizedTest
+ @MethodSource("data3")
+ public void testResolving(TestValidatingIO.Encoding encoding, int skipLevel, String jsonWriterSchema,
+ String writerCalls, Object[] writerValues, String jsonReaderSchema, String readerCalls, Object[] readerValues)
+ throws IOException {
+ Schema writerSchema = new Schema.Parser().parse(jsonWriterSchema);
+ byte[] bytes = TestValidatingIO.make(writerSchema, writerCalls, writerValues, encoding);
+ Schema readerSchema = new Schema.Parser().parse(jsonReaderSchema);
+ TestValidatingIO.print(encoding, skipLevel, writerSchema, readerSchema, writerValues, readerValues);
+ TestResolvingIO.check(writerSchema, readerSchema, bytes, readerCalls, readerValues, encoding, skipLevel);
}
- @Parameterized.Parameters
- public static Collection data3() {
- Collection ret = Arrays.asList(TestValidatingIO.convertTo2dArray(TestResolvingIO.encodings,
- TestResolvingIO.skipLevels, dataForResolvingTests()));
- return ret;
+ public static Stream data3() {
+ return TestValidatingIO.convertTo2dStream(TestResolvingIO.encodings, TestResolvingIO.skipLevels,
+ dataForResolvingTests());
}
private static Object[][] dataForResolvingTests() {
@@ -101,7 +80,7 @@ private static Object[][] dataForResolvingTests() {
"{\"type\":\"record\",\"name\":\"outer\",\"fields\":[" + "{\"name\": \"g1\", "
+ "\"type\":{\"type\":\"record\",\"name\":\"inner\",\"fields\":["
+ "{\"name\":\"f1\", \"type\":\"int\", \"default\": 101}," + "{\"name\":\"f2\", \"type\":\"int\"}]}}, "
- + "{\"name\": \"g2\", \"type\": \"long\"}]}}",
+ + "{\"name\": \"g2\", \"type\": \"long\"}]}",
"RRIIL", new Object[] { 10, 101, 11L } },
// Default value for a record.
{ "{\"type\":\"record\",\"name\":\"outer\",\"fields\":[" + "{\"name\": \"g2\", \"type\": \"long\"}]}", "L",
diff --git a/lang/java/avro/src/test/java/org/apache/avro/io/TestValidatingIO.java b/lang/java/avro/src/test/java/org/apache/avro/io/TestValidatingIO.java
index 3056d5430af..063414fbb43 100644
--- a/lang/java/avro/src/test/java/org/apache/avro/io/TestValidatingIO.java
+++ b/lang/java/avro/src/test/java/org/apache/avro/io/TestValidatingIO.java
@@ -17,9 +17,15 @@
*/
package org.apache.avro.io;
-import static org.junit.Assert.assertArrayEquals;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.fail;
+import org.apache.avro.Schema;
+import org.apache.avro.util.Utf8;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
@@ -27,20 +33,14 @@
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Random;
-import org.apache.avro.Schema;
-import org.apache.avro.util.Utf8;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.Parameterized;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
+import java.util.Spliterator;
+import java.util.Spliterators;
+import java.util.stream.Stream;
+import java.util.stream.StreamSupport;
-@RunWith(Parameterized.class)
public class TestValidatingIO {
enum Encoding {
BINARY, BLOCKING_BINARY, JSON,
@@ -48,30 +48,19 @@ enum Encoding {
private static final Logger LOG = LoggerFactory.getLogger(TestValidatingIO.class);
- private Encoding eEnc;
- private int iSkipL;
- private String sJsSch;
- private String sCl;
-
- public TestValidatingIO(Encoding enc, int skip, String js, String cls) {
- this.eEnc = enc;
- this.iSkipL = skip;
- this.sJsSch = js;
- this.sCl = cls;
- }
-
private static final int COUNT = 1;
- @Test
- public void testMain() throws IOException {
+ @ParameterizedTest
+ @MethodSource("data")
+ public void testMain(Encoding enc, int skip, String js, String cls) throws IOException {
for (int i = 0; i < COUNT; i++) {
- testOnce(new Schema.Parser().parse(sJsSch), sCl, iSkipL, eEnc);
+ testOnce(new Schema.Parser().parse(js), cls, skip, enc);
}
}
private void testOnce(Schema schema, String calls, int skipLevel, Encoding encoding) throws IOException {
Object[] values = randomValues(calls);
- print(eEnc, iSkipL, schema, schema, values, values);
+ print(encoding, skipLevel, schema, schema, values, values);
byte[] bytes = make(schema, calls, values, encoding);
check(schema, bytes, calls, values, skipLevel, encoding);
}
@@ -204,7 +193,7 @@ public static void generate(Encoder vw, String calls, Object[] values) throws IO
break;
}
default:
- fail();
+ Assertions.fail();
break;
}
}
@@ -254,7 +243,7 @@ public static Object[] randomValues(String calls) {
case 's':
break;
default:
- fail();
+ Assertions.fail();
break;
}
}
@@ -324,25 +313,25 @@ public static void check(String msg, Decoder vi, String calls, Object[] values,
vi.readNull();
break;
case 'B':
- assertEquals(msg, values[p++], vi.readBoolean());
+ Assertions.assertEquals(values[p++], vi.readBoolean(), msg);
break;
case 'I':
- assertEquals(msg, values[p++], vi.readInt());
+ Assertions.assertEquals(values[p++], vi.readInt(), msg);
break;
case 'L':
- assertEquals(msg, values[p++], vi.readLong());
+ Assertions.assertEquals(values[p++], vi.readLong(), msg);
break;
case 'F':
if (!(values[p] instanceof Float))
- fail();
+ Assertions.fail();
float f = (Float) values[p++];
- assertEquals(msg, f, vi.readFloat(), Math.abs(f / 1000));
+ Assertions.assertEquals(f, vi.readFloat(), Math.abs(f / 1000));
break;
case 'D':
if (!(values[p] instanceof Double))
- fail();
+ Assertions.fail();
double d = (Double) values[p++];
- assertEquals(msg, d, vi.readDouble(), Math.abs(d / 1000));
+ Assertions.assertEquals(d, vi.readDouble(), Math.abs(d / 1000), msg);
break;
case 'S':
extractInt(cs);
@@ -351,7 +340,7 @@ public static void check(String msg, Decoder vi, String calls, Object[] values,
p++;
} else {
String s = (String) values[p++];
- assertEquals(msg, new Utf8(s), vi.readString(null));
+ Assertions.assertEquals(new Utf8(s), vi.readString(null), msg);
}
break;
case 'K':
@@ -361,7 +350,7 @@ public static void check(String msg, Decoder vi, String calls, Object[] values,
p++;
} else {
String s = (String) values[p++];
- assertEquals(msg, new Utf8(s), vi.readString(null));
+ Assertions.assertEquals(new Utf8(s), vi.readString(null), msg);
}
break;
case 'b':
@@ -374,7 +363,7 @@ public static void check(String msg, Decoder vi, String calls, Object[] values,
ByteBuffer bb2 = vi.readBytes(null);
byte[] actBytes = new byte[bb2.remaining()];
System.arraycopy(bb2.array(), bb2.position(), actBytes, 0, bb2.remaining());
- assertArrayEquals(msg, bb, actBytes);
+ Assertions.assertArrayEquals(bb, actBytes, msg);
}
break;
case 'f': {
@@ -386,7 +375,7 @@ public static void check(String msg, Decoder vi, String calls, Object[] values,
byte[] bb = (byte[]) values[p++];
byte[] actBytes = new byte[len];
vi.readFixed(actBytes);
- assertArrayEquals(msg, bb, actBytes);
+ Assertions.assertArrayEquals(bb, actBytes, msg);
}
}
break;
@@ -395,7 +384,7 @@ public static void check(String msg, Decoder vi, String calls, Object[] values,
if (level == skipLevel) {
vi.readEnum();
} else {
- assertEquals(msg, e, vi.readEnum());
+ Assertions.assertEquals(e, vi.readEnum(), msg);
}
}
break;
@@ -422,16 +411,16 @@ public static void check(String msg, Decoder vi, String calls, Object[] values,
continue;
}
case ']':
- assertEquals(msg, 0, counts[level]);
+ Assertions.assertEquals(0, counts[level], msg);
if (!isEmpty[level]) {
- assertEquals(msg, 0, vi.arrayNext());
+ Assertions.assertEquals(0, vi.arrayNext(), msg);
}
level--;
break;
case '}':
- assertEquals(0, counts[level]);
+ Assertions.assertEquals(0, counts[level]);
if (!isEmpty[level]) {
- assertEquals(msg, 0, vi.mapNext());
+ Assertions.assertEquals(0, vi.mapNext(), msg);
}
level--;
break;
@@ -450,28 +439,28 @@ public static void check(String msg, Decoder vi, String calls, Object[] values,
continue;
case 'U': {
int idx = extractInt(cs);
- assertEquals(msg, idx, vi.readIndex());
+ Assertions.assertEquals(idx, vi.readIndex(), msg);
continue;
}
case 'R':
((ResolvingDecoder) vi).readFieldOrder();
continue;
default:
- fail(msg);
+ Assertions.fail(msg);
}
} catch (RuntimeException e) {
throw new RuntimeException(msg, e);
}
}
- assertEquals(msg, values.length, p);
+ Assertions.assertEquals(values.length, p, msg);
}
private static int skip(String msg, InputScanner cs, Decoder vi, boolean isArray) throws IOException {
final char end = isArray ? ']' : '}';
if (isArray) {
- assertEquals(msg, 0, vi.skipArray());
+ Assertions.assertEquals(0, vi.skipArray(), msg);
} else if (end == '}') {
- assertEquals(msg, 0, vi.skipMap());
+ Assertions.assertEquals(0, vi.skipMap(), msg);
}
int level = 0;
int p = 0;
@@ -507,9 +496,8 @@ private static int skip(String msg, InputScanner cs, Decoder vi, boolean isArray
throw new RuntimeException("Don't know how to skip");
}
- @Parameterized.Parameters
- public static Collection data() {
- return Arrays.asList(convertTo2dArray(encodings, skipLevels, testSchemas()));
+ public static Stream data() {
+ return convertTo2dStream(encodings, skipLevels, testSchemas());
}
private static Object[][] encodings = new Object[][] { { Encoding.BINARY }, { Encoding.BLOCKING_BINARY },
@@ -517,19 +505,11 @@ public static Collection data() {
private static Object[][] skipLevels = new Object[][] { { -1 }, { 0 }, { 1 }, { 2 }, };
- public static Object[][] convertTo2dArray(final Object[][]... values) {
- ArrayList ret = new ArrayList<>();
-
+ public static Stream convertTo2dStream(final Object[][]... values) {
Iterator iter = cartesian(values);
- while (iter.hasNext()) {
- Object[] objects = iter.next();
- ret.add(objects);
- }
- Object[][] retArrays = new Object[ret.size()][];
- for (int i = 0; i < ret.size(); i++) {
- retArrays[i] = ret.get(i);
- }
- return retArrays;
+ Stream stream = StreamSupport.stream(Spliterators.spliteratorUnknownSize(iter, Spliterator.ORDERED),
+ false);
+ return stream.map(Arguments::of);
}
/**
diff --git a/lang/java/avro/src/test/java/org/apache/avro/io/parsing/TestResolvingGrammarGenerator.java b/lang/java/avro/src/test/java/org/apache/avro/io/parsing/TestResolvingGrammarGenerator.java
index 4eac760cec7..c6d8856733b 100644
--- a/lang/java/avro/src/test/java/org/apache/avro/io/parsing/TestResolvingGrammarGenerator.java
+++ b/lang/java/avro/src/test/java/org/apache/avro/io/parsing/TestResolvingGrammarGenerator.java
@@ -21,8 +21,10 @@
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.StringReader;
+import java.io.UncheckedIOException;
import java.util.Arrays;
import java.util.Collection;
+import java.util.stream.Stream;
import com.fasterxml.jackson.core.JsonFactory;
import com.fasterxml.jackson.databind.JsonNode;
@@ -38,29 +40,21 @@
import org.apache.avro.generic.GenericRecordBuilder;
import org.apache.avro.io.Encoder;
import org.apache.avro.io.EncoderFactory;
-import org.junit.Assert;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.Parameterized;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
import static org.apache.avro.TestSchemas.ENUM1_AB_SCHEMA_NAMESPACE_1;
import static org.apache.avro.TestSchemas.ENUM1_AB_SCHEMA_NAMESPACE_2;
-@RunWith(Parameterized.class)
public class TestResolvingGrammarGenerator {
- private final Schema schema;
- private final JsonNode data;
-
- public TestResolvingGrammarGenerator(String jsonSchema, String jsonData) throws IOException {
- this.schema = new Schema.Parser().parse(jsonSchema);
- JsonFactory factory = new JsonFactory();
- ObjectMapper mapper = new ObjectMapper(factory);
-
- this.data = mapper.readTree(new StringReader(jsonData));
- }
- @Test
- public void test() throws IOException {
+ @ParameterizedTest
+ @MethodSource("data")
+ void test(Schema schema, JsonNode data) throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
EncoderFactory factory = EncoderFactory.get();
Encoder e = factory.validatingEncoder(schema, factory.binaryEncoder(baos, null));
@@ -70,7 +64,7 @@ public void test() throws IOException {
}
@Test
- public void testRecordMissingRequiredFieldError() throws Exception {
+ void recordMissingRequiredFieldError() throws Exception {
Schema schemaWithoutField = SchemaBuilder.record("MyRecord").namespace("ns").fields().name("field1").type()
.stringType().noDefault().endRecord();
Schema schemaWithField = SchemaBuilder.record("MyRecord").namespace("ns").fields().name("field1").type()
@@ -79,15 +73,15 @@ public void testRecordMissingRequiredFieldError() throws Exception {
byte[] data = writeRecord(schemaWithoutField, record);
try {
readRecord(schemaWithField, data);
- Assert.fail("Expected exception not thrown");
+ Assertions.fail("Expected exception not thrown");
} catch (AvroTypeException typeException) {
- Assert.assertEquals("Incorrect exception message",
- "Found ns.MyRecord, expecting ns.MyRecord, missing required field field2", typeException.getMessage());
+ Assertions.assertEquals("Found ns.MyRecord, expecting ns.MyRecord, missing required field field2",
+ typeException.getMessage(), "Incorrect exception message");
}
}
@Test
- public void testDifferingEnumNamespaces() throws Exception {
+ void differingEnumNamespaces() throws Exception {
Schema schema1 = SchemaBuilder.record("MyRecord").fields().name("field").type(ENUM1_AB_SCHEMA_NAMESPACE_1)
.noDefault().endRecord();
Schema schema2 = SchemaBuilder.record("MyRecord").fields().name("field").type(ENUM1_AB_SCHEMA_NAMESPACE_2)
@@ -95,24 +89,35 @@ public void testDifferingEnumNamespaces() throws Exception {
GenericData.EnumSymbol genericEnumSymbol = new GenericData.EnumSymbol(ENUM1_AB_SCHEMA_NAMESPACE_1, "A");
GenericData.Record record = new GenericRecordBuilder(schema1).set("field", genericEnumSymbol).build();
byte[] data = writeRecord(schema1, record);
- Assert.assertEquals(genericEnumSymbol, readRecord(schema1, data).get("field"));
- Assert.assertEquals(genericEnumSymbol, readRecord(schema2, data).get("field"));
+ Assertions.assertEquals(genericEnumSymbol, readRecord(schema1, data).get("field"));
+ Assertions.assertEquals(genericEnumSymbol, readRecord(schema2, data).get("field"));
}
- @Parameterized.Parameters
- public static Collection data() {
- Collection ret = Arrays.asList(new Object[][] {
+ public static Stream data() {
+ Collection ret = Arrays.asList(new String[][] {
{ "{ \"type\": \"record\", \"name\": \"r\", \"fields\": [ " + " { \"name\" : \"f1\", \"type\": \"int\" }, "
- + " { \"name\" : \"f2\", \"type\": \"float\" } " + "] } }", "{ \"f2\": 10.4, \"f1\": 10 } " },
- { "{ \"type\": \"enum\", \"name\": \"e\", \"symbols\": " + "[ \"s1\", \"s2\"] } }", " \"s1\" " },
- { "{ \"type\": \"enum\", \"name\": \"e\", \"symbols\": " + "[ \"s1\", \"s2\"] } }", " \"s2\" " },
+ + " { \"name\" : \"f2\", \"type\": \"float\" } " + "] }", "{ \"f2\": 10.4, \"f1\": 10 } " },
+ { "{ \"type\": \"enum\", \"name\": \"e\", \"symbols\": " + "[ \"s1\", \"s2\"] }", " \"s1\" " },
+ { "{ \"type\": \"enum\", \"name\": \"e\", \"symbols\": " + "[ \"s1\", \"s2\"] }", " \"s2\" " },
{ "{ \"type\": \"fixed\", \"name\": \"f\", \"size\": 10 }", "\"hello\"" },
{ "{ \"type\": \"array\", \"items\": \"int\" }", "[ 10, 20, 30 ]" },
{ "{ \"type\": \"map\", \"values\": \"int\" }", "{ \"k1\": 10, \"k3\": 20, \"k3\": 30 }" },
{ "[ \"int\", \"long\" ]", "10" }, { "\"string\"", "\"hello\"" }, { "\"bytes\"", "\"hello\"" },
{ "\"int\"", "10" }, { "\"long\"", "10" }, { "\"float\"", "10.0" }, { "\"double\"", "10.0" },
{ "\"boolean\"", "true" }, { "\"boolean\"", "false" }, { "\"null\"", "null" }, });
- return ret;
+
+ final JsonFactory factory = new JsonFactory();
+ final ObjectMapper mapper = new ObjectMapper(factory);
+
+ return ret.stream().map((String[] args) -> {
+ Schema schema = new Schema.Parser().parse(args[0]);
+ try {
+ JsonNode data = mapper.readTree(new StringReader(args[1]));
+ return Arguments.of(schema, data);
+ } catch (IOException ex) {
+ throw new UncheckedIOException(ex);
+ }
+ });
}
private byte[] writeRecord(Schema schema, GenericData.Record record) throws Exception {
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 2915f96e6f5..5f52a2cf789 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
@@ -536,6 +536,34 @@ public static interface P2 {
void error() throws E1;
}
+ private static class NullableDefaultTest {
+ @Nullable
+ @AvroDefault("1")
+ int foo;
+ }
+
+ @Test
+ public void testAvroNullableDefault() {
+ check(NullableDefaultTest.class,
+ "{\"type\":\"record\",\"name\":\"NullableDefaultTest\","
+ + "\"namespace\":\"org.apache.avro.reflect.TestReflect\",\"fields\":["
+ + "{\"name\":\"foo\",\"type\":[\"null\",\"int\"],\"default\":1}]}");
+ }
+
+ private static class UnionDefaultTest {
+ @Union({ Integer.class, String.class })
+ @AvroDefault("1")
+ Object foo;
+ }
+
+ @Test
+ public void testAvroUnionDefault() {
+ check(UnionDefaultTest.class,
+ "{\"type\":\"record\",\"name\":\"UnionDefaultTest\","
+ + "\"namespace\":\"org.apache.avro.reflect.TestReflect\",\"fields\":["
+ + "{\"name\":\"foo\",\"type\":[\"int\",\"string\"],\"default\":1}]}");
+ }
+
@Test
void p2() throws Exception {
Schema e1 = ReflectData.get().getSchema(E1.class);
@@ -977,7 +1005,8 @@ public static interface C {
void forwardReference() {
ReflectData data = ReflectData.get();
Protocol reflected = data.getProtocol(C.class);
- Protocol reparsed = Protocol.parse(reflected.toString());
+ String ref = reflected.toString();
+ Protocol reparsed = Protocol.parse(ref);
assertEquals(reflected, reparsed);
assert (reparsed.getTypes().contains(data.getSchema(A.class)));
assert (reparsed.getTypes().contains(data.getSchema(B1.class)));
@@ -1231,7 +1260,7 @@ private static class Z {
@Test
void dollarTerminatedNamespaceCompatibility() {
ReflectData data = ReflectData.get();
- Schema s = new Schema.Parser().setValidate(false).parse(
+ Schema s = new Schema.Parser(Schema.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\":[]}");
@@ -1241,7 +1270,7 @@ void dollarTerminatedNamespaceCompatibility() {
void dollarTerminatedNestedStaticClassNamespaceCompatibility() {
ReflectData data = ReflectData.get();
// Older versions of Avro generated this namespace on nested records.
- Schema s = new Schema.Parser().setValidate(false).parse(
+ Schema s = new Schema.Parser(Schema.NameValidator.NO_VALIDATION).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"));
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 f25f022aab3..485a765d7a8 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
@@ -768,6 +768,6 @@ public boolean equals(Object obj) {
return false;
}
RecordWithTimestamps that = (RecordWithTimestamps) obj;
- return Objects.equals(that.localDateTime, that.localDateTime);
+ return Objects.equals(localDateTime, that.localDateTime);
}
}
diff --git a/lang/java/avro/src/test/java/org/apache/avro/specific/TestSpecificRecordWithUnion.java b/lang/java/avro/src/test/java/org/apache/avro/specific/TestSpecificRecordWithUnion.java
index c3b330b28c1..e64b3f4c220 100644
--- a/lang/java/avro/src/test/java/org/apache/avro/specific/TestSpecificRecordWithUnion.java
+++ b/lang/java/avro/src/test/java/org/apache/avro/specific/TestSpecificRecordWithUnion.java
@@ -29,7 +29,8 @@
import org.apache.avro.io.DatumWriter;
import org.apache.avro.io.BinaryEncoder;
import org.apache.avro.io.Decoder;
-import org.junit.Test;
+
+import org.junit.jupiter.api.Test;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
@@ -40,7 +41,7 @@
public class TestSpecificRecordWithUnion {
@Test
- public void testUnionLogicalDecimalConversion() throws IOException {
+ void unionLogicalDecimalConversion() throws IOException {
final TestUnionRecord record = TestUnionRecord.newBuilder().setAmount(BigDecimal.ZERO).build();
final Schema schema = SchemaBuilder.unionOf().nullType().and().type(record.getSchema()).endUnion();
diff --git a/lang/java/avro/src/test/java/org/apache/avro/util/TestUtf8.java b/lang/java/avro/src/test/java/org/apache/avro/util/TestUtf8.java
index c7605770e6f..e0977ff9f96 100644
--- a/lang/java/avro/src/test/java/org/apache/avro/util/TestUtf8.java
+++ b/lang/java/avro/src/test/java/org/apache/avro/util/TestUtf8.java
@@ -28,6 +28,8 @@
import java.io.ObjectOutputStream;
import java.nio.charset.StandardCharsets;
+import org.apache.avro.SystemLimitException;
+import org.apache.avro.TestSystemLimitException;
import org.junit.jupiter.api.Test;
public class TestUtf8 {
@@ -96,6 +98,26 @@ void hashCodeReused() {
assertEquals(3198781, u.hashCode());
}
+ @Test
+ void oversizeUtf8() {
+ Utf8 u = new Utf8();
+ u.setByteLength(1024);
+ assertEquals(1024, u.getByteLength());
+ assertThrows(UnsupportedOperationException.class,
+ () -> u.setByteLength(TestSystemLimitException.MAX_ARRAY_VM_LIMIT + 1));
+
+ try {
+ System.setProperty(SystemLimitException.MAX_STRING_LENGTH_PROPERTY, Long.toString(1000L));
+ TestSystemLimitException.resetLimits();
+
+ Exception ex = assertThrows(SystemLimitException.class, () -> u.setByteLength(1024));
+ assertEquals("String length 1024 exceeds maximum allowed", ex.getMessage());
+ } finally {
+ System.clearProperty(SystemLimitException.MAX_STRING_LENGTH_PROPERTY);
+ TestSystemLimitException.resetLimits();
+ }
+ }
+
@Test
void serialization() throws IOException, ClassNotFoundException {
try (ByteArrayOutputStream bos = new ByteArrayOutputStream();
diff --git a/lang/java/avro/src/test/java/org/apache/avro/util/WeakIdentityHashMapTest.java b/lang/java/avro/src/test/java/org/apache/avro/util/WeakIdentityHashMapTest.java
new file mode 100644
index 00000000000..8ecda312032
--- /dev/null
+++ b/lang/java/avro/src/test/java/org/apache/avro/util/WeakIdentityHashMapTest.java
@@ -0,0 +1,89 @@
+/*
+ * 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.util;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * This test aims to stress WeakIdentityHashMap class in multithread env.
+ */
+class WeakIdentityHashMapTest {
+
+ private static final int TEST_SIZE = 4001;
+
+ List data = new ArrayList<>(TEST_SIZE);
+
+ final WeakIdentityHashMap map = new WeakIdentityHashMap<>();
+
+ List exceptions = new ArrayList<>(TEST_SIZE);
+
+ @Test
+ void stressMap() {
+
+ for (int i = 1; i <= TEST_SIZE; i++) {
+ data.add("Data_" + i);
+ }
+
+ List threads = new ArrayList<>(80);
+ for (int i = 0; i <= 80; i++) {
+ final int seed = (i + 1) * 100;
+ Runnable runnable = () -> rundata(seed);
+ Thread t = new Thread(runnable);
+ threads.add(t);
+ }
+ threads.forEach(Thread::start);
+ threads.forEach((Thread t) -> {
+ try {
+ t.join();
+ } catch (InterruptedException e) {
+ throw new RuntimeException(e);
+ }
+ });
+ Assertions.assertTrue(exceptions.isEmpty());
+ }
+
+ void rundata(int seed) {
+ try {
+ for (int i = 1; i <= TEST_SIZE; i++) {
+ String keyValue = data.get((i + seed) % TEST_SIZE);
+ map.put(keyValue, keyValue);
+ if (i % 200 == 0) {
+ sleep();
+ }
+ String keyValueRemove = data.get(((i + seed) * 3) % TEST_SIZE);
+ map.remove(keyValueRemove);
+ }
+ } catch (RuntimeException ex) {
+ exceptions.add(ex);
+ }
+ }
+
+ void sleep() {
+ try {
+ Thread.sleep(5);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+ }
+
+}
diff --git a/lang/java/avro/src/test/resources/META-INF/services/org.apache.avro.Conversion b/lang/java/avro/src/test/resources/META-INF/services/org.apache.avro.Conversion
new file mode 100644
index 00000000000..890ba764260
--- /dev/null
+++ b/lang/java/avro/src/test/resources/META-INF/services/org.apache.avro.Conversion
@@ -0,0 +1,17 @@
+# 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.
+
+org.apache.avro.CustomTypeConverter
diff --git a/lang/java/avro/src/test/resources/META-INF/services/org.apache.avro.LogicalTypes$LogicalTypeFactory b/lang/java/avro/src/test/resources/META-INF/services/org.apache.avro.LogicalTypes$LogicalTypeFactory
index e111a25c43f..b55c233ae46 100644
--- a/lang/java/avro/src/test/resources/META-INF/services/org.apache.avro.LogicalTypes$LogicalTypeFactory
+++ b/lang/java/avro/src/test/resources/META-INF/services/org.apache.avro.LogicalTypes$LogicalTypeFactory
@@ -14,4 +14,4 @@
# See the License for the specific language governing permissions and
# limitations under the License.
-org.apache.avro.DummyLogicalTypeFactory
+org.apache.avro.CustomTypeLogicalTypeFactory
diff --git a/lang/java/avro/src/test/resources/multipleFile/ApplicationEvent.avsc b/lang/java/avro/src/test/resources/multipleFile/ApplicationEvent.avsc
new file mode 100644
index 00000000000..6902084350f
--- /dev/null
+++ b/lang/java/avro/src/test/resources/multipleFile/ApplicationEvent.avsc
@@ -0,0 +1,28 @@
+{
+ "namespace": "model",
+ "type": "record",
+ "doc": "",
+ "name": "ApplicationEvent",
+ "fields": [
+ {
+ "name": "applicationId",
+ "type": "string",
+ "doc": "Application ID"
+ },
+ {
+ "name": "status",
+ "type": "string",
+ "doc": "Application Status"
+ },
+ {
+ "name": "documents",
+ "type": ["null", {
+ "type": "array",
+ "items": "model.DocumentInfo"
+ }],
+ "doc": "",
+ "default": null
+ }
+ ]
+
+}
diff --git a/lang/java/avro/src/test/resources/multipleFile/DocumentInfo.avsc b/lang/java/avro/src/test/resources/multipleFile/DocumentInfo.avsc
new file mode 100644
index 00000000000..95dd4243ea6
--- /dev/null
+++ b/lang/java/avro/src/test/resources/multipleFile/DocumentInfo.avsc
@@ -0,0 +1,19 @@
+{
+ "namespace": "model",
+ "type": "record",
+ "doc": "",
+ "name": "DocumentInfo",
+ "fields": [
+ {
+ "name": "documentId",
+ "type": "string",
+ "doc": "Document ID"
+ },
+ {
+ "name": "filePath",
+ "type": "string",
+ "doc": "Document Path"
+ }
+ ]
+
+}
diff --git a/lang/java/avro/src/test/resources/multipleFile/MyResponse.avsc b/lang/java/avro/src/test/resources/multipleFile/MyResponse.avsc
new file mode 100644
index 00000000000..ac6d08291d9
--- /dev/null
+++ b/lang/java/avro/src/test/resources/multipleFile/MyResponse.avsc
@@ -0,0 +1,14 @@
+{
+ "namespace": "model",
+ "type": "record",
+ "doc": "",
+ "name": "MyResponse",
+ "fields": [
+ {
+ "name": "isSuccessful",
+ "type": "boolean",
+ "doc": "Indicator for successful or unsuccessful call"
+ }
+ ]
+
+}
diff --git a/lang/java/avro/src/test/resources/multipleFile/README.md b/lang/java/avro/src/test/resources/multipleFile/README.md
new file mode 100644
index 00000000000..fe3541b660e
--- /dev/null
+++ b/lang/java/avro/src/test/resources/multipleFile/README.md
@@ -0,0 +1,8 @@
+## test for parsing multiple files.
+This folder aims to test `public List Schema.parse(Iterable sources) throws IOException` method.
+
+The objective is to check that a record schema define in a file can be use in another record schema as a field type.
+Here, ApplicationEvent.avsc file contains a field of type DocumentInfo, defined in file DocumentInfo.avsc.
+
+The is written at TestSchema.testParseMultipleFile.
+
diff --git a/lang/java/compiler/pom.xml b/lang/java/compiler/pom.xml
index 53719816387..2019318ee97 100644
--- a/lang/java/compiler/pom.xml
+++ b/lang/java/compiler/pom.xml
@@ -137,6 +137,7 @@
org.apache.avro.compiler.specific.SchemaTask
${project.basedir}/src/test/resources/full_record_v1.avsc
${project.basedir}/src/test/resources/full_record_v2.avsc
+ ${project.basedir}/src/test/resources/regression_error_field_in_record.avsc
${project.basedir}/target/generated-test-sources/javacc
@@ -182,37 +183,8 @@
-
-
-
- org.eclipse.m2e
- lifecycle-mapping
- 1.0.0
-
-
-
-
-
- org.codehaus.mojo
- exec-maven-plugin
- [1.0,)
-
- exec
-
-
-
-
-
-
-
-
-
-
-
-
-
${project.groupId}
@@ -241,4 +213,42 @@
+
+
+ m2e
+
+ m2e.version
+
+
+
+
+
+ org.eclipse.m2e
+ lifecycle-mapping
+ 1.0.0
+
+
+
+
+
+ org.codehaus.mojo
+ exec-maven-plugin
+ [1.0,)
+
+ exec
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/lang/java/compiler/src/main/java/org/apache/avro/compiler/idl/SchemaResolver.java b/lang/java/compiler/src/main/java/org/apache/avro/compiler/idl/SchemaResolver.java
index 193f871178b..6a1a137898d 100644
--- a/lang/java/compiler/src/main/java/org/apache/avro/compiler/idl/SchemaResolver.java
+++ b/lang/java/compiler/src/main/java/org/apache/avro/compiler/idl/SchemaResolver.java
@@ -23,6 +23,7 @@
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
+import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Function;
import org.apache.avro.Protocol;
@@ -44,6 +45,8 @@ private SchemaResolver() {
private static final String UR_SCHEMA_NS = "org.apache.avro.compiler";
+ private static final AtomicInteger COUNTER = new AtomicInteger();
+
/**
* Create a schema to represent a "unresolved" schema. (used to represent a
* schema where the definition is not known at the time) This concept might be
@@ -53,8 +56,8 @@ private SchemaResolver() {
* @return
*/
static Schema unresolvedSchema(final String name) {
- Schema schema = Schema.createRecord(UR_SCHEMA_NAME, "unresolved schema", UR_SCHEMA_NS, false,
- Collections.EMPTY_LIST);
+ Schema schema = Schema.createRecord(UR_SCHEMA_NAME + '_' + COUNTER.getAndIncrement(), "unresolved schema",
+ UR_SCHEMA_NS, false, Collections.EMPTY_LIST);
schema.addProp(UR_SCHEMA_ATTR, name);
return schema;
}
@@ -66,8 +69,8 @@ static Schema unresolvedSchema(final String name) {
* @return
*/
static boolean isUnresolvedSchema(final Schema schema) {
- return (schema.getType() == Schema.Type.RECORD && schema.getProp(UR_SCHEMA_ATTR) != null
- && UR_SCHEMA_NAME.equals(schema.getName()) && UR_SCHEMA_NS.equals(schema.getNamespace()));
+ return (schema.getType() == Schema.Type.RECORD && schema.getProp(UR_SCHEMA_ATTR) != null && schema.getName() != null
+ && schema.getName().startsWith(UR_SCHEMA_NAME) && UR_SCHEMA_NS.equals(schema.getNamespace()));
}
/**
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 b35adbd9313..ec8ff778983 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
@@ -21,7 +21,6 @@
import java.util.Collections;
import java.util.Deque;
import java.util.IdentityHashMap;
-import java.util.Map;
import java.util.Set;
import java.util.function.Supplier;
import java.util.stream.Collectors;
@@ -67,10 +66,7 @@ public static void copyLogicalTypes(final Schema from, final Schema to) {
}
public static void copyProperties(final JsonProperties from, final JsonProperties to) {
- Map objectProps = from.getObjectProps();
- for (Map.Entry entry : objectProps.entrySet()) {
- to.addProp(entry.getKey(), entry.getValue());
- }
+ from.forEachProperty(to::addProp);
}
public static boolean hasGeneratedJavaClass(final Schema schema) {
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 3210bd11c58..117fd2ed6ee 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
@@ -129,6 +129,10 @@ void addLogicalTypeConversions(SpecificData specificData) {
private String suffix = ".java";
private List additionalVelocityTools = Collections.emptyList();
+ private String recordSpecificClass = "org.apache.avro.specific.SpecificRecordBase";
+
+ private String errorSpecificClass = "org.apache.avro.specific.SpecificExceptionBase";
+
/*
* Used in the record.vm template.
*/
@@ -175,8 +179,20 @@ public SpecificCompiler(Protocol protocol) {
}
public SpecificCompiler(Schema schema) {
+ this(Collections.singleton(schema));
+ }
+
+ public SpecificCompiler(Collection schemas) {
+ this();
+ for (Schema schema : schemas) {
+ enqueue(schema);
+ }
+ this.protocol = null;
+ }
+
+ public SpecificCompiler(Iterable schemas) {
this();
- enqueue(schema);
+ schemas.forEach(this::enqueue);
this.protocol = null;
}
@@ -676,9 +692,7 @@ private Protocol addStringType(Protocol p) {
Protocol newP = new Protocol(p.getName(), p.getDoc(), p.getNamespace());
Map types = new LinkedHashMap<>();
- for (Map.Entry a : p.getObjectProps().entrySet()) {
- newP.addProp(a.getKey(), a.getValue());
- }
+ p.forEachProperty(newP::addProp);
// annotate types
Collection namedTypes = new LinkedHashSet<>();
@@ -956,19 +970,21 @@ public int getNonNullIndex(Schema s) {
* record.vm can handle the schema being presented.
*/
public boolean isCustomCodable(Schema schema) {
- if (schema.isError())
- return false;
return isCustomCodable(schema, new HashSet<>());
}
private boolean isCustomCodable(Schema schema, Set seen) {
if (!seen.add(schema))
+ // Recursive call: assume custom codable until a caller on the call stack proves
+ // otherwise.
return true;
if (schema.getLogicalType() != null)
return false;
boolean result = true;
switch (schema.getType()) {
case RECORD:
+ if (schema.isError())
+ return false;
for (Schema.Field f : schema.getFields())
result &= isCustomCodable(f.schema(), seen);
break;
@@ -1071,7 +1087,7 @@ public static String javaEscape(String o) {
* Utility for template use. Escapes comment end with HTML entities.
*/
public static String escapeForJavadoc(String s) {
- return s.replace("*/", "*/");
+ return s.replace("*/", "*/").replace("<", "<").replace(">", ">");
}
/**
@@ -1272,10 +1288,7 @@ private static String generateMethodName(Schema schema, Field field, String pref
// Check for the special case in which the schema defines two fields whose
// names are identical except for the case of the first character:
- char firstChar = field.name().charAt(0);
- String conflictingFieldName = (Character.isLowerCase(firstChar) ? Character.toUpperCase(firstChar)
- : Character.toLowerCase(firstChar)) + (field.name().length() > 1 ? field.name().substring(1) : "");
- boolean fieldNameConflict = schema.getField(conflictingFieldName) != null;
+ 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,
@@ -1295,16 +1308,75 @@ private static String generateMethodName(Schema schema, Field field, String pref
methodBuilder.append(postfix);
// If there is a field name conflict append $0 or $1
- if (fieldNameConflict) {
+ if (indexNameConflict >= 0) {
if (methodBuilder.charAt(methodBuilder.length() - 1) != '$') {
methodBuilder.append('$');
}
- methodBuilder.append(Character.isLowerCase(firstChar) ? '0' : '1');
+ methodBuilder.append(indexNameConflict);
}
return methodBuilder.toString();
}
+ /**
+ * Calc name index for getter / setter field in case of conflict as example,
+ * having a schema with fields __X, _X, _x, X, x should result with indexes __X:
+ * 3, _X: 2, _x: 1, X: 0 x: None (-1)
+ *
+ * @param fieldName : field name.
+ * @param schema : schema.
+ * @return index for field.
+ */
+ private static int calcNameIndex(String fieldName, Schema schema) {
+ // get name without underscore at start
+ // and calc number of other similar fields with same subname.
+ int countSimilar = 0;
+ String pureFieldName = fieldName;
+ while (!pureFieldName.isEmpty() && pureFieldName.charAt(0) == '_') {
+ pureFieldName = pureFieldName.substring(1);
+ if (schema.getField(pureFieldName) != null) {
+ countSimilar++;
+ }
+ String reversed = reverseFirstLetter(pureFieldName);
+ if (schema.getField(reversed) != null) {
+ countSimilar++;
+ }
+ }
+ // field name start with upper have +1
+ String reversed = reverseFirstLetter(fieldName);
+ if (!pureFieldName.isEmpty() && Character.isUpperCase(pureFieldName.charAt(0))
+ && schema.getField(reversed) != null) {
+ countSimilar++;
+ }
+
+ int ret = -1; // if no similar name, no index.
+ if (countSimilar > 0) {
+ ret = countSimilar - 1; // index is count similar -1 (start with $0)
+ }
+
+ return ret;
+ }
+
+ /**
+ * Reverse first letter upper <=> lower. __Name <=> __name
+ *
+ * @param name : input name.
+ * @return name with change case of first letter.
+ */
+ private static String reverseFirstLetter(String name) {
+ StringBuilder builder = new StringBuilder(name);
+ int index = 0;
+ while (builder.length() > index && builder.charAt(index) == '_') {
+ index++;
+ }
+ if (builder.length() > index) {
+ char c = builder.charAt(index);
+ char inverseC = Character.isLowerCase(c) ? Character.toUpperCase(c) : Character.toLowerCase(c);
+ builder.setCharAt(index, inverseC);
+ }
+ return builder.toString();
+ }
+
/**
* Tests whether an unboxed Java type can be set to null
*/
@@ -1336,4 +1408,20 @@ public static void main(String[] args) throws Exception {
public void setOutputCharacterEncoding(String outputCharacterEncoding) {
this.outputCharacterEncoding = outputCharacterEncoding;
}
+
+ public String getSchemaParentClass(boolean isError) {
+ if (isError) {
+ return this.errorSpecificClass;
+ } else {
+ return this.recordSpecificClass;
+ }
+ }
+
+ public void setRecordSpecificClass(final String recordSpecificClass) {
+ this.recordSpecificClass = recordSpecificClass;
+ }
+
+ public void setErrorSpecificClass(final String errorSpecificClass) {
+ this.errorSpecificClass = errorSpecificClass;
+ }
}
diff --git a/lang/java/compiler/src/main/javacc/org/apache/avro/compiler/idl/idl.jj b/lang/java/compiler/src/main/javacc/org/apache/avro/compiler/idl/idl.jj
index 4deaa68a5a7..117764497e3 100644
--- a/lang/java/compiler/src/main/javacc/org/apache/avro/compiler/idl/idl.jj
+++ b/lang/java/compiler/src/main/javacc/org/apache/avro/compiler/idl/idl.jj
@@ -88,7 +88,10 @@ import org.apache.commons.text.StringEscapeUtils;
*
* Note: each instance is not thread-safe, but multiple separate
* instances are safely independent.
+ *
+ * @deprecated Use the new org.apache.avro.idl.IdlReader from avro-idl instead.
*/
+@Deprecated
public class Idl implements Closeable {
static JsonNodeFactory FACTORY = JsonNodeFactory.instance;
private static final String OPTIONAL_NULLABLE_TYPE_PROPERTY = "org.apache.avro.compiler.idl.Idl.NullableType.optional";
@@ -1284,7 +1287,7 @@ Schema ImportSchema() : {
{
try (InputStream stream=findFile(importFile).openStream()){
Parser parser = new Schema.Parser();
- parser.addTypes(names); // inherit names
+ parser.addTypes(names.values()); // inherit names
Schema value = parser.parse(stream);
names = parser.getTypes(); // update names
return value;
diff --git a/lang/java/compiler/src/main/velocity/org/apache/avro/compiler/specific/templates/java/classic/record.vm b/lang/java/compiler/src/main/velocity/org/apache/avro/compiler/specific/templates/java/classic/record.vm
index d5dd7831e1f..2e3bb075961 100755
--- a/lang/java/compiler/src/main/velocity/org/apache/avro/compiler/specific/templates/java/classic/record.vm
+++ b/lang/java/compiler/src/main/velocity/org/apache/avro/compiler/specific/templates/java/classic/record.vm
@@ -36,7 +36,7 @@ import org.apache.avro.message.SchemaStore;
@$annotation
#end
@org.apache.avro.specific.AvroGenerated
-public class ${this.mangleTypeIdentifier($schema.getName())}#if ($schema.isError()) extends org.apache.avro.specific.SpecificExceptionBase#else extends org.apache.avro.specific.SpecificRecordBase#end implements org.apache.avro.specific.SpecificRecord {
+public class ${this.mangleTypeIdentifier($schema.getName())} extends ${this.getSchemaParentClass($schema.isError())} implements org.apache.avro.specific.SpecificRecord {
private static final long serialVersionUID = ${this.fingerprint64($schema)}L;
#set ($schemaString = $this.javaSplit($schema.toString()))
@@ -228,10 +228,10 @@ public class ${this.mangleTypeIdentifier($schema.getName())}#if ($schema.isError
#foreach ($field in $schema.getFields())
#if (${this.gettersReturnOptional} && (!${this.optionalGettersForNullableFieldsOnly} || ${field.schema().isNullable()}))
/**
- * Gets the value of the '${this.mangle($field.name(), $schema.isError())}' field as an Optional<${this.javaType($field.schema())}>.
+ * Gets the value of the '${this.mangle($field.name(), $schema.isError())}' field as an Optional<${this.escapeForJavadoc(${this.javaType($field.schema())})}>.
#if ($field.doc()) * $field.doc()
#end
- * @return The value wrapped in an Optional<${this.javaType($field.schema())}>.
+ * @return The value wrapped in an Optional<${this.escapeForJavadoc(${this.javaType($field.schema())})}>.
*/
public Optional<${this.javaType($field.schema())}> ${this.generateGetMethod($schema, $field)}() {
return Optional.<${this.javaType($field.schema())}>ofNullable(${this.mangle($field.name(), $schema.isError())});
@@ -257,10 +257,10 @@ public class ${this.mangleTypeIdentifier($schema.getName())}#if ($schema.isError
#if (${this.createOptionalGetters})
/**
- * Gets the value of the '${this.mangle($field.name(), $schema.isError())}' field as an Optional<${this.javaType($field.schema())}>.
+ * Gets the value of the '${this.mangle($field.name(), $schema.isError())}' field as an Optional<${this.escapeForJavadoc(${this.javaType($field.schema())})}>.
#if ($field.doc()) * $field.doc()
#end
- * @return The value wrapped in an Optional<${this.javaType($field.schema())}>.
+ * @return The value wrapped in an Optional<${this.escapeForJavadoc(${this.javaType($field.schema())})}>.
*/
public Optional<${this.javaType($field.schema())}> ${this.generateGetOptionalMethod($schema, $field)}() {
return Optional.<${this.javaType($field.schema())}>ofNullable(${this.mangle($field.name(), $schema.isError())});
@@ -413,10 +413,10 @@ public class ${this.mangleTypeIdentifier($schema.getName())}#if ($schema.isError
#if (${this.createOptionalGetters})
/**
- * Gets the value of the '${this.mangle($field.name(), $schema.isError())}' field as an Optional<${this.javaType($field.schema())}>.
+ * Gets the value of the '${this.mangle($field.name(), $schema.isError())}' field as an Optional<${this.escapeForJavadoc(${this.javaType($field.schema())})}>.
#if ($field.doc()) * $field.doc()
#end
- * @return The value wrapped in an Optional<${this.javaType($field.schema())}>.
+ * @return The value wrapped in an Optional<${this.escapeForJavadoc(${this.javaType($field.schema())})}>.
*/
public Optional<${this.javaType($field.schema())}> ${this.generateGetOptionalMethod($schema, $field)}() {
return Optional.<${this.javaType($field.schema())}>ofNullable(${this.mangle($field.name(), $schema.isError())});
@@ -844,7 +844,7 @@ $I }
$I long size${nv} = in.readMapStart();
$I $t m${nv} = ${var}; // Need fresh name due to limitation of macro system
$I if (m${nv} == null) {
-$I m${nv} = new java.util.HashMap<${kt},${vt}>((int)size${nv});
+$I m${nv} = new java.util.HashMap<${kt},${vt}>((int)(size${nv} * 4)/3 + 1);
$I $var = m${nv};
$I } else m${nv}.clear();
$I for ( ; 0 < size${nv}; size${nv} = in.mapNext()) {
diff --git a/lang/java/compiler/src/test/idl/input/bar.avpr b/lang/java/compiler/src/test/idl/input/bar.avpr
index 5e9b194a060..ea8b0d4befb 100644
--- a/lang/java/compiler/src/test/idl/input/bar.avpr
+++ b/lang/java/compiler/src/test/idl/input/bar.avpr
@@ -1,2 +1,3 @@
{"protocol": "org.foo.Bar",
- "messages": { "bar": {"request": [], "response": "null"}}}
+ "types": [{"name": "AorB", "type": "enum", "symbols": ["A", "B"], "default": "A"}],
+ "messages": { "bar": {"request": [{"name": "choice", "type": "AorB"}],"response": "null"}}}
diff --git a/lang/java/compiler/src/test/idl/input/union.avdl b/lang/java/compiler/src/test/idl/input/union.avdl
new file mode 100644
index 00000000000..19f37f2f748
--- /dev/null
+++ b/lang/java/compiler/src/test/idl/input/union.avdl
@@ -0,0 +1,16 @@
+@namespace("org.apache.avro.gen")
+protocol UnionFwd {
+
+ record TestRecord {
+ union {SR1, SR2} unionField;
+ }
+
+ record SR1 {
+ string field;
+ }
+
+ record SR2 {
+ string field;
+ }
+
+}
diff --git a/lang/java/compiler/src/test/idl/output/import.avpr b/lang/java/compiler/src/test/idl/output/import.avpr
index e6701ad94e1..b0093492d95 100644
--- a/lang/java/compiler/src/test/idl/output/import.avpr
+++ b/lang/java/compiler/src/test/idl/output/import.avpr
@@ -3,6 +3,11 @@
"namespace" : "org.foo",
"doc" : "Licensed to the Apache Software Foundation (ASF) under one\nor more contributor license agreements. See the NOTICE file\ndistributed with this work for additional information\nregarding copyright ownership. The ASF licenses this file\nto you under the Apache License, Version 2.0 (the\n\"License\"); you may not use this file except in compliance\nwith the License. You may obtain a copy of the License at\n\n https://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.",
"types" : [ {
+ "type" : "enum",
+ "name" : "AorB",
+ "symbols" : ["A", "B"],
+ "default" : "A"
+ }, {
"type" : "enum",
"name" : "Position",
"namespace" : "avro.examples.baseball",
@@ -111,7 +116,10 @@
"response" : "null"
},
"bar" : {
- "request" : [ ],
+ "request" : [ {
+ "name" : "choice",
+ "type" : "AorB"
+ } ],
"response" : "null"
},
"bazm" : {
diff --git a/lang/java/compiler/src/test/idl/output/nestedimport.avpr b/lang/java/compiler/src/test/idl/output/nestedimport.avpr
index 80273627109..f1060b0d743 100644
--- a/lang/java/compiler/src/test/idl/output/nestedimport.avpr
+++ b/lang/java/compiler/src/test/idl/output/nestedimport.avpr
@@ -4,6 +4,12 @@
"doc" : "Licensed to the Apache Software Foundation (ASF) under one\nor more contributor license agreements. See the NOTICE file\ndistributed with this work for additional information\nregarding copyright ownership. The ASF licenses this file\nto you under the Apache License, Version 2.0 (the\n\"License\"); you may not use this file except in compliance\nwith the License. You may obtain a copy of the License at\n\n https://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.",
"version" : "1.0.5",
"types" : [ {
+ "type" : "enum",
+ "name" : "AorB",
+ "namespace" : "org.foo",
+ "symbols" : ["A", "B"],
+ "default" : "A"
+ }, {
"type" : "enum",
"name" : "Position",
"namespace" : "avro.examples.baseball",
@@ -66,7 +72,10 @@
"response" : "null"
},
"bar" : {
- "request" : [ ],
+ "request" : [ {
+ "name" : "choice",
+ "type" : "org.foo.AorB"
+ } ],
"response" : "null"
}
}
diff --git a/lang/java/compiler/src/test/idl/output/union.avpr b/lang/java/compiler/src/test/idl/output/union.avpr
new file mode 100644
index 00000000000..61748d179e3
--- /dev/null
+++ b/lang/java/compiler/src/test/idl/output/union.avpr
@@ -0,0 +1,38 @@
+{
+ "protocol": "UnionFwd",
+ "namespace": "org.apache.avro.gen",
+ "types": [
+ {
+ "type": "record",
+ "name": "TestRecord",
+ "fields": [
+ {
+ "name": "unionField",
+ "type": [
+ {
+ "type": "record",
+ "name": "SR1",
+ "fields": [
+ {
+ "name": "field",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "type": "record",
+ "name": "SR2",
+ "fields": [
+ {
+ "name": "field",
+ "type": "string"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ],
+ "messages": {}
+}
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 05bc7b2f531..cc3fcd31240 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
@@ -929,4 +929,59 @@ public LogicalType fromSchema(Schema schema) {
}
}
+ @Test
+ void fieldWithUnderscore_avro3826() {
+ String jsonSchema = "{\n" + " \"name\": \"Value\",\n" + " \"type\": \"record\",\n" + " \"fields\": [\n"
+ + " { \"name\": \"__deleted\", \"type\": \"string\"\n" + " }\n" + " ]\n" + "}";
+ Collection outputs = new SpecificCompiler(new Schema.Parser().parse(jsonSchema))
+ .compile();
+ assertEquals(1, outputs.size());
+ SpecificCompiler.OutputFile outputFile = outputs.iterator().next();
+ assertTrue(outputFile.contents.contains("getDeleted()"));
+ assertFalse(outputFile.contents.contains("$0"));
+ assertFalse(outputFile.contents.contains("$1"));
+
+ String jsonSchema2 = "{\n" + " \"name\": \"Value\", \"type\": \"record\",\n" + " \"fields\": [\n"
+ + " { \"name\": \"__deleted\", \"type\": \"string\"},\n"
+ + " { \"name\": \"_deleted\", \"type\": \"string\"}\n" + " ]\n" + "}";
+ Collection outputs2 = new SpecificCompiler(new Schema.Parser().parse(jsonSchema2))
+ .compile();
+ assertEquals(1, outputs2.size());
+ SpecificCompiler.OutputFile outputFile2 = outputs2.iterator().next();
+
+ assertTrue(outputFile2.contents.contains("getDeleted()"));
+ assertTrue(outputFile2.contents.contains("getDeleted$0()"));
+ assertFalse(outputFile.contents.contains("$1"));
+
+ String jsonSchema3 = "{\n" + " \"name\": \"Value\", \"type\": \"record\",\n" + " \"fields\": [\n"
+ + " { \"name\": \"__deleted\", \"type\": \"string\"},\n"
+ + " { \"name\": \"_deleted\", \"type\": \"string\"},\n"
+ + " { \"name\": \"deleted\", \"type\": \"string\"}\n" + " ]\n" + "}";
+ Collection outputs3 = new SpecificCompiler(new Schema.Parser().parse(jsonSchema3))
+ .compile();
+ assertEquals(1, outputs3.size());
+ SpecificCompiler.OutputFile outputFile3 = outputs3.iterator().next();
+
+ assertTrue(outputFile3.contents.contains("getDeleted()"));
+ assertTrue(outputFile3.contents.contains("getDeleted$0()"));
+ assertTrue(outputFile3.contents.contains("getDeleted$1()"));
+ assertFalse(outputFile3.contents.contains("$2"));
+
+ String jsonSchema4 = "{\n" + " \"name\": \"Value\", \"type\": \"record\",\n" + " \"fields\": [\n"
+ + " { \"name\": \"__deleted\", \"type\": \"string\"},\n"
+ + " { \"name\": \"_deleted\", \"type\": \"string\"},\n"
+ + " { \"name\": \"deleted\", \"type\": \"string\"},\n"
+ + " { \"name\": \"Deleted\", \"type\": \"string\"}\n" + " ]\n" + "}";
+ Collection outputs4 = new SpecificCompiler(new Schema.Parser().parse(jsonSchema4))
+ .compile();
+ assertEquals(1, outputs4.size());
+ SpecificCompiler.OutputFile outputFile4 = outputs4.iterator().next();
+
+ assertTrue(outputFile4.contents.contains("getDeleted()"));
+ assertTrue(outputFile4.contents.contains("getDeleted$0()"));
+ assertTrue(outputFile4.contents.contains("getDeleted$1()"));
+ assertTrue(outputFile4.contents.contains("getDeleted$2()"));
+ assertFalse(outputFile4.contents.contains("$3"));
+ }
+
}
diff --git a/lang/java/compiler/src/test/java/org/apache/avro/specific/TestGeneratedCode.java b/lang/java/compiler/src/test/java/org/apache/avro/specific/TestGeneratedCode.java
index e541a244b7a..9a334a45ab0 100644
--- a/lang/java/compiler/src/test/java/org/apache/avro/specific/TestGeneratedCode.java
+++ b/lang/java/compiler/src/test/java/org/apache/avro/specific/TestGeneratedCode.java
@@ -20,6 +20,8 @@
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
+import java.util.HashMap;
+import java.util.Map;
import org.apache.avro.Schema;
import org.apache.avro.io.Encoder;
@@ -28,6 +30,8 @@
import org.apache.avro.io.DecoderFactory;
import org.apache.avro.io.DatumReader;
import org.apache.avro.io.DatumWriter;
+import org.apache.avro.specific.test.RecordWithErrorField;
+import org.apache.avro.specific.test.TestError;
import org.apache.avro.util.Utf8;
import org.junit.Assert;
@@ -71,8 +75,9 @@ void withoutSchemaMigration() throws IOException {
@Test
void withSchemaMigration() throws IOException {
+ Map map = new HashMap<>();
FullRecordV2 src = new FullRecordV2(true, 731, 87231, 38L, 54.2832F, "Hi there",
- ByteBuffer.wrap(Utf8.getBytesFor("Hello, world!")));
+ ByteBuffer.wrap(Utf8.getBytesFor("Hello, world!")), map);
assertTrue(((SpecificRecordBase) src).hasCustomCoders(), "Test schema must allow for custom coders.");
ByteArrayOutputStream out = new ByteArrayOutputStream(1024);
@@ -89,4 +94,28 @@ void withSchemaMigration() throws IOException {
FullRecordV1 expected = new FullRecordV1(true, 87231, 731L, 54.2832F, 38.0, null, "Hello, world!");
Assert.assertEquals(expected, dst);
}
+
+ @Test
+ public void withErrorField() throws IOException {
+ TestError srcError = TestError.newBuilder().setMessage$("Oops").build();
+ RecordWithErrorField src = new RecordWithErrorField("Hi there", srcError);
+ Assert.assertFalse("Test schema with error field cannot allow for custom coders.",
+ ((SpecificRecordBase) src).hasCustomCoders());
+ Schema schema = RecordWithErrorField.getClassSchema();
+
+ ByteArrayOutputStream out = new ByteArrayOutputStream(1024);
+ Encoder e = EncoderFactory.get().directBinaryEncoder(out, null);
+ DatumWriter w = (DatumWriter) MODEL.createDatumWriter(schema);
+ w.write(src, e);
+ e.flush();
+
+ ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray());
+ Decoder d = DecoderFactory.get().directBinaryDecoder(in, null);
+ DatumReader r = (DatumReader) MODEL.createDatumReader(schema);
+ RecordWithErrorField dst = r.read(null, d);
+
+ TestError expectedError = TestError.newBuilder().setMessage$("Oops").build();
+ RecordWithErrorField expected = new RecordWithErrorField("Hi there", expectedError);
+ Assert.assertEquals(expected, dst);
+ }
}
diff --git a/lang/java/compiler/src/test/resources/full_record_v2.avsc b/lang/java/compiler/src/test/resources/full_record_v2.avsc
index b80b9b4ae9d..0a033cf55be 100644
--- a/lang/java/compiler/src/test/resources/full_record_v2.avsc
+++ b/lang/java/compiler/src/test/resources/full_record_v2.avsc
@@ -24,6 +24,7 @@
}, {
"name" : "h",
"type" : "bytes"
- } ]
+ },
+ { "name" : "myMap", "type" : { "type" : "map", "values" : "string" } }]
}
diff --git a/lang/java/compiler/src/test/resources/regression_error_field_in_record.avsc b/lang/java/compiler/src/test/resources/regression_error_field_in_record.avsc
new file mode 100644
index 00000000000..e2fdcb9ad93
--- /dev/null
+++ b/lang/java/compiler/src/test/resources/regression_error_field_in_record.avsc
@@ -0,0 +1,22 @@
+{
+ "type" : "record",
+ "name" : "RecordWithErrorField",
+ "doc" : "With custom coders in Avro 1.9, previously successful records with error fields now fail to compile.",
+ "namespace" : "org.apache.avro.specific.test",
+ "fields" : [ {
+ "name" : "s",
+ "type" : [ "null", "string" ],
+ "default" : null
+ }, {
+ "name": "e",
+ "type": [ "null", {
+ "type" : "error",
+ "name" : "TestError",
+ "fields" : [ {
+ "name" : "message",
+ "type" : "string"
+ } ]
+ } ],
+ "default": null
+ } ]
+}
diff --git a/lang/java/grpc/pom.xml b/lang/java/grpc/pom.xml
index f04dd468fcf..d895a1ba3a1 100644
--- a/lang/java/grpc/pom.xml
+++ b/lang/java/grpc/pom.xml
@@ -24,7 +24,7 @@
org.apache.avro
avro-parent
1.12.0-SNAPSHOT
- ../
+ ../pom.xml
avro-grpc
diff --git a/lang/java/grpc/src/test/avro/TestService.avdl b/lang/java/grpc/src/test/avro/TestService.avdl
index 9a4629a8f5c..6c5f6a038b8 100644
--- a/lang/java/grpc/src/test/avro/TestService.avdl
+++ b/lang/java/grpc/src/test/avro/TestService.avdl
@@ -1,4 +1,4 @@
-/**
+/*
* 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
diff --git a/lang/java/idl/pom.xml b/lang/java/idl/pom.xml
new file mode 100644
index 00000000000..6363ec150a5
--- /dev/null
+++ b/lang/java/idl/pom.xml
@@ -0,0 +1,172 @@
+
+
+
+ 4.0.0
+
+
+ avro-parent
+ org.apache.avro
+ 1.12.0-SNAPSHOT
+ ../pom.xml
+
+
+ avro-idl
+
+ Apache Avro IDL
+ bundle
+ https://avro.apache.org
+ Compilers for Avro IDL and Avro Specific Java API
+
+
+ ${project.parent.parent.basedir}
+
+ !org.apache.avro.idl*,
+ org.apache.avro*;version="${project.version}",
+ org.apache.commons.text*,
+ *
+
+ org.apache.avro.idl*;version="${project.version}"
+ 4.9.3
+
+
+
+
+
+ src/main/resources
+
+
+
+
+ src/test/resources
+
+
+ src/test/idl
+
+
+
+
+ org.apache.maven.plugins
+ maven-jar-plugin
+
+
+
+ org.apache.avro.idl
+
+
+
+
+
+ prepare-test-jar
+ generate-test-resources
+
+ test-jar
+
+
+ test-resource
+ src/test/idl/putOnClassPath
+ putOnClassPath
+ ${project.build.testOutputDirectory}
+
+
+
+
+
+ org.antlr
+ antlr4-maven-plugin
+ ${antlr.version}
+
+
+ antlr
+
+ antlr4
+
+
+
+
+ ${project.basedir}/../../../share/idl_grammar
+ ${project.basedir}/../../../share/idl_grammar/imports
+ true
+ false
+
+
+
+
+
+
+
+ ${project.groupId}
+ avro
+ ${project.version}
+
+
+ org.antlr
+ antlr4-runtime
+ ${antlr.version}
+
+
+ org.apache.commons
+ commons-text
+ ${commons-text.version}
+
+
+ com.fasterxml.jackson.core
+ jackson-databind
+
+
+
+
+
+ m2e
+
+ m2e.version
+
+
+
+
+
+ org.eclipse.m2e
+ lifecycle-mapping
+ 1.0.0
+
+
+
+
+
+ org.codehaus.mojo
+ exec-maven-plugin
+ [1.0,)
+
+ exec
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/lang/java/idl/src/main/java/org/apache/avro/idl/IdlFile.java b/lang/java/idl/src/main/java/org/apache/avro/idl/IdlFile.java
new file mode 100644
index 00000000000..b3777c9f790
--- /dev/null
+++ b/lang/java/idl/src/main/java/org/apache/avro/idl/IdlFile.java
@@ -0,0 +1,137 @@
+/*
+ * 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.idl;
+
+import org.apache.avro.Protocol;
+import org.apache.avro.Schema;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+/**
+ * A parsed IdlFile. Provides access to the named schemas in the IDL file and
+ * the protocol containing the schemas.
+ */
+public class IdlFile {
+ private final Schema mainSchema;
+ private final Protocol protocol;
+ private final String namespace;
+ private final Map namedSchemas;
+ private final List warnings;
+
+ IdlFile(Protocol protocol, List warnings) {
+ this(protocol.getNamespace(), protocol.getTypes(), null, protocol, warnings);
+ }
+
+ IdlFile(String namespace, Schema mainSchema, Iterable schemas, List warnings) {
+ this(namespace, schemas, mainSchema, null, warnings);
+ }
+
+ private IdlFile(String namespace, Iterable schemas, Schema mainSchema, Protocol protocol,
+ List warnings) {
+ this.namespace = namespace;
+ this.namedSchemas = new LinkedHashMap<>();
+ for (Schema namedSchema : schemas) {
+ this.namedSchemas.put(namedSchema.getFullName(), namedSchema);
+ }
+ this.mainSchema = mainSchema;
+ this.protocol = protocol;
+ this.warnings = Collections.unmodifiableList(new ArrayList<>(warnings));
+ }
+
+ /**
+ * The (main) schema defined by the IDL file.
+ */
+ public Schema getMainSchema() {
+ return mainSchema;
+ }
+
+ /**
+ * The protocol defined by the IDL file.
+ */
+ public Protocol getProtocol() {
+ return protocol;
+ }
+
+ public List getWarnings() {
+ return warnings;
+ }
+
+ public List getWarnings(String importFile) {
+ return warnings.stream()
+ .map(warning -> importFile + ' ' + Character.toLowerCase(warning.charAt(0)) + warning.substring(1))
+ .collect(Collectors.toList());
+ }
+
+ /**
+ * The default namespace to resolve schema names against.
+ */
+ public String getNamespace() {
+ return namespace;
+ }
+
+ /**
+ * The named schemas defined by the IDL file, mapped by their full name.
+ */
+ public Map getNamedSchemas() {
+ return Collections.unmodifiableMap(namedSchemas);
+ }
+
+ /**
+ * Get a named schema defined by the IDL file, by name. The name can be a simple
+ * name in the default namespace of the IDL file (e.g., the namespace of the
+ * protocol), or a full name.
+ *
+ * @param name the full name of the schema, or a simple name
+ * @return the schema, or {@code null} if it does not exist
+ */
+ public Schema getNamedSchema(String name) {
+ Schema result = namedSchemas.get(name);
+ if (result != null) {
+ return result;
+ }
+ if (namespace != null && !name.contains(".")) {
+ result = namedSchemas.get(namespace + '.' + name);
+ }
+ return result;
+ }
+
+ // Visible for testing
+ String outputString() {
+ if (protocol != null) {
+ return protocol.toString();
+ }
+ if (mainSchema != null) {
+ return mainSchema.toString();
+ }
+ if (namedSchemas.isEmpty()) {
+ return "[]";
+ } else {
+ StringBuilder buffer = new StringBuilder();
+ for (Schema schema : namedSchemas.values()) {
+ buffer.append(',').append(schema);
+ }
+ buffer.append(']').setCharAt(0, '[');
+ return buffer.toString();
+ }
+ }
+}
diff --git a/lang/java/idl/src/main/java/org/apache/avro/idl/IdlReader.java b/lang/java/idl/src/main/java/org/apache/avro/idl/IdlReader.java
new file mode 100644
index 00000000000..f2419f5f551
--- /dev/null
+++ b/lang/java/idl/src/main/java/org/apache/avro/idl/IdlReader.java
@@ -0,0 +1,1067 @@
+/*
+ * 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.idl;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.BooleanNode;
+import com.fasterxml.jackson.databind.node.DoubleNode;
+import com.fasterxml.jackson.databind.node.IntNode;
+import com.fasterxml.jackson.databind.node.LongNode;
+import com.fasterxml.jackson.databind.node.NullNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.fasterxml.jackson.databind.node.TextNode;
+import org.antlr.v4.runtime.BaseErrorListener;
+import org.antlr.v4.runtime.CharStream;
+import org.antlr.v4.runtime.CharStreams;
+import org.antlr.v4.runtime.CommonTokenStream;
+import org.antlr.v4.runtime.ParserRuleContext;
+import org.antlr.v4.runtime.RecognitionException;
+import org.antlr.v4.runtime.Recognizer;
+import org.antlr.v4.runtime.Token;
+import org.apache.avro.JsonProperties;
+import org.apache.avro.LogicalType;
+import org.apache.avro.LogicalTypes;
+import org.apache.avro.Protocol;
+import org.apache.avro.Schema;
+import org.apache.avro.SchemaParseException;
+import org.apache.avro.idl.IdlParser.ArrayTypeContext;
+import org.apache.avro.idl.IdlParser.EnumDeclarationContext;
+import org.apache.avro.idl.IdlParser.EnumSymbolContext;
+import org.apache.avro.idl.IdlParser.FieldDeclarationContext;
+import org.apache.avro.idl.IdlParser.FixedDeclarationContext;
+import org.apache.avro.idl.IdlParser.FormalParameterContext;
+import org.apache.avro.idl.IdlParser.FullTypeContext;
+import org.apache.avro.idl.IdlParser.IdentifierContext;
+import org.apache.avro.idl.IdlParser.IdlFileContext;
+import org.apache.avro.idl.IdlParser.ImportStatementContext;
+import org.apache.avro.idl.IdlParser.JsonArrayContext;
+import org.apache.avro.idl.IdlParser.JsonLiteralContext;
+import org.apache.avro.idl.IdlParser.JsonObjectContext;
+import org.apache.avro.idl.IdlParser.JsonPairContext;
+import org.apache.avro.idl.IdlParser.JsonValueContext;
+import org.apache.avro.idl.IdlParser.MapTypeContext;
+import org.apache.avro.idl.IdlParser.MessageDeclarationContext;
+import org.apache.avro.idl.IdlParser.NamespaceDeclarationContext;
+import org.apache.avro.idl.IdlParser.NullableTypeContext;
+import org.apache.avro.idl.IdlParser.PrimitiveTypeContext;
+import org.apache.avro.idl.IdlParser.ProtocolDeclarationBodyContext;
+import org.apache.avro.idl.IdlParser.ProtocolDeclarationContext;
+import org.apache.avro.idl.IdlParser.RecordBodyContext;
+import org.apache.avro.idl.IdlParser.RecordDeclarationContext;
+import org.apache.avro.idl.IdlParser.ResultTypeContext;
+import org.apache.avro.idl.IdlParser.SchemaPropertyContext;
+import org.apache.avro.idl.IdlParser.UnionTypeContext;
+import org.apache.avro.idl.IdlParser.VariableDeclarationContext;
+import org.apache.avro.util.internal.Accessor;
+import org.apache.commons.text.StringEscapeUtils;
+
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URI;
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Deque;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.function.Consumer;
+import java.util.function.Predicate;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import static java.util.Collections.singleton;
+import static java.util.Collections.unmodifiableMap;
+
+public class IdlReader {
+ /**
+ * Simple error listener. Throws a runtime exception because ANTLR does not give
+ * easy access to the (reasonably readable) error message elsewhere.
+ */
+ private static final BaseErrorListener SIMPLE_AVRO_ERROR_LISTENER = new BaseErrorListener() {
+ @Override
+ public void syntaxError(Recognizer, ?> recognizer, Object offendingSymbol, int line, int charPositionInLine,
+ String msg, RecognitionException e) {
+ throw new SchemaParseException("line " + line + ":" + charPositionInLine + " " + msg);
+ }
+ };
+ private static final String OPTIONAL_NULLABLE_TYPE_PROPERTY = "org.apache.avro.idl.Idl.NullableType.optional";
+ /**
+ * Pattern to match the common whitespace indents in a multi-line String.
+ * Doesn't match a single-line String, fully matches any multi-line String.
+ *
+ * To use: match on a {@link String#trim() trimmed} String, and then replace all
+ * newlines followed by the group "indent" with a newline.
+ */
+ private static final Pattern WS_INDENT = Pattern.compile("(?U).*\\R(?\\h*).*(?:\\R\\k.*)*");
+ /**
+ * Pattern to match the whitespace indents plus common stars (1 or 2) in a
+ * multi-line String. If a String fully matches, replace all occurrences of a
+ * newline followed by whitespace and then the group "stars" with a newline.
+ *
+ * Note: partial matches are invalid.
+ */
+ private static final Pattern STAR_INDENT = Pattern.compile("(?U)(?\\*{1,2}).*(?:\\R\\h*\\k.*)*");
+ /**
+ * Predicate to check for valid names. Should probably be delegated to the
+ * Schema class.
+ */
+ private static final Predicate