View Javadoc
1   package com.atlassian.plugin.spring.scanner.core;
2   
3   import com.atlassian.plugin.spring.scanner.annotation.Profile;
4   import javassist.bytecode.ClassFile;
5   import org.slf4j.Logger;
6   
7   import java.io.BufferedInputStream;
8   import java.io.DataInputStream;
9   import java.io.File;
10  import java.io.FileInputStream;
11  import java.io.IOException;
12  import java.io.InputStream;
13  import java.net.URISyntaxException;
14  import java.net.URL;
15  import java.util.List;
16  import java.util.Map;
17  import java.util.Set;
18  import java.util.TreeMap;
19  import java.util.TreeSet;
20  
21  import static java.util.Collections.emptySet;
22  
23  /**
24   * This little guy can help find the package-info.java of a given class file and cache what profiles are in play and
25   * also what profiles are in a given class file.
26   */
27  class ProfileFinder {
28      private static final String PACKAGE_INFO = "package-info";
29      private final Set<URL> buildPath;
30      private final Logger log;
31      private final JavassistHelper javassistHelper;
32      private final Map<String, Set<String>> mapOfPackagesToProfiles = new TreeMap<String, Set<String>>();
33  
34      public ProfileFinder(final Set<URL> buildPath, final Logger log) {
35          this.buildPath = buildPath;
36          this.log = log;
37          this.javassistHelper = new JavassistHelper();
38      }
39  
40      boolean isPackageClass(ClassFile classFile) {
41          return classFile.getName().endsWith("." + PACKAGE_INFO);
42      }
43  
44      /**
45       * Called to get the Profiles that apply to this class file
46       *
47       * @param classFile the class file in question
48       * @return a set of applicable profiles
49       */
50      Set<String> getProfiles(ClassFile classFile) {
51          String classFileName = classFile.getName();
52          if (log.isDebugEnabled()) {
53              log.debug(String.format("Checking for profile annotations on %s", classFileName));
54          }
55  
56          if (isPackageClass(classFile)) {
57              recordPackageClassProfileAnnotations(classFile);
58              return emptySet();
59          } else {
60              Set<String> packageInfoProfileAnnotations = getPackageInfoProfileAnnotations(classFile);
61              Set<String> classSpecificProfiles = findProfiles(classFile);
62              //
63              // if we have specific @Profile annotations on the class then they override the package level ones
64              if (!classSpecificProfiles.isEmpty()) {
65                  return classSpecificProfiles;
66              }
67              return packageInfoProfileAnnotations;
68          }
69      }
70  
71      /**
72       * Records in the cache just the package info class file profiles
73       *
74       * @param packageInfoClassFile should only be a package-info.class
75       */
76      private void recordPackageClassProfileAnnotations(final ClassFile packageInfoClassFile) {
77          String packageName = getClassPackageAsFileName(packageInfoClassFile);
78          Set<String> profiles = mapOfPackagesToProfiles.get(packageName);
79          //
80          // it possible that we have seen a package class member BEFORE we have seen the package-info class
81          // and hence the other paths will have recorded the package-info profile information already
82          //
83          if (profiles == null) {
84              mapOfPackagesToProfiles.put(packageName, findProfiles(packageInfoClassFile));
85          }
86      }
87  
88      private Set<String> getPackageInfoProfileAnnotations(final ClassFile classFile) {
89          //
90          // we need to look to see if we have seen this package before
91          String packageName = getClassPackageAsFileName(classFile);
92          Set<String> profiles = mapOfPackagesToProfiles.get(packageName);
93          if (profiles != null) {
94              return profiles;
95          }
96          //
97          // ok we haven't see the package-info class yet.  So we do a preemptive lookup
98  
99          ClassFile packageInfoClassFile = loadPackageInfoClass(packageName);
100         if (packageInfoClassFile != null) {
101             recordPackageClassProfileAnnotations(packageInfoClassFile);
102         } else {
103             //
104             // ok we have no package-info class so record that for posterity
105             mapOfPackagesToProfiles.put(packageName, emptySet());
106         }
107         //
108         // now lookup again cause it is now recorded
109         return mapOfPackagesToProfiles.get(packageName);
110     }
111 
112     private ClassFile loadPackageInfoClass(final String packageName) {
113         for (URL url : buildPath) {
114             try {
115                 File parentFile = new File(url.toURI());
116                 File packageInfoFile = new File(parentFile, packageName + "/" + "package-info.class");
117                 if (!packageInfoFile.exists()) {
118                     continue;
119                 }
120 
121                 try (InputStream inputStream = new FileInputStream(packageInfoFile)) {
122                     DataInputStream dis = new DataInputStream(new BufferedInputStream(inputStream));
123                     return new ClassFile(dis);
124                 } catch (IOException e) {
125                     log.error(String.format("Could not create class file from %s", packageInfoFile), e);
126                 }
127             } catch (URISyntaxException e) {
128                 log.error(String.format("Unable to load package-info file %s from %s", packageName, url));
129             }
130         }
131         return null;
132     }
133 
134     private Set<String> findProfiles(final ClassFile classFile) {
135         List<String> classAnnotationNames = javassistHelper.getClassAnnotationNames(classFile);
136         Set<String> profiles = new TreeSet<String>();
137         for (String annotationType : classAnnotationNames) {
138             if (Profile.class.getCanonicalName().equals(annotationType)) {
139                 Set<String> profileValues = javassistHelper.getAnnotationMemberSet(classFile, annotationType, "value");
140                 profiles.addAll(profileValues);
141             }
142             //
143             // we have support for Spring 3.1 profile in a preemptive attempt that MAYBE one day we might get there :)
144             if ("org.springframework.context.annotation.Profile".equals(annotationType)) {
145                 Set<String> profileValues = javassistHelper.getAnnotationMemberSet(classFile, annotationType, "value");
146                 profiles.addAll(profileValues);
147             }
148         }
149         return profiles;
150     }
151 
152     private String getClassPackageAsFileName(final ClassFile classFile) {
153         String fileName = classFile.getName();
154         int lastDot = fileName.lastIndexOf('.');
155         if (lastDot == -1) {
156             return fileName;
157         }
158         String packageName = fileName.substring(0, lastDot);
159         return packageName.replace('.', '/');
160     }
161 }