All Downloads are FREE. Search and download functionalities are using the official Maven repository.

retrofit.processor.BuilderSpec Maven / Gradle / Ivy

The newest version!
/*
 * 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 retrofit.processor;

import retrofit.http.Retrofit;
import com.google.auto.common.MoreElements;
import com.google.auto.common.MoreTypes;
import com.google.common.base.Equivalence;
import com.google.common.base.Optional;
import com.google.common.collect.FluentIterable;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;

import java.beans.Introspector;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;

import javax.annotation.processing.ProcessingEnvironment;
import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.TypeParameterElement;
import javax.lang.model.element.VariableElement;
import javax.lang.model.type.DeclaredType;
import javax.lang.model.type.ErrorType;
import javax.lang.model.type.TypeKind;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.type.TypeVariable;
import javax.lang.model.type.TypeVisitor;
import javax.lang.model.util.ElementFilter;
import javax.lang.model.util.Elements;
import javax.lang.model.util.SimpleTypeVisitor6;
import javax.lang.model.util.Types;

/**
 * Support for Retrofit builders.
 *
 * @author Éamonn McManus
 */
class BuilderSpec {
  private static final Equivalence TYPE_EQUIVALENCE = MoreTypes.equivalence();

  private final TypeElement autoValueClass;
  private final Elements elementUtils;
  private final ErrorReporter errorReporter;

  BuilderSpec(
      TypeElement autoValueClass,
      ProcessingEnvironment processingEnv,
      ErrorReporter errorReporter) {
    this.autoValueClass = autoValueClass;
    this.elementUtils = processingEnv.getElementUtils();
    this.errorReporter = errorReporter;
  }

  private static final Set CLASS_OR_INTERFACE =
      Sets.immutableEnumSet(ElementKind.CLASS, ElementKind.INTERFACE);

  /**
   * Determines if the {@code @Retrofit} class for this instance has a correct nested
   * {@code @Retrofit.Builder} class or interface and return a representation of it in an
   * {@code Optional} if so.
   */
  Optional getBuilder() {
    Optional builderTypeElement = Optional.absent();
    for (TypeElement containedClass : ElementFilter.typesIn(autoValueClass.getEnclosedElements())) {
      if (MoreElements.isAnnotationPresent(containedClass, Retrofit.Builder.class)) {
        if (!CLASS_OR_INTERFACE.contains(containedClass.getKind())) {
          errorReporter.reportError(
              "@Retrofit.Builder can only apply to a class or an interface", containedClass);
        } else if (builderTypeElement.isPresent()) {
          errorReporter.reportError(
              autoValueClass + " already has a Builder: " + builderTypeElement.get(),
              containedClass);
        } else {
          builderTypeElement = Optional.of(containedClass);
        }
      }
    }

    Optional validateMethod = Optional.absent();
    for (ExecutableElement containedMethod :
        ElementFilter.methodsIn(autoValueClass.getEnclosedElements())) {
      if (MoreElements.isAnnotationPresent(containedMethod, Retrofit.Validate.class)) {
        if (containedMethod.getModifiers().contains(Modifier.STATIC)) {
          errorReporter.reportError(
              "@Retrofit.Validate cannot apply to a static method", containedMethod);
        } else if (!containedMethod.getParameters().isEmpty()) {
          errorReporter.reportError(
              "@Retrofit.Validate method must not have parameters", containedMethod);
        } else if (containedMethod.getReturnType().getKind() != TypeKind.VOID) {
          errorReporter.reportError(
              "Return type of @Retrofit.Validate method must be void", containedMethod);
        } else if (validateMethod.isPresent()) {
          errorReporter.reportError(
              "There can only be one @Retrofit.Validate method", containedMethod);
        } else {
          validateMethod = Optional.of(containedMethod);
        }
      }
    }

    if (builderTypeElement.isPresent()) {
      return builderFrom(builderTypeElement.get(), validateMethod);
    } else {
      if (validateMethod.isPresent()) {
        errorReporter.reportError(
            "@Retrofit.Validate is only meaningful if there is an @Retrofit.Builder",
            validateMethod.get());
      }
      return Optional.absent();
    }
  }

  /**
   * Representation of an {@code Retrofit.Builder} class or interface.
   */
  class Builder {
    private final TypeElement builderTypeElement;
    private final ExecutableElement buildMethod;
    private final ImmutableList setters;
    private final Optional validateMethod;

    Builder(
        TypeElement builderTypeElement,
        ExecutableElement build,
        List setters,
        Optional validateMethod) {
      this.builderTypeElement = builderTypeElement;
      this.buildMethod = build;
      this.setters = ImmutableList.copyOf(setters);
      this.validateMethod = validateMethod;
    }

    ExecutableElement buildMethod() {
      return buildMethod;
    }

    /**
     * Returns a map from property name to setter method. If the setter methods are invalid
     * (for example not every getter has a setter, or some setters don't correspond to getters)
     * then emits an error message and returns null.
     *
     * @param getterToPropertyName a list of getter methods, such as {@code abstract String foo();}
     *                             or {@code abstract String getFoo();}.
     */
    private Map makeSetterMap(
        Map getterToPropertyName) {

      // Map property names to types based on the getters.
      Map getterMap = new TreeMap();
      for (Map.Entry entry : getterToPropertyName.entrySet()) {
        getterMap.put(entry.getValue(), entry.getKey().getReturnType());
      }

      Map noPrefixMap = Maps.newLinkedHashMap();
      Map prefixMap = Maps.newLinkedHashMap();

      boolean ok = true;
      // For each setter, check that its name and type correspond to a getter, and remove it from
      // the map if so.
      for (ExecutableElement setter : setters) {
        Map map = noPrefixMap;
        String name = setter.getSimpleName().toString();
        TypeMirror type = getterMap.get(name);
        if (type == null && name.startsWith("set")) {
          name = Introspector.decapitalize(name.substring(3));
          type = getterMap.get(name);
          map = prefixMap;
        }
        if (type == null) {
          errorReporter.reportError(
              "Method does not correspond to a property of " + autoValueClass, setter);
          ok = false;
        } else {
          VariableElement parameter = Iterables.getOnlyElement(setter.getParameters());
          if (TYPE_EQUIVALENCE.equivalent(type, parameter.asType())) {
            getterMap.remove(name);
            map.put(name, setter);
          } else {
            errorReporter.reportError("Parameter type should be " + type, parameter);
            ok = false;
          }
        }
      }
      if (!ok) {
        return null;
      }

      boolean prefixing = !prefixMap.isEmpty();
      if (prefixing && !noPrefixMap.isEmpty()) {
        errorReporter.reportError(
            "If any setter methods use the setFoo convention then all must",
            noPrefixMap.values().iterator().next());
        return null;
      }

      // If there are any properties left in the map then we didn't see setters for them. Report
      // an error for each one separately.
      if (!getterMap.isEmpty()) {
        for (Map.Entry entry : getterMap.entrySet()) {
          String setterName = prefixing ? prefixWithSet(entry.getKey()) : entry.getKey();
          String error = String.format(
              "Expected a method with this signature: %s%s %s(%s)",
              builderTypeElement,
              TypeSimplifier.actualTypeParametersString(builderTypeElement),
              setterName,
              entry.getValue());
          errorReporter.reportError(error, builderTypeElement);
        }
        return null;
      }

      return noPrefixMap.isEmpty() ? prefixMap : noPrefixMap;
    }

    private String prefixWithSet(String propertyName) {
      // This is not internationalizationally correct, but it corresponds to what
      // Introspector.decapitalize does.
      return "set" + Character.toUpperCase(propertyName.charAt(0)) + propertyName.substring(1);
    }

    /**
     * Finds any methods in the set that return the builder type. If the builder has type parameters
     * {@code }, then the return type of the method must be {@code Builder} with
     * the same parameter names. We enforce elsewhere that the names and bounds of the builder
     * parameters must be the same as those of the @Retrofit class. Here's a correct example:
     * 
     * {@code @Retrofit abstract class Foo {
     *   abstract int someProperty();
     *
     *   abstract Builder toBuilder();
     *
     *   interface Builder {...}
     * }}
     * 
*

*

We currently impose that there cannot be more than one such method.

*/ ImmutableSet toBuilderMethods( Types typeUtils, Set abstractMethods) { ImmutableList builderTypeParamNames = FluentIterable.from(builderTypeElement.getTypeParameters()) .transform(SimpleNameFunction.INSTANCE) .toList(); ImmutableSet.Builder methods = ImmutableSet.builder(); for (ExecutableElement method : abstractMethods) { if (builderTypeElement.equals(typeUtils.asElement(method.getReturnType()))) { methods.add(method); DeclaredType returnType = MoreTypes.asDeclared(method.getReturnType()); ImmutableList.Builder typeArguments = ImmutableList.builder(); for (TypeMirror typeArgument : returnType.getTypeArguments()) { if (typeArgument.getKind().equals(TypeKind.TYPEVAR)) { typeArguments.add(typeUtils.asElement(typeArgument).getSimpleName().toString()); } } if (!builderTypeParamNames.equals(typeArguments.build())) { errorReporter.reportError( "Builder converter method should return " + builderTypeElement + TypeSimplifier.actualTypeParametersString(builderTypeElement), method); } } } ImmutableSet builderMethods = methods.build(); if (builderMethods.size() > 1) { errorReporter.reportError( "There can be at most one builder converter method", builderMethods.iterator().next()); } return builderMethods; } void defineVars( RetrofitTemplateVars vars, TypeSimplifier typeSimplifier, Map getterToPropertyName) { Map propertyNameToSetter = makeSetterMap(getterToPropertyName); if (propertyNameToSetter == null) { return; } vars.builderIsInterface = builderTypeElement.getKind() == ElementKind.INTERFACE; vars.builderTypeName = TypeSimplifier.classNameOf(builderTypeElement); vars.builderFormalTypes = typeSimplifier.formalTypeParametersString(builderTypeElement); vars.builderActualTypes = TypeSimplifier.actualTypeParametersString(builderTypeElement); vars.buildMethodName = buildMethod.getSimpleName().toString(); if (validateMethod.isPresent()) { vars.validators = ImmutableSet.of(validateMethod.get().getSimpleName().toString()); } else { vars.validators = ImmutableSet.of(); } ImmutableMap.Builder setterNameBuilder = ImmutableMap.builder(); for (Map.Entry entry : propertyNameToSetter.entrySet()) { setterNameBuilder.put(entry.getKey(), entry.getValue().getSimpleName().toString()); } vars.builderSetterNames = setterNameBuilder.build(); } } /** * Returns a representation of the given {@code @Retrofit.Builder} class or interface. If the * class or interface has abstract methods that could not be part of any builder, emits error * messages and returns null. */ private Optional builderFrom( TypeElement builderTypeElement, Optional validateMethod) { // We require the builder to have the same type parameters as the @Retrofit class, meaning the // same names and bounds. In principle the type parameters could have different names, but that // would be confusing, and our code would reject it anyway because it wouldn't consider that // the return type of Foo build() was really the same as the declaration of Foo. This // check produces a better error message in that case and similar ones. boolean ok = true; int nTypeParameters = autoValueClass.getTypeParameters().size(); if (nTypeParameters != builderTypeElement.getTypeParameters().size()) { ok = false; } else { for (int i = 0; i < nTypeParameters; i++) { TypeParameterElement autoValueParam = autoValueClass.getTypeParameters().get(i); TypeParameterElement builderParam = builderTypeElement.getTypeParameters().get(i); if (!autoValueParam.getSimpleName().equals(builderParam.getSimpleName())) { ok = false; break; } Set autoValueBounds = new TypeMirrorSet(autoValueParam.getBounds()); Set builderBounds = new TypeMirrorSet(builderParam.getBounds()); if (!autoValueBounds.equals(builderBounds)) { ok = false; break; } } } if (!ok) { errorReporter.reportError( "Type parameters of " + builderTypeElement + " must have same names and bounds as " + "type parameters of " + autoValueClass, builderTypeElement); return Optional.absent(); } String typeParams = TypeSimplifier.actualTypeParametersString(autoValueClass); List buildMethods = new ArrayList(); List setterMethods = new ArrayList(); // For each abstract method (in builderTypeElement or inherited), check that it is either // a setter method or a build method. A setter method has one argument and returns // builderTypeElement. A build method has no arguments and returns the @Retrofit class. // Record each method in one of the two lists. for (ExecutableElement method : abstractMethods(builderTypeElement)) { boolean thisOk = false; int nParameters = method.getParameters().size(); if (nParameters == 0 && TYPE_EQUIVALENCE.equivalent(method.getReturnType(), autoValueClass.asType())) { buildMethods.add(method); thisOk = true; } else if (nParameters == 1 && TYPE_EQUIVALENCE.equivalent(method.getReturnType(), builderTypeElement.asType())) { setterMethods.add(method); thisOk = true; } if (!thisOk) { errorReporter.reportError( "Builder methods must either have no arguments and return " + autoValueClass + typeParams + " or have one argument and return " + builderTypeElement + typeParams, method); ok = false; } } if (buildMethods.isEmpty()) { errorReporter.reportError( "Builder must have a single no-argument method returning " + autoValueClass + typeParams, builderTypeElement); ok = false; } else if (buildMethods.size() > 1) { // More than one eligible build method. Emit an error for each one, that is attached to // that build method. for (ExecutableElement buildMethod : buildMethods) { errorReporter.reportError( "Builder must have a single no-argument method returning " + autoValueClass + typeParams, buildMethod); } ok = false; } if (ok) { return Optional.of(new Builder( builderTypeElement, Iterables.getOnlyElement(buildMethods), setterMethods, validateMethod)); } else { return Optional.absent(); } } // Return a list of all abstract methods in the given TypeElement or inherited from ancestors. private List abstractMethods(TypeElement typeElement) { List methods = new ArrayList(); addAbstractMethods(typeElement.asType(), methods); return methods; } private static final TypeVisitor AS_ELEMENT_VISITOR = new SimpleTypeVisitor6() { @Override protected Element defaultAction(TypeMirror e, Void p) { throw new IllegalArgumentException(e + "cannot be converted to an Element"); } @Override public Element visitDeclared(DeclaredType t, Void p) { return t.asElement(); } @Override public Element visitError(ErrorType t, Void p) { return t.asElement(); } @Override public Element visitTypeVariable(TypeVariable t, Void p) { return t.asElement(); } }; private void addAbstractMethods( TypeMirror typeMirror, List abstractMethods) { if (typeMirror.getKind() != TypeKind.DECLARED) { return; } TypeElement typeElement = MoreElements.asType(typeMirror.accept(AS_ELEMENT_VISITOR, null)); addAbstractMethods(typeElement.getSuperclass(), abstractMethods); for (TypeMirror interfaceMirror : typeElement.getInterfaces()) { addAbstractMethods(interfaceMirror, abstractMethods); } for (ExecutableElement method : ElementFilter.methodsIn(typeElement.getEnclosedElements())) { for (Iterator it = abstractMethods.iterator(); it.hasNext(); ) { ExecutableElement maybeOverridden = it.next(); if (elementUtils.overrides(method, maybeOverridden, typeElement)) { it.remove(); } } if (method.getModifiers().contains(Modifier.ABSTRACT)) { abstractMethods.add(method); } } } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy