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