paperparcel.PaperParcelDescriptor Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of paperparcel-compiler Show documentation
Show all versions of paperparcel-compiler Show documentation
Android Parcelable boilerplate annotation processor
The newest version!
/*
* Copyright (C) 2016 Bradley Campbell.
*
* 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 paperparcel;
import com.google.auto.common.MoreElements;
import com.google.auto.common.Visibility;
import com.google.auto.value.AutoValue;
import com.google.common.base.Optional;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.VariableElement;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.type.UnknownTypeException;
import javax.lang.model.util.Elements;
import javax.lang.model.util.Types;
/** Represents a {@link PaperParcel} annotated object */
@AutoValue
abstract class PaperParcelDescriptor {
private static final String GET_PREFIX = "get";
private static final String IS_PREFIX = "is";
private static final String HAS_PREFIX = "has";
private static final String SET_PREFIX = "set";
/** The original {@link TypeElement} that this class is describing */
abstract TypeElement element();
/** All fields that should be passed to the constructor, in order */
abstract ImmutableList constructorFields();
/** Returns true if the constructor is non-private */
abstract boolean isConstructorVisible();
/** The fields that should be written directly, or an empty list if none */
abstract ImmutableList writableFields();
/**
* The fields that should be written via setter methods, mapped to the setter method itself.
* Returns an empty map if there are no fields to be set via setter methods.
*/
abstract ImmutableMap setterMethodMap();
/** A list of all of the non-private fields, or an empty list if none */
abstract ImmutableList readableFields();
/**
* The fields that should be read from getter methods, mapped to the getter method itself.
* Returns an empty map if there are no fields to be read from getter methods.
*/
abstract ImmutableMap getterMethodMap();
/**
* Returns all of the adapters required for each field in the annotated class, indexed by the
* field they are required for
*/
abstract ImmutableMap adapters();
/**
* Returns true if this class is a singleton. Singletons are defined as per
* {@link Utils#isSingleton(Types, TypeElement)}
*/
abstract boolean isSingleton();
private static boolean isGetterMethod(String fieldName, String methodName) {
fieldName = stripKaptEscaping(fieldName);
methodName = stripKotlinInternalVisibilityQualifier(methodName);
return methodName.equals(fieldName)
|| methodName.equals(IS_PREFIX + Strings.capitalizeAsciiOnly(fieldName))
|| methodName.equals(HAS_PREFIX + Strings.capitalizeAsciiOnly(fieldName))
|| methodName.equals(GET_PREFIX + Strings.capitalizeAsciiOnly(fieldName))
|| methodName.equals(GET_PREFIX + Strings.capitalizeFirstWordAsciiOnly(fieldName));
}
private static boolean isSetterMethod(String fieldName, String methodName) {
fieldName = stripKaptEscaping(fieldName);
methodName = stripKotlinInternalVisibilityQualifier(methodName);
return methodName.equals(fieldName)
|| (startsWithPrefix(IS_PREFIX, fieldName) && methodName.equals(SET_PREFIX + fieldName.substring(IS_PREFIX.length())))
|| (startsWithPrefix(HAS_PREFIX, fieldName) && methodName.equals(SET_PREFIX + fieldName.substring(HAS_PREFIX.length())))
|| methodName.equals(SET_PREFIX + Strings.capitalizeAsciiOnly(fieldName))
|| methodName.equals(SET_PREFIX + Strings.capitalizeFirstWordAsciiOnly(fieldName));
}
private static boolean startsWithPrefix(String prefix, String name) {
if (!name.startsWith(prefix)) return false;
if (name.length() == prefix.length()) return false;
char c = name.charAt(prefix.length());
return !('a' <= c && c <= 'z');
}
private static String stripKaptEscaping(String name) {
// As of kotlin 1.1.0, kapt escapes stub property names that are java keywords with underscores
// in order to produce valid java syntax. This function strips these underscores so we can find
// the matching getter and setter methods.
if (!name.startsWith("_")) return name;
if (!name.endsWith("_")) return name;
if (name.length() == 1) return name;
String stripped = name.substring(1, name.length() - 1);
if (SourceVersion.isKeyword(stripped)) return stripped;
return name;
}
private static String stripKotlinInternalVisibilityQualifier(String name) {
if (!name.contains("$")) return name;
return name.substring(0, name.indexOf("$"));
}
@AutoValue
static abstract class NonReadableFieldsException extends Exception {
/** A list containing all of the non-readable fields. */
abstract ImmutableList nonReadableFields();
static NonReadableFieldsException create(ImmutableList nonReadableFields) {
return new AutoValue_PaperParcelDescriptor_NonReadableFieldsException(nonReadableFields);
}
}
@AutoValue
static abstract class NonWritableFieldsException extends Exception {
/**
* Contains all of the valid constructor elements, mapped to all of the fields that couldn't be
* written when using that constructor.
*
* Valid constructors are found if all parameters had a corresponding field (same name and
* the types are assignable). Private constructors are not included.
*
* If there were no valid constructors, this returns an empty map.
*/
abstract ImmutableMap> allNonWritableFieldsMap();
/**
* Contains all of the invalid constructor elements, mapped to the list of unassignable fields
* found when parsing that constructor.
*
* Invalid constructors are found if not all parameters had a corresponding field (same name
* and the types are assignable). Private constructors are not included.
*
* If there were no invalid constructors, this returns an empty map.
*/
abstract ImmutableMap> unassignableConstructorParameterMap();
static NonWritableFieldsException create(
ImmutableMap> allNonWritableFieldsMap,
ImmutableMap> unassignableConstructorParameterMap) {
return new AutoValue_PaperParcelDescriptor_NonWritableFieldsException(
allNonWritableFieldsMap, unassignableConstructorParameterMap);
}
}
static final class Factory {
private final Elements elements;
private final Types types;
private final AdapterDescriptor.Factory adapterFactory;
private final FieldDescriptor.Factory fieldDescriptorFactory;
Factory(
Elements elements,
Types types,
AdapterDescriptor.Factory adapterFactory,
FieldDescriptor.Factory fieldDescriptorFactory) {
this.elements = elements;
this.types = types;
this.adapterFactory = adapterFactory;
this.fieldDescriptorFactory = fieldDescriptorFactory;
}
PaperParcelDescriptor create(TypeElement element, OptionsDescriptor options)
throws NonWritableFieldsException, NonReadableFieldsException {
ImmutableList fields = Utils.getFieldsToParcel(element, options);
//noinspection deprecation: Support for kapt2
ImmutableSet methods =
MoreElements.getLocalAndInheritedMethods(element, elements);
ImmutableList constructors =
Utils.orderedConstructorsIn(element, options.reflectAnnotations());
ImmutableList constructorFields;
boolean isConstructorVisible;
ImmutableList writableFields;
ImmutableMap setterMethodMap;
ImmutableList readableFields;
ImmutableMap getterMethodMap;
ImmutableMap adapters;
boolean singleton = Utils.isSingleton(types, element);
if (!singleton) {
WriteInfo writeInfo = WriteInfo.create(element, types, fieldDescriptorFactory, fields,
methods, constructors, options.reflectAnnotations());
constructorFields = writeInfo.constructorFields();
isConstructorVisible = writeInfo.isConstructorVisible();
writableFields = writeInfo.writableFields();
setterMethodMap = writeInfo.setterMethodMap();
ReadInfo readInfo = ReadInfo.create(element, types, fieldDescriptorFactory, fields,
methods, options.reflectAnnotations());
readableFields = readInfo.readableFields();
getterMethodMap = readInfo.getterMethodMap();
adapters = getAdapterMap(readInfo, options.allowSerializable());
} else {
constructorFields = ImmutableList.of();
isConstructorVisible = false;
writableFields = ImmutableList.of();
setterMethodMap = ImmutableMap.of();
readableFields = ImmutableList.of();
getterMethodMap = ImmutableMap.of();
adapters = ImmutableMap.of();
}
return new AutoValue_PaperParcelDescriptor(
element,
constructorFields,
isConstructorVisible,
writableFields,
setterMethodMap,
readableFields,
getterMethodMap,
adapters,
singleton);
}
private ImmutableMap getAdapterMap(ReadInfo readInfo,
boolean allowSerializable) {
ImmutableMap.Builder fieldAdapterMap =
ImmutableMap.builder();
if (readInfo != null) {
for (FieldDescriptor field : readInfo.readableFields()) {
addAdapterForField(fieldAdapterMap, field, allowSerializable);
}
for (FieldDescriptor field : readInfo.getterMethodMap().keySet()) {
addAdapterForField(fieldAdapterMap, field, allowSerializable);
}
}
return fieldAdapterMap.build();
}
private void addAdapterForField(
ImmutableMap.Builder fieldAdapterMap,
FieldDescriptor field, boolean allowSerializable) {
TypeMirror fieldType = field.type().get();
//noinspection ConstantConditions
if (!fieldType.getKind().isPrimitive()) {
AdapterDescriptor adapter = adapterFactory.create(fieldType, allowSerializable);
if (adapter != null) {
fieldAdapterMap.put(field, adapter);
} else {
throw new UnknownTypeException(fieldType, field.element());
}
}
}
}
private static class WriteInfo {
private final ImmutableList constructorFields;
private final boolean isConstructorVisible;
private final ImmutableList writableFields;
private final ImmutableMap setterMethodMap;
WriteInfo(
ImmutableList constructorFields,
boolean isConstructorVisible,
ImmutableList writableFields,
ImmutableMap setterMethodMap) {
this.constructorFields = constructorFields;
this.isConstructorVisible = isConstructorVisible;
this.writableFields = writableFields;
this.setterMethodMap = setterMethodMap;
}
ImmutableList constructorFields() {
return constructorFields;
}
boolean isConstructorVisible() {
return isConstructorVisible;
}
ImmutableList writableFields() {
return writableFields;
}
ImmutableMap setterMethodMap() {
return setterMethodMap;
}
static WriteInfo create(
TypeElement owner,
Types types,
FieldDescriptor.Factory fieldDescriptorFactory,
ImmutableList fields,
ImmutableSet methods,
ImmutableList constructors,
ImmutableList reflectAnnotations) throws NonWritableFieldsException {
ImmutableMap fieldNamesToField = fieldNamesToField(fields);
ImmutableMap.Builder>
allNonWritableFieldsMapBuilder = ImmutableMap.builder();
ImmutableMap.Builder>
unassignableConstructorParameterMapBuilder = ImmutableMap.builder();
for (ExecutableElement constructor : constructors) {
// Create a mutable copy of fieldNamesToField so we can remove elements from it as we iterate
// to keep track of which elements we have seen
Map nonConstructorFieldsMap = new LinkedHashMap<>(fieldNamesToField);
ImmutableList.Builder unassignableParametersBuilder =
ImmutableList.builder();
ImmutableList.Builder constructorFieldDescriptorsBuilder =
ImmutableList.builder();
// Iterate over parameters and check two things:
// 1) The parameter has a field with the same name
// 2) The parameter type is assignable to the field
List extends VariableElement> parameters = constructor.getParameters();
for (VariableElement parameter : parameters) {
String parameterName = parameter.getSimpleName().toString();
// All wildcards need to be stripped from the parameter type as a work around
// for kotlin data classes generating non-assignable constructor parameters
// with generic types.
TypeMirror parameterType =
Utils.replaceTypeVariablesWithUpperBounds(types, parameter.asType());
VariableElement fieldOrNull = fieldNamesToField.get(parameterName);
if (fieldOrNull != null && types.isAssignable(parameterType,
Utils.replaceTypeVariablesWithUpperBounds(types, fieldOrNull.asType()))) {
nonConstructorFieldsMap.remove(parameterName);
constructorFieldDescriptorsBuilder.add(fieldDescriptorFactory.create(owner,
fieldOrNull));
} else {
unassignableParametersBuilder.add(parameter);
}
}
// Check if there were any unassignable parameters in the constructor. If so, skip.
ImmutableList unassignableParameters = unassignableParametersBuilder.build();
if (unassignableParameters.size() > 0) {
unassignableConstructorParameterMapBuilder.put(constructor, unassignableParameters);
continue;
}
// Check that the remaining parameters are assignable directly, or via setters.
ImmutableList.Builder nonWritableFieldsBuilder = ImmutableList.builder();
ImmutableList nonConstructorFields =
ImmutableList.copyOf(nonConstructorFieldsMap.values());
ImmutableList.Builder writableFieldsBuilder = ImmutableList.builder();
ImmutableMap.Builder setterMethodMapBuilder =
ImmutableMap.builder();
for (VariableElement field : nonConstructorFields) {
if (isWritableDirectly(field)) {
writableFieldsBuilder.add(fieldDescriptorFactory.create(owner, field));
} else {
Optional setterMethod = getSetterMethod(types, field, methods);
if (setterMethod.isPresent()) {
setterMethodMapBuilder.put(fieldDescriptorFactory.create(owner, field),
setterMethod.get());
} else if (Utils.usesAnyAnnotationsFrom(field, reflectAnnotations)) {
writableFieldsBuilder.add(fieldDescriptorFactory.create(owner, field));
} else {
nonWritableFieldsBuilder.add(field);
}
}
}
ImmutableList nonWritableFields = nonWritableFieldsBuilder.build();
if (nonWritableFields.size() != 0) {
// Map all of the non-writable fields to the corresponding constructor for (potential)
// error handling later
allNonWritableFieldsMapBuilder.put(constructor, nonWritableFields);
} else {
// All fields are writable using this constructor
return new WriteInfo(
constructorFieldDescriptorsBuilder.build(),
Visibility.ofElement(constructor) != Visibility.PRIVATE,
writableFieldsBuilder.build(), setterMethodMapBuilder.build());
}
}
// Throw an error if fields are not writable
throw NonWritableFieldsException.create(
allNonWritableFieldsMapBuilder.build(),
unassignableConstructorParameterMapBuilder.build());
}
private static Optional getSetterMethod(
Types types, VariableElement field, ImmutableSet allMethods) {
String fieldName = field.getSimpleName().toString();
TypeMirror fieldType = Utils.replaceTypeVariablesWithUpperBounds(types, field.asType());
for (ExecutableElement method : allMethods) {
List extends VariableElement> parameters = method.getParameters();
if (parameters.size() == 1
&& isSetterMethod(fieldName, method.getSimpleName().toString())
&& method.getTypeParameters().size() == 0
&& types.isAssignable(Utils.replaceTypeVariablesWithUpperBounds(
types, parameters.get(0).asType()), fieldType)) {
return Optional.of(method);
}
}
return Optional.absent();
}
private static ImmutableMap fieldNamesToField(
ImmutableList fields) {
ImmutableMap.Builder fieldNamesToField = ImmutableMap.builder();
for (VariableElement field : fields) {
fieldNamesToField.put(field.getSimpleName().toString(), field);
}
return fieldNamesToField.build();
}
private static boolean isWritableDirectly(VariableElement field) {
Set fieldModifiers = field.getModifiers();
return !fieldModifiers.contains(Modifier.PRIVATE)
&& !fieldModifiers.contains(Modifier.FINAL);
}
}
private static class ReadInfo {
private final ImmutableList readableFields;
private final ImmutableMap getterMethodMap;
private ReadInfo(
ImmutableList readableFields,
ImmutableMap getterMethodMap) {
this.readableFields = readableFields;
this.getterMethodMap = getterMethodMap;
}
ImmutableList readableFields() {
return readableFields;
}
ImmutableMap getterMethodMap() {
return getterMethodMap;
}
static ReadInfo create(
TypeElement owner,
Types types,
FieldDescriptor.Factory fieldDescriptorFactory,
ImmutableList fields,
ImmutableSet methods,
ImmutableList reflectAnnotations) throws NonReadableFieldsException {
ImmutableList.Builder readableFieldsBuilder = ImmutableList.builder();
ImmutableMap.Builder getterMethodMapBuilder =
ImmutableMap.builder();
ImmutableList.Builder nonReadableFieldsBuilder = ImmutableList.builder();
for (VariableElement field : fields) {
if (isReadableDirectly(field)) {
readableFieldsBuilder.add(fieldDescriptorFactory.create(owner, field));
} else {
Optional accessorMethod = getAccessorMethod(types, field, methods);
if (accessorMethod.isPresent()) {
getterMethodMapBuilder.put(fieldDescriptorFactory.create(owner, field), accessorMethod.get());
} else if (Utils.usesAnyAnnotationsFrom(field, reflectAnnotations)) {
readableFieldsBuilder.add(fieldDescriptorFactory.create(owner, field));
} else {
nonReadableFieldsBuilder.add(field);
}
}
}
ImmutableList nonReadableFields = nonReadableFieldsBuilder.build();
if (nonReadableFields.size() > 0) {
throw NonReadableFieldsException.create(nonReadableFields);
}
return new ReadInfo(readableFieldsBuilder.build(), getterMethodMapBuilder.build());
}
private static Optional getAccessorMethod(
Types types, VariableElement field, ImmutableSet methods) {
String fieldName = field.getSimpleName().toString();
TypeMirror fieldType = field.asType();
for (ExecutableElement method : methods) {
if (method.getParameters().size() == 0
&& isGetterMethod(fieldName, method.getSimpleName().toString())
&& method.getTypeParameters().size() == 0
&& types.isAssignable(method.getReturnType(), fieldType)) {
return Optional.of(method);
}
}
return Optional.absent();
}
private static boolean isReadableDirectly(VariableElement field) {
Set fieldModifiers = field.getModifiers();
return !fieldModifiers.contains(Modifier.PRIVATE);
}
}
}