View Javadoc
1   package com.atlassian.plugin.spring.scanner.maven;
2   
3   import com.atlassian.plugin.spring.scanner.core.AtlassianSpringByteCodeScanner;
4   import com.atlassian.plugin.spring.scanner.core.ByteCodeScannerConfiguration;
5   import com.google.common.base.Function;
6   import com.google.common.base.Predicate;
7   import com.google.common.collect.Iterables;
8   import com.google.common.collect.Lists;
9   import com.google.common.collect.Sets;
10  import org.apache.maven.artifact.Artifact;
11  import org.apache.maven.artifact.resolver.filter.ArtifactFilter;
12  import org.apache.maven.model.Dependency;
13  import org.apache.maven.plugin.AbstractMojo;
14  import org.apache.maven.plugin.MojoExecutionException;
15  import org.apache.maven.plugins.annotations.Component;
16  import org.apache.maven.plugins.annotations.LifecyclePhase;
17  import org.apache.maven.plugins.annotations.Mojo;
18  import org.apache.maven.plugins.annotations.Parameter;
19  import org.apache.maven.plugins.annotations.ResolutionScope;
20  import org.apache.maven.project.MavenProject;
21  import org.apache.maven.shared.dependency.graph.DependencyGraphBuilder;
22  import org.apache.maven.shared.dependency.graph.DependencyGraphBuilderException;
23  import org.apache.maven.shared.dependency.graph.DependencyNode;
24  import org.reflections.util.ClasspathHelper;
25  
26  import javax.annotation.Nullable;
27  import java.io.File;
28  import java.net.MalformedURLException;
29  import java.net.URL;
30  import java.util.ArrayList;
31  import java.util.Collections;
32  import java.util.List;
33  import java.util.NoSuchElementException;
34  import java.util.Set;
35  import java.util.stream.Collectors;
36  
37  import static java.lang.String.format;
38  import static org.reflections.util.Utils.isEmpty;
39  
40  /**
41   * Maven plugin for atlassian-spring-scanning.
42   * <p>
43   * Use it by configuring the pom with:
44   * <pre><code>
45   * &lt;build&gt;
46   *     &lt;plugins&gt;
47   *         &lt;plugin&gt;
48   *             &lt;groupId&gt;com.atlassian.plugins&lt;/groupId&gt;
49   *             &lt;artifactId&gt;atlassian-spring-scanner-maven-plugin&lt;/artifactId&gt;
50   *             &lt;version&gt;${project.version}#60;/version&gt;
51   *             &lt;executions&gt;
52   *                 &lt;execution&gt;
53   *                     &lt;goals&gt;
54   *                        &lt;goal&gt;atlassian-spring-scanner&lt;/goal&gt;
55   *                     &lt;/goals&gt;
56   *                     &lt;phase&gt;process-classes&lt;/phase&gt;
57   *                  &lt;/execution&gt;
58   *             &lt;/executions&gt;
59   *             &lt;configuration&gt;
60   *                 &lt;... optional configuration here&gt;
61   *             &lt;/configuration&gt;
62   *         &lt;/plugin&gt;
63   *     &lt;/plugins&gt;
64   * &lt;/build&gt;
65   * </code></pre>
66   */
67  @Mojo(
68          name = "atlassian-spring-scanner",
69          threadSafe = true,
70          defaultPhase = LifecyclePhase.PREPARE_PACKAGE,
71          requiresDependencyResolution = ResolutionScope.COMPILE_PLUS_RUNTIME
72  )
73  public class AtlassianSpringScannerMojo extends AbstractMojo {
74      private static final String OUR_NAME = "Atlassian Spring Byte Code Scanner";
75      private static final String DEFAULT_INCLUDE_EXCLUDE = "-java\\..*, -javax\\..*, -sun\\..*, -com\\.sun\\..*";
76  
77      @Parameter(defaultValue = "${project}", readonly = true)
78      private MavenProject project;
79  
80      @Parameter(defaultValue = DEFAULT_INCLUDE_EXCLUDE)
81      private String includeExclude;
82  
83      @Parameter(defaultValue = "false")
84      private Boolean parallel;
85  
86      @Parameter(defaultValue = "false")
87      private Boolean verbose;
88  
89      @Parameter(defaultValue = "false")
90      private Boolean permitDuplicateImports;
91  
92      @Parameter()
93      private List<Dependency> scannedDependencies = new ArrayList<Dependency>();
94  
95      @Component(hint = "default")
96      private DependencyGraphBuilder dependencyGraphBuilder;
97  
98      public void execute() throws MojoExecutionException {
99          getLog().info("Starting " + OUR_NAME + "...");
100         getLog().info("");
101         long then = System.currentTimeMillis();
102 
103         String outputDirectory = resolveOutputDirectory();
104         if (!new File(outputDirectory).exists()) {
105             getLog().warn(format("Skipping because %s was not found", outputDirectory));
106             return;
107         }
108 
109         warnInvalidScannedDependencies();
110 
111         ByteCodeScannerConfiguration.Builder config = ByteCodeScannerConfiguration.builder()
112                 .setOutputDirectory(outputDirectory)
113                 .setClassPathUrls(parseUrls())
114                 .setIncludeExclude(includeExclude)
115                 .setLog(makeLogger())
116                 .setVerbose(verbose)
117                 .setPermitDuplicateImports(permitDuplicateImports);
118 
119         // go!
120         AtlassianSpringByteCodeScanner scanner = new AtlassianSpringByteCodeScanner(config.build());
121 
122         long ms = System.currentTimeMillis() - then;
123         getLog().info("");
124         getLog().info(format("\tAnalysis ran in %d ms.", ms));
125         getLog().info(format("\tEncountered %d total classes", scanner.getStats().getClassesEncountered()));
126         getLog().info(format("\tProcessed %d annotated classes", scanner.getStats().getComponentClassesEncountered()));
127 
128         if (!scanner.getErrors().getErrorsEncountered().isEmpty()) {
129             final String error = format("\t %d errors encountered during class analysis: \n\t %s",
130                     scanner.getErrors().getErrorsEncountered().size(),
131                     scanner.getErrors().getErrorsEncountered().stream().collect(Collectors.joining("\n\t")));
132             getLog().error(error);
133             throw new IllegalStateException(error);
134         }
135     }
136 
137     private org.slf4j.Logger makeLogger() {
138         return new MavenLogAdapter(getLog());
139     }
140 
141     private Set<URL> parseUrls() throws MojoExecutionException {
142         final Set<URL> urls = Sets.newHashSet();
143         URL outputDirUrl = parseOutputDirUrl();
144         urls.add(outputDirUrl);
145 
146         if (!isEmpty(includeExclude)) {
147             for (String string : includeExclude.split(",")) {
148                 String trimmed = string.trim();
149                 char prefix = trimmed.charAt(0);
150                 String pattern = trimmed.substring(1);
151                 if (prefix == '+') {
152                     logVerbose(format("\tAdding include / exclude %s", prefix));
153                     urls.addAll(ClasspathHelper.forPackage(pattern));
154                 }
155             }
156         }
157 
158         final Set<URL> dependencyJars = Sets.newLinkedHashSet();
159         final Iterable<Artifact> projectArtifacts = getProjectArtifacts();
160         final Iterable<Artifact> scannedArtifacts = resolveArtifacts(getScannedArtifacts(), projectArtifacts);
161         final Iterable<Artifact> ignoredArtifacts = Iterables.filter(projectArtifacts, new Predicate<Artifact>() {
162             @Override
163             public boolean apply(@Nullable Artifact input) {
164                 return !Iterables.any(scannedArtifacts, artifactMatchesGAV(input));
165             }
166         });
167 
168         for (Artifact artifact : scannedArtifacts) {
169             logVerbose(format("\t(/) Including dependency for scanning %s:%s:%s", artifact.getGroupId(), artifact.getArtifactId(), artifact.getScope()));
170             File file = artifact.getFile();
171             try {
172                 URL url = file.toURI().toURL();
173                 dependencyJars.add(url);
174             } catch (MalformedURLException e) {
175                 getLog().warn(format("Enable to create URL from plugin artifact : %s", file), e);
176             }
177         }
178 
179         for (Artifact artifact : ignoredArtifacts) {
180             logVerbose(format("\t(X) Ignoring dependency for scanning %s:%s:%s", artifact.getGroupId(), artifact.getArtifactId(), artifact.getScope()));
181         }
182 
183         urls.addAll(dependencyJars);
184         getLog().info("\t(/) The following directory will be scanned for annotations :");
185         getLog().info(format("\t\t%s", outputDirUrl));
186         if (dependencyJars.size() > 0) {
187             getLog().info("");
188             getLog().info("\t(/) The following dependencies will also be scanned for annotations : ");
189             getLog().info("");
190             for (URL jar : dependencyJars) {
191                 getLog().info(format("\t\t%s", jar));
192             }
193         }
194 
195         return urls;
196     }
197 
198     private void logVerbose(String message) {
199         if (verbose) {
200             getLog().info(message);
201         }
202     }
203 
204     private boolean isSensibleScope(final Artifact artifact) {
205         return !Artifact.SCOPE_TEST.equals(artifact.getScope());
206     }
207 
208     private URL parseOutputDirUrl() throws MojoExecutionException {
209         try {
210             File outputDirectoryFile = new File(resolveOutputDirectory() + '/');
211             return outputDirectoryFile.toURI().toURL();
212         } catch (MalformedURLException e) {
213             throw new MojoExecutionException(e.getMessage(), e);
214         }
215     }
216 
217     private String resolveOutputDirectory() {
218         return getProject().getBuild().getOutputDirectory();
219     }
220 
221     private MavenProject getProject() {
222         return project;
223     }
224 
225     private Predicate<Artifact> artifactMatchesGAV(final Artifact artifact) {
226         return new Predicate<Artifact>() {
227             @Override
228             public boolean apply(final Artifact input) {
229                 return artifact.getGroupId().equals(input.getGroupId()) &&
230                         artifact.getArtifactId().equals(input.getArtifactId()) &&
231                         artifact.getVersion().equals(input.getVersion()
232                         );
233             }
234         };
235     }
236 
237     /**
238      * The artifacts in the project list are resolved, the scanned ones are not but rather are logical and hence we
239      * have to make them real
240      *
241      * @param scannedArtifacts the GA artifacts we want scanned
242      * @param projectArtifacts the resolved list including the above
243      * @return resolved versions of the artifacts
244      */
245     private Iterable<Artifact> resolveArtifacts(List<Artifact> scannedArtifacts, final Iterable<Artifact> projectArtifacts) {
246         return Iterables.transform(scannedArtifacts, new Function<Artifact, Artifact>() {
247             @Override
248             public Artifact apply(Artifact input) {
249                 try {
250                     return Iterables.find(projectArtifacts, artifactMatchesGAV(input));
251                 } catch (NoSuchElementException e) {
252                     throw new RuntimeException("Unable to find " + input, e);
253                 }
254             }
255         });
256     }
257 
258     private DependencyNode getDependencyGraph(MavenProject project) {
259 
260         try {
261             return dependencyGraphBuilder.buildDependencyGraph(project, new ArtifactFilter() {
262                 @Override
263                 public boolean include(Artifact artifact) {
264                     return isSensibleScope(artifact);
265                 }
266             });
267         } catch (DependencyGraphBuilderException e) {
268             throw new RuntimeException(e);
269         }
270     }
271 
272 
273     private List<Artifact> getProjectArtifacts() {
274         ArrayList<Artifact> artifacts = Lists.newArrayList(project.getArtifacts());
275         Collections.sort(artifacts);
276         return artifacts;
277     }
278 
279     private List<Artifact> getScannedArtifacts() {
280         DependencyNode dependencyGraph = getDependencyGraph(project);
281         return ScannedDependencyArtifactBuilder.buildScannedArtifacts(dependencyGraph, scannedDependencies);
282     }
283 
284     private void warnInvalidScannedDependencies() {
285         for (Dependency dependency : scannedDependencies) {
286             if ((dependency.getArtifactId().contains("*")) && (!"*".equals(dependency.getArtifactId()))) {
287                 getLog().warn(format("Invalid artifact ID %s in scannedDependencies. Partial wildcards are not currently supported.", dependency.getArtifactId()));
288             }
289         }
290     }
291 }