com.f2prateek.dart.common.AbstractDartProcessor Maven / Gradle / Ivy
The newest version!
/*
* Copyright 2013 Jake Wharton
* Copyright 2014 Prateek Srivastava (@f2prateek)
*
* 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.f2prateek.dart.common;
import com.f2prateek.dart.InjectExtra;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.lang.annotation.Annotation;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.Filer;
import javax.annotation.processing.ProcessingEnvironment;
import javax.annotation.processing.RoundEnvironment;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.Element;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.TypeElement;
import javax.lang.model.type.DeclaredType;
import javax.lang.model.type.TypeKind;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.util.Elements;
import javax.lang.model.util.Types;
import static javax.lang.model.element.ElementKind.CLASS;
import static javax.lang.model.element.Modifier.PRIVATE;
import static javax.lang.model.element.Modifier.STATIC;
import static javax.tools.Diagnostic.Kind.ERROR;
/**
* Base class of the annotation processors of Dart.
* It collects all information about extra injections to be performed
* when scanning the {@link com.f2prateek.dart.InjectExtra} annotations of given source files.
* The collected information is stored in a collection of {@code InjectionTarget}.
* All annotations processors in Dart support the option {@code #OPTION_DART_DEBUG}
* that will log information about annotation processor and generated code.
*
* @see #findAndParseTargets(RoundEnvironment)
*/
public abstract class AbstractDartProcessor extends AbstractProcessor {
public static final String OPTION_DART_DEBUG = "dart.debug";
// @formatter:off
//You must go to Preferences->Code Style->General->Formatter Control
// and check Enable formatter markers in comments for this to work.
private static final Set JAVA_KEYWORDS = new HashSet<>(Arrays.asList(
"abstract", "assert", "boolean", "break", "byte", "case",
"catch", "char", "class", "const", "continue", "enum",
"default", "do", "double", "else", "extends", "while",
"false", "final", "finally", "float", "for",
"goto", "if", "implements", "import", "instanceof",
"int", "interface", "long", "native", "new",
"null", "package", "private", "protected", "public",
"return", "short", "static", "strictfp", "super",
"switch", "synchronized", "this", "throw", "throws",
"transient", "true", "try", "void", "volatile"
));
// @formatter:on
private List singleCollections;
private List doubleCollections;
private Elements elementUtils;
protected Types typeUtils;
protected Filer filer;
protected boolean isDebugEnabled;
protected boolean usesParcelerOption = true;
protected abstract Map findAndParseTargets(RoundEnvironment env);
@Override public synchronized void init(ProcessingEnvironment env) {
super.init(env);
elementUtils = env.getElementUtils();
typeUtils = env.getTypeUtils();
filer = env.getFiler();
//note for maintenance : here we use class names directly
//as a general rule of thumb, we should not use classes in an annotation processor
//as those classes are different from classes seen at runtime.
//Using class created a bug in apps built by gradle as SparseArray was not found during
//annotation processing time.
singleCollections = getTypeElements(new String[] {
"java.util.List", "java.util.ArrayList", "java.util.LinkedList", "java.util.Set",
"java.util.HashSet", "java.util.SortedSet", "java.util.TreeSet", "java.util.LinkedHashSet",
"android.util.SparseArray"
});
doubleCollections = getTypeElements(new String[] {
"java.util.Map", "java.util.HashMap", "java.util.LinkedHashMap", "java.util.SortedMap",
"java.util.TreeMap"
});
final Map options = env.getOptions();
isDebugEnabled |= options.containsKey(OPTION_DART_DEBUG) && Boolean.parseBoolean(
options.get(OPTION_DART_DEBUG));
}
@Override public Set getSupportedAnnotationTypes() {
Set supportTypes = new LinkedHashSet<>();
supportTypes.add(InjectExtra.class.getCanonicalName());
return supportTypes;
}
@Override public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latestSupported();
}
@Override public Set getSupportedOptions() {
Set supportedOptions = new LinkedHashSet<>();
supportedOptions.add(OPTION_DART_DEBUG);
return supportedOptions;
}
@SuppressWarnings("This method is only used when debugging/creating tests.")
public void setIsDebugEnabled(boolean isDebugEnabled) {
this.isDebugEnabled = isDebugEnabled;
}
@SuppressWarnings("This method is only used for testing.")
public boolean isDebugEnabled() {
return isDebugEnabled;
}
public void setUsesParcelerOption(boolean usesParcelerOption) {
this.usesParcelerOption = usesParcelerOption;
}
protected InjectionTarget getOrCreateTargetClass(Map targetClassMap,
TypeElement typeElement) {
InjectionTarget injectionTarget = targetClassMap.get(typeElement);
if (injectionTarget == null) {
final String targetType = typeElement.getQualifiedName().toString();
final String classPackage = getPackageName(typeElement);
final String className = getClassName(typeElement, classPackage);
final boolean isAbstractType = typeElement.getModifiers().contains(Modifier.ABSTRACT);
injectionTarget = new InjectionTarget(classPackage, className, targetType, isAbstractType);
targetClassMap.put(typeElement, injectionTarget);
}
return injectionTarget;
}
/** Finds the parent injector type in the supplied set, if any. */
protected String findParentFqcn(TypeElement typeElement, Set parents) {
TypeMirror type;
while (true) {
type = typeElement.getSuperclass();
if (type.getKind() == TypeKind.NONE) {
return null;
}
typeElement = (TypeElement) ((DeclaredType) type).asElement();
if (containsTypeMirror(parents, type)) {
String packageName = getPackageName(typeElement);
return packageName + "." + getClassName(typeElement, packageName);
}
}
}
protected void error(Element element, String message, Object... args) {
processingEnv.getMessager().printMessage(ERROR, String.format(message, args), element);
}
protected void parseInjectExtraAnnotatedElements(RoundEnvironment env,
Map targetClassMap, Set erasedTargetTypes) {
for (Element element : env.getElementsAnnotatedWith(InjectExtra.class)) {
try {
parseInjectExtra(element, targetClassMap, erasedTargetTypes);
} catch (Exception e) {
StringWriter stackTrace = new StringWriter();
e.printStackTrace(new PrintWriter(stackTrace));
error(element, "Unable to generate extra injector when parsing @InjectExtra.\n\n%s",
stackTrace.toString());
}
}
}
private void parseInjectExtra(Element element, Map targetClassMap,
Set erasedTargetTypes) {
TypeElement enclosingElement = (TypeElement) element.getEnclosingElement();
// Verify common generated code restrictions.
if (!isValidUsageOfInjectExtra(InjectExtra.class, element)) {
return;
}
String annotationValue = element.getAnnotation(InjectExtra.class).value();
if (!isNullOrEmpty(annotationValue) && !isValidJavaIdentifier(annotationValue)) {
throw new IllegalArgumentException("Keys have to be valid java variable identifiers. "
+ "https://docs.oracle.com/cd/E19798-01/821-1841/bnbuk/index.html");
}
// Assemble information on the injection point.
String name = element.getSimpleName().toString();
String key = isNullOrEmpty(annotationValue) ? name : annotationValue;
TypeMirror type = element.asType();
boolean required = isRequiredInjection(element);
boolean parcel = isParcelerAvailable() && isValidExtraTypeForParceler(type);
InjectionTarget injectionTarget = getOrCreateTargetClass(targetClassMap, enclosingElement);
injectionTarget.addField(key, name, type, required, parcel);
// Add the type-erased version to the valid injection targets set.
TypeMirror erasedTargetType = typeUtils.erasure(enclosingElement.asType());
erasedTargetTypes.add(erasedTargetType);
}
private boolean isValidUsageOfInjectExtra(Class extends Annotation> annotationClass,
Element element) {
boolean valid = true;
TypeElement enclosingElement = (TypeElement) element.getEnclosingElement();
// Verify modifiers.
Set modifiers = element.getModifiers();
if (modifiers.contains(PRIVATE) || modifiers.contains(STATIC)) {
error(element, "@%s fields must not be private or static. (%s.%s)",
annotationClass.getSimpleName(), enclosingElement.getQualifiedName(),
element.getSimpleName());
valid = false;
}
//Verify that the type is primitive or serializable or parcelable
TypeMirror typeElement = element.asType();
if (!isValidExtraType(typeElement) && !(isParcelerAvailable() && isValidExtraTypeForParceler(
typeElement))) {
error(element, "@%s field must be a primitive or Serializable or Parcelable (%s.%s). "
+ "If you use Parceler, all types supported by Parceler are allowed.",
annotationClass.getSimpleName(), enclosingElement.getQualifiedName(),
element.getSimpleName());
valid = false;
}
// Verify containing type.
if (enclosingElement.getKind() != CLASS) {
error(enclosingElement, "@%s fields may only be contained in classes. (%s.%s)",
annotationClass.getSimpleName(), enclosingElement.getQualifiedName(),
element.getSimpleName());
valid = false;
}
// Verify containing class visibility is not private.
if (enclosingElement.getModifiers().contains(PRIVATE)) {
error(enclosingElement, "@%s fields may not be contained in private classes. (%s.%s)",
annotationClass.getSimpleName(), enclosingElement.getQualifiedName(),
element.getSimpleName());
valid = false;
}
return valid;
}
private boolean isValidExtraType(TypeMirror type) {
return isSerializable(type) || isParcelable(type) || isCharSequence(type);
}
private boolean isValidExtraTypeForParceler(TypeMirror type) {
return isValidForParceler(type, false);
}
private boolean isValidForParceler(TypeMirror type, boolean subCollection) {
if (subCollection && (isSerializable(type) || isParcelable(type))) {
return true;
}
if (isAnnotatedWithParcel(type)) {
return true;
}
if (type instanceof DeclaredType) {
DeclaredType declaredType = (DeclaredType) type;
if (existsWithin(type, singleCollections)) {
return isValidForParceler(declaredType.getTypeArguments().get(0), true);
}
if (existsWithin(type, doubleCollections)) {
return isValidForParceler(declaredType.getTypeArguments().get(0), true)
&& isValidForParceler(declaredType.getTypeArguments().get(1), true);
}
}
return false;
}
private boolean isSerializable(TypeMirror type) {
TypeMirror serializableTypeMirror =
elementUtils.getTypeElement("java.io.Serializable").asType();
return typeUtils.isAssignable(type, serializableTypeMirror);
}
private boolean isParcelable(TypeMirror type) {
TypeMirror parcelableTypeMirror = elementUtils.getTypeElement("android.os.Parcelable").asType();
return typeUtils.isAssignable(type, parcelableTypeMirror);
}
private boolean isCharSequence(TypeMirror type) {
TypeMirror charSequenceTypeMirror =
elementUtils.getTypeElement("java.lang.CharSequence").asType();
return typeUtils.isAssignable(type, charSequenceTypeMirror);
}
private boolean isParcelerAvailable() {
return usesParcelerOption && elementUtils.getTypeElement("org.parceler.Parcel") != null;
}
private boolean isAnnotatedWithParcel(TypeMirror type) {
return hasAnnotationWithFqcn(typeUtils.asElement(type), "org.parceler.Parcel");
}
private boolean existsWithin(TypeMirror type, List supportedTypes) {
for (Element supportedType : supportedTypes) {
if (typeUtils.erasure(type).equals(typeUtils.erasure(supportedType.asType()))) {
return true;
}
}
return false;
}
private List getTypeElements(String[] classNames) {
List elements = new ArrayList<>();
for (String className : classNames) {
elements.add(elementUtils.getTypeElement(className));
}
return elements;
}
/**
* Returns {@code true} if the an annotation is found on the given element with the given class
* name (not fully qualified).
*/
private static boolean hasAnnotationWithName(Element element, String simpleName) {
for (AnnotationMirror mirror : element.getAnnotationMirrors()) {
final Element annnotationElement = mirror.getAnnotationType().asElement();
String annotationName = annnotationElement.getSimpleName().toString();
if (simpleName.equals(annotationName)) {
return true;
}
}
return false;
}
/**
* Returns {@code true} if an injection is deemed to be required. Returns false when a field is
* annotated with any annotation named {@code Optional} or {@code Nullable}.
*/
private static boolean isRequiredInjection(Element element) {
return !hasAnnotationWithName(element, "Nullable") && !hasAnnotationWithName(element,
"Optional");
}
/**
* Returns {@code true} if the an annotation is found on the given element with the given class
* name (must be a fully qualified class name).
*/
private static boolean hasAnnotationWithFqcn(Element element, String annotationClassNameName) {
if (element != null) {
for (AnnotationMirror annotationMirror : element.getAnnotationMirrors()) {
if (annotationMirror.getAnnotationType()
.asElement()
.toString()
.equals(annotationClassNameName)) {
return true;
}
}
}
return false;
}
private static String getClassName(TypeElement type, String packageName) {
int packageLen = packageName.length() + 1;
return type.getQualifiedName().toString().substring(packageLen).replace('.', '$');
}
/**
* Returns true if the string is null or 0-length.
*
* @param str the string to be examined
* @return true if str is null or zero length
*/
private static boolean isNullOrEmpty(String str) {
return str == null || str.trim().length() == 0;
}
private boolean containsTypeMirror(Collection mirrors, TypeMirror query) {
// Ensure we are checking against a type-erased version for normalization purposes.
query = typeUtils.erasure(query);
for (TypeMirror mirror : mirrors) {
if (typeUtils.isSameType(mirror, query)) {
return true;
}
}
return false;
}
private String getPackageName(TypeElement type) {
return elementUtils.getPackageOf(type).getQualifiedName().toString();
}
/**
* Returns true if the string is a valid Java identifier.
* See Identifiers
*
* @param str the string to be examined
* @return true if str is a valid Java identifier
*/
static boolean isValidJavaIdentifier(String str) {
if (isNullOrEmpty(str)) {
return false;
}
if (JAVA_KEYWORDS.contains(str)) {
return false;
}
if (!Character.isJavaIdentifierStart(str.charAt(0))) {
return false;
}
for (int i = 1; i < str.length(); i++) {
if (!Character.isJavaIdentifierPart(str.charAt(i))) {
return false;
}
}
return true;
}
}