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