1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 package org.apache.maven.plugin.internal;
20
21 import javax.inject.Named;
22 import javax.inject.Singleton;
23
24 import java.io.File;
25 import java.nio.file.Path;
26 import java.util.Arrays;
27 import java.util.Collection;
28 import java.util.Collections;
29 import java.util.EnumSet;
30 import java.util.HashMap;
31 import java.util.LinkedHashMap;
32 import java.util.LinkedHashSet;
33 import java.util.List;
34 import java.util.Locale;
35 import java.util.Map;
36 import java.util.Set;
37 import java.util.concurrent.ConcurrentHashMap;
38 import java.util.stream.Collectors;
39
40 import org.apache.maven.eventspy.AbstractEventSpy;
41 import org.apache.maven.execution.ExecutionEvent;
42 import org.apache.maven.execution.MavenSession;
43 import org.apache.maven.model.InputLocation;
44 import org.apache.maven.plugin.PluginValidationManager;
45 import org.apache.maven.plugin.descriptor.MojoDescriptor;
46 import org.apache.maven.plugin.descriptor.PluginDescriptor;
47 import org.eclipse.aether.RepositorySystemSession;
48 import org.eclipse.aether.artifact.Artifact;
49 import org.eclipse.aether.util.ConfigUtils;
50 import org.slf4j.Logger;
51 import org.slf4j.LoggerFactory;
52
53 @Singleton
54 @Named
55 public final class DefaultPluginValidationManager extends AbstractEventSpy implements PluginValidationManager {
56
57
58
59
60 static final Collection<String> EXPECTED_PROVIDED_SCOPE_EXCLUSIONS_GA =
61 Collections.unmodifiableCollection(Arrays.asList(
62 "org.apache.maven:maven-archiver", "org.apache.maven:maven-jxr", "org.apache.maven:plexus-utils"));
63
64 private static final String ISSUES_KEY = DefaultPluginValidationManager.class.getName() + ".issues";
65
66 private static final String PLUGIN_EXCLUDES_KEY = DefaultPluginValidationManager.class.getName() + ".excludes";
67
68 private static final String MAVEN_PLUGIN_VALIDATION_KEY = "maven.plugin.validation";
69
70 private static final String MAVEN_PLUGIN_VALIDATION_EXCLUDES_KEY = "maven.plugin.validation.excludes";
71
72 private static final ValidationReportLevel DEFAULT_VALIDATION_LEVEL = ValidationReportLevel.INLINE;
73
74 private static final Collection<ValidationReportLevel> INLINE_VALIDATION_LEVEL = Collections.unmodifiableCollection(
75 Arrays.asList(ValidationReportLevel.INLINE, ValidationReportLevel.BRIEF));
76
77 private enum ValidationReportLevel {
78 NONE,
79 INLINE,
80 SUMMARY,
81 BRIEF,
82
83 VERBOSE
84 }
85
86 private final Logger logger = LoggerFactory.getLogger(getClass());
87
88 @Override
89 public void onEvent(Object event) {
90 if (event instanceof ExecutionEvent) {
91 ExecutionEvent executionEvent = (ExecutionEvent) event;
92 if (executionEvent.getType() == ExecutionEvent.Type.SessionStarted) {
93 RepositorySystemSession repositorySystemSession =
94 executionEvent.getSession().getRepositorySession();
95 validationReportLevel(repositorySystemSession);
96 validationPluginExcludes(repositorySystemSession);
97 } else if (executionEvent.getType() == ExecutionEvent.Type.SessionEnded) {
98 reportSessionCollectedValidationIssues(executionEvent.getSession());
99 }
100 }
101 }
102
103 private List<?> validationPluginExcludes(RepositorySystemSession session) {
104 return (List<?>) session.getData().computeIfAbsent(PLUGIN_EXCLUDES_KEY, () -> parsePluginExcludes(session));
105 }
106
107 private List<String> parsePluginExcludes(RepositorySystemSession session) {
108 String excludes = ConfigUtils.getString(session, null, MAVEN_PLUGIN_VALIDATION_EXCLUDES_KEY);
109 if (excludes == null || excludes.isEmpty()) {
110 return Collections.emptyList();
111 }
112 return Arrays.stream(excludes.split(","))
113 .map(String::trim)
114 .filter(s -> !s.isEmpty())
115 .collect(Collectors.toList());
116 }
117
118 private ValidationReportLevel validationReportLevel(RepositorySystemSession session) {
119 return (ValidationReportLevel) session.getData()
120 .computeIfAbsent(ValidationReportLevel.class, () -> parseValidationReportLevel(session));
121 }
122
123 private ValidationReportLevel parseValidationReportLevel(RepositorySystemSession session) {
124 String level = ConfigUtils.getString(session, null, MAVEN_PLUGIN_VALIDATION_KEY);
125 if (level == null || level.isEmpty()) {
126 return DEFAULT_VALIDATION_LEVEL;
127 }
128 try {
129 return ValidationReportLevel.valueOf(level.toUpperCase(Locale.ENGLISH));
130 } catch (IllegalArgumentException e) {
131 logger.warn(
132 "Invalid value specified for property {}: '{}'. Supported values are (case insensitive): {}",
133 MAVEN_PLUGIN_VALIDATION_KEY,
134 level,
135 Arrays.toString(ValidationReportLevel.values()));
136 return DEFAULT_VALIDATION_LEVEL;
137 }
138 }
139
140 private String pluginKey(String groupId, String artifactId, String version) {
141 return groupId + ":" + artifactId + ":" + version;
142 }
143
144 private String pluginKey(MojoDescriptor mojoDescriptor) {
145 PluginDescriptor pd = mojoDescriptor.getPluginDescriptor();
146 return pluginKey(pd.getGroupId(), pd.getArtifactId(), pd.getVersion());
147 }
148
149 private String pluginKey(Artifact pluginArtifact) {
150 return pluginKey(pluginArtifact.getGroupId(), pluginArtifact.getArtifactId(), pluginArtifact.getVersion());
151 }
152
153 private void mayReportInline(RepositorySystemSession session, IssueLocality locality, String issue) {
154 if (locality == IssueLocality.INTERNAL) {
155 ValidationReportLevel validationReportLevel = validationReportLevel(session);
156 if (INLINE_VALIDATION_LEVEL.contains(validationReportLevel)) {
157 logger.warn(" {}", issue);
158 }
159 }
160 }
161
162 @Override
163 public void reportPluginValidationIssue(
164 IssueLocality locality, RepositorySystemSession session, Artifact pluginArtifact, String issue) {
165 String pluginKey = pluginKey(pluginArtifact);
166 if (validationPluginExcludes(session).contains(pluginKey)) {
167 return;
168 }
169 PluginValidationIssues pluginIssues =
170 pluginIssues(session).computeIfAbsent(pluginKey, k -> new PluginValidationIssues());
171 pluginIssues.reportPluginIssue(locality, null, issue);
172 mayReportInline(session, locality, issue);
173 }
174
175 @Override
176 public void reportPluginValidationIssue(
177 IssueLocality locality, MavenSession mavenSession, MojoDescriptor mojoDescriptor, String issue) {
178 String pluginKey = pluginKey(mojoDescriptor);
179 if (validationPluginExcludes(mavenSession.getRepositorySession()).contains(pluginKey)) {
180 return;
181 }
182 PluginValidationIssues pluginIssues = pluginIssues(mavenSession.getRepositorySession())
183 .computeIfAbsent(pluginKey, k -> new PluginValidationIssues());
184 pluginIssues.reportPluginIssue(locality, pluginDeclaration(mavenSession, mojoDescriptor), issue);
185 mayReportInline(mavenSession.getRepositorySession(), locality, issue);
186 }
187
188 @Override
189 public void reportPluginMojoValidationIssue(
190 IssueLocality locality,
191 MavenSession mavenSession,
192 MojoDescriptor mojoDescriptor,
193 Class<?> mojoClass,
194 String issue) {
195 String pluginKey = pluginKey(mojoDescriptor);
196 if (validationPluginExcludes(mavenSession.getRepositorySession()).contains(pluginKey)) {
197 return;
198 }
199 PluginValidationIssues pluginIssues = pluginIssues(mavenSession.getRepositorySession())
200 .computeIfAbsent(pluginKey, k -> new PluginValidationIssues());
201 pluginIssues.reportPluginMojoIssue(
202 locality, pluginDeclaration(mavenSession, mojoDescriptor), mojoInfo(mojoDescriptor, mojoClass), issue);
203 mayReportInline(mavenSession.getRepositorySession(), locality, issue);
204 }
205
206 private void reportSessionCollectedValidationIssues(MavenSession mavenSession) {
207 if (!logger.isWarnEnabled()) {
208 return;
209 }
210 ValidationReportLevel validationReportLevel = validationReportLevel(mavenSession.getRepositorySession());
211 if (validationReportLevel == ValidationReportLevel.NONE
212 || validationReportLevel == ValidationReportLevel.INLINE) {
213 return;
214 }
215 ConcurrentHashMap<String, PluginValidationIssues> issuesMap = pluginIssues(mavenSession.getRepositorySession());
216 EnumSet<IssueLocality> issueLocalitiesToReport = validationReportLevel == ValidationReportLevel.SUMMARY
217 || validationReportLevel == ValidationReportLevel.VERBOSE
218 ? EnumSet.allOf(IssueLocality.class)
219 : EnumSet.of(IssueLocality.EXTERNAL);
220
221 if (hasAnythingToReport(issuesMap, issueLocalitiesToReport)) {
222 logger.warn("");
223 logger.warn("Plugin {} validation issues were detected in following plugin(s)", issueLocalitiesToReport);
224 logger.warn("");
225 for (Map.Entry<String, PluginValidationIssues> entry : issuesMap.entrySet()) {
226 PluginValidationIssues issues = entry.getValue();
227 if (!hasAnythingToReport(issues, issueLocalitiesToReport)) {
228 continue;
229 }
230 logger.warn(" * {}", entry.getKey());
231 if (validationReportLevel == ValidationReportLevel.VERBOSE) {
232 if (!issues.pluginDeclarations.isEmpty()) {
233 logger.warn(" Declared at location(s):");
234 for (String pluginDeclaration : issues.pluginDeclarations) {
235 logger.warn(" * {}", pluginDeclaration);
236 }
237 }
238 if (!issues.pluginIssues.isEmpty()) {
239 for (IssueLocality issueLocality : issueLocalitiesToReport) {
240 Set<String> pluginIssues = issues.pluginIssues.get(issueLocality);
241 if (pluginIssues != null && !pluginIssues.isEmpty()) {
242 logger.warn(" Plugin {} issue(s):", issueLocality);
243 for (String pluginIssue : pluginIssues) {
244 logger.warn(" * {}", pluginIssue);
245 }
246 }
247 }
248 }
249 if (!issues.mojoIssues.isEmpty()) {
250 for (IssueLocality issueLocality : issueLocalitiesToReport) {
251 Map<String, LinkedHashSet<String>> mojoIssues = issues.mojoIssues.get(issueLocality);
252 if (mojoIssues != null && !mojoIssues.isEmpty()) {
253 logger.warn(" Mojo {} issue(s):", issueLocality);
254 for (String mojoInfo : mojoIssues.keySet()) {
255 logger.warn(" * Mojo {}", mojoInfo);
256 for (String mojoIssue : mojoIssues.get(mojoInfo)) {
257 logger.warn(" - {}", mojoIssue);
258 }
259 }
260 }
261 }
262 }
263 logger.warn("");
264 }
265 }
266 logger.warn("");
267 if (validationReportLevel == ValidationReportLevel.VERBOSE) {
268 logger.warn(
269 "Fix reported issues by adjusting plugin configuration or by upgrading above listed plugins. If no upgrade available, please notify plugin maintainers about reported issues.");
270 }
271 logger.warn(
272 "For more or less details, use 'maven.plugin.validation' property with one of the values (case insensitive): {}",
273 Arrays.toString(ValidationReportLevel.values()));
274 logger.warn("");
275 }
276 }
277
278 private boolean hasAnythingToReport(
279 Map<String, PluginValidationIssues> issuesMap, EnumSet<IssueLocality> issueLocalitiesToReport) {
280 for (PluginValidationIssues issues : issuesMap.values()) {
281 if (hasAnythingToReport(issues, issueLocalitiesToReport)) {
282 return true;
283 }
284 }
285 return false;
286 }
287
288 private boolean hasAnythingToReport(PluginValidationIssues issues, EnumSet<IssueLocality> issueLocalitiesToReport) {
289 for (IssueLocality issueLocality : issueLocalitiesToReport) {
290 Set<String> pluginIssues = issues.pluginIssues.get(issueLocality);
291 if (pluginIssues != null && !pluginIssues.isEmpty()) {
292 return true;
293 }
294 Map<String, LinkedHashSet<String>> mojoIssues = issues.mojoIssues.get(issueLocality);
295 if (mojoIssues != null && !mojoIssues.isEmpty()) {
296 return true;
297 }
298 }
299 return false;
300 }
301
302 private String pluginDeclaration(MavenSession mavenSession, MojoDescriptor mojoDescriptor) {
303 InputLocation inputLocation =
304 mojoDescriptor.getPluginDescriptor().getPlugin().getLocation("");
305 if (inputLocation != null && inputLocation.getSource() != null) {
306 StringBuilder stringBuilder = new StringBuilder();
307 stringBuilder.append(inputLocation.getSource().getModelId());
308 String location = inputLocation.getSource().getLocation();
309 if (location != null) {
310 if (location.contains("://")) {
311 stringBuilder.append(" (").append(location).append(")");
312 } else {
313 Path topLevelBasedir =
314 mavenSession.getTopLevelProject().getBasedir().toPath();
315 Path locationPath =
316 new File(location).toPath().toAbsolutePath().normalize();
317 if (locationPath.startsWith(topLevelBasedir)) {
318 locationPath = topLevelBasedir.relativize(locationPath);
319 }
320 stringBuilder.append(" (").append(locationPath).append(")");
321 }
322 }
323 stringBuilder.append(" @ line ").append(inputLocation.getLineNumber());
324 return stringBuilder.toString();
325 } else {
326 return "unknown";
327 }
328 }
329
330 private String mojoInfo(MojoDescriptor mojoDescriptor, Class<?> mojoClass) {
331 return mojoDescriptor.getFullGoalName() + " (" + mojoClass.getName() + ")";
332 }
333
334 @SuppressWarnings("unchecked")
335 private ConcurrentHashMap<String, PluginValidationIssues> pluginIssues(RepositorySystemSession session) {
336 return (ConcurrentHashMap<String, PluginValidationIssues>)
337 session.getData().computeIfAbsent(ISSUES_KEY, ConcurrentHashMap::new);
338 }
339
340 private static class PluginValidationIssues {
341 private final LinkedHashSet<String> pluginDeclarations;
342
343 private final HashMap<IssueLocality, LinkedHashSet<String>> pluginIssues;
344
345 private final HashMap<IssueLocality, LinkedHashMap<String, LinkedHashSet<String>>> mojoIssues;
346
347 private PluginValidationIssues() {
348 this.pluginDeclarations = new LinkedHashSet<>();
349 this.pluginIssues = new HashMap<>();
350 this.mojoIssues = new HashMap<>();
351 }
352
353 private synchronized void reportPluginIssue(
354 IssueLocality issueLocality, String pluginDeclaration, String issue) {
355 if (pluginDeclaration != null) {
356 pluginDeclarations.add(pluginDeclaration);
357 }
358 pluginIssues
359 .computeIfAbsent(issueLocality, k -> new LinkedHashSet<>())
360 .add(issue);
361 }
362
363 private synchronized void reportPluginMojoIssue(
364 IssueLocality issueLocality, String pluginDeclaration, String mojoInfo, String issue) {
365 if (pluginDeclaration != null) {
366 pluginDeclarations.add(pluginDeclaration);
367 }
368 mojoIssues
369 .computeIfAbsent(issueLocality, k -> new LinkedHashMap<>())
370 .computeIfAbsent(mojoInfo, k -> new LinkedHashSet<>())
371 .add(issue);
372 }
373 }
374 }