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