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