blob: 9bcf5614cc9cc3f61dcefbc00a101506f2dcc005 [file] [log] [blame]
/*
* Copyright (C) 2014 Google, 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.google.auto.value.processor;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.stream.Collectors.toList;
import com.google.common.collect.Multimap;
import com.google.escapevelocity.Template;
import java.io.IOException;
import java.io.Writer;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.zip.CRC32;
import javax.annotation.processing.ProcessingEnvironment;
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.AnnotationValue;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.TypeElement;
import javax.lang.model.type.TypeMirror;
import javax.tools.Diagnostic;
import javax.tools.JavaFileObject;
/**
* Generates GWT serialization code for {@code @AutoValue} classes also marked
* {@code @GwtCompatible(serializable = true)}.
*
* @author Éamonn McManus
*/
class GwtSerialization {
private final GwtCompatibility gwtCompatibility;
private final ProcessingEnvironment processingEnv;
private final TypeElement type;
GwtSerialization(
GwtCompatibility gwtCompatibility, ProcessingEnvironment processingEnv, TypeElement type) {
this.gwtCompatibility = gwtCompatibility;
this.processingEnv = processingEnv;
this.type = type;
}
private boolean shouldWriteGwtSerializer() {
Optional<AnnotationMirror> optionalGwtCompatible = gwtCompatibility.gwtCompatibleAnnotation();
if (optionalGwtCompatible.isPresent()) {
AnnotationMirror gwtCompatible = optionalGwtCompatible.get();
for (Map.Entry<ExecutableElement, AnnotationValue> entry :
GwtCompatibility.getElementValues(gwtCompatible).entrySet()) {
if (entry.getKey().getSimpleName().contentEquals("serializable")
&& entry.getValue().getValue().equals(true)) {
return true;
}
}
}
return false;
}
/**
* Writes the GWT serializer for the given type, if appropriate. An {@code @AutoValue} class gets
* a GWT serializer if it is annotated with {@code @GwtCompatible(serializable = true)}, where the
* {@code @GwtCompatible} annotation can come from any package.
*
* <p>If the type is com.example.Foo then the generated AutoValue subclass is
* com.example.AutoValue_Foo and the GWT serializer is
* com.example.AutoValue_Foo_CustomFieldSerializer.
*
* @param autoVars the template variables defined for this type.
*/
void maybeWriteGwtSerializer(AutoValueTemplateVars autoVars) {
if (shouldWriteGwtSerializer()) {
GwtTemplateVars vars = new GwtTemplateVars();
vars.pkg = autoVars.pkg;
vars.subclass = autoVars.finalSubclass;
vars.formalTypes = autoVars.formalTypes;
vars.actualTypes = autoVars.actualTypes;
vars.useBuilder = !autoVars.builderTypeName.isEmpty();
vars.builderSetters = autoVars.builderSetters;
vars.generated = autoVars.generated;
String className =
(vars.pkg.isEmpty() ? "" : vars.pkg + ".") + vars.subclass + "_CustomFieldSerializer";
vars.serializerClass = TypeSimplifier.simpleNameOf(className);
vars.props = autoVars.props.stream().map(Property::new).collect(toList());
vars.classHashString = computeClassHash(autoVars.props, vars.pkg);
String text = vars.toText();
text = TypeEncoder.decode(text, processingEnv, vars.pkg, type.asType());
writeSourceFile(className, text, type);
}
}
public static class Property {
private final AutoValueProcessor.Property property;
private final boolean isCastingUnchecked;
Property(AutoValueProcessor.Property property) {
this.property = property;
this.isCastingUnchecked = TypeSimplifier.isCastingUnchecked(property.getTypeMirror());
}
@Override
public String toString() {
return property.toString();
}
public String getGetter() {
return property.getGetter();
}
public String getType() {
return property.getType();
}
public String getName() {
return property.getName();
}
/**
* Returns the suffix in serializer method names for values of the given type. For example, if
* the type is "int" then the returned value will be "Int" because the serializer methods are
* called readInt and writeInt. There are methods for all primitive types and String; every
* other type uses readObject and writeObject.
*/
public String getGwtType() {
TypeMirror typeMirror = property.getTypeMirror();
String type = typeMirror.toString();
if (property.getKind().isPrimitive()) {
return Character.toUpperCase(type.charAt(0)) + type.substring(1);
} else if (type.equals("java.lang.String")) {
return "String";
} else {
return "Object";
}
}
/**
* Returns a string to be inserted before the call to the readFoo() call so that the expression
* can be assigned to the given type. For primitive types and String, the readInt() etc methods
* already return the right type so the string is empty. For other types, the string is a cast
* like "(Foo) ".
*/
public String getGwtCast() {
if (property.getKind().isPrimitive() || getType().equals("String")) {
return "";
} else {
return "(" + getType() + ") ";
}
}
public boolean isCastingUnchecked() {
return isCastingUnchecked;
}
}
@SuppressWarnings("unused") // some fields are only read through reflection
static class GwtTemplateVars extends TemplateVars {
/** The properties defined by the parent class's abstract methods. */
List<Property> props;
/**
* The package of the class with the {@code @AutoValue} annotation and its generated subclass.
*/
String pkg;
/** The simple name of the generated subclass. */
String subclass;
/**
* The formal generic signature of the class with the {@code @AutoValue} annotation and its
* generated subclass. This is empty, or contains type variables with optional bounds, for
* example {@code <K, V extends K>}.
*/
String formalTypes;
/**
* The generic signature used by the generated subclass for its superclass reference. This is
* empty, or contains only type variables with no bounds, for example {@code <K, V>}.
*/
String actualTypes;
/** True if the {@code @AutoValue} class is constructed using a generated builder. */
Boolean useBuilder;
/**
* A multimap from property names (like foo) to the corresponding setter methods (foo or
* setFoo).
*/
Multimap<String, BuilderSpec.PropertySetter> builderSetters;
/** The simple name of the generated GWT serializer class. */
String serializerClass;
/**
* The encoding of the {@code Generated} class. Empty if no {@code Generated} class is
* available.
*/
String generated;
/** A string that should change if any salient details of the serialized class change. */
String classHashString;
private static final Template TEMPLATE = parsedTemplateForResource("gwtserializer.vm");
@Override
Template parsedTemplate() {
return TEMPLATE;
}
}
private void writeSourceFile(String className, String text, TypeElement originatingType) {
try {
JavaFileObject sourceFile =
processingEnv.getFiler().createSourceFile(className, originatingType);
try (Writer writer = sourceFile.openWriter()) {
writer.write(text);
}
} catch (IOException e) {
processingEnv
.getMessager()
.printMessage(
Diagnostic.Kind.ERROR, "Could not write generated class " + className + ": " + e);
}
}
// Compute a hash that is guaranteed to change if the names, types, or order of the fields
// change. We use TypeEncoder so that we can get a defined string for types, since
// TypeMirror.toString() isn't guaranteed to remain the same.
private String computeClassHash(Iterable<AutoValueProcessor.Property> props, String pkg) {
CRC32 crc = new CRC32();
String encodedType = TypeEncoder.encode(type.asType()) + ":";
String decodedType = TypeEncoder.decode(encodedType, processingEnv, "", null);
if (!decodedType.startsWith(pkg)) {
// This is for compatibility with the way an earlier version did things. Preserving hash
// codes probably isn't vital, since client and server should be in sync.
decodedType = pkg + "." + decodedType;
}
crc.update(decodedType.getBytes(UTF_8));
for (AutoValueProcessor.Property prop : props) {
String encodedProp = prop + ":" + TypeEncoder.encode(prop.getTypeMirror()) + ";";
String decodedProp = TypeEncoder.decode(encodedProp, processingEnv, pkg, null);
crc.update(decodedProp.getBytes(UTF_8));
}
return String.format("%08x", crc.getValue());
}
}