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