View Javadoc
1   /*
2    * Copyright © 2017, Salesforce.com, Inc
3    * All rights reserved.
4    *
5    * Redistribution and use in source and binary forms, with or without
6    * modification, are permitted provided that the following conditions are met:
7    *     * Redistributions of source code must retain the above copyright
8    *       notice, this list of conditions and the following disclaimer.
9    *     * Redistributions in binary form must reproduce the above copyright
10   *       notice, this list of conditions and the following disclaimer in the
11   *       documentation and/or other materials provided with the distribution.
12   *     * Neither the name of the <organization> nor the
13   *       names of its contributors may be used to endorse or promote products
14   *       derived from this software without specific prior written permission.
15   *
16   * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
17   * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18   * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
19   * DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
20   * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
21   * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
22   * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
23   * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
24   * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
25   * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26   */
27  package com.salesforce.aptspring.processor;
28  
29  import java.util.AbstractMap;
30  import java.util.ArrayList;
31  import java.util.Arrays;
32  import java.util.Collections;
33  import java.util.List;
34  import java.util.Map;
35  import java.util.Map.Entry;
36  import java.util.Set;
37  import java.util.function.Predicate;
38  import java.util.stream.Collector;
39  import java.util.stream.Collectors;
40  import java.util.stream.Stream;
41  
42  import javax.annotation.processing.Messager;
43  import javax.lang.model.element.Element;
44  import javax.lang.model.element.ElementKind;
45  import javax.lang.model.element.ExecutableElement;
46  import javax.lang.model.element.Modifier;
47  import javax.lang.model.element.TypeElement;
48  import javax.lang.model.element.VariableElement;
49  import javax.lang.model.type.TypeKind;
50  import javax.tools.Diagnostic.Kind;
51  
52  import com.salesforce.apt.graph.model.DefinitionModel;
53  import com.salesforce.apt.graph.model.ExpectedModel;
54  import com.salesforce.apt.graph.model.InstanceDependencyModel;
55  import com.salesforce.apt.graph.model.InstanceModel;
56  import com.salesforce.aptspring.Verified;
57  
58  public class SpringAnnotationParser {
59    
60    private static final List<Modifier> DISALLOWED_ON_METHOD = Collections.unmodifiableList(Arrays.asList(
61        Modifier.ABSTRACT,
62        Modifier.DEFAULT,
63        Modifier.FINAL,
64        Modifier.NATIVE,
65        Modifier.STATIC,
66        Modifier.VOLATILE
67        //Modifier.PRIVATE, //checks to see if the method is public, these checks would be redundant.
68        //Modifier.PROTECTED
69        ));
70    
71    private static final String QUALIFIER_TYPE = "org.springframework.beans.factory.annotation.Qualifier";
72    
73    private static final String VALUE_TYPE = "org.springframework.beans.factory.annotation.Value";
74    
75    private static final String COMPONENTSCAN_TYPE = "org.springframework.context.annotation.ComponentScan";
76    
77    private static final String COMPONENTSCANS_TYPE = "org.springframework.context.annotation.ComponentScans";
78    
79    private static final String CONFIGURATION_TYPE = "org.springframework.context.annotation.Configuration";
80    
81    private static final String COMPONENT_TYPE = "org.springframework.stereotype.Component";
82    
83    private static final String IMPORT_TYPE = "org.springframework.context.annotation.Import";
84    
85    private static final String IMPORT_RESOURCE_TYPE = "org.springframework.context.annotation.ImportResource";
86      
87    private static final String AUTOWIRED_TYPE = "org.springframework.beans.factory.annotation.Autowired";
88    
89    private static final String DEFAULT_ANNOTATION_VALUE = "value";
90  
91    private static final Map<String, String> BANNED_ANNOTATIONS = Collections.unmodifiableMap(Stream.of(
92              entry(COMPONENTSCAN_TYPE, "You may not use @ComponentScan(s) on @Verified classes"),
93              entry(COMPONENTSCANS_TYPE, "You may not use @ComponentScan(s) on @Verified classes"),
94              entry(IMPORT_RESOURCE_TYPE, "You may not use @ImportResource on @Verified classes"))
95            .collect(entriesToMap()));
96  
97    private static final Map<String, String> COMPONENT_BANNED_ANNOTATIONS = Collections.unmodifiableMap(
98        Stream.concat(BANNED_ANNOTATIONS.entrySet().stream(),
99           Stream.of(
100            entry(IMPORT_TYPE, "You may not use @Import on @Verified @Component classes")))
101       .collect(entriesToMap()));  
102           
103 
104   private static final Map<String, String> BEAN_LITE_BANNED_ANNOTATIONS = Collections.unmodifiableMap(
105       Stream.concat(BANNED_ANNOTATIONS.entrySet().stream(),
106         Stream.of(
107           entry(CONFIGURATION_TYPE, "@Verified annotation must only be used on @Bean LITE factory classes or @Component classes"),
108           entry(COMPONENT_TYPE, "You may not use @Component on @Verified classes with @Bean methods")))
109       .collect(entriesToMap()));
110 
111   
112   /**
113    * Will return true if a class level contains exactly a constant final static private literal field.
114    */
115   private Predicate<VariableElement> staticPrivateFinalLiteralField = ve -> ve.getModifiers()
116       .containsAll(Arrays.asList(Modifier.PRIVATE, Modifier.STATIC, Modifier.FINAL))
117       && ve.getModifiers().size() == 3
118       && ve.getConstantValue() != null;
119 
120   /**
121    * Will return true if a class level contains exactly a final private field without a constant value.
122    */
123   private Predicate<VariableElement> privateFinalField = ve -> ve.getModifiers()
124       .containsAll(Arrays.asList(Modifier.PRIVATE, Modifier.FINAL))
125       && ve.getModifiers().size() == 2
126       && ve.getConstantValue() == null;  
127   
128   /**
129    * Used to construct entries for maps.
130    * @param key entry's key
131    * @param value entry's value
132    * @return the SimpleEntry we want to construct.
133    */
134   private static <K, V> Map.Entry<K, V> entry(K key, V value) {
135     return new AbstractMap.SimpleEntry<>(key, value);
136   }
137   
138   /**
139    * Converts a Stream&lt;Entry&lt;X,Y&gt;&gt; to a Map&lt;X,Y&gt;.
140    * @return a Map representing the contents of the stream.
141    */
142   private static <K, U> Collector<Map.Entry<K, U>, ?, Map<K, U>> entriesToMap() {
143     return Collectors.toMap((e) -> e.getKey(), (e) -> e.getValue());
144   } 
145  
146   /**
147    * Read a TypeElement to get application structure.
148    * 
149    * @param te definition type element.
150    * @param messager presents error messages for the compiler to pass to the user.
151    * @return the {@link DefinitionModel} parsed from a properly annotated {@link TypeElement}
152    * @deprecated please see {@link SpringAnnotationParser#extractDefinition(TypeElement, Messager)}
153    */
154   public static DefinitionModel parseDefinition(TypeElement te, Messager messager) {  
155     return new SpringAnnotationParser().extractDefinition(te, messager);
156   }
157   
158   /**
159    * Read a TypeElement to get application structure.
160    * 
161    * @param te definition type element.
162    * @param messager presents error messages for the compiler to pass to the user.
163    * @return the {@link DefinitionModel} parsed from a properly annotated {@link TypeElement}
164    */
165   public DefinitionModel extractDefinition(TypeElement te, Messager messager) {  
166     Verified verified = te.getAnnotation(Verified.class);
167     DefinitionModel model = new DefinitionModel(te, verified == null ? false : verified.root());
168 
169     errorIfInnerClass(te, messager);
170     
171     model.addDependencyNames(getImportsTypes(te));
172     String[] componentBeanNames  = AnnotationValueExtractor.getAnnotationValue(te, COMPONENT_TYPE, DEFAULT_ANNOTATION_VALUE);
173     if (componentBeanNames != null) {
174       errorOnBannedTypeToMessage(te, messager, COMPONENT_BANNED_ANNOTATIONS);
175       addModelsFromComponent(te, model, componentBeanNames, messager);
176     } else {
177       errorOnBannedTypeToMessage(te, messager, BEAN_LITE_BANNED_ANNOTATIONS);
178       for (Element enclosed : te.getEnclosedElements()) {
179         addBeanMethodsFromBeanLiteConfig(messager, model, enclosed);
180       }
181     }
182     
183     for (String expectedBean : verified.expectedBeans()) {
184       model.addDefinition(new ExpectedModel(expectedBean, te));
185     }
186     return model;
187   }
188   
189   private List<Modifier> getIllegalModifiers(Set<Modifier> existing, List<Modifier> illegal) {
190     List<Modifier> modifiers = new ArrayList<>(existing);
191     modifiers.removeIf(modifier -> !illegal.contains(modifier));
192     modifiers.sort((m1, m2) ->  m1.name().compareTo(m2.name())); //in case someone reorders
193     return modifiers;
194   }
195   
196   private boolean parseBeanMethod(ExecutableElement beanMethod, String[] beanNames, Messager messager) {
197     boolean valid = true;
198     if (beanNames.length == 0) {
199       valid = false;
200       messager.printMessage(Kind.ERROR, "All @Bean annotations must define at least one name for a bean.", beanMethod);
201     }
202     if (beanMethod.getReturnType().getKind() != TypeKind.DECLARED) {
203       valid = false;
204       messager.printMessage(Kind.ERROR, "@Bean methods must return an Object", beanMethod);
205     }
206     if (!beanMethod.getModifiers().contains(Modifier.PUBLIC)) {
207       valid = false;
208       messager.printMessage(Kind.ERROR, "@Bean methods must be marked public", beanMethod);
209     }
210     List<Modifier> illegalModifiers = getIllegalModifiers(beanMethod.getModifiers(), DISALLOWED_ON_METHOD);
211     if (illegalModifiers.size() != 0) {
212       valid = false;
213       messager.printMessage(Kind.ERROR, "Illegal modifiers found on spring @Bean method: "
214            + illegalModifiers.stream().map(m -> m.name()).collect(Collectors.joining(", ")),
215           beanMethod);
216     }
217     return valid;
218   }
219 
220   private void addBeanMethodsFromBeanLiteConfig(Messager messager, DefinitionModel model, Element enclosed) {
221     switch (enclosed.getKind()) {
222       case METHOD: 
223         ExecutableElement execelement = (ExecutableElement) enclosed;
224         String[] beanNames = AnnotationValueExtractor
225             .getAnnotationValue(execelement, "org.springframework.context.annotation.Bean", "name");
226         
227         if (beanNames != null) {
228           List<InstanceDependencyModel> dependencies = execElementDependency(messager, model, execelement);          
229           if (parseBeanMethod(execelement, beanNames, messager)) {
230             List<String> names = new ArrayList<>(Arrays.asList(beanNames));
231             String defaultName = names.get(0);
232             names.remove(defaultName);
233             model.addDefinition(new InstanceModel(defaultName, model.getIdentity(), execelement,
234                 execelement.getReturnType().toString(), dependencies, names));
235           }
236         } else {
237           messager.printMessage(Kind.ERROR, "All methods on @Configuration must have @Bean annotation", execelement);
238         }
239         break;
240       case FIELD:
241         if (!staticPrivateFinalLiteralField.test((VariableElement) enclosed)) {
242           messager.printMessage(Kind.ERROR, "Only private static final constants are permitted in @Verified @Configuration classes",
243               enclosed);
244         }
245         break;
246       case ENUM_CONSTANT: 
247         if (!staticPrivateFinalLiteralField.test((VariableElement) enclosed)) {
248           messager.printMessage(Kind.ERROR, "Only private static final constants are permitted in @Verified @Configuration classes",
249               enclosed);
250         }
251         break;
252       case CONSTRUCTOR:
253         ExecutableElement constelement = (ExecutableElement) enclosed;
254         if (!constelement.getModifiers().contains(Modifier.PUBLIC)) {
255           messager.printMessage(Kind.ERROR, "@Configuration should not have any non-public constructors.", enclosed);
256         }
257         if (constelement.getParameters().size() > 0) {
258           messager.printMessage(Kind.ERROR, "@Configuration should not have any non-default constructors.", enclosed);
259         }
260         break;
261       default:
262         messager.printMessage(Kind.ERROR, "Only @Bean methods, private static final literals, and default constructors "
263             + "are allowed on @Configuration classes", enclosed);
264         break;
265     }
266   }
267 
268   /**
269    * This method is called on {@link ExecutableElement}.
270    * Bean methods on an @Configuration bean, or Constructors on @Component classes.
271    * This method parses the @Qualifier, or @Value annotations if a @Verified=root, and reads the types of each parameter.
272    * That data is used to build a list of {@link InstanceDependencyModel}'s which are part of an {@link InstanceModel}.
273    * All parameters must have an @Qualifier or @Value, and the annotations can not be mixed, errors will result otherwise.
274    * 
275    * @param messager APT messager that will receive error messages.
276    * @param model the DefinitionModel being parse, which may be a @Configuration or @Component annotated entity.
277    * @param execelement the bean method if an @Configuration, or the constructor if an @Component.
278    * @return the dependencies of the to be constructed {@link InstanceModel}
279    */
280   private List<InstanceDependencyModel> execElementDependency(Messager messager, DefinitionModel model,
281       ExecutableElement execelement) {
282     List<InstanceDependencyModel> dependencies = new ArrayList<>();
283     boolean hasValues = false;
284     boolean hasQualifiers = false;
285     for (VariableElement varelement : execelement.getParameters()) {
286       
287       String[] qualifierNames = AnnotationValueExtractor
288           .getAnnotationValue(varelement, QUALIFIER_TYPE, DEFAULT_ANNOTATION_VALUE);
289       String[] valueNames = AnnotationValueExtractor
290           .getAnnotationValue(varelement, VALUE_TYPE, DEFAULT_ANNOTATION_VALUE);
291       
292       if ((qualifierNames == null || qualifierNames.length == 0) 
293           && (valueNames == null || valueNames.length == 0)) {
294         messager.printMessage(Kind.ERROR, "All parameters must have an @Qualifier or a @Value annotation with a value", varelement);
295       } 
296       if (qualifierNames != null && qualifierNames.length > 0) {
297         dependencies.add(new InstanceDependencyModel(qualifierNames[0], varelement.asType().toString()));
298         hasQualifiers = true;
299       }
300       if (valueNames != null && valueNames.length > 0) {
301         //ignore values as they will be used to build beans and pass the data on, and
302         //are not beans themselves... and cannot be intermingled with @Qualifiers.
303         hasValues = true;
304       }
305     }
306     if (hasValues && hasQualifiers) {
307       messager.printMessage(Kind.ERROR, "No method may define both @Qualifier or a @Value annotations,"
308           + " keep property values in there own beans", execelement);
309     }
310     if (hasValues &&  !model.isRootNode()) {
311       messager.printMessage(Kind.ERROR, "Only @Verified(root=true) nodes may use @Value annotations to create beans,"
312           + " decouples spring graph from environment", execelement);
313     }
314     return dependencies;
315   }
316 
317   /**
318    * Builds an instance model by finding the autowired constructor of an @Component model.
319    * 
320    * @param te the TypeElement corresponding to the @Component class.
321    * @param dm the definitionModel built from the @Component.
322    * @param names the list if names in an @Component annotation.  Users must explicitly define one.
323    * @param messager errors are added to this APT messager.
324    */
325   private void addModelsFromComponent(TypeElement te, DefinitionModel dm, String[] names, Messager messager) {
326     List<InstanceDependencyModel> dependencies = new ArrayList<>();
327     ExecutableElement chosenConstructor = findAutowiredConstructor(extractConstructorsFromComponent(te));
328     if (chosenConstructor == null) {
329       messager.printMessage(Kind.ERROR, "No single default constructor or single @Autowired constructor", te);
330     } else {
331       dependencies = execElementDependency(messager, dm, chosenConstructor);
332       //dm.getExpectedDefinitions()
333     }
334     te.getEnclosedElements().stream()
335         .filter(el -> el instanceof VariableElement)
336         .map(el -> (VariableElement) el)
337         .filter(ve -> !staticPrivateFinalLiteralField.test(ve) && !privateFinalField.test(ve))
338         .forEach(ve -> messager
339             .printMessage(Kind.ERROR, "@Component classes my only have static final constant fields or final private fields", ve));
340     
341     if (names.length > 0) {
342       InstanceModel model = new InstanceModel(names[0],
343           dm.getIdentity(), 
344           chosenConstructor, 
345           te.getQualifiedName().toString(),
346           dependencies, 
347           new ArrayList<>());
348       
349       dm.addDefinition(model);
350       for (InstanceDependencyModel dep : dependencies) {
351         ExpectedModel expectedModel = new ExpectedModel(dep.getIdentity());
352         expectedModel.addDefinitionReferenceToType(model.getIdentity(), dep.getType());
353         dm.addDefinition(expectedModel);
354       }
355     } else {
356       messager.printMessage(Kind.ERROR, "@Component classes must have a name", te);
357     }
358   }
359 
360   /**
361    * Analyzes a list of constructors from an @Component, looking for a single constructor, or if multiple
362    * constructors exist, a single constructor marked with @Autowire.
363    *
364    * @param constructors a list of constructors from an @Component.
365    * @return the executable element, or null.
366    */
367   private ExecutableElement findAutowiredConstructor(List<ExecutableElement> constructors) {
368     ExecutableElement chosenConstructor = null;
369     if (constructors.size() == 1) {
370       chosenConstructor = constructors.get(0);
371     } else {
372       chosenConstructor = constructors.stream()
373         .filter(ex -> AnnotationValueExtractor.getAnnotationValue(ex, AUTOWIRED_TYPE, "") != null)
374         .limit(2) //stop at two. efficiency.
375         .reduce((a, b) -> null) //if more than one return null.
376         .orElse(null);
377     }
378     return chosenConstructor;
379   }
380 
381   /**
382    * Given an @Component's {@link TypeElement} find's all constructors of that type.
383    * 
384    * @param te a representation of an @Component class.
385    * @return a list of executable elements representing all found constructors.
386    */
387   private List<ExecutableElement> extractConstructorsFromComponent(TypeElement te) {
388     return te.getEnclosedElements().stream()
389       .filter(enclosed -> enclosed instanceof ExecutableElement)
390       .filter(enclosed -> "<init>".equals(enclosed.getSimpleName().toString()))
391       .map(enclosed -> (ExecutableElement) enclosed)
392       .collect(Collectors.toList());
393   }
394 
395   private void errorIfInnerClass(TypeElement te, Messager messager) {
396     if (te.getEnclosingElement().getKind() != ElementKind.PACKAGE && !isTest(te)) {
397       messager.printMessage(Kind.ERROR, "The class must be a top level class, not an internal class", te);
398     }
399   }
400   
401   /**
402    * Returns true if the verified class is in a class that maven
403    * would consider a test.   May give false positives in class dir.
404    * TODO: should probably have a property to say if in tests.
405    * @param te the enclosed type
406    * @return true if determined to be in a test.
407    */
408   private boolean isTest(TypeElement te) {
409     String className = te.getEnclosingElement().getSimpleName().toString();
410     return className.startsWith("Test") 
411         || className.endsWith("Test") 
412         || className.endsWith("Tests")
413         || className.endsWith("TestCase");
414   }
415   
416   private void errorOnBannedTypeToMessage(Element el, Messager messager, Map<String, String> typeToMessage) {
417     for (Entry<String, String> banned : typeToMessage.entrySet()) {
418       if (AnnotationValueExtractor.getAnnotationValue(el, banned.getKey(), "") != null) {
419         messager.printMessage(Kind.ERROR, banned.getValue(), el);
420       }
421     } 
422   }
423   
424   private List<String> getImportsTypes(TypeElement element) {
425     String[] values = AnnotationValueExtractor
426         .getAnnotationValue(element, IMPORT_TYPE, DEFAULT_ANNOTATION_VALUE);
427     if (values == null) {
428       return new ArrayList<>();
429     } else {
430       return Arrays.asList(values);
431     }
432   }
433 
434 }