blob: 448a7682d5a7b6fefb164b870d81930ab325d58c [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.avro.compiler.specific;
import java.io.File;
import java.io.FileOutputStream;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.io.FileWriter;
import java.io.IOException;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
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.DateConversion;
import org.apache.avro.data.TimeConversions.TimeConversion;
import org.apache.avro.data.TimeConversions.TimeMicrosConversion;
import org.apache.avro.data.TimeConversions.TimestampConversion;
import org.apache.avro.data.TimeConversions.TimestampMicrosConversion;
import org.apache.avro.specific.SpecificData;
import org.codehaus.jackson.JsonNode;
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.apache.velocity.runtime.RuntimeServices;
import org.apache.velocity.runtime.log.LogChute;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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="http://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 static enum FieldVisibility {
PUBLIC, PUBLIC_DEPRECATED, PRIVATE
}
private static final SpecificData SPECIFIC = new SpecificData();
static {
SPECIFIC.addLogicalTypeConversion(new DateConversion());
SPECIFIC.addLogicalTypeConversion(new TimeConversion());
SPECIFIC.addLogicalTypeConversion(new TimestampConversion());
SPECIFIC.addLogicalTypeConversion(new TimeMicrosConversion());
SPECIFIC.addLogicalTypeConversion(new TimestampMicrosConversion());
SPECIFIC.addLogicalTypeConversion(new Conversions.DecimalConversion());
}
private final Set<Schema> queue = new HashSet<Schema>();
private Protocol protocol;
private VelocityEngine velocityEngine;
private String templateDir;
private FieldVisibility fieldVisibility = FieldVisibility.PUBLIC_DEPRECATED;
private boolean createSetters = true;
private boolean createAllArgsConstructor = true;
private String outputCharacterEncoding;
private boolean enableDecimalLogicalType = false;
private String suffix = ".java";
/*
* 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<String>(Arrays.asList(new String[] {
"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<String>(
Arrays.asList(new String[] { "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;
}
SpecificCompiler() {
this.templateDir =
System.getProperty("org.apache.avro.specific.templates",
"/org/apache/avro/compiler/specific/templates/java/classic/");
initializeVelocity();
}
/** 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;
}
/**
* 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;
}
private static String logChuteName = null;
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.loader", "class, file");
velocityEngine.addProperty("class.resource.loader.class",
"org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader");
velocityEngine.addProperty("file.resource.loader.class",
"org.apache.velocity.runtime.resource.loader.FileResourceLoader");
velocityEngine.addProperty("file.resource.loader.path", "/, .");
velocityEngine.setProperty("runtime.references.strict", true);
// try to use Slf4jLogChute, but if we can't use the null one.
if (null == logChuteName) {
// multiple threads can get here concurrently, but that's ok.
try {
new Slf4jLogChute();
logChuteName = Slf4jLogChute.class.getName();
} catch (Exception e) {
logChuteName = "org.apache.velocity.runtime.log.NullLogChute";
}
}
velocityEngine.setProperty("runtime.log.logsystem.class", logChuteName);
}
/**
* 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;
if (outputCharacterEncoding != null) {
fw = new OutputStreamWriter(new FileOutputStream(f), outputCharacterEncoding);
} else {
fw = new FileWriter(f);
}
try {
fw.write(FILE_HEADER);
fw.write(contents);
} finally {
fw.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<OutputFile>();
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);
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)
new Slf4jLogChute().log(LogChute.WARN_ID, "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);
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<Schema,Schema>();
// Copy properties
for (Map.Entry<String,JsonNode> prop : p.getJsonProps().entrySet())
newP.addProp(prop.getKey(), prop.getValue()); // copy props
// annotate types
Collection<Schema> namedTypes = new LinkedHashSet<Schema>();
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.getName(), m.getDoc(), m.getJsonProps(),
addStringType(m.getRequest(), types))
: newP.createMessage(m.getName(), m.getDoc(), m.getJsonProps(),
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 LinkedHashMap<Schema,Schema>());
}
// 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);
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<Field>();
for (Field f : s.getFields()) {
Schema fSchema = addStringType(f.schema(), seen);
Field newF =
new Field(f.name(), fSchema, f.doc(), f.defaultValue(), f.order());
for (Map.Entry<String,JsonNode> p : f.getJsonProps().entrySet())
newF.addProp(p.getKey(), p.getValue()); // copy props
for (String a : f.aliases())
newF.addAlias(a); // copy aliases
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<Schema>();
for (Schema branch : s.getTypes())
types.add(addStringType(branch, seen));
result = Schema.createUnion(types);
break;
}
for (Map.Entry<String,JsonNode> p : s.getJsonProps().entrySet())
result.addProp(p.getKey(), p.getValue()); // copy props
seen.put(s, result);
return result;
}
private String getStringType(JsonNode overrideClassProperty) {
if (overrideClassProperty != null)
return overrideClassProperty.getTextValue();
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);
}
}
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.getJsonProp(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.getJsonProp(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 = SPECIFIC
.getConversionFor(schema.getLogicalType());
if (conversion != null) {
return conversion.getConvertedType().getName();
}
}
return null;
}
/** Utility for template use. Returns the unboxed java type for a Schema. */
public String javaUnbox(Schema schema) {
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";
default: return javaType(schema, false);
}
}
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.date().equals(schema.getLogicalType())) {
return "DATE_CONVERSION";
} else if (LogicalTypes.timeMillis().equals(schema.getLogicalType())) {
return "TIME_CONVERSION";
} else if (LogicalTypes.timeMicros().equals(schema.getLogicalType())) {
return "TIME_MICROS_CONVERSION";
} else if (LogicalTypes.timestampMillis().equals(schema.getLogicalType())) {
return "TIMESTAMP_CONVERSION";
} else if (LogicalTypes.timestampMicros().equals(schema.getLogicalType())) {
return "TIMESTAMP_MICROS_CONVERSION";
} else if (LogicalTypes.Decimal.class.equals(schema.getLogicalType().getClass())) {
return enableDecimalLogicalType ? "DECIMAL_CONVERSION" : "null";
}
return "null";
}
/** Utility for template use. Returns the java annotations for a schema. */
public String[] javaAnnotations(JsonProperties props) {
JsonNode value = props.getJsonProp("javaAnnotation");
if (value == null)
return new String[0];
if (value.isTextual())
return new String[] { value.getTextValue() };
if (value.isArray()) {
int i = 0;
String[] result = new String[value.size()];
for (JsonNode v : value)
result[i++] = v.getTextValue();
return result;
}
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("\""); // 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(Object o) {
return o.toString().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 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]));
}
public static final class Slf4jLogChute implements LogChute {
private Logger logger = LoggerFactory.getLogger("AvroVelocityLogChute");
@Override
public void init(RuntimeServices rs) throws Exception {
// nothing to do
}
@Override
public void log(int level, String message) {
switch (level) {
case LogChute.DEBUG_ID:
logger.debug(message);
break;
case LogChute.TRACE_ID:
logger.trace(message);
break;
case LogChute.WARN_ID:
logger.warn(message);
break;
case LogChute.ERROR_ID:
logger.error(message);
break;
default:
case LogChute.INFO_ID:
logger.info(message);
break;
}
}
@Override
public void log(int level, String message, Throwable t) {
switch (level) {
case LogChute.DEBUG_ID:
logger.debug(message, t);
break;
case LogChute.TRACE_ID:
logger.trace(message, t);
break;
case LogChute.WARN_ID:
logger.warn(message, t);
break;
case LogChute.ERROR_ID:
logger.error(message, t);
break;
default:
case LogChute.INFO_ID:
logger.info(message, t);
break;
}
}
@Override
public boolean isLevelEnabled(int level) {
switch (level) {
case LogChute.DEBUG_ID:
return logger.isDebugEnabled();
case LogChute.TRACE_ID:
return logger.isTraceEnabled();
case LogChute.WARN_ID:
return logger.isWarnEnabled();
case LogChute.ERROR_ID:
return logger.isErrorEnabled();
default:
case LogChute.INFO_ID:
return logger.isInfoEnabled();
}
}
}
/** 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;
}
}