blob: 9258a9960bc11363eae33287d21a1c0591292ceb [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
*
* 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.compiler.specific;
import java.io.File;
import java.io.FileOutputStream;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.io.IOException;
import java.io.StringWriter;
import java.lang.reflect.InvocationTargetException;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.apache.avro.Conversion;
import org.apache.avro.Conversions;
import org.apache.avro.LogicalTypes;
import org.apache.avro.data.TimeConversions;
import org.apache.avro.specific.SpecificData;
import org.apache.avro.Protocol;
import org.apache.avro.Protocol.Message;
import org.apache.avro.Schema;
import org.apache.avro.Schema.Field;
import org.apache.avro.SchemaNormalization;
import org.apache.avro.JsonProperties;
import org.apache.avro.generic.GenericData;
import org.apache.avro.generic.GenericData.StringType;
import org.apache.velocity.Template;
import org.apache.velocity.VelocityContext;
import org.apache.velocity.app.VelocityEngine;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.apache.avro.specific.SpecificData.RESERVED_WORDS;
/**
* Generate specific Java interfaces and classes for protocols and schemas.
*
* Java reserved keywords are mangled to preserve compilation.
*/
public class SpecificCompiler {
/*
* From Section 4.10 of the Java VM Specification: A method descriptor is valid
* only if it represents method parameters with a total length of 255 or less,
* where that length includes the contribution for this in the case of instance
* or interface method invocations. The total length is calculated by summing
* the contributions of the individual parameters, where a parameter of type
* long or double contributes two units to the length and a parameter of any
* other type contributes one unit.
*
* Arguments of type Double/Float contribute 2 "parameter units" to this limit,
* all other types contribute 1 "parameter unit". All instance methods for a
* class are passed a reference to the instance (`this), and hence, they are
* permitted at most `JVM_METHOD_ARG_LIMIT-1` "parameter units" for their
* arguments.
*
* @see <a href=
* "https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-4.html#jvms-4.10">
* JVM Spec: Section 4.10</a>
*/
private static final int JVM_METHOD_ARG_LIMIT = 255;
/*
* Note: This is protected instead of private only so it's visible for testing.
*/
protected static final int MAX_FIELD_PARAMETER_UNIT_COUNT = JVM_METHOD_ARG_LIMIT - 1;
public enum FieldVisibility {
PUBLIC, PUBLIC_DEPRECATED, PRIVATE
}
void addLogicalTypeConversions(SpecificData specificData) {
specificData.addLogicalTypeConversion(new TimeConversions.DateConversion());
specificData.addLogicalTypeConversion(new TimeConversions.TimeMillisConversion());
specificData.addLogicalTypeConversion(new TimeConversions.TimeMicrosConversion());
specificData.addLogicalTypeConversion(new TimeConversions.TimestampMillisConversion());
specificData.addLogicalTypeConversion(new TimeConversions.TimestampMicrosConversion());
}
private final SpecificData specificData = new SpecificData();
private final Set<Schema> queue = new HashSet<>();
private Protocol protocol;
private VelocityEngine velocityEngine;
private String templateDir;
private FieldVisibility fieldVisibility = FieldVisibility.PRIVATE;
private boolean createOptionalGetters = false;
private boolean gettersReturnOptional = false;
private boolean optionalGettersForNullableFieldsOnly = false;
private boolean createSetters = true;
private boolean createAllArgsConstructor = true;
private String outputCharacterEncoding;
private boolean enableDecimalLogicalType = false;
private String suffix = ".java";
private List<Object> additionalVelocityTools = Collections.emptyList();
/*
* Used in the record.vm template.
*/
public boolean isCreateAllArgsConstructor() {
return createAllArgsConstructor;
}
/* Reserved words for accessor/mutator methods */
private static final Set<String> ACCESSOR_MUTATOR_RESERVED_WORDS = new HashSet<>(
Arrays.asList("class", "schema", "classSchema"));
static {
// Add reserved words to accessor/mutator reserved words
ACCESSOR_MUTATOR_RESERVED_WORDS.addAll(RESERVED_WORDS);
}
/* Reserved words for error types */
private static final Set<String> ERROR_RESERVED_WORDS = new HashSet<>(Arrays.asList("message", "cause"));
static {
// Add accessor/mutator reserved words to error reserved words
ERROR_RESERVED_WORDS.addAll(ACCESSOR_MUTATOR_RESERVED_WORDS);
}
private static final String FILE_HEADER = "/**\n" + " * Autogenerated by Avro\n" + " *\n"
+ " * DO NOT EDIT DIRECTLY\n" + " */\n";
public SpecificCompiler(Protocol protocol) {
this();
// enqueue all types
for (Schema s : protocol.getTypes()) {
enqueue(s);
}
this.protocol = protocol;
}
public SpecificCompiler(Schema schema) {
this();
enqueue(schema);
this.protocol = null;
}
/**
* Creates a specific compiler with the given type to use for date/time related
* logical types.
*/
SpecificCompiler() {
this.templateDir = System.getProperty("org.apache.avro.specific.templates",
"/org/apache/avro/compiler/specific/templates/java/classic/");
initializeVelocity();
initializeSpecificData();
}
/**
* Set additional Velocity tools (simple POJOs) to be injected into the Velocity
* template context.
*/
public void setAdditionalVelocityTools(List<Object> additionalVelocityTools) {
this.additionalVelocityTools = additionalVelocityTools;
}
/**
* Set the resource directory where templates reside. First, the compiler checks
* the system path for the specified file, if not it is assumed that it is
* present on the classpath.
*/
public void setTemplateDir(String templateDir) {
this.templateDir = templateDir;
}
/** Set the resource file suffix, .java or .xxx */
public void setSuffix(String suffix) {
this.suffix = suffix;
}
/**
* @return true if the record fields should be marked as deprecated
*/
public boolean deprecatedFields() {
return (this.fieldVisibility == FieldVisibility.PUBLIC_DEPRECATED);
}
/**
* @return true if the record fields should be public
*/
public boolean publicFields() {
return (this.fieldVisibility == FieldVisibility.PUBLIC
|| this.fieldVisibility == FieldVisibility.PUBLIC_DEPRECATED);
}
/**
* @return true if the record fields should be private
*/
public boolean privateFields() {
return (this.fieldVisibility == FieldVisibility.PRIVATE);
}
/**
* Sets the field visibility option.
*/
public void setFieldVisibility(FieldVisibility fieldVisibility) {
this.fieldVisibility = fieldVisibility;
}
public boolean isCreateSetters() {
return this.createSetters;
}
/**
* Set to false to not create setter methods for the fields of the record.
*/
public void setCreateSetters(boolean createSetters) {
this.createSetters = createSetters;
}
public boolean isCreateOptionalGetters() {
return this.createOptionalGetters;
}
/**
* Set to false to not create the getters that return an Optional.
*/
public void setCreateOptionalGetters(boolean createOptionalGetters) {
this.createOptionalGetters = createOptionalGetters;
}
public boolean isGettersReturnOptional() {
return this.gettersReturnOptional;
}
/**
* Set to false to not create the getters that return an Optional.
*/
public void setGettersReturnOptional(boolean gettersReturnOptional) {
this.gettersReturnOptional = gettersReturnOptional;
}
public boolean isOptionalGettersForNullableFieldsOnly() {
return optionalGettersForNullableFieldsOnly;
}
/**
* Set to true to create the Optional getters only for nullable fields.
*/
public void setOptionalGettersForNullableFieldsOnly(boolean optionalGettersForNullableFieldsOnly) {
this.optionalGettersForNullableFieldsOnly = optionalGettersForNullableFieldsOnly;
}
/**
* Set to true to use {@link java.math.BigDecimal} instead of
* {@link java.nio.ByteBuffer} for logical type "decimal"
*/
public void setEnableDecimalLogicalType(boolean enableDecimalLogicalType) {
this.enableDecimalLogicalType = enableDecimalLogicalType;
}
public void addCustomConversion(Class<?> conversionClass) {
try {
final Conversion<?> conversion = (Conversion<?>) conversionClass.getDeclaredConstructor().newInstance();
specificData.addLogicalTypeConversion(conversion);
} catch (IllegalAccessException | InstantiationException | NoSuchMethodException | InvocationTargetException e) {
throw new RuntimeException("Failed to instantiate conversion class " + conversionClass, e);
}
}
public Collection<String> getUsedConversionClasses(Schema schema) {
LinkedHashMap<String, Conversion<?>> classnameToConversion = new LinkedHashMap<>();
for (Conversion<?> conversion : specificData.getConversions()) {
classnameToConversion.put(conversion.getConvertedType().getCanonicalName(), conversion);
}
Collection<String> result = new HashSet<>();
for (String className : getClassNamesOfPrimitiveFields(schema)) {
if (classnameToConversion.containsKey(className)) {
result.add(classnameToConversion.get(className).getClass().getCanonicalName());
}
}
return result;
}
private Set<String> getClassNamesOfPrimitiveFields(Schema schema) {
Set<String> result = new HashSet<>();
getClassNamesOfPrimitiveFields(schema, result, new HashSet<>());
return result;
}
private void getClassNamesOfPrimitiveFields(Schema schema, Set<String> result, Set<Schema> seenSchemas) {
if (seenSchemas.contains(schema)) {
return;
}
seenSchemas.add(schema);
switch (schema.getType()) {
case RECORD:
for (Schema.Field field : schema.getFields()) {
getClassNamesOfPrimitiveFields(field.schema(), result, seenSchemas);
}
break;
case MAP:
getClassNamesOfPrimitiveFields(schema.getValueType(), result, seenSchemas);
break;
case ARRAY:
getClassNamesOfPrimitiveFields(schema.getElementType(), result, seenSchemas);
break;
case UNION:
for (Schema s : schema.getTypes())
getClassNamesOfPrimitiveFields(s, result, seenSchemas);
break;
case NULL:
break;
case ENUM:
case FIXED:
case STRING:
case BYTES:
case INT:
case LONG:
case FLOAT:
case DOUBLE:
case BOOLEAN:
result.add(javaType(schema, true));
break;
default:
throw new RuntimeException("Unknown type: " + schema);
}
}
private void initializeVelocity() {
this.velocityEngine = new VelocityEngine();
// These properties tell Velocity to use its own classpath-based
// loader, then drop down to check the root and the current folder
velocityEngine.addProperty("resource.loaders", "class, file");
velocityEngine.addProperty("resource.loader.class.class",
"org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader");
velocityEngine.addProperty("resource.loader.file.class",
"org.apache.velocity.runtime.resource.loader.FileResourceLoader");
velocityEngine.addProperty("resource.loader.file.path", "/, .");
velocityEngine.setProperty("runtime.strict_mode.enable", true);
// Set whitespace gobbling to Backward Compatible (BC)
// https://velocity.apache.org/engine/2.0/developer-guide.html#space-gobbling
velocityEngine.setProperty("parser.space_gobbling", "bc");
}
private void initializeSpecificData() {
addLogicalTypeConversions(specificData);
specificData.addLogicalTypeConversion(new Conversions.DecimalConversion());
}
/**
* Captures output file path and contents.
*/
static class OutputFile {
String path;
String contents;
String outputCharacterEncoding;
/**
* Writes output to path destination directory when it is newer than src,
* creating directories as necessary. Returns the created file.
*/
File writeToDestination(File src, File destDir) throws IOException {
File f = new File(destDir, path);
if (src != null && f.exists() && f.lastModified() >= src.lastModified())
return f; // already up to date: ignore
f.getParentFile().mkdirs();
Writer fw = null;
FileOutputStream fos = null;
try {
if (outputCharacterEncoding != null) {
fos = new FileOutputStream(f);
fw = new OutputStreamWriter(fos, outputCharacterEncoding);
} else {
fw = Files.newBufferedWriter(f.toPath(), UTF_8);
}
fw.write(FILE_HEADER);
fw.write(contents);
} finally {
if (fw != null)
fw.close();
if (fos != null)
fos.close();
}
return f;
}
}
/**
* Generates Java interface and classes for a protocol.
*
* @param src the source Avro protocol file
* @param dest the directory to place generated files in
*/
public static void compileProtocol(File src, File dest) throws IOException {
compileProtocol(new File[] { src }, dest);
}
/**
* Generates Java interface and classes for a number of protocol files.
*
* @param srcFiles the source Avro protocol files
* @param dest the directory to place generated files in
*/
public static void compileProtocol(File[] srcFiles, File dest) throws IOException {
for (File src : srcFiles) {
Protocol protocol = Protocol.parse(src);
SpecificCompiler compiler = new SpecificCompiler(protocol);
compiler.compileToDestination(src, dest);
}
}
/** Generates Java classes for a schema. */
public static void compileSchema(File src, File dest) throws IOException {
compileSchema(new File[] { src }, dest);
}
/** Generates Java classes for a number of schema files. */
public static void compileSchema(File[] srcFiles, File dest) throws IOException {
Schema.Parser parser = new Schema.Parser();
for (File src : srcFiles) {
Schema schema = parser.parse(src);
SpecificCompiler compiler = new SpecificCompiler(schema);
compiler.compileToDestination(src, dest);
}
}
/** Recursively enqueue schemas that need a class generated. */
private void enqueue(Schema schema) {
if (queue.contains(schema))
return;
switch (schema.getType()) {
case RECORD:
queue.add(schema);
for (Schema.Field field : schema.getFields())
enqueue(field.schema());
break;
case MAP:
enqueue(schema.getValueType());
break;
case ARRAY:
enqueue(schema.getElementType());
break;
case UNION:
for (Schema s : schema.getTypes())
enqueue(s);
break;
case ENUM:
case FIXED:
queue.add(schema);
break;
case STRING:
case BYTES:
case INT:
case LONG:
case FLOAT:
case DOUBLE:
case BOOLEAN:
case NULL:
break;
default:
throw new RuntimeException("Unknown type: " + schema);
}
}
/** Generate java classes for enqueued schemas. */
Collection<OutputFile> compile() {
List<OutputFile> out = new ArrayList<>(queue.size() + 1);
for (Schema schema : queue) {
out.add(compile(schema));
}
if (protocol != null) {
out.add(compileInterface(protocol));
}
return out;
}
/** Generate output under dst, unless existing file is newer than src. */
public void compileToDestination(File src, File dst) throws IOException {
for (Schema schema : queue) {
OutputFile o = compile(schema);
o.writeToDestination(src, dst);
}
if (protocol != null) {
compileInterface(protocol).writeToDestination(src, dst);
}
}
private String renderTemplate(String templateName, VelocityContext context) {
Template template;
try {
template = this.velocityEngine.getTemplate(templateName);
} catch (Exception e) {
throw new RuntimeException(e);
}
StringWriter writer = new StringWriter();
template.merge(context, writer);
return writer.toString();
}
OutputFile compileInterface(Protocol protocol) {
protocol = addStringType(protocol); // annotate protocol as needed
VelocityContext context = new VelocityContext();
context.put("protocol", protocol);
context.put("this", this);
for (Object velocityTool : additionalVelocityTools) {
String toolName = velocityTool.getClass().getSimpleName().toLowerCase();
context.put(toolName, velocityTool);
}
String out = renderTemplate(templateDir + "protocol.vm", context);
OutputFile outputFile = new OutputFile();
String mangledName = mangle(protocol.getName());
outputFile.path = makePath(mangledName, protocol.getNamespace());
outputFile.contents = out;
outputFile.outputCharacterEncoding = outputCharacterEncoding;
return outputFile;
}
// package private for testing purposes
String makePath(String name, String space) {
if (space == null || space.isEmpty()) {
return name + suffix;
} else {
return space.replace('.', File.separatorChar) + File.separatorChar + name + suffix;
}
}
/**
* Returns the number of parameter units required by fields for the
* AllArgsConstructor.
*
* @param record a Record schema
*/
protected int calcAllArgConstructorParameterUnits(Schema record) {
if (record.getType() != Schema.Type.RECORD)
throw new RuntimeException("This method must only be called for record schemas.");
return record.getFields().size();
}
protected void validateRecordForCompilation(Schema record) {
this.createAllArgsConstructor = calcAllArgConstructorParameterUnits(record) <= MAX_FIELD_PARAMETER_UNIT_COUNT;
if (!this.createAllArgsConstructor) {
Logger logger = LoggerFactory.getLogger(SpecificCompiler.class);
logger.warn("Record '" + record.getFullName() + "' contains more than " + MAX_FIELD_PARAMETER_UNIT_COUNT
+ " parameters which exceeds the JVM "
+ "spec for the number of permitted constructor arguments. Clients must "
+ "rely on the builder pattern to create objects instead. For more info " + "see JIRA ticket AVRO-1642.");
}
}
OutputFile compile(Schema schema) {
schema = addStringType(schema); // annotate schema as needed
String output = "";
VelocityContext context = new VelocityContext();
context.put("this", this);
context.put("schema", schema);
for (Object velocityTool : additionalVelocityTools) {
String toolName = velocityTool.getClass().getSimpleName().toLowerCase();
context.put(toolName, velocityTool);
}
switch (schema.getType()) {
case RECORD:
validateRecordForCompilation(schema);
output = renderTemplate(templateDir + "record.vm", context);
break;
case ENUM:
output = renderTemplate(templateDir + "enum.vm", context);
break;
case FIXED:
output = renderTemplate(templateDir + "fixed.vm", context);
break;
case BOOLEAN:
case NULL:
break;
default:
throw new RuntimeException("Unknown type: " + schema);
}
OutputFile outputFile = new OutputFile();
String name = mangle(schema.getName());
outputFile.path = makePath(name, schema.getNamespace());
outputFile.contents = output;
outputFile.outputCharacterEncoding = outputCharacterEncoding;
return outputFile;
}
private StringType stringType = StringType.CharSequence;
/** Set the Java type to be emitted for string schemas. */
public void setStringType(StringType t) {
this.stringType = t;
}
// annotate map and string schemas with string type
private Protocol addStringType(Protocol p) {
if (stringType != StringType.String)
return p;
Protocol newP = new Protocol(p.getName(), p.getDoc(), p.getNamespace());
Map<Schema, Schema> types = new LinkedHashMap<>();
for (Map.Entry<String, Object> a : p.getObjectProps().entrySet()) {
newP.addProp(a.getKey(), a.getValue());
}
// annotate types
Collection<Schema> namedTypes = new LinkedHashSet<>();
for (Schema s : p.getTypes())
namedTypes.add(addStringType(s, types));
newP.setTypes(namedTypes);
// annotate messages
Map<String, Message> newM = newP.getMessages();
for (Message m : p.getMessages().values())
newM.put(m.getName(),
m.isOneWay() ? newP.createMessage(m, addStringType(m.getRequest(), types))
: newP.createMessage(m, addStringType(m.getRequest(), types), addStringType(m.getResponse(), types),
addStringType(m.getErrors(), types)));
return newP;
}
private Schema addStringType(Schema s) {
if (stringType != StringType.String)
return s;
return addStringType(s, new HashMap<>());
}
// annotate map and string schemas with string type
private Schema addStringType(Schema s, Map<Schema, Schema> seen) {
if (seen.containsKey(s))
return seen.get(s); // break loops
Schema result = s;
switch (s.getType()) {
case STRING:
result = Schema.create(Schema.Type.STRING);
if (s.getLogicalType() == null) {
GenericData.setStringType(result, stringType);
}
break;
case RECORD:
result = Schema.createRecord(s.getFullName(), s.getDoc(), null, s.isError());
for (String alias : s.getAliases())
result.addAlias(alias, null); // copy aliases
seen.put(s, result);
List<Field> newFields = new ArrayList<>(s.getFields().size());
for (Field f : s.getFields()) {
Schema fSchema = addStringType(f.schema(), seen);
Field newF = new Field(f, fSchema);
newFields.add(newF);
}
result.setFields(newFields);
break;
case ARRAY:
Schema e = addStringType(s.getElementType(), seen);
result = Schema.createArray(e);
break;
case MAP:
Schema v = addStringType(s.getValueType(), seen);
result = Schema.createMap(v);
GenericData.setStringType(result, stringType);
break;
case UNION:
List<Schema> types = new ArrayList<>(s.getTypes().size());
for (Schema branch : s.getTypes())
types.add(addStringType(branch, seen));
result = Schema.createUnion(types);
break;
}
result.addAllProps(s);
if (s.getLogicalType() != null) {
s.getLogicalType().addToSchema(result);
}
seen.put(s, result);
return result;
}
/**
* Utility for template use (and also internal use). Returns a string giving the
* FQN of the Java type to be used for a string schema or for the key of a map
* schema. (It's an error to call this on a schema other than a string or map.)
*/
public String getStringType(Schema s) {
String prop;
switch (s.getType()) {
case MAP:
prop = SpecificData.KEY_CLASS_PROP;
break;
case STRING:
prop = SpecificData.CLASS_PROP;
break;
default:
throw new IllegalArgumentException("Can't check string-type of non-string/map type: " + s);
}
return getStringType(s.getObjectProp(prop));
}
private String getStringType(Object overrideClassProperty) {
if (overrideClassProperty != null)
return overrideClassProperty.toString();
switch (stringType) {
case String:
return "java.lang.String";
case Utf8:
return "org.apache.avro.util.Utf8";
case CharSequence:
return "java.lang.CharSequence";
default:
throw new RuntimeException("Unknown string type: " + stringType);
}
}
/**
* Utility for template use. Returns true iff a STRING-schema or the key of a
* MAP-schema is what SpecificData defines as "stringable" (which means we need
* to call toString on it before before writing it).
*/
public boolean isStringable(Schema schema) {
String t = getStringType(schema);
return !(t.equals("java.lang.String") || t.equals("java.lang.CharSequence")
|| t.equals("org.apache.avro.util.Utf8"));
}
private static final Schema NULL_SCHEMA = Schema.create(Schema.Type.NULL);
/** Utility for template use. Returns the java type for a Schema. */
public String javaType(Schema schema) {
return javaType(schema, true);
}
private String javaType(Schema schema, boolean checkConvertedLogicalType) {
if (checkConvertedLogicalType) {
String convertedLogicalType = getConvertedLogicalType(schema);
if (convertedLogicalType != null) {
return convertedLogicalType;
}
}
switch (schema.getType()) {
case RECORD:
case ENUM:
case FIXED:
return mangle(schema.getFullName());
case ARRAY:
return "java.util.List<" + javaType(schema.getElementType()) + ">";
case MAP:
return "java.util.Map<" + getStringType(schema.getObjectProp(SpecificData.KEY_CLASS_PROP)) + ","
+ javaType(schema.getValueType()) + ">";
case UNION:
List<Schema> types = schema.getTypes(); // elide unions with null
if ((types.size() == 2) && types.contains(NULL_SCHEMA))
return javaType(types.get(types.get(0).equals(NULL_SCHEMA) ? 1 : 0));
return "java.lang.Object";
case STRING:
return getStringType(schema.getObjectProp(SpecificData.CLASS_PROP));
case BYTES:
return "java.nio.ByteBuffer";
case INT:
return "java.lang.Integer";
case LONG:
return "java.lang.Long";
case FLOAT:
return "java.lang.Float";
case DOUBLE:
return "java.lang.Double";
case BOOLEAN:
return "java.lang.Boolean";
case NULL:
return "java.lang.Void";
default:
throw new RuntimeException("Unknown type: " + schema);
}
}
private String getConvertedLogicalType(Schema schema) {
if (enableDecimalLogicalType || !(schema.getLogicalType() instanceof LogicalTypes.Decimal)) {
Conversion<?> conversion = specificData.getConversionFor(schema.getLogicalType());
if (conversion != null) {
return conversion.getConvertedType().getName();
}
}
return null;
}
/**
* Utility for template use.
*/
public String generateSetterCode(Schema schema, String name, String pname) {
Conversion<?> conversion = specificData.getConversionFor(schema.getLogicalType());
if (conversion != null) {
return conversion.adjustAndSetValue("this." + name, pname);
}
return "this." + name + " = " + pname + ";";
}
/**
* Utility for template use. Returns the unboxed java type for a Schema.
*
* @deprecated use javaUnbox(Schema, boolean), kept for backward compatibiliby
* of custom templates
*/
@Deprecated
public String javaUnbox(Schema schema) {
return javaUnbox(schema, false);
}
/**
* Utility for template use. Returns the unboxed java type for a Schema
* including the void type.
*/
public String javaUnbox(Schema schema, boolean unboxNullToVoid) {
String convertedLogicalType = getConvertedLogicalType(schema);
if (convertedLogicalType != null) {
return convertedLogicalType;
}
switch (schema.getType()) {
case INT:
return "int";
case LONG:
return "long";
case FLOAT:
return "float";
case DOUBLE:
return "double";
case BOOLEAN:
return "boolean";
case NULL:
if (unboxNullToVoid) {
// Used for preventing unnecessary returns for RPC methods without response but
// with error(s)
return "void";
}
default:
return javaType(schema, false);
}
}
/**
* Utility for template use. Return a string with a given number of spaces to be
* used for indentation purposes.
*/
public String indent(int n) {
return new String(new char[n]).replace('\0', ' ');
}
/**
* Utility for template use. For a two-branch union type with one null branch,
* returns the index of the null branch. It's an error to use on anything other
* than a two-branch union with on null branch.
*/
public int getNonNullIndex(Schema s) {
if (s.getType() != Schema.Type.UNION || s.getTypes().size() != 2 || !s.getTypes().contains(NULL_SCHEMA))
throw new IllegalArgumentException("Can only be used on 2-branch union with a null branch: " + s);
return (s.getTypes().get(0).equals(NULL_SCHEMA) ? 1 : 0);
}
/**
* Utility for template use. Returns true if the encode/decode logic in
* 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<Schema> seen) {
if (!seen.add(schema))
return true;
if (schema.getLogicalType() != null)
return false;
boolean result = true;
switch (schema.getType()) {
case RECORD:
for (Schema.Field f : schema.getFields())
result &= isCustomCodable(f.schema(), seen);
break;
case MAP:
result = isCustomCodable(schema.getValueType(), seen);
break;
case ARRAY:
result = isCustomCodable(schema.getElementType(), seen);
break;
case UNION:
List<Schema> types = schema.getTypes();
// Only know how to handle "nulling" unions for now
if (types.size() != 2 || !types.contains(NULL_SCHEMA))
return false;
for (Schema s : types)
result &= isCustomCodable(s, seen);
break;
default:
}
return result;
}
public boolean hasLogicalTypeField(Schema schema) {
for (Schema.Field field : schema.getFields()) {
if (field.schema().getLogicalType() != null) {
return true;
}
}
return false;
}
public String conversionInstance(Schema schema) {
if (schema == null || schema.getLogicalType() == null) {
return "null";
}
if (LogicalTypes.Decimal.class.equals(schema.getLogicalType().getClass()) && !enableDecimalLogicalType) {
return "null";
}
final Conversion<Object> conversion = specificData.getConversionFor(schema.getLogicalType());
if (conversion != null) {
return "new " + conversion.getClass().getCanonicalName() + "()";
}
return "null";
}
/** Utility for template use. Returns the java annotations for a schema. */
public String[] javaAnnotations(JsonProperties props) {
final Object value = props.getObjectProp("javaAnnotation");
if (value == null)
return new String[0];
if (value instanceof String)
return new String[] { value.toString() };
if (value instanceof List) {
final List<?> list = (List<?>) value;
final List<String> annots = new ArrayList<>(list.size());
for (Object o : list) {
annots.add(o.toString());
}
return annots.toArray(new String[0]);
}
return new String[0];
}
// maximum size for string constants, to avoid javac limits
int maxStringChars = 8192;
/**
* Utility for template use. Takes a (potentially overly long) string and splits
* it into a quoted, comma-separted sequence of escaped strings.
*
* @param s The string to split
* @return A sequence of quoted, comma-separated, escaped strings
*/
public String javaSplit(String s) throws IOException {
StringBuilder b = new StringBuilder(s.length());
b.append("\""); // initial quote
for (int i = 0; i < s.length(); i += maxStringChars) {
if (i != 0)
b.append("\",\""); // insert quote-comma-quote
String chunk = s.substring(i, Math.min(s.length(), i + maxStringChars));
b.append(javaEscape(chunk)); // escape chunks
}
b.append("\""); // final quote
return b.toString();
}
/** Utility for template use. Escapes quotes and backslashes. */
public static String javaEscape(String o) {
return o.replace("\\", "\\\\").replace("\"", "\\\"");
}
/** Utility for template use. Escapes comment end with HTML entities. */
public static String escapeForJavadoc(String s) {
return s.replace("*/", "*&#47;");
}
/** Utility for template use. Returns empty string for null. */
public static String nullToEmpty(String x) {
return x == null ? "" : x;
}
/** Utility for template use. Adds a dollar sign to reserved words. */
public static String mangle(String word) {
return mangle(word, false);
}
/** Utility for template use. Adds a dollar sign to reserved words. */
public static String mangle(String word, boolean isError) {
return mangle(word, isError ? ERROR_RESERVED_WORDS : RESERVED_WORDS);
}
/** Utility for template use. Adds a dollar sign to reserved words. */
public static String mangle(String word, Set<String> reservedWords) {
return mangle(word, reservedWords, false);
}
/** Utility for template use. Adds a dollar sign to reserved words. */
public static String mangle(String word, Set<String> reservedWords, boolean isMethod) {
if (word.contains(".")) {
// If the 'word' is really a full path of a class we must mangle just the
// classname
int lastDot = word.lastIndexOf(".");
String packageName = word.substring(0, lastDot + 1);
String className = word.substring(lastDot + 1);
return packageName + mangle(className, reservedWords, isMethod);
}
if (reservedWords.contains(word) || (isMethod && reservedWords
.contains(Character.toLowerCase(word.charAt(0)) + ((word.length() > 1) ? word.substring(1) : "")))) {
return word + "$";
}
return word;
}
/** Utility for use by templates. Return schema fingerprint as a long. */
public static long fingerprint64(Schema schema) {
return SchemaNormalization.parsingFingerprint64(schema);
}
/**
* Generates the name of a field accessor method.
*
* @param schema the schema in which the field is defined.
* @param field the field for which to generate the accessor name.
* @return the name of the accessor method for the given field.
*/
public static String generateGetMethod(Schema schema, Field field) {
return generateMethodName(schema, field, "get", "");
}
/**
* Generates the name of a field accessor method that returns a Java 8 Optional.
*
* @param schema the schema in which the field is defined.
* @param field the field for which to generate the accessor name.
* @return the name of the accessor method for the given field.
*/
public static String generateGetOptionalMethod(Schema schema, Field field) {
return generateMethodName(schema, field, "getOptional", "");
}
/**
* Generates the name of a field mutator method.
*
* @param schema the schema in which the field is defined.
* @param field the field for which to generate the mutator name.
* @return the name of the mutator method for the given field.
*/
public static String generateSetMethod(Schema schema, Field field) {
return generateMethodName(schema, field, "set", "");
}
/**
* Generates the name of a field "has" method.
*
* @param schema the schema in which the field is defined.
* @param field the field for which to generate the "has" method name.
* @return the name of the has method for the given field.
*/
public static String generateHasMethod(Schema schema, Field field) {
return generateMethodName(schema, field, "has", "");
}
/**
* Generates the name of a field "clear" method.
*
* @param schema the schema in which the field is defined.
* @param field the field for which to generate the accessor name.
* @return the name of the has method for the given field.
*/
public static String generateClearMethod(Schema schema, Field field) {
return generateMethodName(schema, field, "clear", "");
}
/** Utility for use by templates. Does this schema have a Builder method? */
public static boolean hasBuilder(Schema schema) {
switch (schema.getType()) {
case RECORD:
return true;
case UNION:
List<Schema> types = schema.getTypes(); // elide unions with null
if ((types.size() == 2) && types.contains(NULL_SCHEMA)) {
return hasBuilder(types.get(types.get(0).equals(NULL_SCHEMA) ? 1 : 0));
}
return false;
default:
return false;
}
}
/**
* Generates the name of a field Builder accessor method.
*
* @param schema the schema in which the field is defined.
* @param field the field for which to generate the Builder accessor name.
* @return the name of the Builder accessor method for the given field.
*/
public static String generateGetBuilderMethod(Schema schema, Field field) {
return generateMethodName(schema, field, "get", "Builder");
}
/**
* Generates the name of a field Builder mutator method.
*
* @param schema the schema in which the field is defined.
* @param field the field for which to generate the Builder mutator name.
* @return the name of the Builder mutator method for the given field.
*/
public static String generateSetBuilderMethod(Schema schema, Field field) {
return generateMethodName(schema, field, "set", "Builder");
}
/**
* Generates the name of a field Builder "has" method.
*
* @param schema the schema in which the field is defined.
* @param field the field for which to generate the "has" Builder method name.
* @return the name of the "has" Builder method for the given field.
*/
public static String generateHasBuilderMethod(Schema schema, Field field) {
return generateMethodName(schema, field, "has", "Builder");
}
/**
* Generates a method name from a field name.
*
* @param schema the schema in which the field is defined.
* @param field the field for which to generate the accessor name.
* @param prefix method name prefix, e.g. "get" or "set".
* @param postfix method name postfix, e.g. "" or "Builder".
* @return the generated method name.
*/
private static String generateMethodName(Schema schema, Field field, String prefix, String postfix) {
// 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;
StringBuilder methodBuilder = new StringBuilder(prefix);
String fieldName = mangle(field.name(), schema.isError() ? ERROR_RESERVED_WORDS : ACCESSOR_MUTATOR_RESERVED_WORDS,
true);
boolean nextCharToUpper = true;
for (int ii = 0; ii < fieldName.length(); ii++) {
if (fieldName.charAt(ii) == '_') {
nextCharToUpper = true;
} else if (nextCharToUpper) {
methodBuilder.append(Character.toUpperCase(fieldName.charAt(ii)));
nextCharToUpper = false;
} else {
methodBuilder.append(fieldName.charAt(ii));
}
}
methodBuilder.append(postfix);
// If there is a field name conflict append $0 or $1
if (fieldNameConflict) {
if (methodBuilder.charAt(methodBuilder.length() - 1) != '$') {
methodBuilder.append('$');
}
methodBuilder.append(Character.isLowerCase(firstChar) ? '0' : '1');
}
return methodBuilder.toString();
}
/** Tests whether an unboxed Java type can be set to null */
public static boolean isUnboxedJavaTypeNullable(Schema schema) {
switch (schema.getType()) {
// Primitives can't be null; assume anything else can
case INT:
case LONG:
case FLOAT:
case DOUBLE:
case BOOLEAN:
return false;
default:
return true;
}
}
public static void main(String[] args) throws Exception {
// compileSchema(new File(args[0]), new File(args[1]));
compileProtocol(new File(args[0]), new File(args[1]));
}
/**
* Sets character encoding for generated java file
*
* @param outputCharacterEncoding Character encoding for output files (defaults
* to system encoding)
*/
public void setOutputCharacterEncoding(String outputCharacterEncoding) {
this.outputCharacterEncoding = outputCharacterEncoding;
}
}