| /* |
| * 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; |
| } |
| } |
| } |