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.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       * The collection of "G:A" combinations that do NOT belong to Maven Core, hence, should be excluded from
59       * "expected in provided scope" type of checks.
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, // mute validation completely (validation issue collection still happens, it is just not reported!)
80          INLINE, // inline, each "internal" problem one line next to mojo invocation
81          SUMMARY, // at end, list of plugin GAVs along with ANY validation issues
82          BRIEF, // each "internal" problem one line next to mojo invocation
83          // and at end list of plugin GAVs along with "external" issues
84          VERBOSE // at end, list of plugin GAVs along with detailed report of ANY validation issues
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); // this will parse and store it in session.data
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; // nothing can be reported
210         }
211         ValidationReportLevel validationReportLevel = validationReportLevel(mavenSession.getRepositorySession());
212         if (validationReportLevel == ValidationReportLevel.NONE
213                 || validationReportLevel == ValidationReportLevel.INLINE) {
214             return; // we were asked to not report anything OR reporting already happened inline
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             // Sorting the plugins
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 }