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