View Javadoc
1   package com.atlassian.plugin.spring.scanner.core;
2   
3   import com.atlassian.plugin.spring.scanner.annotation.component.ClasspathComponent;
4   import com.atlassian.plugin.spring.scanner.annotation.export.ExportAsService;
5   import com.atlassian.plugin.spring.scanner.annotation.export.ModuleType;
6   import com.atlassian.plugin.spring.scanner.annotation.imports.ComponentImport;
7   import com.atlassian.plugin.spring.scanner.util.CommonConstants;
8   import javassist.bytecode.AnnotationsAttribute;
9   import javassist.bytecode.ClassFile;
10  import javassist.bytecode.Descriptor;
11  import javassist.bytecode.FieldInfo;
12  import javassist.bytecode.MethodInfo;
13  import javassist.bytecode.annotation.Annotation;
14  import org.reflections.Reflections;
15  import org.reflections.scanners.AbstractScanner;
16  import org.reflections.util.ConfigurationBuilder;
17  import org.reflections.util.FilterBuilder;
18  import org.slf4j.Logger;
19  import org.springframework.stereotype.Component;
20  
21  import javax.inject.Named;
22  import java.util.ArrayList;
23  import java.util.Arrays;
24  import java.util.LinkedList;
25  import java.util.List;
26  import java.util.HashMap;
27  import java.util.HashSet;
28  import java.util.Map;
29  import java.util.Set;
30  
31  import static com.google.common.base.Strings.isNullOrEmpty;
32  import static java.lang.String.format;
33  
34  /**
35   * A reflections scanner than knows about well-known injection annotations such as {@link Component},
36   * {@link Named} etc..
37   * <p>
38   * This scanner will be passed every class file in the build directory and it will examine it for Annotations of
39   * interest.
40   * <p>
41   * This class uses a mix of the higher level Reflections code to get class information and also uses the lower level
42   * Javassist byte code helpers to get the rest of the information.  We prefer the former but end up having to use the
43   * latter to get the job done.
44   */
45  @SuppressWarnings("unchecked")
46  public class AtlassianSpringByteCodeScanner extends AbstractScanner {
47      private final Logger log;
48      private final Stats stats;
49      private final Errors errors;
50      private final SpringIndexWriter springIndexWriter;
51      private final ProfileFinder profileFinder;
52      private final JavassistHelper javassistHelper;
53      private final AnnotationValidator annotationValidator;
54      private boolean dbg;
55  
56      public AtlassianSpringByteCodeScanner(final ByteCodeScannerConfiguration configuration) {
57          this.log = configuration.getLog();
58          this.springIndexWriter = new SpringIndexWriter(configuration.getOutputDirectory());
59          this.javassistHelper = new JavassistHelper();
60          this.profileFinder = new ProfileFinder(configuration.getClassPathUrls(), log);
61          this.annotationValidator = new AnnotationValidator();
62          this.stats = new Stats();
63          this.errors = new Errors();
64          this.dbg = log.isDebugEnabled() || configuration.isVerbose();
65  
66          go(configuration);
67  
68          if (! configuration.isPermitDuplicateImports()) {
69              this.annotationValidator.validate(this.errors);
70          }
71      }
72  
73      private void go(final ByteCodeScannerConfiguration configuration) {
74          //
75          ConfigurationBuilder config = new ConfigurationBuilder();
76  
77          config.setUrls(configuration.getClassPathUrls());
78  
79          if (!isNullOrEmpty(configuration.getIncludeExclude())) {
80              config.filterInputsBy(FilterBuilder.parse(configuration.getIncludeExclude()));
81          }
82  
83          //
84          // we use our own specific scanner which means that the underlying Reflections code will
85          // not be able to consume our output.  But that's ok.
86          //
87          config.setScanners(this);
88  
89          // we don want reflections logs thank you
90          try {
91              Reflections.log = null;
92          } catch (Error e) {
93              //ignore
94          }
95  
96          // this will cause the scanner code to run!
97          new Reflections(config);
98  
99          // and write the results
100         springIndexWriter.writeIndexes();
101     }
102 
103     public Stats getStats() {
104         return stats;
105     }
106 
107 
108     public Errors getErrors() {
109         return errors;
110     }
111 
112     @Override
113     public void scan(final Object cls) {
114         ClassFile classFile = (ClassFile) cls;
115         try {
116             scanClass(classFile);
117         } catch (Exception e) {
118             log.error(format("Unable to run byte code scanner on class %s. Continuing to the next class...", cls));
119         }
120     }
121 
122     private void scanClass(final ClassFile classFile) {
123         stats.classesEncountered++;
124 
125         Set<String> profiles = profileFinder.getProfiles(classFile);
126 
127         String className = getMetadataAdapter().getClassName(classFile);
128         List<String> classAnnotationNames = getMetadataAdapter().getClassAnnotationNames(classFile);
129 
130         for (String annotationType : classAnnotationNames) {
131             if (isSuitableClassAnnotation(annotationType)) {
132                 // quick check to see that the class makes sense
133                 if (!isSuitableClass(classFile)) {
134                     debug(format("\t(X) Class not suitable '%s' for annotation '%s'", className, annotationType));
135                     return;
136                 }
137                 stats.componentClassesEncountered++;
138                 String nameFromAnnotation = javassistHelper.getAnnotationMember(classFile, annotationType, "value");
139 
140 
141                 debug(format("(/) Found annotation '%s' inside class '%s' with name '%s'", annotationType, className, nameFromAnnotation));
142 
143                 springIndexWriter.encounteredAnnotation(profiles, annotationType, nameFromAnnotation, className);
144                 annotationValidator.encounteredAnnotation(annotationType, className, className);
145 
146                 //
147                 // if its a @ModuleType then we need to add extra fix-up to the host container so that
148                 // can work more easily out of the box.  We just slip in a Component with the right class.  Easy!
149                 if (ModuleType.class.getCanonicalName().equals(annotationType)) {
150                     springIndexWriter.encounteredAnnotation(profiles, Component.class.getCanonicalName(), "", CommonConstants.HOST_CONTAINER_CLASS);
151                 }
152             }
153         }
154 
155         // find constructor parameter imports etc...
156         visitConstructors(classFile, profiles);
157         //
158         // find field annotated imports etc..
159         visitFields(classFile, profiles);
160     }
161 
162     private void debug(String msg) {
163         if (dbg) {
164             log.info("\t" + msg);
165         }
166     }
167 
168     private boolean isSuitableClass(final ClassFile classFile) {
169         String className = classFile.getName();
170         if (classFile.isInterface()) {
171             log.error(format("Found a type [%s] annotated as a component, but the type is not a concrete class. NOT adding to index file!!", className));
172             return false;
173         }
174         if (classFile.isAbstract()) {
175             log.error(format("Found a type [%s] annotated as a component, but the type is abstract. NOT adding to index file!!", className));
176             return false;
177         }
178         // package-info don't count either but its not an error to encounter one
179         return !profileFinder.isPackageClass(classFile);
180     }
181 
182     private boolean isSuitableClassAnnotation(final String annotationType) {
183         return super.acceptResult(annotationType) && springIndexWriter.isInteresting(annotationType);
184     }
185 
186     /**
187      * This will visit the constructors of the class and see if they have a annotations that may be interesting for the
188      * scanner
189      *
190      * @param classFile the class we are inspecting
191      * @param profiles  the profiles that are in play for the index
192      */
193     private void visitConstructors(final ClassFile classFile, final Set<String> profiles) {
194         String className = classFile.getName();
195         List<MethodInfo> methods = classFile.getMethods();
196         for (MethodInfo method : methods) {
197             String methodName = method.getName();
198             if (method.isConstructor()) {
199                 // parameter 'names' in this case is actually parameter types
200                 List<String> parameterTypes = getMetadataAdapter().getParameterNames(method);
201                 for (int i = 0; i < parameterTypes.size(); i++) {
202                     String parameterType = parameterTypes.get(i);
203                     List<Annotation> parameterAnnotationNames = javassistHelper.getParameterAnnotations(method, i);
204 
205                     for (Annotation annotation : parameterAnnotationNames) {
206                         String annotationType = annotation.getTypeName();
207                         if (acceptResult(annotationType) && springIndexWriter.isParameterOrFieldAnnotation(annotationType)) {
208                             String nameFromAnnotation = javassistHelper.getAnnotationMember(annotation, "value");
209 
210                             debug(format("(/) Found '%s' inside class '%s' method '%s' parameter '%s'", annotationType, className, methodName, parameterType));
211 
212                             springIndexWriter.encounteredAnnotation(profiles, annotationType, nameFromAnnotation, parameterType);
213                             annotationValidator.encounteredAnnotation(annotationType, parameterType, className);
214                         }
215                     }
216 
217                 }
218             }
219         }
220     }
221 
222     private void visitFields(final ClassFile classFile, final Set<String> profiles) {
223         String className = classFile.getName();
224         List<FieldInfo> fields = classFile.getFields();
225         for (FieldInfo field : fields) {
226             final List<String> annotationTypes = new LinkedList<>();
227             String fieldName = field.getName();
228             AnnotationsAttribute annotations = (AnnotationsAttribute) field.getAttribute(AnnotationsAttribute.visibleTag);
229             if (annotations != null) {
230                 for (Annotation annotation : annotations.getAnnotations()) {
231                     String annotationType = annotation.getTypeName();
232                     annotationTypes.add(annotationType);
233 
234                     if (acceptResult(annotationType) && springIndexWriter.isParameterOrFieldAnnotation(annotationType)) {
235                         String fieldType = Descriptor.toClassName(field.getDescriptor());
236                         String nameFromAnnotation = javassistHelper.getAnnotationMember(annotation, "value");
237 
238                         debug(format("(/) Found '%s' inside class '%s' on field '%s' of type '%s'", annotationType, className, fieldName, fieldType));
239 
240                         springIndexWriter.encounteredAnnotation(profiles, annotationType, nameFromAnnotation, fieldType);
241                         annotationValidator.encounteredAnnotation(annotationType, fieldType, className);
242                     }
243                 }
244             }
245 
246             if (annotationTypes.contains(ComponentImport.class.getCanonicalName())) {
247 
248                 final List<String> productImportsPresentOnField = new ArrayList<>(SpringIndexWriter.KNOWN_PRODUCT_IMPORT_ANNOTATIONS);
249 
250                 productImportsPresentOnField.retainAll(annotationTypes);
251 
252                 if (!productImportsPresentOnField.isEmpty()) {
253                     errors.addError(String.format("ComponentImport annotation cannot be used with product specific component imports: %s found on %s.%s",
254                             Arrays.toString(productImportsPresentOnField.toArray()), classFile.getName(), fieldName));
255                 }
256             }
257         }
258     }
259 
260     public static class Stats {
261         private int classesEncountered;
262         private int componentClassesEncountered;
263 
264         public int getClassesEncountered() {
265             return classesEncountered;
266         }
267 
268         public int getComponentClassesEncountered() {
269             return componentClassesEncountered;
270         }
271     }
272 
273     public static class Errors {
274         private final List<String> errorsEncountered = new ArrayList<>();
275 
276         public void addError(String error) {
277             errorsEncountered.add(error);
278         }
279 
280         public List<String> getErrorsEncountered() {
281             return errorsEncountered;
282         }
283     }
284 
285     /**
286      * Mantains a collection of annotated component names, and for each annotation,
287      * the locations where the annotation was found.
288      */
289     public static class AnnotationReferences {
290         private Map<String, List<String>> references;
291 
292         public AnnotationReferences() {
293             this.references = new HashMap<>();
294         }
295 
296         public void addReference(String component, String whereReferenced) {
297             List<String> sources = references.computeIfAbsent(component, k -> new ArrayList());
298             sources.add(whereReferenced);
299         }
300 
301         public List<String> getReferences(String component) {
302             return references.get(component);
303         }
304 
305         public Set<String> intersect(AnnotationReferences that) {
306             Set intersectSet = new HashSet(this.references.keySet());
307             intersectSet.retainAll(that.references.keySet());
308             return intersectSet;
309         }
310     }
311 
312     public static class AnnotationValidator {
313         private final Set<String> declaredComponentAnnotationTypes;
314         private final Set<String> importedComponentAnnotationTypes;
315 
316         private AnnotationReferences declaredComponents;
317         private AnnotationReferences importedComponents;
318 
319         public AnnotationValidator() {
320             this.declaredComponents = new AnnotationReferences();
321 
322             // All annotation types that cause us to create a component
323             this.declaredComponentAnnotationTypes = new HashSet<>();
324             this.declaredComponentAnnotationTypes.add(ExportAsService.class.getCanonicalName());
325             this.declaredComponentAnnotationTypes.add(Component.class.getCanonicalName());
326             this.declaredComponentAnnotationTypes.add(Named.class.getCanonicalName());
327 
328             this.importedComponents = new AnnotationReferences();
329 
330             // All annotation types that cause us to import a component
331             this.importedComponentAnnotationTypes = new HashSet<>();
332             this.importedComponentAnnotationTypes.add(ComponentImport.class.getCanonicalName());
333             this.importedComponentAnnotationTypes.add(ClasspathComponent.class.getCanonicalName());
334             this.importedComponentAnnotationTypes.addAll(SpringIndexWriter.KNOWN_PRODUCT_IMPORT_ANNOTATIONS);
335         }
336 
337         public void encounteredAnnotation(String annotationType, String component, String whereReferenced) {
338             if (importedComponentAnnotationTypes.contains(annotationType)) {
339                 importedComponents.addReference(component, whereReferenced);
340             } else if (declaredComponentAnnotationTypes.contains(annotationType)) {
341                 declaredComponents.addReference(component, whereReferenced);
342             }
343         }
344 
345         public void validate(Errors errors) {
346             Set<String> conflictingComponents = importedComponents.intersect(declaredComponents);
347 
348             for (String component : conflictingComponents) {
349                 List<String> imports = importedComponents.getReferences(component);
350                 List<String> declarations = declaredComponents.getReferences(component);
351 
352                 errors.addError(format("Cannot import a component within a plugin that declares the same component. " +
353                                         "Component '%s' was declared in %s but imported in %s",
354                                         component,
355                                         Arrays.toString(declarations.toArray()),
356                                         Arrays.toString(imports.toArray())));
357             }
358         }
359     }
360 }