blob: 5143d8bfa5164abb2bdb1830e27db4838fff68f6 [file] [log] [blame]
/*
* Copyright 2020 Google LLC
*
* 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.extension.serializable.processor;
import static com.google.auto.common.MoreStreams.toImmutableList;
import static com.google.auto.common.MoreStreams.toImmutableMap;
import static com.google.auto.value.extension.serializable.processor.ClassNames.SERIALIZABLE_AUTO_VALUE_NAME;
import static java.util.stream.Collectors.joining;
import com.google.auto.common.GeneratedAnnotationSpecs;
import com.google.auto.common.MoreTypes;
import com.google.auto.service.AutoService;
import com.google.auto.value.extension.AutoValueExtension;
import com.google.auto.value.extension.serializable.serializer.SerializerFactoryLoader;
import com.google.auto.value.extension.serializable.serializer.interfaces.Serializer;
import com.google.auto.value.extension.serializable.serializer.interfaces.SerializerFactory;
import com.google.common.base.Equivalence;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.squareup.javapoet.AnnotationSpec;
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.ParameterizedTypeName;
import com.squareup.javapoet.TypeName;
import com.squareup.javapoet.TypeSpec;
import com.squareup.javapoet.TypeVariableName;
import java.io.Serializable;
import java.util.List;
import java.util.Optional;
import java.util.function.Function;
import javax.annotation.processing.ProcessingEnvironment;
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.TypeElement;
import javax.lang.model.type.TypeMirror;
/**
* An AutoValue extension that enables classes with unserializable fields to be serializable.
*
* <p>For this extension to work:
*
* <ul>
* <li>The AutoValue class must implement {@link Serializable}.
* <li>Unserializable fields in the AutoValue class must be supported by a {@link
* com.google.auto.value.extension.serializable.serializer.interfaces.SerializerExtension}.
* </ul>
*/
@AutoService(AutoValueExtension.class)
public final class SerializableAutoValueExtension extends AutoValueExtension {
@Override
public boolean applicable(Context context) {
return hasSerializableInterface(context) && hasSerializableAutoValueAnnotation(context);
}
@Override
public IncrementalExtensionType incrementalType(ProcessingEnvironment processingEnvironment) {
return IncrementalExtensionType.ISOLATING;
}
@Override
public String generateClass(
Context context, String className, String classToExtend, boolean isFinal) {
return new Generator(context, className, classToExtend, isFinal).generate();
}
private static final class Generator {
private final Context context;
private final String className;
private final String classToExtend;
private final boolean isFinal;
private final ImmutableList<PropertyMirror> propertyMirrors;
private final ImmutableList<TypeVariableName> typeVariableNames;
private final ProxyGenerator proxyGenerator;
Generator(Context context, String className, String classToExtend, boolean isFinal) {
this.context = context;
this.className = className;
this.classToExtend = classToExtend;
this.isFinal = isFinal;
this.propertyMirrors =
context.propertyTypes().entrySet().stream()
.map(
entry ->
new PropertyMirror(
/* type= */ entry.getValue(),
/* name= */ entry.getKey(),
/* method= */ context
.properties()
.get(entry.getKey())
.getSimpleName()
.toString()))
.collect(toImmutableList());
this.typeVariableNames =
context.autoValueClass().getTypeParameters().stream()
.map(TypeVariableName::get)
.collect(toImmutableList());
TypeName classTypeName =
getClassTypeName(ClassName.get(context.packageName(), className), typeVariableNames);
this.proxyGenerator =
new ProxyGenerator(
classTypeName, typeVariableNames, propertyMirrors, buildSerializersMap());
}
private String generate() {
ClassName superclass = ClassName.get(context.packageName(), classToExtend);
Optional<AnnotationSpec> generatedAnnotationSpec =
GeneratedAnnotationSpecs.generatedAnnotationSpec(
context.processingEnvironment().getElementUtils(),
context.processingEnvironment().getSourceVersion(),
SerializableAutoValueExtension.class);
TypeSpec.Builder subclass =
TypeSpec.classBuilder(className)
.superclass(getClassTypeName(superclass, typeVariableNames))
.addTypeVariables(typeVariableNames)
.addModifiers(isFinal ? Modifier.FINAL : Modifier.ABSTRACT)
.addMethod(constructor())
.addMethod(writeReplace())
.addType(proxyGenerator.generate());
generatedAnnotationSpec.ifPresent(subclass::addAnnotation);
return JavaFile.builder(context.packageName(), subclass.build()).build().toString();
}
/** Creates a constructor that calls super with all the AutoValue fields. */
private MethodSpec constructor() {
MethodSpec.Builder constructor =
MethodSpec.constructorBuilder()
.addStatement(
"super($L)",
propertyMirrors.stream().map(PropertyMirror::getName).collect(joining(", ")));
for (PropertyMirror propertyMirror : propertyMirrors) {
constructor.addParameter(TypeName.get(propertyMirror.getType()), propertyMirror.getName());
}
return constructor.build();
}
/**
* Creates an implementation of writeReplace that delegates serialization to its inner Proxy
* class.
*/
private MethodSpec writeReplace() {
ImmutableList<CodeBlock> properties =
propertyMirrors.stream()
.map(propertyMirror -> CodeBlock.of("$L()", propertyMirror.getMethod()))
.collect(toImmutableList());
return MethodSpec.methodBuilder("writeReplace")
.returns(Object.class)
.addStatement(
"return new $T($L)",
getClassTypeName(
ClassName.get(
context.packageName(),
className,
SerializableAutoValueExtension.ProxyGenerator.PROXY_CLASS_NAME),
typeVariableNames),
CodeBlock.join(properties, ", "))
.build();
}
private ImmutableMap<Equivalence.Wrapper<TypeMirror>, Serializer> buildSerializersMap() {
SerializerFactory factory =
SerializerFactoryLoader.getFactory(context.processingEnvironment());
return propertyMirrors.stream()
.map(PropertyMirror::getType)
.map(MoreTypes.equivalence()::wrap)
.distinct()
.collect(
toImmutableMap(
Function.identity(), equivalence -> factory.getSerializer(equivalence.get())));
}
/** Adds type parameters to the given {@link ClassName}, if available. */
private static TypeName getClassTypeName(
ClassName className, List<TypeVariableName> typeVariableNames) {
return typeVariableNames.isEmpty()
? className
: ParameterizedTypeName.get(className, typeVariableNames.toArray(new TypeName[] {}));
}
}
/** A generator of nested serializable Proxy classes. */
private static final class ProxyGenerator {
private static final String PROXY_CLASS_NAME = "Proxy$";
private final TypeName outerClassTypeName;
private final ImmutableList<TypeVariableName> typeVariableNames;
private final ImmutableList<PropertyMirror> propertyMirrors;
private final ImmutableMap<Equivalence.Wrapper<TypeMirror>, Serializer> serializersMap;
ProxyGenerator(
TypeName outerClassTypeName,
ImmutableList<TypeVariableName> typeVariableNames,
ImmutableList<PropertyMirror> propertyMirrors,
ImmutableMap<Equivalence.Wrapper<TypeMirror>, Serializer> serializersMap) {
this.outerClassTypeName = outerClassTypeName;
this.typeVariableNames = typeVariableNames;
this.propertyMirrors = propertyMirrors;
this.serializersMap = serializersMap;
}
private TypeSpec generate() {
TypeSpec.Builder proxy =
TypeSpec.classBuilder(PROXY_CLASS_NAME)
.addModifiers(Modifier.STATIC)
.addTypeVariables(typeVariableNames)
.addSuperinterface(Serializable.class)
.addField(serialVersionUid())
.addFields(properties())
.addMethod(constructor())
.addMethod(readResolve());
return proxy.build();
}
private static FieldSpec serialVersionUid() {
return FieldSpec.builder(
long.class, "serialVersionUID", Modifier.PRIVATE, Modifier.STATIC, Modifier.FINAL)
.initializer("0")
.build();
}
/** Maps each AutoValue property to a serializable type. */
private List<FieldSpec> properties() {
return propertyMirrors.stream()
.map(
propertyMirror ->
FieldSpec.builder(
TypeName.get(
serializersMap
.get(MoreTypes.equivalence().wrap(propertyMirror.getType()))
.proxyFieldType()),
propertyMirror.getName(),
Modifier.PRIVATE)
.build())
.collect(toImmutableList());
}
/** Creates a constructor that converts the AutoValue's properties to serializable values. */
private MethodSpec constructor() {
MethodSpec.Builder constructor = MethodSpec.constructorBuilder();
for (PropertyMirror propertyMirror : propertyMirrors) {
Serializer serializer =
serializersMap.get(MoreTypes.equivalence().wrap(propertyMirror.getType()));
String name = propertyMirror.getName();
constructor.addParameter(TypeName.get(propertyMirror.getType()), name);
constructor.addStatement(
CodeBlock.of("this.$L = $L", name, serializer.toProxy(CodeBlock.of(name))));
}
return constructor.build();
}
/**
* Creates an implementation of {@code readResolve} that returns the serializable values in the
* Proxy object back to their original types.
*/
private MethodSpec readResolve() {
return MethodSpec.methodBuilder("readResolve")
.returns(Object.class)
.addException(Exception.class)
.addStatement(
"return new $T($L)",
outerClassTypeName,
CodeBlock.join(
propertyMirrors.stream().map(this::resolve).collect(toImmutableList()), ", "))
.build();
}
/** Maps a serializable type back to its original AutoValue property. */
private CodeBlock resolve(PropertyMirror propertyMirror) {
return serializersMap
.get(MoreTypes.equivalence().wrap(propertyMirror.getType()))
.fromProxy(CodeBlock.of(propertyMirror.getName()));
}
}
private static boolean hasSerializableInterface(Context context) {
final TypeMirror serializableTypeMirror =
context
.processingEnvironment()
.getElementUtils()
.getTypeElement(Serializable.class.getCanonicalName())
.asType();
return context
.processingEnvironment()
.getTypeUtils()
.isAssignable(context.autoValueClass().asType(), serializableTypeMirror);
}
private static boolean hasSerializableAutoValueAnnotation(Context context) {
return context.autoValueClass().getAnnotationMirrors().stream()
.map(AnnotationMirror::getAnnotationType)
.map(MoreTypes::asTypeElement)
.map(TypeElement::getQualifiedName)
.anyMatch(name -> name.contentEquals(SERIALIZABLE_AUTO_VALUE_NAME));
}
}