blob: 1853458ee97511d6a94f8fa27fcc4550891023eb [file] [log] [blame]
/*
* 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
*
* http://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.kafka.message;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Base64;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.regex.Pattern;
public final class FieldSpec {
private static final Pattern VALID_FIELD_NAMES = Pattern.compile("[A-Za-z]([A-Za-z0-9]*)");
private final String name;
private final Versions versions;
private final List<FieldSpec> fields;
private final FieldType type;
private final boolean mapKey;
private final Versions nullableVersions;
private final String fieldDefault;
private final boolean ignorable;
private final EntityType entityType;
private final String about;
private final Versions taggedVersions;
private final Optional<Versions> flexibleVersions;
private final Optional<Integer> tag;
private final boolean zeroCopy;
@JsonCreator
public FieldSpec(@JsonProperty("name") String name,
@JsonProperty("versions") String versions,
@JsonProperty("fields") List<FieldSpec> fields,
@JsonProperty("type") String type,
@JsonProperty("mapKey") boolean mapKey,
@JsonProperty("nullableVersions") String nullableVersions,
@JsonProperty("default") String fieldDefault,
@JsonProperty("ignorable") boolean ignorable,
@JsonProperty("entityType") EntityType entityType,
@JsonProperty("about") String about,
@JsonProperty("taggedVersions") String taggedVersions,
@JsonProperty("flexibleVersions") String flexibleVersions,
@JsonProperty("tag") Integer tag,
@JsonProperty("zeroCopy") boolean zeroCopy) {
this.name = Objects.requireNonNull(name);
if (!VALID_FIELD_NAMES.matcher(this.name).matches()) {
throw new RuntimeException("Invalid field name " + this.name);
}
this.taggedVersions = Versions.parse(taggedVersions, Versions.NONE);
// If versions is not set, but taggedVersions is, default to taggedVersions.
this.versions = Versions.parse(versions, this.taggedVersions.empty() ?
null : this.taggedVersions);
if (this.versions == null) {
throw new RuntimeException("You must specify the version of the " +
name + " structure.");
}
this.fields = Collections.unmodifiableList(fields == null ?
Collections.emptyList() : new ArrayList<>(fields));
this.type = FieldType.parse(Objects.requireNonNull(type));
this.mapKey = mapKey;
this.nullableVersions = Versions.parse(nullableVersions, Versions.NONE);
if (!this.nullableVersions.empty()) {
if (!this.type.canBeNullable()) {
throw new RuntimeException("Type " + this.type + " cannot be nullable.");
}
}
this.fieldDefault = fieldDefault == null ? "" : fieldDefault;
this.ignorable = ignorable;
this.entityType = (entityType == null) ? EntityType.UNKNOWN : entityType;
this.entityType.verifyTypeMatches(name, this.type);
this.about = about == null ? "" : about;
if (!this.fields().isEmpty()) {
if (!this.type.isArray() && !this.type.isStruct()) {
throw new RuntimeException("Non-array or Struct field " + name + " cannot have fields");
}
}
if (flexibleVersions == null || flexibleVersions.isEmpty()) {
this.flexibleVersions = Optional.empty();
} else {
this.flexibleVersions = Optional.of(Versions.parse(flexibleVersions, null));
if (!(this.type.isString() || this.type.isBytes())) {
// For now, only allow flexibleVersions overrides for the string and bytes
// types. Overrides are only needed to keep compatibility with some old formats,
// so there isn't any need to support them for all types.
throw new RuntimeException("Invalid flexibleVersions override for " + name +
". Only fields of type string or bytes can specify a flexibleVersions " +
"override.");
}
}
this.tag = Optional.ofNullable(tag);
if (this.tag.isPresent() && mapKey) {
throw new RuntimeException("Tagged fields cannot be used as keys.");
}
checkTagInvariants();
this.zeroCopy = zeroCopy;
if (this.zeroCopy && !this.type.isBytes()) {
throw new RuntimeException("Invalid zeroCopy value for " + name +
". Only fields of type bytes can use zeroCopy flag.");
}
}
private void checkTagInvariants() {
if (this.tag.isPresent()) {
if (this.tag.get() < 0) {
throw new RuntimeException("Field " + name + " specifies a tag of " + this.tag.get() +
". Tags cannot be negative.");
}
if (this.taggedVersions.empty()) {
throw new RuntimeException("Field " + name + " specifies a tag of " + this.tag.get() +
", but has no tagged versions. If a tag is specified, taggedVersions must " +
"be specified as well.");
}
Versions nullableTaggedVersions = this.nullableVersions.intersect(this.taggedVersions);
if (!(nullableTaggedVersions.empty() || nullableTaggedVersions.equals(this.taggedVersions))) {
throw new RuntimeException("Field " + name + " specifies nullableVersions " +
this.nullableVersions + " and taggedVersions " + this.taggedVersions + ". " +
"Either all tagged versions must be nullable, or none must be.");
}
if (this.taggedVersions.highest() < Short.MAX_VALUE) {
throw new RuntimeException("Field " + name + " specifies taggedVersions " +
this.taggedVersions + ", which is not open-ended. taggedVersions must " +
"be either none, or an open-ended range (that ends with a plus sign).");
}
if (!this.taggedVersions.intersect(this.versions).equals(this.taggedVersions)) {
throw new RuntimeException("Field " + name + " specifies taggedVersions " +
this.taggedVersions + ", and versions " + this.versions + ". " +
"taggedVersions must be a subset of versions.");
}
} else if (!this.taggedVersions.empty()) {
throw new RuntimeException("Field " + name + " does not specify a tag, " +
"but specifies tagged versions of " + this.taggedVersions + ". " +
"Please specify a tag, or remove the taggedVersions.");
}
}
@JsonProperty("name")
public String name() {
return name;
}
String capitalizedCamelCaseName() {
return MessageGenerator.capitalizeFirst(name);
}
String camelCaseName() {
return MessageGenerator.lowerCaseFirst(name);
}
String snakeCaseName() {
return MessageGenerator.toSnakeCase(name);
}
public Versions versions() {
return versions;
}
@JsonProperty("versions")
public String versionsString() {
return versions.toString();
}
@JsonProperty("fields")
public List<FieldSpec> fields() {
return fields;
}
@JsonProperty("type")
public String typeString() {
return type.toString();
}
public FieldType type() {
return type;
}
@JsonProperty("mapKey")
public boolean mapKey() {
return mapKey;
}
public Versions nullableVersions() {
return nullableVersions;
}
@JsonProperty("nullableVersions")
public String nullableVersionsString() {
return nullableVersions.toString();
}
@JsonProperty("default")
public String defaultString() {
return fieldDefault;
}
@JsonProperty("ignorable")
public boolean ignorable() {
return ignorable;
}
@JsonProperty("entityType")
public EntityType entityType() {
return entityType;
}
@JsonProperty("about")
public String about() {
return about;
}
@JsonProperty("taggedVersions")
public String taggedVersionsString() {
return taggedVersions.toString();
}
public Versions taggedVersions() {
return taggedVersions;
}
@JsonProperty("flexibleVersions")
public String flexibleVersionsString() {
return flexibleVersions.isPresent() ? flexibleVersions.get().toString() : null;
}
public Optional<Versions> flexibleVersions() {
return flexibleVersions;
}
@JsonProperty("tag")
public Integer tagInteger() {
return tag.orElse(null);
}
public Optional<Integer> tag() {
return tag;
}
@JsonProperty("zeroCopy")
public boolean zeroCopy() {
return zeroCopy;
}
/**
* Get a string representation of the field default.
*
* @param headerGenerator The header generator in case we need to add imports.
* @param structRegistry The struct registry in case we need to look up structs.
*
* @return A string that can be used for the field default in the
* generated code.
*/
String fieldDefault(HeaderGenerator headerGenerator,
StructRegistry structRegistry) {
if (type instanceof FieldType.BoolFieldType) {
if (fieldDefault.isEmpty()) {
return "false";
} else if (fieldDefault.equalsIgnoreCase("true")) {
return "true";
} else if (fieldDefault.equalsIgnoreCase("false")) {
return "false";
} else {
throw new RuntimeException("Invalid default for boolean field " +
name + ": " + fieldDefault);
}
} else if ((type instanceof FieldType.Int8FieldType) ||
(type instanceof FieldType.Int16FieldType) ||
(type instanceof FieldType.Uint16FieldType) ||
(type instanceof FieldType.Uint32FieldType) ||
(type instanceof FieldType.Int32FieldType) ||
(type instanceof FieldType.Int64FieldType)) {
int base = 10;
String defaultString = fieldDefault;
if (defaultString.startsWith("0x")) {
base = 16;
defaultString = defaultString.substring(2);
}
if (type instanceof FieldType.Int8FieldType) {
if (defaultString.isEmpty()) {
return "(byte) 0";
} else {
try {
Byte.valueOf(defaultString, base);
} catch (NumberFormatException e) {
throw new RuntimeException("Invalid default for int8 field " +
name + ": " + defaultString, e);
}
return "(byte) " + fieldDefault;
}
} else if (type instanceof FieldType.Int16FieldType) {
if (defaultString.isEmpty()) {
return "(short) 0";
} else {
try {
Short.valueOf(defaultString, base);
} catch (NumberFormatException e) {
throw new RuntimeException("Invalid default for int16 field " +
name + ": " + defaultString, e);
}
return "(short) " + fieldDefault;
}
} else if (type instanceof FieldType.Uint16FieldType) {
if (defaultString.isEmpty()) {
return "0";
} else {
try {
int value = Integer.valueOf(defaultString, base);
if (value < 0 || value > MessageGenerator.UNSIGNED_SHORT_MAX) {
throw new RuntimeException("Invalid default for uint16 field " +
name + ": out of range.");
}
} catch (NumberFormatException e) {
throw new RuntimeException("Invalid default for uint16 field " +
name + ": " + defaultString, e);
}
return fieldDefault;
}
} else if (type instanceof FieldType.Uint32FieldType) {
if (defaultString.isEmpty()) {
return "0";
} else {
try {
long value = Long.valueOf(defaultString, base);
if (value < 0 || value > MessageGenerator.UNSIGNED_INT_MAX) {
throw new RuntimeException("Invalid default for uint32 field " +
name + ": out of range.");
}
} catch (NumberFormatException e) {
throw new RuntimeException("Invalid default for uint32 field " +
name + ": " + defaultString, e);
}
return fieldDefault;
}
} else if (type instanceof FieldType.Int32FieldType) {
if (defaultString.isEmpty()) {
return "0";
} else {
try {
Integer.valueOf(defaultString, base);
} catch (NumberFormatException e) {
throw new RuntimeException("Invalid default for int32 field " +
name + ": " + defaultString, e);
}
return fieldDefault;
}
} else if (type instanceof FieldType.Int64FieldType) {
if (defaultString.isEmpty()) {
return "0L";
} else {
try {
Long.valueOf(defaultString, base);
} catch (NumberFormatException e) {
throw new RuntimeException("Invalid default for int64 field " +
name + ": " + defaultString, e);
}
return fieldDefault + "L";
}
} else {
throw new RuntimeException("Unsupported field type " + type);
}
} else if (type instanceof FieldType.UUIDFieldType) {
headerGenerator.addImport(MessageGenerator.UUID_CLASS);
if (fieldDefault.isEmpty()) {
return "Uuid.ZERO_UUID";
} else {
try {
ByteBuffer uuidBytes = ByteBuffer.wrap(Base64.getUrlDecoder().decode(fieldDefault));
uuidBytes.getLong();
uuidBytes.getLong();
} catch (IllegalArgumentException e) {
throw new RuntimeException("Invalid default for uuid field " +
name + ": " + fieldDefault, e);
}
headerGenerator.addImport(MessageGenerator.UUID_CLASS);
return "Uuid.fromString(\"" + fieldDefault + "\")";
}
} else if (type instanceof FieldType.Float64FieldType) {
if (fieldDefault.isEmpty()) {
return "0.0";
} else {
try {
Double.parseDouble(fieldDefault);
} catch (NumberFormatException e) {
throw new RuntimeException("Invalid default for float64 field " +
name + ": " + fieldDefault, e);
}
return "Double.parseDouble(\"" + fieldDefault + "\")";
}
} else if (type instanceof FieldType.StringFieldType) {
if (fieldDefault.equals("null")) {
validateNullDefault();
return "null";
} else {
return "\"" + fieldDefault + "\"";
}
} else if (type.isBytes()) {
if (fieldDefault.equals("null")) {
validateNullDefault();
return "null";
} else if (!fieldDefault.isEmpty()) {
throw new RuntimeException("Invalid default for bytes field " +
name + ". The only valid default for a bytes field " +
"is empty or null.");
}
if (zeroCopy) {
headerGenerator.addImport(MessageGenerator.BYTE_UTILS_CLASS);
return "ByteUtils.EMPTY_BUF";
} else {
headerGenerator.addImport(MessageGenerator.BYTES_CLASS);
return "Bytes.EMPTY";
}
} else if (type.isRecords()) {
return "null";
} else if (type.isStruct()) {
if (!fieldDefault.isEmpty()) {
throw new RuntimeException("Invalid default for struct field " +
name + ": custom defaults are not supported for struct fields.");
}
return "new " + type.toString() + "()";
} else if (type.isArray()) {
if (fieldDefault.equals("null")) {
validateNullDefault();
return "null";
} else if (!fieldDefault.isEmpty()) {
throw new RuntimeException("Invalid default for array field " +
name + ". The only valid default for an array field " +
"is the empty array or null.");
}
return String.format("new %s(0)",
concreteJavaType(headerGenerator, structRegistry));
} else {
throw new RuntimeException("Unsupported field type " + type);
}
}
private void validateNullDefault() {
if (!(nullableVersions().contains(versions))) {
throw new RuntimeException("null cannot be the default for field " +
name + ", because not all versions of this field are " +
"nullable.");
}
}
/**
* Get the abstract Java type of the field-- for example, List.
*
* @param headerGenerator The header generator in case we need to add imports.
* @param structRegistry The struct registry in case we need to look up structs.
*
* @return The abstract java type name.
*/
String fieldAbstractJavaType(HeaderGenerator headerGenerator,
StructRegistry structRegistry) {
if (type instanceof FieldType.BoolFieldType) {
return "boolean";
} else if (type instanceof FieldType.Int8FieldType) {
return "byte";
} else if (type instanceof FieldType.Int16FieldType) {
return "short";
} else if (type instanceof FieldType.Uint16FieldType) {
return "int";
} else if (type instanceof FieldType.Uint32FieldType) {
return "long";
} else if (type instanceof FieldType.Int32FieldType) {
return "int";
} else if (type instanceof FieldType.Int64FieldType) {
return "long";
} else if (type instanceof FieldType.UUIDFieldType) {
headerGenerator.addImport(MessageGenerator.UUID_CLASS);
return "Uuid";
} else if (type instanceof FieldType.Float64FieldType) {
return "double";
} else if (type.isString()) {
return "String";
} else if (type.isBytes()) {
if (zeroCopy) {
headerGenerator.addImport(MessageGenerator.BYTE_BUFFER_CLASS);
return "ByteBuffer";
} else {
return "byte[]";
}
} else if (type instanceof FieldType.RecordsFieldType) {
headerGenerator.addImport(MessageGenerator.BASE_RECORDS_CLASS);
return "BaseRecords";
} else if (type.isStruct()) {
return MessageGenerator.capitalizeFirst(typeString());
} else if (type.isArray()) {
FieldType.ArrayType arrayType = (FieldType.ArrayType) type;
if (structRegistry.isStructArrayWithKeys(this)) {
headerGenerator.addImport(MessageGenerator.IMPLICIT_LINKED_HASH_MULTI_COLLECTION_CLASS);
return collectionType(arrayType.elementType().toString());
} else {
headerGenerator.addImport(MessageGenerator.LIST_CLASS);
return String.format("List<%s>",
arrayType.elementType().getBoxedJavaType(headerGenerator));
}
} else {
throw new RuntimeException("Unknown field type " + type);
}
}
/**
* Get the concrete Java type of the field-- for example, ArrayList.
*
* @param headerGenerator The header generator in case we need to add imports.
* @param structRegistry The struct registry in case we need to look up structs.
*
* @return The abstract java type name.
*/
String concreteJavaType(HeaderGenerator headerGenerator,
StructRegistry structRegistry) {
if (type.isArray()) {
FieldType.ArrayType arrayType = (FieldType.ArrayType) type;
if (structRegistry.isStructArrayWithKeys(this)) {
return collectionType(arrayType.elementType().toString());
} else {
headerGenerator.addImport(MessageGenerator.ARRAYLIST_CLASS);
return String.format("ArrayList<%s>",
arrayType.elementType().getBoxedJavaType(headerGenerator));
}
} else {
return fieldAbstractJavaType(headerGenerator, structRegistry);
}
}
static String collectionType(String baseType) {
return baseType + "Collection";
}
/**
* Generate an if statement that checks if this field has a non-default value.
*
* @param headerGenerator The header generator in case we need to add imports.
* @param structRegistry The struct registry in case we need to look up structs.
* @param buffer The code buffer to write to.
* @param fieldPrefix The prefix to prepend before references to this field.
* @param nullableVersions The nullable versions to use for this field. This is
* mainly to let us choose to ignore the possibility of
* nulls sometimes (like when dealing with array entries
* that cannot be null).
*/
void generateNonDefaultValueCheck(HeaderGenerator headerGenerator,
StructRegistry structRegistry,
CodeBuffer buffer,
String fieldPrefix,
Versions nullableVersions) {
String fieldDefault = fieldDefault(headerGenerator, structRegistry);
if (type().isArray()) {
if (fieldDefault.equals("null")) {
buffer.printf("if (%s%s != null) {%n", fieldPrefix, camelCaseName());
} else if (nullableVersions.empty()) {
buffer.printf("if (!%s%s.isEmpty()) {%n", fieldPrefix, camelCaseName());
} else {
buffer.printf("if (%s%s == null || !%s%s.isEmpty()) {%n",
fieldPrefix, camelCaseName(), fieldPrefix, camelCaseName());
}
} else if (type().isBytes()) {
if (fieldDefault.equals("null")) {
buffer.printf("if (%s%s != null) {%n", fieldPrefix, camelCaseName());
} else if (nullableVersions.empty()) {
if (zeroCopy()) {
buffer.printf("if (%s%s.hasRemaining()) {%n",
fieldPrefix, camelCaseName());
} else {
buffer.printf("if (%s%s.length != 0) {%n",
fieldPrefix, camelCaseName());
}
} else {
if (zeroCopy()) {
buffer.printf("if (%s%s == null || %s%s.remaining() > 0) {%n",
fieldPrefix, camelCaseName(), fieldPrefix, camelCaseName());
} else {
buffer.printf("if (%s%s == null || %s%s.length != 0) {%n",
fieldPrefix, camelCaseName(), fieldPrefix, camelCaseName());
}
}
} else if (type().isString() || type().isStruct() || type() instanceof FieldType.UUIDFieldType) {
if (fieldDefault.equals("null")) {
buffer.printf("if (%s%s != null) {%n", fieldPrefix, camelCaseName());
} else if (nullableVersions.empty()) {
buffer.printf("if (!%s%s.equals(%s)) {%n",
fieldPrefix, camelCaseName(), fieldDefault);
} else {
buffer.printf("if (%s%s == null || !%s%s.equals(%s)) {%n",
fieldPrefix, camelCaseName(), fieldPrefix, camelCaseName(),
fieldDefault);
}
} else if (type() instanceof FieldType.BoolFieldType) {
buffer.printf("if (%s%s%s) {%n",
fieldDefault.equals("true") ? "!" : "",
fieldPrefix, camelCaseName());
} else {
buffer.printf("if (%s%s != %s) {%n",
fieldPrefix, camelCaseName(), fieldDefault);
}
}
/**
* Generate an if statement that checks if this field is non-default and also
* non-ignorable.
*
* @param headerGenerator The header generator in case we need to add imports.
* @param structRegistry The struct registry in case we need to look up structs.
* @param fieldPrefix The prefix to prepend before references to this field.
* @param buffer The code buffer to write to.
*/
void generateNonIgnorableFieldCheck(HeaderGenerator headerGenerator,
StructRegistry structRegistry,
String fieldPrefix,
CodeBuffer buffer) {
generateNonDefaultValueCheck(headerGenerator, structRegistry,
buffer, fieldPrefix, nullableVersions());
buffer.incrementIndent();
headerGenerator.addImport(MessageGenerator.UNSUPPORTED_VERSION_EXCEPTION_CLASS);
buffer.printf("throw new UnsupportedVersionException(" +
"\"Attempted to write a non-default %s at version \" + _version);%n",
camelCaseName());
buffer.decrementIndent();
buffer.printf("}%n");
}
}