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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
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
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
239
240
241
242
243
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 }