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
36
37
38
39
40
41
42
43
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
85
86
87 config.setScanners(this);
88
89
90 try {
91 Reflections.log = null;
92 } catch (Error e) {
93
94 }
95
96
97 new Reflections(config);
98
99
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
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
148
149 if (ModuleType.class.getCanonicalName().equals(annotationType)) {
150 springIndexWriter.encounteredAnnotation(profiles, Component.class.getCanonicalName(), "", CommonConstants.HOST_CONTAINER_CLASS);
151 }
152 }
153 }
154
155
156 visitConstructors(classFile, profiles);
157
158
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
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
188
189
190
191
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
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
287
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
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
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 }