View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
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       * The collection of "G:A" combinations that do NOT belong to Maven Core, hence, should be excluded from
58       * "expected in provided scope" type of checks.
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, // mute validation completely (validation issue collection still happens, it is just not reported!)
79          INLINE, // inline, each "internal" problem one line next to mojo invocation
80          SUMMARY, // at end, list of plugin GAVs along with ANY validation issues
81          BRIEF, // each "internal" problem one line next to mojo invocation
82          // and at end list of plugin GAVs along with "external" issues
83          VERBOSE // at end, list of plugin GAVs along with detailed report of ANY validation issues
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); // this will parse and store it in session.data
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; // nothing can be reported
209         }
210         ValidationReportLevel validationReportLevel = validationReportLevel(mavenSession.getRepositorySession());
211         if (validationReportLevel == ValidationReportLevel.NONE
212                 || validationReportLevel == ValidationReportLevel.INLINE) {
213             return; // we were asked to not report anything OR reporting already happened inline
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 }