View Javadoc
1   package com.atlassian.plugin.spring.scanner.core;
2   
3   import com.atlassian.plugin.spring.scanner.annotation.component.BambooComponent;
4   import com.atlassian.plugin.spring.scanner.annotation.component.BitbucketComponent;
5   import com.atlassian.plugin.spring.scanner.annotation.component.ClasspathComponent;
6   import com.atlassian.plugin.spring.scanner.annotation.component.ConfluenceComponent;
7   import com.atlassian.plugin.spring.scanner.annotation.component.FecruComponent;
8   import com.atlassian.plugin.spring.scanner.annotation.component.JiraComponent;
9   import com.atlassian.plugin.spring.scanner.annotation.component.RefappComponent;
10  import com.atlassian.plugin.spring.scanner.annotation.component.StashComponent;
11  import com.atlassian.plugin.spring.scanner.annotation.export.ExportAsDevService;
12  import com.atlassian.plugin.spring.scanner.annotation.export.ExportAsService;
13  import com.atlassian.plugin.spring.scanner.annotation.export.ModuleType;
14  import com.atlassian.plugin.spring.scanner.annotation.imports.BambooImport;
15  import com.atlassian.plugin.spring.scanner.annotation.imports.BitbucketImport;
16  import com.atlassian.plugin.spring.scanner.annotation.imports.ComponentImport;
17  import com.atlassian.plugin.spring.scanner.annotation.imports.ConfluenceImport;
18  import com.atlassian.plugin.spring.scanner.annotation.imports.FecruImport;
19  import com.atlassian.plugin.spring.scanner.annotation.imports.JiraImport;
20  import com.atlassian.plugin.spring.scanner.annotation.imports.RefappImport;
21  import com.atlassian.plugin.spring.scanner.annotation.imports.StashImport;
22  import com.atlassian.plugin.spring.scanner.core.vfs.VirtualFile;
23  import com.atlassian.plugin.spring.scanner.core.vfs.VirtualFileFactory;
24  import com.atlassian.plugin.spring.scanner.util.CommonConstants;
25  import com.google.common.base.Function;
26  import com.google.common.collect.ImmutableList;
27  import com.google.common.collect.Maps;
28  import org.springframework.context.annotation.Primary;
29  import org.springframework.stereotype.Component;
30  import org.springframework.stereotype.Controller;
31  import org.springframework.stereotype.Repository;
32  import org.springframework.stereotype.Service;
33  
34  import javax.annotation.Nullable;
35  import javax.inject.Named;
36  import java.io.File;
37  import java.io.IOException;
38  import java.nio.file.FileVisitResult;
39  import java.nio.file.Files;
40  import java.nio.file.Path;
41  import java.nio.file.SimpleFileVisitor;
42  import java.nio.file.attribute.BasicFileAttributes;
43  import java.util.HashMap;
44  import java.util.HashSet;
45  import java.util.List;
46  import java.util.Map;
47  import java.util.Set;
48  import java.util.TreeSet;
49  
50  import static com.atlassian.plugin.spring.scanner.ProductFilter.BAMBOO;
51  import static com.atlassian.plugin.spring.scanner.ProductFilter.BITBUCKET;
52  import static com.atlassian.plugin.spring.scanner.ProductFilter.CONFLUENCE;
53  import static com.atlassian.plugin.spring.scanner.ProductFilter.FECRU;
54  import static com.atlassian.plugin.spring.scanner.ProductFilter.JIRA;
55  import static com.atlassian.plugin.spring.scanner.ProductFilter.REFAPP;
56  import static com.atlassian.plugin.spring.scanner.ProductFilter.STASH;
57  import static com.atlassian.plugin.spring.scanner.util.CommonConstants.COMPONENT_DEV_EXPORT_KEY;
58  import static com.atlassian.plugin.spring.scanner.util.CommonConstants.COMPONENT_EXPORT_KEY;
59  import static com.atlassian.plugin.spring.scanner.util.CommonConstants.COMPONENT_IMPORT_KEY;
60  import static com.atlassian.plugin.spring.scanner.util.CommonConstants.COMPONENT_KEY;
61  import static com.atlassian.plugin.spring.scanner.util.CommonConstants.COMPONENT_PRIMARY_KEY;
62  
63  
64  /**
65   * This class writes out the component and import index files into their specific directories and specific index files
66   * names
67   * <p>
68   * The Reflections / Javassist code deals only in strings and hence this is written as such.
69   */
70  public class SpringIndexWriter {
71      public static final List<String> KNOWN_PRODUCT_IMPORT_ANNOTATIONS = ImmutableList.of(
72              BambooImport.class.getCanonicalName(),
73              BitbucketImport.class.getCanonicalName(),
74              ConfluenceImport.class.getCanonicalName(),
75              FecruImport.class.getCanonicalName(),
76              JiraImport.class.getCanonicalName(),
77              RefappImport.class.getCanonicalName(),
78              StashImport.class.getCanonicalName());
79  
80      private final Map<String, RecordedAnnotations> recordedProfiles = new HashMap<String, RecordedAnnotations>();
81      private final VirtualFileFactory fileFactory;
82  
83      /**
84       * A writer that can record and write index files of components
85       *
86       * @param baseDir the base directory to write to.  Typically this is the class file output directory
87       */
88      public SpringIndexWriter(final String baseDir) {
89          // since we see EVERYTHING in the byte code scanner, we can always be clean
90          // and start from scratch.
91          cleanDirectory(new File(baseDir, CommonConstants.INDEX_FILES_DIR));
92  
93          fileFactory = baseDir != null ? new VirtualFileFactory(new File(baseDir)) : null;
94      }
95  
96      public boolean isInteresting(String annotationType) {
97          return (null != MeaningfulAnnotation.fromCanonicalName(annotationType));
98      }
99  
100     public boolean isParameterOrFieldAnnotation(final String annotationType) {
101         final MeaningfulAnnotation meaningfulAnnotation = MeaningfulAnnotation.fromCanonicalName(annotationType);
102         return (null != meaningfulAnnotation) && meaningfulAnnotation.parameterOrFieldAnnotation;
103     }
104 
105     public void encounteredAnnotation(Set<String> targetProfiles, String annotationType, String nameFromAnnotation, String className) {
106         Set<String> profiles = new HashSet<String>(targetProfiles);
107         if (profiles.isEmpty()) {
108             // when we have no profiles, we go into the magic one called 'default'
109             profiles.add(CommonConstants.DEFAULT_PROFILE_NAME);
110         }
111 
112         for (String profile : profiles) {
113             RecordedAnnotations recordedAnnotations = recordedProfiles.get(profile);
114             if (recordedAnnotations == null) {
115                 recordedAnnotations = new RecordedAnnotations();
116                 recordedProfiles.put(profile, recordedAnnotations);
117             }
118             recordedAnnotations.record(annotationType, nameFromAnnotation, className);
119         }
120 
121     }
122 
123     public void writeIndexes() {
124         for (Map.Entry<String, RecordedAnnotations> annotationsEntry : recordedProfiles.entrySet()) {
125             writeProfileIndexes(annotationsEntry.getKey(), annotationsEntry.getValue());
126         }
127 
128     }
129 
130     private void writeProfileIndexes(final String profileName, final RecordedAnnotations annotations) {
131         //
132         // the javac annotation processing only allows you to open a file for input and output ONCE
133         // for reasons you can look up online.  Basically so its knows what it has seen before.
134         // So if you open a file, write to it and then try to repeat that it will blow up.
135         //
136         // So we need to collapse the map here so that we go from many annotations sharing the
137         // same index file to a map of index file ---> all components for that index file
138         //
139         // and then write it out per unique file so they are only opened once.
140         //
141         Map<String, Set<String>> fileNameToComponents = new HashMap<String, Set<String>>();
142         for (Map.Entry<MeaningfulAnnotation, Set<String>> entry : annotations.getRecordedAnnotations().entrySet()) {
143             MeaningfulAnnotation meaningfulAnnotation = entry.getKey();
144             if (meaningfulAnnotation.isWrittenToDisk()) {
145                 Set<String> perAnnotationComponents = entry.getValue();
146                 String indexFileName = meaningfulAnnotation.getFileName();
147                 Set<String> currentComponents = fileNameToComponents.get(indexFileName);
148                 if (currentComponents == null) {
149                     currentComponents = new TreeSet<String>();
150                     fileNameToComponents.put(indexFileName, currentComponents);
151                 }
152                 currentComponents.addAll(perAnnotationComponents);
153             }
154         }
155         //
156         // now that we have them in file name order we can write them safely under the javac world
157         for (Map.Entry<String, Set<String>> entry : fileNameToComponents.entrySet()) {
158             try {
159                 writeIndexFile(profileName, entry.getKey(), entry.getValue());
160             } catch (IOException e) {
161                 throw new RuntimeException(e);
162             }
163         }
164     }
165 
166     private void writeIndexFile(final String profileName, final String indexFileName, final Set<String> entries)
167             throws IOException {
168         if (fileFactory==null) {
169             return;
170         }
171 
172         File file = makeProfiledFileName(profileName, indexFileName);
173 
174         VirtualFile vf = fileFactory.getFile(file.getPath());
175 
176         Set<String> lines = new TreeSet<String>();
177         // read the existing lines
178         lines.addAll(vf.readLines());
179         lines.addAll(entries);
180         vf.writeLines(lines);
181     }
182 
183     private File makeProfiledFileName(final String profileName, String fileName) {
184         File file = new File(CommonConstants.PROFILE_PREFIX + profileName, fileName);
185         if (CommonConstants.DEFAULT_PROFILE_NAME.equals(profileName)) {
186             // the well known default profile goes into the top level directory
187             // and only explicitly defined profiles go into sub directories
188             file = new File(fileName);
189         }
190         return new File(CommonConstants.INDEX_FILES_DIR, file.getPath());
191     }
192 
193     private void cleanDirectory(final File destination) {
194         if (destination.exists()) {
195             try {
196                 Files.walkFileTree(destination.toPath(), new SimpleFileVisitor<Path>() {
197                     @Override
198                     public FileVisitResult postVisitDirectory(Path dir, IOException e) throws IOException {
199                         if (e != null) {
200                             throw e;
201                         }
202                         Files.delete(dir);
203                         return FileVisitResult.CONTINUE;
204                     }
205 
206                     @Override
207                     public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
208                         Files.delete(file);
209                         return FileVisitResult.CONTINUE;
210                     }
211                 });
212             } catch (IOException e) {
213                 throw new RuntimeException("Unable to delete directory " + destination, e);
214             }
215         }
216     }
217 
218     private enum MeaningfulAnnotation {
219         // Well known spring annotations
220         Component(COMPONENT_KEY, Component.class),
221         Service(COMPONENT_KEY, Service.class),
222         Controller(COMPONENT_KEY, Controller.class),
223         Repository(COMPONENT_KEY, Repository.class),
224         Primary(COMPONENT_PRIMARY_KEY, Primary.class),
225 
226         // JSR annotations
227         Named(COMPONENT_KEY, Named.class, true),
228 
229         // Our component annotations
230         ClasspathComponent(COMPONENT_KEY, ClasspathComponent.class, true),
231         BambooComponent(BAMBOO.getPerProductFile(COMPONENT_KEY), BambooComponent.class, true),
232         BitbucketComponent(BITBUCKET.getPerProductFile(COMPONENT_KEY), BitbucketComponent.class, true),
233         ConfluenceComponent(CONFLUENCE.getPerProductFile(COMPONENT_KEY), ConfluenceComponent.class, true),
234         JiraComponent(JIRA.getPerProductFile(COMPONENT_KEY), JiraComponent.class, true),
235         FecruComponent(FECRU.getPerProductFile(COMPONENT_KEY), FecruComponent.class, true),
236         RefappComponent(REFAPP.getPerProductFile(COMPONENT_KEY), RefappComponent.class, true),
237         StashComponent(STASH.getPerProductFile(COMPONENT_KEY), StashComponent.class, true),
238 
239         // Our import annotations
240         Imports(COMPONENT_IMPORT_KEY, ComponentImport.class, true),
241         BambooImports(BAMBOO.getPerProductFile(COMPONENT_IMPORT_KEY), BambooImport.class, true),
242         BitbucketImports(BITBUCKET.getPerProductFile(COMPONENT_IMPORT_KEY), BitbucketImport.class, true),
243         ConfluenceImports(CONFLUENCE.getPerProductFile(COMPONENT_IMPORT_KEY), ConfluenceImport.class, true),
244         FecruImports(FECRU.getPerProductFile(COMPONENT_IMPORT_KEY), FecruImport.class, true),
245         JiraImports(JIRA.getPerProductFile(COMPONENT_IMPORT_KEY), JiraImport.class, true),
246         RefappImports(REFAPP.getPerProductFile(COMPONENT_IMPORT_KEY), RefappImport.class, true),
247         StashImports(STASH.getPerProductFile(COMPONENT_IMPORT_KEY), StashImport.class, true),
248 
249         // Our export annotations
250         ExportAsService(COMPONENT_EXPORT_KEY, ExportAsService.class, true),
251         ExportAsDevService(COMPONENT_DEV_EXPORT_KEY, ExportAsDevService.class, true),
252         ModuleType(COMPONENT_EXPORT_KEY, ModuleType.class);
253 
254         private static final Map<String, MeaningfulAnnotation> canonicalNameIndex = Maps.uniqueIndex(
255                 ImmutableList.copyOf(values()),
256                 new Function<MeaningfulAnnotation, String>() {
257                     @Override
258                     public String apply(@Nullable final MeaningfulAnnotation meaningfulAnnotation) {
259                         return meaningfulAnnotation.forAnnotation.getCanonicalName();
260                     }
261                 });
262 
263         private final String fileName;
264         private final Class forAnnotation;
265         private final boolean parameterOrFieldAnnotation;
266 
267         MeaningfulAnnotation(final String fileName, final Class forAnnotation) {
268             this(fileName, forAnnotation, false);
269         }
270 
271         MeaningfulAnnotation(final Class forAnnotation) {
272             this(null, forAnnotation, false);
273         }
274 
275         MeaningfulAnnotation(final String fileName, final Class forAnnotation, boolean parameterOrFieldAnnotation) {
276             this.fileName = fileName;
277             this.forAnnotation = forAnnotation;
278             this.parameterOrFieldAnnotation = parameterOrFieldAnnotation;
279         }
280 
281         private static MeaningfulAnnotation fromCanonicalName(String annotationType) {
282             return canonicalNameIndex.get(annotationType);
283         }
284 
285         private String getFileName() {
286             return fileName;
287         }
288 
289         private boolean isWrittenToDisk() {
290             return null != fileName;
291         }
292     }
293 
294     /**
295      * Simple holder class to record sets of annotations as we traverse the classes
296      */
297     private class RecordedAnnotations {
298         final Map<MeaningfulAnnotation, Set<String>> recordedAnnotations = new HashMap<MeaningfulAnnotation, Set<String>>();
299 
300         public Map<MeaningfulAnnotation, Set<String>> getRecordedAnnotations() {
301             return recordedAnnotations;
302         }
303 
304         public void record(final String annotationType, final String nameFromAnnotation, final String className) {
305             StringBuilder sb = new StringBuilder(className);
306             if (nameFromAnnotation != null) {
307                 String trimmed = nameFromAnnotation.trim();
308                 if (!trimmed.isEmpty()) {
309                     sb.append("#").append(trimmed);
310                 }
311             }
312             final MeaningfulAnnotation annotation = MeaningfulAnnotation.fromCanonicalName(annotationType);
313             if (null == annotation) {
314                 throw new IllegalStateException("Stop asking me for the impossible. Annotation " + annotationType + " not found");
315             }
316 
317             addTo(annotation, sb.toString());
318         }
319 
320         private void addTo(final MeaningfulAnnotation meaningfulAnnotation, final String value) {
321             Set<String> values = recordedAnnotations.get(meaningfulAnnotation);
322             if (values == null) {
323                 values = new TreeSet<String>();
324                 recordedAnnotations.put(meaningfulAnnotation, values);
325             }
326             values.add(value);
327         }
328     }
329 
330 
331 }