com.microsoft.thrifty.gen.ThriftyCodeGenerator Maven / Gradle / Ivy
Show all versions of thrifty-java-codegen Show documentation
/*
* Thrifty
*
* Copyright (c) Microsoft Corporation
*
* All rights reserved.
*
* 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
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR
* CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING
* WITHOUT LIMITATION ANY IMPLIED WARRANTIES OR CONDITIONS OF TITLE,
* FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABLITY OR NON-INFRINGEMENT.
*
* See the Apache Version 2.0 License for specific language governing permissions and limitations under the License.
*/
package com.microsoft.thrifty.gen;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
import com.google.common.base.Strings;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Multimap;
import com.microsoft.thrifty.Obfuscated;
import com.microsoft.thrifty.Redacted;
import com.microsoft.thrifty.Struct;
import com.microsoft.thrifty.TType;
import com.microsoft.thrifty.ThriftField;
import com.microsoft.thrifty.compiler.spi.TypeProcessor;
import com.microsoft.thrifty.protocol.Protocol;
import com.microsoft.thrifty.schema.BuiltinType;
import com.microsoft.thrifty.schema.Constant;
import com.microsoft.thrifty.schema.EnumMember;
import com.microsoft.thrifty.schema.EnumType;
import com.microsoft.thrifty.schema.Field;
import com.microsoft.thrifty.schema.FieldNamingPolicy;
import com.microsoft.thrifty.schema.ListType;
import com.microsoft.thrifty.schema.Location;
import com.microsoft.thrifty.schema.MapType;
import com.microsoft.thrifty.schema.NamespaceScope;
import com.microsoft.thrifty.schema.Schema;
import com.microsoft.thrifty.schema.ServiceType;
import com.microsoft.thrifty.schema.SetType;
import com.microsoft.thrifty.schema.StructType;
import com.microsoft.thrifty.schema.ThriftType;
import com.microsoft.thrifty.schema.TypedefType;
import com.microsoft.thrifty.schema.UserType;
import com.squareup.javapoet.AnnotationSpec;
import com.squareup.javapoet.ArrayTypeName;
import com.squareup.javapoet.ClassName;
import com.squareup.javapoet.CodeBlock;
import com.squareup.javapoet.FieldSpec;
import com.squareup.javapoet.JavaFile;
import com.squareup.javapoet.MethodSpec;
import com.squareup.javapoet.NameAllocator;
import com.squareup.javapoet.ParameterizedTypeName;
import com.squareup.javapoet.TypeName;
import com.squareup.javapoet.TypeSpec;
import org.joda.time.format.DateTimeFormatter;
import org.joda.time.format.ISODateTimeFormat;
import javax.annotation.Nullable;
import javax.lang.model.element.Modifier;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
public final class ThriftyCodeGenerator {
private static final String FILE_COMMENT =
"Automatically generated by the Thrifty compiler; do not edit!\n"
+ "Generated on: ";
public static final String ADAPTER_FIELDNAME = "ADAPTER";
private static final DateTimeFormatter DATE_FORMATTER =
ISODateTimeFormat.dateTime().withZoneUTC();
private final TypeResolver typeResolver = new TypeResolver();
private final Schema schema;
private final FieldNamer fieldNamer;
private final ConstantBuilder constantBuilder;
private final ServiceBuilder serviceBuilder;
private TypeProcessor typeProcessor;
private boolean emitAndroidAnnotations;
private boolean emitParcelable;
private boolean emitFileComment = true;
public ThriftyCodeGenerator(Schema schema) {
this(schema, FieldNamingPolicy.DEFAULT);
}
public ThriftyCodeGenerator(Schema schema, FieldNamingPolicy namingPolicy) {
this(
schema,
namingPolicy,
ClassName.get(ArrayList.class),
ClassName.get(HashSet.class),
ClassName.get(HashMap.class));
}
private ThriftyCodeGenerator(
Schema schema,
FieldNamingPolicy namingPolicy,
ClassName listClassName,
ClassName setClassName,
ClassName mapClassName) {
Preconditions.checkNotNull(schema, "schema");
Preconditions.checkNotNull(namingPolicy, "namingPolicy");
Preconditions.checkNotNull(listClassName, "listClassName");
Preconditions.checkNotNull(setClassName, "setClassName");
Preconditions.checkNotNull(mapClassName, "mapClassName");
this.schema = schema;
this.fieldNamer = new FieldNamer(namingPolicy);
typeResolver.setListClass(listClassName);
typeResolver.setSetClass(setClassName);
typeResolver.setMapClass(mapClassName);
constantBuilder = new ConstantBuilder(typeResolver, schema);
serviceBuilder = new ServiceBuilder(typeResolver, constantBuilder, fieldNamer);
}
public ThriftyCodeGenerator withListType(String listClassName) {
typeResolver.setListClass(ClassName.bestGuess(listClassName));
return this;
}
public ThriftyCodeGenerator withSetType(String setClassName) {
typeResolver.setSetClass(ClassName.bestGuess(setClassName));
return this;
}
public ThriftyCodeGenerator withMapType(String mapClassName) {
typeResolver.setMapClass(ClassName.bestGuess(mapClassName));
return this;
}
public ThriftyCodeGenerator emitAndroidAnnotations(boolean shouldEmit) {
emitAndroidAnnotations = shouldEmit;
return this;
}
public ThriftyCodeGenerator emitParcelable(boolean emitParcelable) {
this.emitParcelable = emitParcelable;
return this;
}
public ThriftyCodeGenerator emitFileComment(boolean emitFileComment) {
this.emitFileComment = emitFileComment;
return this;
}
public ThriftyCodeGenerator usingTypeProcessor(TypeProcessor typeProcessor) {
this.typeProcessor = typeProcessor;
return this;
}
public void generate(final File directory) throws IOException {
generate(new FileWriter() {
@Override
public void write(JavaFile file) throws IOException {
if (file != null) {
file.writeTo(directory);
}
}
});
}
public void generate(final Appendable appendable) throws IOException {
generate(new FileWriter() {
@Override
public void write(JavaFile file) throws IOException {
if (file != null) {
file.writeTo(appendable);
}
}
});
}
public ImmutableList generateTypes() {
ImmutableList.Builder generatedTypes = ImmutableList.builder();
for (EnumType type : schema.enums()) {
TypeSpec spec = buildEnum(type);
JavaFile file = assembleJavaFile(type, spec);
if (file != null) {
generatedTypes.add(file);
}
}
for (StructType type : schema.structs()) {
TypeSpec spec = buildStruct(type);
JavaFile file = assembleJavaFile(type, spec);
if (file != null) {
generatedTypes.add(file);
}
}
for (StructType type : schema.exceptions()) {
TypeSpec spec = buildStruct(type);
JavaFile file = assembleJavaFile(type, spec);
if (file != null) {
generatedTypes.add(file);
}
}
for (StructType type : schema.unions()) {
TypeSpec spec = buildStruct(type);
JavaFile file = assembleJavaFile(type, spec);
if (file != null) {
generatedTypes.add(file);
}
}
Multimap constantsByPackage = HashMultimap.create();
for (Constant constant : schema.constants()) {
constantsByPackage.put(constant.getNamespaceFor(NamespaceScope.JAVA), constant);
}
for (Map.Entry> entry : constantsByPackage.asMap().entrySet()) {
String packageName = entry.getKey();
Collection values = entry.getValue();
TypeSpec spec = buildConst(values);
JavaFile file = assembleJavaFile(packageName, spec);
if (file != null) {
generatedTypes.add(file);
}
}
for (ServiceType type : schema.services()) {
TypeSpec spec = serviceBuilder.buildServiceInterface(type);
JavaFile file = assembleJavaFile(type, spec);
if (file == null) {
continue;
}
generatedTypes.add(file);
spec = serviceBuilder.buildService(type, spec);
file = assembleJavaFile(type, spec);
if (file != null) {
generatedTypes.add(file);
}
}
return generatedTypes.build();
}
private interface FileWriter {
void write(@Nullable JavaFile file) throws IOException;
}
private void generate(FileWriter writer) throws IOException {
for (JavaFile file : generateTypes()) {
writer.write(file);
}
}
@Nullable
private JavaFile assembleJavaFile(UserType named, TypeSpec spec) {
String packageName = named.getNamespaceFor(NamespaceScope.JAVA);
if (Strings.isNullOrEmpty(packageName)) {
throw new IllegalArgumentException("A Java package name must be given for java code generation");
}
return assembleJavaFile(packageName, spec, named.location());
}
@Nullable
private JavaFile assembleJavaFile(String packageName, TypeSpec spec) {
return assembleJavaFile(packageName, spec, null);
}
@Nullable
private JavaFile assembleJavaFile(String packageName, TypeSpec spec, Location location) {
if (typeProcessor != null) {
spec = typeProcessor.process(spec);
if (spec == null) {
return null;
}
}
JavaFile.Builder file = JavaFile.builder(packageName, spec)
.skipJavaLangImports(true);
if (emitFileComment) {
file.addFileComment(FILE_COMMENT + DATE_FORMATTER.print(System.currentTimeMillis()));
if (location != null) {
file.addFileComment("\nSource: $L", location);
}
}
return file.build();
}
@VisibleForTesting
@SuppressWarnings("WeakerAccess")
TypeSpec buildStruct(StructType type) {
String packageName = type.getNamespaceFor(NamespaceScope.JAVA);
ClassName structTypeName = ClassName.get(packageName, type.name());
ClassName builderTypeName = structTypeName.nestedClass("Builder");
TypeSpec.Builder structBuilder = TypeSpec.classBuilder(type.name())
.addModifiers(Modifier.PUBLIC, Modifier.FINAL)
.addSuperinterface(Struct.class);
if (type.hasJavadoc()) {
structBuilder.addJavadoc(type.documentation());
}
if (type.isException()) {
structBuilder.superclass(Exception.class);
}
if (type.isDeprecated()) {
structBuilder.addAnnotation(AnnotationSpec.builder(Deprecated.class).build());
}
TypeSpec builderSpec = builderFor(type, structTypeName, builderTypeName);
TypeSpec adapterSpec = adapterFor(type, structTypeName, builderTypeName);
if (emitParcelable) {
generateParcelable(type, structTypeName, structBuilder);
}
structBuilder.addType(builderSpec);
structBuilder.addType(adapterSpec);
structBuilder.addField(FieldSpec.builder(adapterSpec.superinterfaces.get(0), ADAPTER_FIELDNAME)
.addModifiers(Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL)
.initializer("new $N()", adapterSpec)
.build());
MethodSpec.Builder ctor = MethodSpec.constructorBuilder()
.addModifiers(Modifier.PRIVATE)
.addParameter(builderTypeName, "builder");
for (Field field : type.fields()) {
String name = fieldNamer.getName(field);
ThriftType fieldType = field.type();
ThriftType trueType = fieldType.getTrueType();
TypeName fieldTypeName = typeResolver.getJavaClass(trueType);
// Define field
FieldSpec.Builder fieldBuilder = FieldSpec.builder(fieldTypeName, name)
.addModifiers(Modifier.PUBLIC, Modifier.FINAL)
.addAnnotation(fieldAnnotation(field));
if (emitAndroidAnnotations) {
ClassName anno = field.required() ? TypeNames.NOT_NULL : TypeNames.NULLABLE;
fieldBuilder.addAnnotation(anno);
}
if (field.hasJavadoc()) {
fieldBuilder = fieldBuilder.addJavadoc(field.documentation());
}
if (field.isRedacted()) {
fieldBuilder = fieldBuilder.addAnnotation(AnnotationSpec.builder(Redacted.class).build());
}
if (field.isObfuscated()) {
fieldBuilder = fieldBuilder.addAnnotation(AnnotationSpec.builder(Obfuscated.class).build());
}
if (field.isDeprecated()) {
fieldBuilder = fieldBuilder.addAnnotation(AnnotationSpec.builder(Deprecated.class).build());
}
structBuilder.addField(fieldBuilder.build());
// Update the struct ctor
CodeBlock.Builder assignment = CodeBlock.builder().add("$[this.$N = ", name);
if (trueType.isList()) {
if (!field.required()) {
assignment.add("builder.$N == null ? null : ", name);
}
assignment.add("$T.unmodifiableList(builder.$N)",
TypeNames.COLLECTIONS, name);
} else if (trueType.isSet()) {
if (!field.required()) {
assignment.add("builder.$N == null ? null : ", name);
}
assignment.add("$T.unmodifiableSet(builder.$N)",
TypeNames.COLLECTIONS, name);
} else if (trueType.isMap()) {
if (!field.required()) {
assignment.add("builder.$N == null ? null : ", name);
}
assignment.add("$T.unmodifiableMap(builder.$N)",
TypeNames.COLLECTIONS, name);
} else {
assignment.add("builder.$N", name);
}
ctor.addCode(assignment.add(";\n$]").build());
}
structBuilder.addMethod(ctor.build());
structBuilder.addMethod(buildEqualsFor(type));
structBuilder.addMethod(buildHashCodeFor(type));
structBuilder.addMethod(buildToStringFor(type));
structBuilder.addMethod(buildWrite());
return structBuilder.build();
}
private void generateParcelable(StructType structType, ClassName structName, TypeSpec.Builder structBuilder) {
structBuilder.addSuperinterface(TypeNames.PARCELABLE);
structBuilder.addField(FieldSpec.builder(ClassLoader.class, "CLASS_LOADER")
.addModifiers(Modifier.PRIVATE, Modifier.STATIC, Modifier.FINAL)
.initializer("$T.class.getClassLoader()", structName)
.build());
ParameterizedTypeName creatorType = ParameterizedTypeName.get(TypeNames.PARCELABLE_CREATOR, structName);
TypeSpec creator = TypeSpec.anonymousClassBuilder("")
.addSuperinterface(creatorType)
.addMethod(MethodSpec.methodBuilder("createFromParcel")
.addAnnotation(Override.class)
.addModifiers(Modifier.PUBLIC)
.returns(structName)
.addParameter(TypeNames.PARCEL, "source")
.addStatement("return new $T(source)", structName)
.build())
.addMethod(MethodSpec.methodBuilder("newArray")
.addAnnotation(Override.class)
.addModifiers(Modifier.PUBLIC)
.returns(ArrayTypeName.of(structName))
.addParameter(int.class, "size")
.addStatement("return new $T[size]", structName)
.build())
.build();
MethodSpec.Builder parcelCtor = MethodSpec.constructorBuilder()
.addParameter(TypeNames.PARCEL, "in")
.addModifiers(Modifier.PRIVATE);
MethodSpec.Builder parcelWriter = MethodSpec.methodBuilder("writeToParcel")
.addAnnotation(Override.class)
.addModifiers(Modifier.PUBLIC)
.addParameter(TypeNames.PARCEL, "dest")
.addParameter(int.class, "flags");
for (Field field : structType.fields()) {
String name = fieldNamer.getName(field);
TypeName fieldType = typeResolver.getJavaClass(field.type().getTrueType());
parcelCtor.addStatement("this.$N = ($T) in.readValue(CLASS_LOADER)", name, fieldType);
parcelWriter.addStatement("dest.writeValue(this.$N)", name);
}
FieldSpec creatorField = FieldSpec.builder(creatorType, "CREATOR")
.addModifiers(Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL)
.initializer("$L", creator)
.build();
structBuilder
.addField(creatorField)
.addMethod(MethodSpec.methodBuilder("describeContents")
.addAnnotation(Override.class)
.addModifiers(Modifier.PUBLIC)
.returns(int.class)
.addStatement("return 0")
.build())
.addMethod(parcelCtor.build())
.addMethod(parcelWriter.build());
}
private TypeSpec builderFor(
StructType structType,
ClassName structClassName,
ClassName builderClassName) {
TypeName builderSuperclassName = ParameterizedTypeName.get(TypeNames.BUILDER, structClassName);
TypeSpec.Builder builder = TypeSpec.classBuilder("Builder")
.addSuperinterface(builderSuperclassName)
.addModifiers(Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL);
MethodSpec.Builder buildMethodBuilder = MethodSpec.methodBuilder("build")
.addAnnotation(Override.class)
.returns(structClassName)
.addModifiers(Modifier.PUBLIC);
MethodSpec.Builder resetBuilder = MethodSpec.methodBuilder("reset")
.addAnnotation(Override.class)
.addModifiers(Modifier.PUBLIC);
MethodSpec.Builder copyCtor = MethodSpec.constructorBuilder()
.addModifiers(Modifier.PUBLIC)
.addParameter(structClassName, "struct");
MethodSpec.Builder defaultCtor = MethodSpec.constructorBuilder()
.addModifiers(Modifier.PUBLIC);
if (structType.isUnion()) {
buildMethodBuilder.addStatement("int setFields = 0");
}
// Add fields to the struct and set them in the ctor
NameAllocator allocator = new NameAllocator();
for (Field field : structType.fields()) {
String name = fieldNamer.getName(field);
allocator.newName(name, name);
}
AtomicInteger tempNameId = new AtomicInteger(0); // used for generating unique names of temporary values
for (Field field : structType.fields()) {
ThriftType fieldType = field.type().getTrueType();
TypeName javaTypeName = typeResolver.getJavaClass(fieldType);
String fieldName = fieldNamer.getName(field);
FieldSpec.Builder f = FieldSpec.builder(javaTypeName, fieldName, Modifier.PRIVATE);
if (field.hasJavadoc()) {
f.addJavadoc(field.documentation());
}
if (field.defaultValue() != null) {
CodeBlock.Builder initializer = CodeBlock.builder();
constantBuilder.generateFieldInitializer(
initializer,
allocator,
tempNameId,
"this." + fieldName,
fieldType.getTrueType(),
field.defaultValue(),
false);
defaultCtor.addCode(initializer.build());
resetBuilder.addCode(initializer.build());
} else {
resetBuilder.addStatement("this.$N = null", fieldName);
}
builder.addField(f.build());
MethodSpec.Builder setterBuilder = MethodSpec.methodBuilder(fieldName)
.addModifiers(Modifier.PUBLIC)
.returns(builderClassName)
.addParameter(javaTypeName, fieldName);
if (field.required()) {
setterBuilder.beginControlFlow("if ($N == null)", fieldName);
setterBuilder.addStatement(
"throw new $T(\"Required field '$L' cannot be null\")",
NullPointerException.class,
fieldName);
setterBuilder.endControlFlow();
}
setterBuilder
.addStatement("this.$N = $N", fieldName, fieldName)
.addStatement("return this");
builder.addMethod(setterBuilder.build());
if (structType.isUnion()) {
buildMethodBuilder
.addStatement("if (this.$N != null) ++setFields", fieldName);
} else {
if (field.required()) {
buildMethodBuilder.beginControlFlow("if (this.$N == null)", fieldName);
buildMethodBuilder.addStatement(
"throw new $T($S)",
ClassName.get(IllegalStateException.class),
"Required field '" + fieldName + "' is missing");
buildMethodBuilder.endControlFlow();
}
}
copyCtor.addStatement("this.$N = $N.$N", fieldName, "struct", fieldName);
}
if (structType.isUnion()) {
buildMethodBuilder
.beginControlFlow("if (setFields != 1)")
.addStatement(
"throw new $T($S + setFields + $S)",
ClassName.get(IllegalStateException.class),
"Invalid union; ",
" field(s) were set")
.endControlFlow();
}
buildMethodBuilder.addStatement("return new $T(this)", structClassName);
builder.addMethod(defaultCtor.build());
builder.addMethod(copyCtor.build());
builder.addMethod(buildMethodBuilder.build());
builder.addMethod(resetBuilder.build());
return builder.build();
}
private TypeSpec adapterFor(StructType structType, ClassName structClassName, ClassName builderClassName) {
TypeName adapterSuperclass = ParameterizedTypeName.get(
TypeNames.ADAPTER,
structClassName,
builderClassName);
final MethodSpec.Builder write = MethodSpec.methodBuilder("write")
.addAnnotation(Override.class)
.addModifiers(Modifier.PUBLIC)
.addParameter(TypeNames.PROTOCOL, "protocol")
.addParameter(structClassName, "struct")
.addException(TypeNames.IO_EXCEPTION);
final MethodSpec.Builder read = MethodSpec.methodBuilder("read")
.addAnnotation(Override.class)
.addModifiers(Modifier.PUBLIC)
.returns(typeResolver.getJavaClass(structType))
.addParameter(TypeNames.PROTOCOL, "protocol")
.addParameter(builderClassName, "builder")
.addException(TypeNames.IO_EXCEPTION);
final MethodSpec readHelper = MethodSpec.methodBuilder("read")
.addAnnotation(Override.class)
.addModifiers(Modifier.PUBLIC)
.returns(typeResolver.getJavaClass(structType))
.addParameter(TypeNames.PROTOCOL, "protocol")
.addException(TypeNames.IO_EXCEPTION)
.addStatement("return read(protocol, new $T())", builderClassName)
.build();
// First, the writer
write.addStatement("protocol.writeStructBegin($S)", structType.name());
// Then, the reader - set up the field-reading loop.
read.addStatement("protocol.readStructBegin()");
read.beginControlFlow("while (true)");
read.addStatement("$T field = protocol.readFieldBegin()", TypeNames.FIELD_METADATA);
read.beginControlFlow("if (field.typeId == $T.STOP)", TypeNames.TTYPE);
read.addStatement("break");
read.endControlFlow();
if (structType.fields().size() > 0) {
read.beginControlFlow("switch (field.fieldId)");
}
for (Field field : structType.fields()) {
String fieldName = fieldNamer.getName(field);
boolean optional = !field.required(); // could also be default, but same-same to us.
final ThriftType tt = field.type().getTrueType();
byte typeCode = typeResolver.getTypeCode(tt);
// enums are i32 on the wire
if (typeCode == TType.ENUM) {
typeCode = TType.I32;
}
String typeCodeName = TypeNames.getTypeCodeName(typeCode);
// Write
if (optional) {
write.beginControlFlow("if (struct.$N != null)", fieldName);
}
write.addStatement(
"protocol.writeFieldBegin($S, $L, $T.$L)",
field.name(), // make sure that we write the Thrift IDL name, and not the name of the Java field
field.id(),
TypeNames.TTYPE,
typeCodeName);
tt.accept(new GenerateWriterVisitor(typeResolver, write, "protocol", "struct", fieldName));
write.addStatement("protocol.writeFieldEnd()");
if (optional) {
write.endControlFlow();
}
// Read
read.beginControlFlow("case $L:", field.id());
new GenerateReaderVisitor(typeResolver, read, fieldName, field.type().getTrueType()).generate();
read.endControlFlow(); // end case block
read.addStatement("break");
}
write.addStatement("protocol.writeFieldStop()");
write.addStatement("protocol.writeStructEnd()");
if (structType.fields().size() > 0) {
read.beginControlFlow("default:");
read.addStatement("$T.skip(protocol, field.typeId)", TypeNames.PROTO_UTIL);
read.endControlFlow(); // end default
read.addStatement("break");
read.endControlFlow(); // end switch
}
read.addStatement("protocol.readFieldEnd()");
read.endControlFlow(); // end while
read.addStatement("protocol.readStructEnd()");
read.addStatement("return builder.build()");
return TypeSpec.classBuilder(structType.name() + "Adapter")
.addSuperinterface(adapterSuperclass)
.addModifiers(Modifier.PRIVATE, Modifier.STATIC, Modifier.FINAL)
.addMethod(write.build())
.addMethod(read.build())
.addMethod(readHelper)
.build();
}
private MethodSpec buildWrite() {
return MethodSpec.methodBuilder("write")
.addAnnotation(Override.class)
.addModifiers(Modifier.PUBLIC)
.addParameter(Protocol.class, "protocol")
.addStatement("ADAPTER.write(protocol, this)")
.addException(IOException.class)
.build();
}
private MethodSpec buildEqualsFor(StructType struct) {
MethodSpec.Builder equals = MethodSpec.methodBuilder("equals")
.addAnnotation(Override.class)
.addModifiers(Modifier.PUBLIC)
.returns(boolean.class)
.addParameter(Object.class, "other")
.addStatement("if (this == other) return true")
.addStatement("if (other == null) return false");
if (struct.fields().size() > 0) {
equals.addStatement("if (!(other instanceof $L)) return false", struct.name());
equals.addStatement("$1L that = ($1L) other", struct.name());
}
boolean isFirst = true;
for (Field field : struct.fields()) {
String fieldName = fieldNamer.getName(field);
if (isFirst) {
equals.addCode("$[return ");
isFirst = false;
} else {
equals.addCode("\n&& ");
}
if (field.required()) {
equals.addCode("(this.$1N == that.$1N || this.$1N.equals(that.$1N))", fieldName);
} else {
equals.addCode("(this.$1N == that.$1N || (this.$1N != null && this.$1N.equals(that.$1N)))",
fieldName);
}
}
if (struct.fields().size() > 0) {
equals.addCode(";\n$]");
} else {
equals.addStatement("return other instanceof $L", struct.name());
}
return equals.build();
}
private MethodSpec buildHashCodeFor(StructType struct) {
MethodSpec.Builder hashCode = MethodSpec.methodBuilder("hashCode")
.addAnnotation(Override.class)
.addModifiers(Modifier.PUBLIC)
.returns(int.class)
.addStatement("int code = 16777619");
for (Field field : struct.fields()) {
String fieldName = fieldNamer.getName(field);
if (field.required()) {
hashCode.addStatement("code ^= this.$N.hashCode()", fieldName);
} else {
hashCode.addStatement("code ^= (this.$1N == null) ? 0 : this.$1N.hashCode()", fieldName);
}
hashCode.addStatement("code *= 0x811c9dc5");
}
hashCode.addStatement("return code");
return hashCode.build();
}
/**
* Builds a #toString() method for the given struct.
*
* The goal is to produce a method that performs as few string
* concatenations as possible. To do so, we identify what would be
* consecutive constant strings (i.e. field name followed by '='),
* collapsing them into "chunks", then using the chunks to generate
* the actual code.
*
*
This approach, while more complicated to implement than naive
* StringBuilder usage, produces more-efficient and "more pleasing" code.
* Simple structs (e.g. one with only one field, which is redacted) end up
* with simple constants like {@code return "Foo{ssn=<REDACTED>}";}.
*/
private MethodSpec buildToStringFor(StructType struct) {
class Chunk {
private final String format;
private final Object[] args;
private Chunk(String format, Object ...args) {
this.format = format;
this.args = args;
}
}
MethodSpec.Builder toString = MethodSpec.methodBuilder("toString")
.addAnnotation(Override.class)
.addModifiers(Modifier.PUBLIC)
.returns(String.class);
List chunks = new ArrayList<>();
StringBuilder sb = new StringBuilder(struct.name()).append("{");
boolean appendedOneField = false;
for (Field field : struct.fields()) {
String fieldName = fieldNamer.getName(field);
if (appendedOneField) {
sb.append(", ");
} else {
appendedOneField = true;
}
sb.append(fieldName).append("=");
if (field.isRedacted()) {
sb.append("");
} else if (field.isObfuscated()) {
chunks.add(new Chunk("$S", sb.toString()));
sb.setLength(0);
Chunk chunk;
ThriftType fieldType = field.type().getTrueType();
if (fieldType.isList() || fieldType.isSet()) {
String type;
String elementType;
if (fieldType.isList()) {
type = "list";
elementType = ((ListType) fieldType).elementType().name();
} else {
type = "set";
elementType = ((SetType) fieldType).elementType().name();
}
chunk = new Chunk(
"$T.summarizeCollection(this.$L, $S, $S)",
TypeNames.OBFUSCATION_UTIL,
fieldName,
type,
elementType);
} else if (fieldType.isMap()) {
MapType mapType = (MapType) fieldType;
String keyType = mapType.keyType().name();
String valueType = mapType.valueType().name();
chunk = new Chunk(
"$T.summarizeMap(this.$L, $S, $S)",
TypeNames.OBFUSCATION_UTIL,
fieldName,
keyType,
valueType);
} else {
chunk = new Chunk("$T.hash(this.$L)", TypeNames.OBFUSCATION_UTIL, fieldName);
}
chunks.add(chunk);
} else {
chunks.add(new Chunk("$S", sb.toString()));
chunks.add(new Chunk("this.$L", fieldName));
sb.setLength(0);
}
}
sb.append("}");
chunks.add(new Chunk("$S", sb.toString()));
CodeBlock.Builder block = CodeBlock.builder();
boolean firstChunk = true;
for (Chunk chunk : chunks) {
if (firstChunk) {
block.add("$[return ");
firstChunk = false;
} else {
block.add(" + ");
}
block.add(chunk.format, chunk.args);
}
block.add(";$]\n");
toString.addCode(block.build());
return toString.build();
}
@VisibleForTesting
@SuppressWarnings("WeakerAccess")
TypeSpec buildConst(Collection constants) {
TypeSpec.Builder builder = TypeSpec.classBuilder("Constants")
.addModifiers(Modifier.PUBLIC, Modifier.FINAL)
.addMethod(MethodSpec.constructorBuilder()
.addModifiers(Modifier.PRIVATE)
.addCode("// no instances\n")
.build());
final NameAllocator allocator = new NameAllocator();
allocator.newName("Constants", "Constants");
final AtomicInteger scope = new AtomicInteger(0); // used for temporaries in const collections
final CodeBlock.Builder staticInit = CodeBlock.builder();
final AtomicBoolean hasStaticInit = new AtomicBoolean(false);
for (final Constant constant : constants) {
final ThriftType type = constant.type().getTrueType();
TypeName javaType = typeResolver.getJavaClass(type);
// Primitive-typed const fields should be unboxed, but be careful -
// while strings are builtin, they are *not* primitive!
if (type.isBuiltin() && !type.equals(BuiltinType.STRING)) {
javaType = javaType.unbox();
}
final FieldSpec.Builder field = FieldSpec.builder(javaType, constant.name())
.addModifiers(Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL);
if (constant.hasJavadoc()) {
field.addJavadoc(constant.documentation() + "\n\nGenerated from: " + constant.location());
}
if (constant.isDeprecated()) {
field.addAnnotation(AnnotationSpec.builder(Deprecated.class).build());
}
type.accept(new SimpleVisitor() {
@Override
public Void visitBuiltin(ThriftType builtinType) {
field.initializer(constantBuilder.renderConstValue(null, allocator, scope, type, constant.value()));
return null;
}
@Override
public Void visitEnum(EnumType userType) {
field.initializer(constantBuilder.renderConstValue(null, allocator, scope, type, constant.value()));
return null;
}
@Override
public Void visitList(ListType listType) {
if (constant.value().getAsList().isEmpty()) {
field.initializer("$T.emptyList()", TypeNames.COLLECTIONS);
return null;
}
initCollection("list", "unmodifiableList");
return null;
}
@Override
public Void visitSet(SetType setType) {
if (constant.value().getAsList().isEmpty()) {
field.initializer("$T.emptySet()", TypeNames.COLLECTIONS);
return null;
}
initCollection("set", "unmodifiableSet");
return null;
}
@Override
public Void visitMap(MapType mapType) {
if (constant.value().getAsMap().isEmpty()) {
field.initializer("$T.emptyMap()", TypeNames.COLLECTIONS);
return null;
}
initCollection("map", "unmodifiableMap");
return null;
}
private void initCollection(String tempName, String unmodifiableMethod) {
tempName += scope.incrementAndGet();
constantBuilder.generateFieldInitializer(
staticInit,
allocator,
scope,
tempName,
type,
constant.value(),
true);
staticInit.addStatement("$N = $T.$L($N)",
constant.name(),
TypeNames.COLLECTIONS,
unmodifiableMethod,
tempName);
hasStaticInit.set(true);
}
@Override
public Void visitStruct(StructType userType) {
throw new UnsupportedOperationException("Struct-type constants are not supported");
}
@Override
public Void visitTypedef(TypedefType typedefType) {
throw new AssertionError("Typedefs should have been resolved before now");
}
@Override
public Void visitService(ServiceType serviceType) {
throw new AssertionError("Services cannot be constant values");
}
});
builder.addField(field.build());
}
if (hasStaticInit.get()) {
builder.addStaticBlock(staticInit.build());
}
return builder.build();
}
private static AnnotationSpec fieldAnnotation(Field field) {
AnnotationSpec.Builder ann = AnnotationSpec.builder(ThriftField.class)
.addMember("fieldId", "$L", field.id())
.addMember("isRequired", "$L", field.required());
String typedef = field.typedefName();
if (!Strings.isNullOrEmpty(typedef)) {
ann = ann.addMember("typedefName", "$S", typedef);
}
return ann.build();
}
@VisibleForTesting
@SuppressWarnings("WeakerAccess")
TypeSpec buildEnum(EnumType type) {
ClassName enumClassName = ClassName.get(
type.getNamespaceFor(NamespaceScope.JAVA),
type.name());
TypeSpec.Builder builder = TypeSpec.enumBuilder(type.name())
.addModifiers(Modifier.PUBLIC)
.addField(int.class, "value", Modifier.PUBLIC, Modifier.FINAL)
.addMethod(MethodSpec.constructorBuilder()
.addParameter(int.class, "value")
.addStatement("this.$N = $N", "value", "value")
.build());
if (type.hasJavadoc()) {
builder.addJavadoc(type.documentation());
}
if (type.isDeprecated()) {
builder.addAnnotation(AnnotationSpec.builder(Deprecated.class).build());
}
MethodSpec.Builder fromCodeMethod = MethodSpec.methodBuilder("findByValue")
.addModifiers(Modifier.PUBLIC, Modifier.STATIC)
.returns(enumClassName)
.addParameter(int.class, "value")
.beginControlFlow("switch (value)");
for (EnumMember member : type.members()) {
String name = member.name();
int value = member.value();
TypeSpec.Builder memberBuilder = TypeSpec.anonymousClassBuilder("$L", value);
if (member.hasJavadoc()) {
memberBuilder.addJavadoc(member.documentation());
}
if (member.isDeprecated()) {
memberBuilder.addAnnotation(AnnotationSpec.builder(Deprecated.class).build());
}
builder.addEnumConstant(name, memberBuilder.build());
fromCodeMethod.addStatement("case $L: return $N", value, name);
}
fromCodeMethod
.addStatement("default: return null")
.endControlFlow();
builder.addMethod(fromCodeMethod.build());
return builder.build();
}
}