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