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

com.salesforce.aptspring.processor.SpringAnnotationParser Maven / Gradle / Ivy

Go to download

This project contains the apt processor that implements all the checks enumerated in @Verify. It is a self contained, and shaded jar.

The newest version!
/*
 * Copyright © 2017, Salesforce.com, Inc
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *     * Redistributions of source code must retain the above copyright
 *       notice, this list of conditions and the following disclaimer.
 *     * Redistributions in binary form must reproduce the above copyright
 *       notice, this list of conditions and the following disclaimer in the
 *       documentation and/or other materials provided with the distribution.
 *     * Neither the name of the  nor the
 *       names of its contributors may be used to endorse or promote products
 *       derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL  BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
package com.salesforce.aptspring.processor;

import java.util.AbstractMap;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.function.Predicate;
import java.util.stream.Collector;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import javax.annotation.processing.Messager;
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.VariableElement;
import javax.lang.model.type.TypeKind;
import javax.tools.Diagnostic.Kind;

import com.salesforce.apt.graph.model.DefinitionModel;
import com.salesforce.apt.graph.model.ExpectedModel;
import com.salesforce.apt.graph.model.InstanceDependencyModel;
import com.salesforce.apt.graph.model.InstanceModel;
import com.salesforce.aptspring.Verified;

public class SpringAnnotationParser {
  
  private static final List DISALLOWED_ON_METHOD = Collections.unmodifiableList(Arrays.asList(
      Modifier.ABSTRACT,
      Modifier.DEFAULT,
      Modifier.FINAL,
      Modifier.NATIVE,
      Modifier.STATIC,
      Modifier.VOLATILE
      //Modifier.PRIVATE, //checks to see if the method is public, these checks would be redundant.
      //Modifier.PROTECTED
      ));
  
  private static final String QUALIFIER_TYPE = "org.springframework.beans.factory.annotation.Qualifier";
  
  private static final String VALUE_TYPE = "org.springframework.beans.factory.annotation.Value";
  
  private static final String COMPONENTSCAN_TYPE = "org.springframework.context.annotation.ComponentScan";
  
  private static final String COMPONENTSCANS_TYPE = "org.springframework.context.annotation.ComponentScans";
  
  private static final String CONFIGURATION_TYPE = "org.springframework.context.annotation.Configuration";
  
  private static final String COMPONENT_TYPE = "org.springframework.stereotype.Component";
  
  private static final String IMPORT_TYPE = "org.springframework.context.annotation.Import";
  
  private static final String IMPORT_RESOURCE_TYPE = "org.springframework.context.annotation.ImportResource";
    
  private static final String AUTOWIRED_TYPE = "org.springframework.beans.factory.annotation.Autowired";
  
  private static final String DEFAULT_ANNOTATION_VALUE = "value";

  private static final Map BANNED_ANNOTATIONS = Collections.unmodifiableMap(Stream.of(
            entry(COMPONENTSCAN_TYPE, "You may not use @ComponentScan(s) on @Verified classes"),
            entry(COMPONENTSCANS_TYPE, "You may not use @ComponentScan(s) on @Verified classes"),
            entry(IMPORT_RESOURCE_TYPE, "You may not use @ImportResource on @Verified classes"),
            entry(CONFIGURATION_TYPE, "@Verified annotation must only be used on @Bean LITE factory classes or @Component classes"))
          .collect(entriesToMap()));

  private static final Map COMPONENT_BANNED_ANNOTATIONS = Collections.unmodifiableMap(
      Stream.concat(BANNED_ANNOTATIONS.entrySet().stream(),
         Stream.of(
           entry(IMPORT_TYPE, "You may not use @Import on @Verified @Component classes")))
      .collect(entriesToMap()));  
          

  private static final Map BEAN_LITE_BANNED_ANNOTATIONS = Collections.unmodifiableMap(
      Stream.concat(BANNED_ANNOTATIONS.entrySet().stream(),
        Stream.of(
          entry(COMPONENT_TYPE, "You may not use @Component on @Verified classes with @Bean methods")))
      .collect(entriesToMap()));

  
  /**
   * Will return true if a class level contains exactly a constant final static private literal field.
   */
  private Predicate staticPrivateFinalLiteralField = ve -> ve.getModifiers()
      .containsAll(Arrays.asList(Modifier.PRIVATE, Modifier.STATIC, Modifier.FINAL))
      && ve.getModifiers().size() == 3
      && ve.getConstantValue() != null;

  /**
   * Will return true if a class level contains exactly a final private field without a constant value.
   */
  private Predicate privateFinalField = ve -> ve.getModifiers()
      .containsAll(Arrays.asList(Modifier.PRIVATE, Modifier.FINAL))
      && ve.getModifiers().size() == 2
      && ve.getConstantValue() == null;  
  
  /**
   * Used to construct entries for maps.
   * @param key entry's key
   * @param value entry's value
   * @return the SimpleEntry we want to construct.
   */
  private static  Map.Entry entry(K key, V value) {
    return new AbstractMap.SimpleEntry<>(key, value);
  }
  
  /**
   * Converts a Stream<Entry<X,Y>> to a Map<X,Y>.
   * @return a Map representing the contents of the stream.
   */
  private static  Collector, ?, Map> entriesToMap() {
    return Collectors.toMap((e) -> e.getKey(), (e) -> e.getValue());
  } 
 
  /**
   * Read a TypeElement to get application structure.
   * 
   * @param te definition type element.
   * @param messager presents error messages for the compiler to pass to the user.
   * @return the {@link DefinitionModel} parsed from a properly annotated {@link TypeElement}
   * @deprecated please see {@link SpringAnnotationParser#extractDefinition(TypeElement, Messager)}
   */
  public static DefinitionModel parseDefinition(TypeElement te, Messager messager) {  
    return new SpringAnnotationParser().extractDefinition(te, messager);
  }
  
  /**
   * Read a TypeElement to get application structure.
   * 
   * @param te definition type element.
   * @param messager presents error messages for the compiler to pass to the user.
   * @return the {@link DefinitionModel} parsed from a properly annotated {@link TypeElement}
   */
  public DefinitionModel extractDefinition(TypeElement te, Messager messager) {  
    Verified verified = te.getAnnotation(Verified.class);
    DefinitionModel model = new DefinitionModel(te, verified == null ? false : verified.root());

    errorIfInnerClass(te, messager);
    
    model.addDependencyNames(getImportsTypes(te));
    String[] componentBeanNames  = AnnotationValueExtractor.getAnnotationValue(te, COMPONENT_TYPE, DEFAULT_ANNOTATION_VALUE);
    if (componentBeanNames != null) {
      errorOnBannedTypeToMessage(te, messager, COMPONENT_BANNED_ANNOTATIONS);
      addModelsFromComponent(te, model, componentBeanNames, messager);
    } else {
      errorOnBannedTypeToMessage(te, messager, BEAN_LITE_BANNED_ANNOTATIONS);
      for (Element enclosed : te.getEnclosedElements()) {
        addBeanMethodsFromBeanLiteConfig(messager, model, enclosed);
      }
    }
   
    if (verified != null) {
      for (String expectedBean : verified.expectedBeans()) {
        model.addDefinition(new ExpectedModel(expectedBean, te));
      }
    }
    return model;
  }
  
  private List getIllegalModifiers(Set existing, List illegal) {
    List modifiers = new ArrayList<>(existing);
    modifiers.removeIf(modifier -> !illegal.contains(modifier));
    modifiers.sort((m1, m2) ->  m1.name().compareTo(m2.name())); //in case someone reorders
    return modifiers;
  }
  
  private boolean parseBeanMethod(ExecutableElement beanMethod, String[] beanNames, Messager messager) {
    boolean valid = true;
    if (beanNames.length == 0) {
      valid = false;
      messager.printMessage(Kind.ERROR, "All @Bean annotations must define at least one name for a bean.", beanMethod);
    }
    if (beanMethod.getReturnType().getKind() != TypeKind.DECLARED) {
      valid = false;
      messager.printMessage(Kind.ERROR, "@Bean methods must return an Object", beanMethod);
    }
    if (!beanMethod.getModifiers().contains(Modifier.PUBLIC)) {
      valid = false;
      messager.printMessage(Kind.ERROR, "@Bean methods must be marked public", beanMethod);
    }
    List illegalModifiers = getIllegalModifiers(beanMethod.getModifiers(), DISALLOWED_ON_METHOD);
    if (illegalModifiers.size() != 0) {
      valid = false;
      messager.printMessage(Kind.ERROR, "Illegal modifiers found on spring @Bean method: "
           + illegalModifiers.stream().map(m -> m.name()).collect(Collectors.joining(", ")),
          beanMethod);
    }
    return valid;
  }

  private void addBeanMethodsFromBeanLiteConfig(Messager messager, DefinitionModel model, Element enclosed) {
    switch (enclosed.getKind()) {
      case METHOD: 
        ExecutableElement execelement = (ExecutableElement) enclosed;
        String[] beanNames = AnnotationValueExtractor
            .getAnnotationValue(execelement, "org.springframework.context.annotation.Bean", "name");
        
        if (beanNames != null) {
          List dependencies = execElementDependency(messager, model, execelement);          
          if (parseBeanMethod(execelement, beanNames, messager)) {
            List names = new ArrayList<>(Arrays.asList(beanNames));
            String defaultName = names.get(0);
            names.remove(defaultName);
            model.addDefinition(new InstanceModel(defaultName, model.getIdentity(), execelement,
                execelement.getReturnType().toString(), dependencies, names));
          }
        } else {
          messager.printMessage(Kind.ERROR, "All methods on @Configuration must have @Bean annotation", execelement);
        }
        break;
      case FIELD:
        if (!staticPrivateFinalLiteralField.test((VariableElement) enclosed)) {
          messager.printMessage(Kind.ERROR, "Only private static final constants are permitted in @Verified @Configuration classes",
              enclosed);
        }
        break;
      case ENUM_CONSTANT: 
        if (!staticPrivateFinalLiteralField.test((VariableElement) enclosed)) {
          messager.printMessage(Kind.ERROR, "Only private static final constants are permitted in @Verified @Configuration classes",
              enclosed);
        }
        break;
      case CONSTRUCTOR:
        ExecutableElement constelement = (ExecutableElement) enclosed;
        if (!constelement.getModifiers().contains(Modifier.PUBLIC)) {
          messager.printMessage(Kind.ERROR, "@Configuration should not have any non-public constructors.", enclosed);
        }
        if (constelement.getParameters().size() > 0) {
          messager.printMessage(Kind.ERROR, "@Configuration should not have any non-default constructors.", enclosed);
        }
        break;
      default:
        messager.printMessage(Kind.ERROR, "Only @Bean methods, private static final literals, and default constructors "
            + "are allowed on @Configuration classes", enclosed);
        break;
    }
  }

  /**
   * This method is called on {@link ExecutableElement}.
   * Bean methods on an @Configuration bean, or Constructors on @Component classes.
   * This method parses the @Qualifier, or @Value annotations if a @Verified=root, and reads the types of each parameter.
   * That data is used to build a list of {@link InstanceDependencyModel}'s which are part of an {@link InstanceModel}.
   * All parameters must have an @Qualifier or @Value, and the annotations can not be mixed, errors will result otherwise.
   * 
   * @param messager APT messager that will receive error messages.
   * @param model the DefinitionModel being parse, which may be a @Configuration or @Component annotated entity.
   * @param execelement the bean method if an @Configuration, or the constructor if an @Component.
   * @return the dependencies of the to be constructed {@link InstanceModel}
   */
  private List execElementDependency(Messager messager, DefinitionModel model,
      ExecutableElement execelement) {
    List dependencies = new ArrayList<>();
    boolean hasValues = false;
    boolean hasQualifiers = false;
    for (VariableElement varelement : execelement.getParameters()) {
      
      String[] qualifierNames = AnnotationValueExtractor
          .getAnnotationValue(varelement, QUALIFIER_TYPE, DEFAULT_ANNOTATION_VALUE);
      String[] valueNames = AnnotationValueExtractor
          .getAnnotationValue(varelement, VALUE_TYPE, DEFAULT_ANNOTATION_VALUE);
      
      if ((qualifierNames == null || qualifierNames.length == 0) 
          && (valueNames == null || valueNames.length == 0)) {
        messager.printMessage(Kind.ERROR, "All parameters must have an @Qualifier or a @Value annotation with a value", varelement);
      } 
      if (qualifierNames != null && qualifierNames.length > 0) {
        dependencies.add(new InstanceDependencyModel(qualifierNames[0], varelement.asType().toString()));
        hasQualifiers = true;
      }
      if (valueNames != null && valueNames.length > 0) {
        //ignore values as they will be used to build beans and pass the data on, and
        //are not beans themselves... and cannot be intermingled with @Qualifiers.
        hasValues = true;
      }
    }
    if (hasValues && hasQualifiers) {
      messager.printMessage(Kind.ERROR, "No method may define both @Qualifier or a @Value annotations,"
          + " keep property values in there own beans", execelement);
    }
    if (hasValues &&  !model.isRootNode()) {
      messager.printMessage(Kind.ERROR, "Only @Verified(root=true) nodes may use @Value annotations to create beans,"
          + " decouples spring graph from environment", execelement);
    }
    return dependencies;
  }

  /**
   * Builds an instance model by finding the autowired constructor of an @Component model.
   * 
   * @param te the TypeElement corresponding to the @Component class.
   * @param dm the definitionModel built from the @Component.
   * @param names the list if names in an @Component annotation.  Users must explicitly define one.
   * @param messager errors are added to this APT messager.
   */
  private void addModelsFromComponent(TypeElement te, DefinitionModel dm, String[] names, Messager messager) {
    List dependencies = new ArrayList<>();
    ExecutableElement chosenConstructor = findAutowiredConstructor(extractConstructorsFromComponent(te));
    if (chosenConstructor == null) {
      messager.printMessage(Kind.ERROR, "No single default constructor or single @Autowired constructor", te);
    } else {
      dependencies = execElementDependency(messager, dm, chosenConstructor);
      //dm.getExpectedDefinitions()
    }
    te.getEnclosedElements().stream()
        .filter(el -> el instanceof VariableElement)
        .map(el -> (VariableElement) el)
        .filter(ve -> !staticPrivateFinalLiteralField.test(ve) && !privateFinalField.test(ve))
        .forEach(ve -> messager
            .printMessage(Kind.ERROR, "@Component classes my only have static final constant fields or final private fields", ve));
    
    if (names.length > 0) {
      InstanceModel model = new InstanceModel(names[0],
          dm.getIdentity(), 
          chosenConstructor, 
          te.getQualifiedName().toString(),
          dependencies, 
          new ArrayList<>());
      
      dm.addDefinition(model);
      for (InstanceDependencyModel dep : dependencies) {
        ExpectedModel expectedModel = new ExpectedModel(dep.getIdentity());
        expectedModel.addDefinitionReferenceToType(model.getIdentity(), dep.getType());
        dm.addDefinition(expectedModel);
      }
    } else {
      messager.printMessage(Kind.ERROR, "@Component classes must have a name", te);
    }
  }

  /**
   * Analyzes a list of constructors from an @Component, looking for a single constructor, or if multiple
   * constructors exist, a single constructor marked with @Autowire.
   *
   * @param constructors a list of constructors from an @Component.
   * @return the executable element, or null.
   */
  private ExecutableElement findAutowiredConstructor(List constructors) {
    ExecutableElement chosenConstructor = null;
    if (constructors.size() == 1) {
      chosenConstructor = constructors.get(0);
    } else {
      chosenConstructor = constructors.stream()
        .filter(ex -> AnnotationValueExtractor.getAnnotationValue(ex, AUTOWIRED_TYPE, "") != null)
        .limit(2) //stop at two. efficiency.
        .reduce((a, b) -> null) //if more than one return null.
        .orElse(null);
    }
    return chosenConstructor;
  }

  /**
   * Given an @Component's {@link TypeElement} find's all constructors of that type.
   * 
   * @param te a representation of an @Component class.
   * @return a list of executable elements representing all found constructors.
   */
  private List extractConstructorsFromComponent(TypeElement te) {
    return te.getEnclosedElements().stream()
      .filter(enclosed -> enclosed instanceof ExecutableElement)
      .filter(enclosed -> "".equals(enclosed.getSimpleName().toString()))
      .map(enclosed -> (ExecutableElement) enclosed)
      .collect(Collectors.toList());
  }

  private void errorIfInnerClass(TypeElement te, Messager messager) {
    if (te.getEnclosingElement().getKind() != ElementKind.PACKAGE && !isTest(te)) {
      messager.printMessage(Kind.ERROR, "The class must be a top level class, not an internal class", te);
    }
  }
  
  /**
   * Returns true if the verified class is in a class that maven
   * would consider a test.   May give false positives in class dir.
   * TODO: should probably have a property to say if in tests.
   * @param te the enclosed type
   * @return true if determined to be in a test.
   */
  private boolean isTest(TypeElement te) {
    String className = te.getEnclosingElement().getSimpleName().toString();
    return className.startsWith("Test") 
        || className.endsWith("Test") 
        || className.endsWith("Tests")
        || className.endsWith("TestCase");
  }
  
  private void errorOnBannedTypeToMessage(Element el, Messager messager, Map typeToMessage) {
    for (Entry banned : typeToMessage.entrySet()) {
      if (AnnotationValueExtractor.getAnnotationValue(el, banned.getKey(), "") != null) {
        messager.printMessage(Kind.ERROR, banned.getValue(), el);
      }
    } 
  }
  
  private List getImportsTypes(TypeElement element) {
    String[] values = AnnotationValueExtractor
        .getAnnotationValue(element, IMPORT_TYPE, DEFAULT_ANNOTATION_VALUE);
    if (values == null) {
      return new ArrayList<>();
    } else {
      return Arrays.asList(values);
    }
  }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy