blob: 99c078c5b089c5770904b9a4652c61517d108ca1 [file] [log] [blame]
/*
* Copyright 2012-present Facebook, Inc.
*
* 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
*
* 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 com.facebook.buck.parcelable;
import com.google.common.base.Function;
import com.google.common.base.Joiner;
import com.google.common.base.Preconditions;
import com.google.common.base.Splitter;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Iterables;
import java.io.File;
import java.io.IOException;
import java.util.Map;
import javax.annotation.Nullable;
public class Generator {
private final ParcelableClass clazz;
public Generator(ParcelableClass clazz) {
this.clazz = Preconditions.checkNotNull(clazz);
}
public static String generateFromFile(File file) throws IOException {
ParcelableClass parcelableClass = Parser.parse(file);
return new Generator(parcelableClass).generate();
}
public String generate() throws IOException {
PrintfStringBuilder out = new PrintfStringBuilder();
out.appendLines(
"/*",
" * DO NOT MODIFY.",
" * This file was auto-generated by Buck.",
" */");
out.appendLine("package %s;\n", clazz.getPackageName());
for (String importLine : clazz.getImports()) {
out.appendLine(importLine);
}
out.append('\n');
String classDeclaration = clazz.getClassName();
if (clazz.hasSuperClass()) {
classDeclaration += " extends " + clazz.getSuperClassName();
}
out.appendLine("public class %s implements Parcelable {\n", classDeclaration);
insertFields(out);
// Jackson and other de-serializers often require a no-arg constructor.
insertEmptyConstructor(out);
insertFieldBasedConstructor(out);
insertParcelBasedConstructor(out);
insertDescribeContents(out);
insertWriteToParcel(out);
insertCreator(out);
// Close top-level class definition.
out.appendLine("}");
return out.toString();
}
private void insertFields(PrintfStringBuilder out) throws IOException {
final String defaultFieldVisibility = clazz.getDefaultFieldVisibility();
for (ParcelableField field : clazz.getFields()) {
if (field.getJsonProperty() != null) {
String jsonProperty = field.getJsonProperty().isEmpty()
? ""
: String.format("(\"%s\")", field.getJsonProperty());
out.appendLine(" @JsonProperty%s", jsonProperty);
}
String visibility = field.getVisibility();
if (visibility == null) {
visibility = defaultFieldVisibility;
}
if (!visibility.isEmpty()) {
visibility += ' ';
}
String mutability = field.isMutable() ? "" : "final ";
out.appendLine(" %s%s%s %s;",
visibility,
mutability,
field.getType(),
field.getName());
out.append('\n');
}
}
private static final Map<String, String> TYPE_TO_DEFAULT_VALUE =
ImmutableMap.<String, String>builder()
.put("byte", "0")
.put("int", "0")
.put("long", "0")
.put("float", "0")
.put("double", "0")
.put("boolean", "false")
.build();
private void insertEmptyConstructor(PrintfStringBuilder out) throws IOException {
out.appendLine(" public %s() {", clazz.getClassName());
if (clazz.hasSuperClass()) {
out.appendLine(" super();");
}
for (ParcelableField field : clazz.getFields()) {
String defaultValue = field.getDefaultValue();
if (defaultValue == null) {
defaultValue = TYPE_TO_DEFAULT_VALUE.get(field.getType());
if (defaultValue == null) {
defaultValue = "null";
}
}
out.appendLine(" this.%s = %s;", field.getName(), defaultValue);
}
out.appendLine(" }");
out.append('\n');
}
private void insertFieldBasedConstructor(PrintfStringBuilder out) throws IOException {
out.appendLine(" public %s(", clazz.getClassName());
if (clazz.hasSuperClass()) {
String rawSuperParams = Strings.nullToEmpty(clazz.getRawSuperParams());
if (!rawSuperParams.isEmpty() && !rawSuperParams.endsWith(",")) {
rawSuperParams += ",";
}
out.appendLine(" %s", rawSuperParams);
}
final String defaultFieldVisibility = clazz.getDefaultFieldVisibility();
Joiner.on(",\n").appendTo(out, Iterables.transform(clazz.getFields(),
new Function<ParcelableField, String>() {
@Override
public String apply(ParcelableField field) {
String visibility = field.getVisibility();
if (visibility == null) {
visibility = defaultFieldVisibility;
}
return String.format(" %s %s", field.getType(), field.getName());
}
}));
// Close the param list and open the body.
out.appendLine(") {");
if (clazz.hasSuperClass()) {
// rawSuperParams will be of the form "boolean isMale, int age".
// They need to be filtered into param names to pass to super().
String rawSuperParams = Strings.nullToEmpty(clazz.getRawSuperParams());
String superParamNames = Joiner.on(", ").join(Iterables.transform(
Splitter.on(',').trimResults().omitEmptyStrings().split(rawSuperParams),
new Function<String, String>() {
@Override
public String apply(String typeAndName) {
return typeAndName.split("\\s+")[1];
}
}));
out.appendLine(" super(%s);", superParamNames);
}
for (ParcelableField field : clazz.getFields()) {
out.appendLine(" this.%s = %s;", field.getName(), field.getName());
}
// Close constructor body.
out.appendLine(" }");
}
private static final Map<String, ReadWriteMethod> SIMPLE_TYPE_MAPPINGS =
ImmutableMap.<String, ReadWriteMethod>builder()
.put("byte", ReadWriteMethod.both("Byte"))
.put("double", ReadWriteMethod.both("Double"))
.put("float", ReadWriteMethod.both("Float"))
.put("int", ReadWriteMethod.both("Int"))
.put("long", ReadWriteMethod.both("Long"))
.put("String", ReadWriteMethod.both("String"))
.put("List<String>", new ReadWriteMethod("createStringArrayList", "writeStringList"))
.put("String[]", new ReadWriteMethod("createStringArray", "writeStringArray"))
.build();
private void insertParcelBasedConstructor(PrintfStringBuilder out) throws IOException {
out.append('\n');
out.appendLine(" public %s(Parcel parcel) {", clazz.getClassName());
if (clazz.hasSuperClass()) {
out.appendLine(" super(parcel);");
}
for (ParcelableField field : clazz.getFields()) {
String type = field.getType();
String fieldName = field.getName();
ReadWriteMethod readWriteMethod = SIMPLE_TYPE_MAPPINGS.get(type);
if (readWriteMethod != null && readWriteMethod.hasReadMethod()) {
out.appendLine(" this.%s = parcel.%s();", fieldName, readWriteMethod.readMethod);
} else if ("boolean".equals(type)) {
out.appendLine(" this.%s = parcel.readByte() != 0;", fieldName);
} else if ("List".equals(type)) {
// If we do not have any type information, then we cannot use readTypedList().
out.appendLine(" this.%s = parcel.readArrayList(null /* classLoader */);", fieldName);
} else if (type.startsWith("List<")) {
String genericType = type.substring("List<".length(), type.length() - 1);
// We prefer readTypedList when we have type information, as it ostensibly stricter for
// checking. May need to revisit this if the genericType is a supertype that has subclasses
// with their own CREATOR that should be used.
out.appendLine(" this.%s = Lists.newArrayList();", fieldName);
out.appendLine(" parcel.readTypedList(this.%s, %s.CREATOR);", fieldName, genericType);
} else {
out.appendLine(" this.%s = parcel.readParcelable(%s.class.getClassLoader());",
fieldName,
field.getType());
}
}
out.appendLine(" }");
}
private void insertDescribeContents(PrintfStringBuilder out) throws IOException {
out.append('\n');
out.appendLines(
" @Override",
" public int describeContents() {",
" return 0;",
" }");
}
private void insertWriteToParcel(PrintfStringBuilder out) throws IOException {
out.append('\n');
out.appendLines(
" @Override",
" public void writeToParcel(Parcel dest, int flags) {");
for (ParcelableField field : clazz.getFields()) {
String type = field.getType();
String fieldName = field.getName();
ReadWriteMethod readWriteMethod = SIMPLE_TYPE_MAPPINGS.get(type);
if (readWriteMethod != null && readWriteMethod.hasWriteMethod()) {
out.appendLine(" dest.%s(this.%s);", readWriteMethod.writeMethod, fieldName);
} else if ("boolean".equals(type)) {
out.appendLine(" dest.writeByte((byte)(this.%s ? 1 : 0));", fieldName);
} else if ("List".equals(type)) {
out.appendLine(" dest.writeList(this.%s);", fieldName);
} else if (type.startsWith("List<")) {
out.appendLine(" dest.writeTypedList(this.%s);", fieldName);
} else {
// Everything else is assumed to be a Parcelable.
// This heuristic could be incorrect, so it will have to be improved over time.
out.appendLine(" dest.writeParcelable(this.%s, flags);", fieldName);
}
}
out.appendLines(" }");
}
private void insertCreator(PrintfStringBuilder out) throws IOException {
out.append('\n');
String creatorClassName = clazz.getCreatorClassName();
out.appendLine(" public static final Parcelable.Creator<%s> CREATOR = " +
"new Parcelable.Creator<%s>() {",
creatorClassName,
creatorClassName);
out.appendLine(" public %s createFromParcel(Parcel in) {", creatorClassName);
out.appendLine(" return new %s(in);", creatorClassName);
out.appendLine(" }");
out.appendLine(" public %s[] newArray(int size) {", creatorClassName);
out.appendLine(" return new %s[size];", creatorClassName);
out.appendLine(" }");
out.appendLine(" };");
}
private static class ReadWriteMethod {
@Nullable private final String readMethod;
@Nullable private final String writeMethod;
private ReadWriteMethod(String readMethod, String writeMethod) {
this.readMethod = readMethod;
this.writeMethod = writeMethod;
}
public static ReadWriteMethod both(String name) {
return new ReadWriteMethod("read" + name, "write" + name);
}
public boolean hasReadMethod() {
return readMethod != null;
}
public boolean hasWriteMethod() {
return writeMethod != null;
}
}
}