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.util.Arrays;
26  import java.util.LinkedHashMap;
27  import java.util.LinkedHashSet;
28  import java.util.Locale;
29  import java.util.Map;
30  import java.util.concurrent.ConcurrentHashMap;
31  
32  import org.apache.maven.AbstractMavenLifecycleParticipant;
33  import org.apache.maven.execution.MavenSession;
34  import org.apache.maven.model.InputLocation;
35  import org.apache.maven.plugin.PluginValidationManager;
36  import org.apache.maven.plugin.descriptor.MojoDescriptor;
37  import org.apache.maven.plugin.descriptor.PluginDescriptor;
38  import org.apache.maven.project.MavenProject;
39  import org.eclipse.aether.RepositorySystemSession;
40  import org.eclipse.aether.artifact.Artifact;
41  import org.eclipse.aether.util.ConfigUtils;
42  import org.slf4j.Logger;
43  import org.slf4j.LoggerFactory;
44  
45  @Singleton
46  @Named
47  public final class DefaultPluginValidationManager extends AbstractMavenLifecycleParticipant
48          implements PluginValidationManager {
49  
50      private static final String ISSUES_KEY = DefaultPluginValidationManager.class.getName() + ".issues";
51  
52      private static final String MAVEN_PLUGIN_VALIDATION_KEY = "maven.plugin.validation";
53  
54      private enum ValidationLevel {
55          BRIEF,
56          DEFAULT,
57          VERBOSE
58      }
59  
60      private final Logger logger = LoggerFactory.getLogger(getClass());
61  
62      @Override
63      public void afterSessionEnd(MavenSession session) {
64          reportSessionCollectedValidationIssues(session);
65      }
66  
67      private ValidationLevel validationLevel(RepositorySystemSession session) {
68          String level = ConfigUtils.getString(session, null, MAVEN_PLUGIN_VALIDATION_KEY);
69          if (level == null || level.isEmpty()) {
70              return ValidationLevel.DEFAULT;
71          }
72          try {
73              return ValidationLevel.valueOf(level.toUpperCase(Locale.ENGLISH));
74          } catch (IllegalArgumentException e) {
75              logger.warn(
76                      "Invalid value specified for property {}: '{}'. Supported values are (case insensitive): {}",
77                      MAVEN_PLUGIN_VALIDATION_KEY,
78                      level,
79                      Arrays.toString(ValidationLevel.values()));
80              return ValidationLevel.DEFAULT;
81          }
82      }
83  
84      private String pluginKey(String groupId, String artifactId, String version) {
85          return groupId + ":" + artifactId + ":" + version;
86      }
87  
88      private String pluginKey(MojoDescriptor mojoDescriptor) {
89          PluginDescriptor pd = mojoDescriptor.getPluginDescriptor();
90          return pluginKey(pd.getGroupId(), pd.getArtifactId(), pd.getVersion());
91      }
92  
93      private String pluginKey(Artifact pluginArtifact) {
94          return pluginKey(pluginArtifact.getGroupId(), pluginArtifact.getArtifactId(), pluginArtifact.getVersion());
95      }
96  
97      @Override
98      public void reportPluginValidationIssue(RepositorySystemSession session, Artifact pluginArtifact, String issue) {
99          String pluginKey = pluginKey(pluginArtifact);
100         PluginValidationIssues pluginIssues =
101                 pluginIssues(session).computeIfAbsent(pluginKey, k -> new PluginValidationIssues());
102         pluginIssues.reportPluginIssue(null, null, issue);
103     }
104 
105     @Override
106     public void reportPluginValidationIssue(MavenSession mavenSession, MojoDescriptor mojoDescriptor, String issue) {
107         String pluginKey = pluginKey(mojoDescriptor);
108         PluginValidationIssues pluginIssues = pluginIssues(mavenSession.getRepositorySession())
109                 .computeIfAbsent(pluginKey, k -> new PluginValidationIssues());
110         pluginIssues.reportPluginIssue(
111                 pluginDeclaration(mavenSession, mojoDescriptor), pluginOccurrence(mavenSession), issue);
112     }
113 
114     @Override
115     public void reportPluginMojoValidationIssue(
116             MavenSession mavenSession, MojoDescriptor mojoDescriptor, Class<?> mojoClass, String issue) {
117         String pluginKey = pluginKey(mojoDescriptor);
118         PluginValidationIssues pluginIssues = pluginIssues(mavenSession.getRepositorySession())
119                 .computeIfAbsent(pluginKey, k -> new PluginValidationIssues());
120         pluginIssues.reportPluginMojoIssue(
121                 pluginDeclaration(mavenSession, mojoDescriptor),
122                 pluginOccurrence(mavenSession),
123                 mojoInfo(mojoDescriptor, mojoClass),
124                 issue);
125     }
126 
127     private void reportSessionCollectedValidationIssues(MavenSession mavenSession) {
128         if (!logger.isWarnEnabled()) {
129             return; // nothing can be reported
130         }
131         ValidationLevel validationLevel = validationLevel(mavenSession.getRepositorySession());
132         ConcurrentHashMap<String, PluginValidationIssues> issuesMap = pluginIssues(mavenSession.getRepositorySession());
133         if (!issuesMap.isEmpty()) {
134 
135             logger.warn("");
136             logger.warn("Plugin validation issues were detected in {} plugin(s)", issuesMap.size());
137             logger.warn("");
138             if (validationLevel == ValidationLevel.BRIEF) {
139                 return;
140             }
141 
142             for (Map.Entry<String, PluginValidationIssues> entry : issuesMap.entrySet()) {
143                 logger.warn(" * {}", entry.getKey());
144                 if (validationLevel == ValidationLevel.VERBOSE) {
145                     PluginValidationIssues issues = entry.getValue();
146                     if (!issues.pluginDeclarations.isEmpty()) {
147                         logger.warn("  Declared at location(s):");
148                         for (String pluginDeclaration : issues.pluginDeclarations) {
149                             logger.warn("   * {}", pluginDeclaration);
150                         }
151                     }
152                     if (!issues.pluginOccurrences.isEmpty()) {
153                         logger.warn("  Used in module(s):");
154                         for (String pluginOccurrence : issues.pluginOccurrences) {
155                             logger.warn("   * {}", pluginOccurrence);
156                         }
157                     }
158                     if (!issues.pluginIssues.isEmpty()) {
159                         logger.warn("  Plugin issue(s):");
160                         for (String pluginIssue : issues.pluginIssues) {
161                             logger.warn("   * {}", pluginIssue);
162                         }
163                     }
164                     if (!issues.mojoIssues.isEmpty()) {
165                         logger.warn("  Mojo issue(s):");
166                         for (String mojoInfo : issues.mojoIssues.keySet()) {
167                             logger.warn("   * Mojo {}", mojoInfo);
168                             for (String mojoIssue : issues.mojoIssues.get(mojoInfo)) {
169                                 logger.warn("     - {}", mojoIssue);
170                             }
171                         }
172                     }
173                     logger.warn("");
174                 }
175             }
176             logger.warn("");
177             if (validationLevel == ValidationLevel.VERBOSE) {
178                 logger.warn(
179                         "Fix reported issues by adjusting plugin configuration or by upgrading above listed plugins. If no upgrade available, please notify plugin maintainers about reported issues.");
180             }
181             logger.warn(
182                     "For more or less details, use 'maven.plugin.validation' property with one of the values (case insensitive): {}",
183                     Arrays.toString(ValidationLevel.values()));
184             logger.warn("");
185         }
186     }
187 
188     private String pluginDeclaration(MavenSession mavenSession, MojoDescriptor mojoDescriptor) {
189         InputLocation inputLocation =
190                 mojoDescriptor.getPluginDescriptor().getPlugin().getLocation("");
191         if (inputLocation != null && inputLocation.getSource() != null) {
192             StringBuilder stringBuilder = new StringBuilder();
193             stringBuilder.append(inputLocation.getSource().getModelId());
194             String location = inputLocation.getSource().getLocation();
195             if (location != null) {
196                 if (location.contains("://")) {
197                     stringBuilder.append(" (").append(location).append(")");
198                 } else {
199                     File rootBasedir = mavenSession.getTopLevelProject().getBasedir();
200                     File locationFile = new File(location);
201                     if (location.startsWith(rootBasedir.getPath())) {
202                         stringBuilder
203                                 .append(" (")
204                                 .append(rootBasedir.toPath().relativize(locationFile.toPath()))
205                                 .append(")");
206                     } else {
207                         stringBuilder.append(" (").append(location).append(")");
208                     }
209                 }
210             }
211             stringBuilder.append(" @ line ").append(inputLocation.getLineNumber());
212             return stringBuilder.toString();
213         } else {
214             return "unknown";
215         }
216     }
217 
218     private String pluginOccurrence(MavenSession mavenSession) {
219         MavenProject prj = mavenSession.getCurrentProject();
220         String result = prj.getGroupId() + ":" + prj.getArtifactId() + ":" + prj.getVersion();
221         File currentPom = prj.getFile();
222         if (currentPom != null) {
223             File rootBasedir = mavenSession.getTopLevelProject().getBasedir();
224             result += " (" + rootBasedir.toPath().relativize(currentPom.toPath()) + ")";
225         }
226         return result;
227     }
228 
229     private String mojoInfo(MojoDescriptor mojoDescriptor, Class<?> mojoClass) {
230         return mojoDescriptor.getFullGoalName() + " (" + mojoClass.getName() + ")";
231     }
232 
233     @SuppressWarnings("unchecked")
234     private ConcurrentHashMap<String, PluginValidationIssues> pluginIssues(RepositorySystemSession session) {
235         return (ConcurrentHashMap<String, PluginValidationIssues>)
236                 session.getData().computeIfAbsent(ISSUES_KEY, ConcurrentHashMap::new);
237     }
238 
239     private static class PluginValidationIssues {
240         private final LinkedHashSet<String> pluginDeclarations;
241 
242         private final LinkedHashSet<String> pluginOccurrences;
243 
244         private final LinkedHashSet<String> pluginIssues;
245 
246         private final LinkedHashMap<String, LinkedHashSet<String>> mojoIssues;
247 
248         private PluginValidationIssues() {
249             this.pluginDeclarations = new LinkedHashSet<>();
250             this.pluginOccurrences = new LinkedHashSet<>();
251             this.pluginIssues = new LinkedHashSet<>();
252             this.mojoIssues = new LinkedHashMap<>();
253         }
254 
255         private synchronized void reportPluginIssue(String pluginDeclaration, String pluginOccurrence, String issue) {
256             if (pluginDeclaration != null) {
257                 pluginDeclarations.add(pluginDeclaration);
258             }
259             if (pluginOccurrence != null) {
260                 pluginOccurrences.add(pluginOccurrence);
261             }
262             pluginIssues.add(issue);
263         }
264 
265         private synchronized void reportPluginMojoIssue(
266                 String pluginDeclaration, String pluginOccurrence, String mojoInfo, String issue) {
267             if (pluginDeclaration != null) {
268                 pluginDeclarations.add(pluginDeclaration);
269             }
270             if (pluginOccurrence != null) {
271                 pluginOccurrences.add(pluginOccurrence);
272             }
273             mojoIssues.computeIfAbsent(mojoInfo, k -> new LinkedHashSet<>()).add(issue);
274         }
275     }
276 }