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.plugins.help;
20  
21  import java.io.File;
22  import java.io.IOException;
23  import java.lang.reflect.InvocationTargetException;
24  import java.lang.reflect.Method;
25  import java.net.URL;
26  import java.net.URLClassLoader;
27  import java.util.ArrayList;
28  import java.util.List;
29  import java.util.Map;
30  import java.util.StringTokenizer;
31  import java.util.regex.Matcher;
32  import java.util.regex.Pattern;
33  import java.util.stream.Collectors;
34  
35  import org.apache.maven.RepositoryUtils;
36  import org.apache.maven.lifecycle.DefaultLifecycles;
37  import org.apache.maven.lifecycle.Lifecycle;
38  import org.apache.maven.lifecycle.internal.MojoDescriptorCreator;
39  import org.apache.maven.lifecycle.mapping.LifecycleMapping;
40  import org.apache.maven.model.Plugin;
41  import org.apache.maven.model.building.ModelBuildingRequest;
42  import org.apache.maven.plugin.MavenPluginManager;
43  import org.apache.maven.plugin.MojoExecutionException;
44  import org.apache.maven.plugin.MojoFailureException;
45  import org.apache.maven.plugin.descriptor.MojoDescriptor;
46  import org.apache.maven.plugin.descriptor.Parameter;
47  import org.apache.maven.plugin.descriptor.PluginDescriptor;
48  import org.apache.maven.plugin.prefix.NoPluginFoundForPrefixException;
49  import org.apache.maven.plugin.version.DefaultPluginVersionRequest;
50  import org.apache.maven.plugin.version.PluginVersionResolutionException;
51  import org.apache.maven.plugin.version.PluginVersionResolver;
52  import org.apache.maven.plugin.version.PluginVersionResult;
53  import org.apache.maven.plugins.annotations.Component;
54  import org.apache.maven.plugins.annotations.Mojo;
55  import org.apache.maven.project.DefaultProjectBuildingRequest;
56  import org.apache.maven.project.MavenProject;
57  import org.apache.maven.project.ProjectBuildingRequest;
58  import org.apache.maven.reporting.MavenReport;
59  import org.apache.maven.shared.utils.logging.MessageUtils;
60  import org.apache.maven.tools.plugin.generator.HtmlToPlainTextConverter;
61  import org.codehaus.plexus.util.StringUtils;
62  import org.eclipse.aether.artifact.Artifact;
63  import org.eclipse.aether.artifact.DefaultArtifact;
64  
65  /**
66   * Displays a list of the attributes for a Maven Plugin and/or goals (aka Mojo - Maven plain Old Java Object).
67   *
68   * @see <a href="http://maven.apache.org/general.html#What_is_a_Mojo">What is a Mojo?</a>
69   * @since 2.0
70   */
71  @Mojo(name = "describe", requiresProject = false, aggregator = true)
72  public class DescribeMojo extends AbstractHelpMojo {
73      /**
74       * The default indent size when writing description's Mojo.
75       */
76      private static final int INDENT_SIZE = 2;
77  
78      /**
79       * For unknown values
80       */
81      private static final String UNKNOWN = "Unknown";
82  
83      /**
84       * For not defined values
85       */
86      private static final String NOT_DEFINED = "Not defined";
87  
88      /**
89       * For deprecated values
90       */
91      private static final String NO_REASON = "No reason given";
92  
93      private static final Pattern EXPRESSION = Pattern.compile("^\\$\\{([^}]+)\\}$");
94  
95      // ----------------------------------------------------------------------
96      // Mojo components
97      // ----------------------------------------------------------------------
98  
99      /**
100      * Component used to get a plugin descriptor from a given plugin.
101      */
102     @Component
103     protected MavenPluginManager pluginManager;
104 
105     /**
106      * Component used to get a plugin by its prefix and get mojo descriptors.
107      */
108     @Component
109     private MojoDescriptorCreator mojoDescriptorCreator;
110 
111     /**
112      * Component used to resolve the version for a plugin.
113      */
114     @Component
115     private PluginVersionResolver pluginVersionResolver;
116 
117     /**
118      * The Maven default built-in lifecycles.
119      */
120     @Component
121     private DefaultLifecycles defaultLifecycles;
122 
123     /**
124      * A map from each packaging to its lifecycle mapping.
125      */
126     @Component
127     private Map<String, LifecycleMapping> lifecycleMappings;
128 
129     // ----------------------------------------------------------------------
130     // Mojo parameters
131     // ----------------------------------------------------------------------
132 
133     /**
134      * The Maven Plugin to describe. This must be specified in one of three ways:
135      * <br/>
136      * <ol>
137      * <li>plugin-prefix, i.e. 'help'</li>
138      * <li>groupId:artifactId, i.e. 'org.apache.maven.plugins:maven-help-plugin'</li>
139      * <li>groupId:artifactId:version, i.e. 'org.apache.maven.plugins:maven-help-plugin:2.0'</li>
140      * </ol>
141      */
142     @org.apache.maven.plugins.annotations.Parameter(property = "plugin", alias = "prefix")
143     private String plugin;
144 
145     /**
146      * The Maven Plugin <code>groupId</code> to describe.
147      * <br/>
148      * <b>Note</b>: Should be used with <code>artifactId</code> parameter.
149      */
150     @org.apache.maven.plugins.annotations.Parameter(property = "groupId")
151     private String groupId;
152 
153     /**
154      * The Maven Plugin <code>artifactId</code> to describe.
155      * <br/>
156      * <b>Note</b>: Should be used with <code>groupId</code> parameter.
157      */
158     @org.apache.maven.plugins.annotations.Parameter(property = "artifactId")
159     private String artifactId;
160 
161     /**
162      * The Maven Plugin <code>version</code> to describe.
163      * <br/>
164      * <b>Note</b>: Should be used with <code>groupId/artifactId</code> parameters.
165      */
166     @org.apache.maven.plugins.annotations.Parameter(property = "version")
167     private String version;
168 
169     /**
170      * The goal name of a Mojo to describe within the specified Maven Plugin.
171      * If this parameter is specified, only the corresponding goal (Mojo) will be described,
172      * rather than the whole Plugin.
173      *
174      * @since 2.1
175      */
176     @org.apache.maven.plugins.annotations.Parameter(property = "goal")
177     private String goal;
178 
179     /**
180      * This flag specifies that a detailed (verbose) list of goal (Mojo) information should be given.
181      *
182      * @since 2.1
183      */
184     @org.apache.maven.plugins.annotations.Parameter(property = "detail", defaultValue = "false")
185     private boolean detail;
186 
187     /**
188      * This flag specifies that a minimal list of goal (Mojo) information should be given.
189      *
190      * @since 2.1
191      */
192     @org.apache.maven.plugins.annotations.Parameter(property = "minimal", defaultValue = "false")
193     private boolean minimal;
194 
195     /**
196      * A Maven command like a single goal or a single phase following the Maven command line:
197      * <br/>
198      * <code>mvn [options] [&lt;goal(s)&gt;] [&lt;phase(s)&gt;]</code>
199      *
200      * @since 2.1
201      */
202     @org.apache.maven.plugins.annotations.Parameter(property = "cmd")
203     private String cmd;
204 
205     // ----------------------------------------------------------------------
206     // Public methods
207     // ----------------------------------------------------------------------
208 
209     /**
210      * {@inheritDoc}
211      */
212     public void execute() throws MojoExecutionException, MojoFailureException {
213         StringBuilder descriptionBuffer = new StringBuilder();
214 
215         boolean describePlugin = true;
216         if (StringUtils.isNotEmpty(cmd)) {
217             describePlugin = describeCommand(descriptionBuffer);
218         }
219 
220         if (describePlugin) {
221             PluginInfo pi = parsePluginLookupInfo();
222             PluginDescriptor descriptor = lookupPluginDescriptor(pi);
223             if (StringUtils.isNotEmpty(goal)) {
224                 MojoDescriptor mojo = descriptor.getMojo(goal);
225                 if (mojo == null) {
226                     throw new MojoFailureException(
227                             "The goal '" + goal + "' does not exist in the plugin '" + pi.getPrefix() + "'");
228                 }
229                 describeMojo(mojo, descriptionBuffer);
230             } else {
231                 describePlugin(descriptor, descriptionBuffer);
232             }
233         }
234 
235         writeDescription(descriptionBuffer);
236     }
237 
238     // ----------------------------------------------------------------------
239     // Private methods
240     // ----------------------------------------------------------------------
241 
242     /**
243      * Method to write the Mojo description into the output file
244      *
245      * @param descriptionBuffer contains the description to be written to the file
246      * @throws MojoExecutionException if any
247      */
248     private void writeDescription(StringBuilder descriptionBuffer) throws MojoExecutionException {
249         if (output != null) {
250             try {
251                 writeFile(output, descriptionBuffer);
252             } catch (IOException e) {
253                 throw new MojoExecutionException("Cannot write plugin/goal description to output: " + output, e);
254             }
255 
256             getLog().info("Wrote descriptions to: " + output);
257         } else {
258             getLog().info(descriptionBuffer.toString());
259         }
260     }
261 
262     /**
263      * Method for retrieving the description of the plugin
264      *
265      * @param pi holds information of the plugin whose description is to be retrieved
266      * @return a PluginDescriptor where the plugin description is to be retrieved
267      * @throws MojoExecutionException if the plugin could not be verify
268      * @throws MojoFailureException   if groupId or artifactId is empty
269      */
270     private PluginDescriptor lookupPluginDescriptor(PluginInfo pi) throws MojoExecutionException, MojoFailureException {
271         Plugin forLookup = null;
272         if (StringUtils.isNotEmpty(pi.getPrefix())) {
273             try {
274                 forLookup = mojoDescriptorCreator.findPluginForPrefix(pi.getPrefix(), session);
275             } catch (NoPluginFoundForPrefixException e) {
276                 throw new MojoExecutionException("Unable to find the plugin with prefix: " + pi.getPrefix(), e);
277             }
278         } else if (StringUtils.isNotEmpty(pi.getGroupId()) && StringUtils.isNotEmpty(pi.getArtifactId())) {
279             forLookup = new Plugin();
280             forLookup.setGroupId(pi.getGroupId());
281             forLookup.setArtifactId(pi.getArtifactId());
282         }
283         if (forLookup == null) {
284             String msg = "You must specify either: both 'groupId' and 'artifactId' parameters OR a 'plugin' parameter"
285                     + " OR a 'cmd' parameter. For instance:" + LS
286                     + "  # mvn help:describe -Dcmd=install" + LS
287                     + "or" + LS
288                     + "  # mvn help:describe -Dcmd=help:describe" + LS
289                     + "or" + LS
290                     + "  # mvn help:describe -Dplugin=org.apache.maven.plugins:maven-help-plugin" + LS
291                     + "or" + LS
292                     + "  # mvn help:describe -DgroupId=org.apache.maven.plugins -DartifactId=maven-help-plugin" + LS
293                     + LS
294                     + "Try 'mvn help:help -Ddetail=true' for more information.";
295             throw new MojoFailureException(msg);
296         }
297 
298         if (StringUtils.isNotEmpty(pi.getVersion())) {
299             forLookup.setVersion(pi.getVersion());
300         } else {
301             try {
302                 DefaultPluginVersionRequest versionRequest = new DefaultPluginVersionRequest(forLookup, session);
303                 versionRequest.setPom(project.getModel());
304                 PluginVersionResult versionResult = pluginVersionResolver.resolve(versionRequest);
305                 forLookup.setVersion(versionResult.getVersion());
306             } catch (PluginVersionResolutionException e) {
307                 throw new MojoExecutionException(
308                         "Unable to resolve the version of the plugin with prefix: " + pi.getPrefix(), e);
309             }
310         }
311 
312         try {
313             return pluginManager.getPluginDescriptor(
314                     forLookup, project.getRemotePluginRepositories(), session.getRepositorySession());
315         } catch (Exception e) {
316             throw new MojoExecutionException(
317                     "Error retrieving plugin descriptor for:" + LS + LS + "groupId: '"
318                             + groupId + "'" + LS + "artifactId: '" + artifactId + "'" + LS + "version: '" + version
319                             + "'" + LS
320                             + LS,
321                     e);
322         }
323     }
324 
325     /**
326      * Method for parsing the plugin parameter
327      *
328      * @return Plugin info containing information about the plugin whose description is to be retrieved
329      * @throws MojoFailureException if <code>plugin<*code> parameter is not conform to
330      *                              <code>groupId:artifactId[:version]</code>
331      */
332     private PluginInfo parsePluginLookupInfo() throws MojoFailureException {
333         PluginInfo pi = new PluginInfo();
334         if (StringUtils.isNotEmpty(plugin)) {
335             if (plugin.indexOf(':') > -1) {
336                 String[] pluginParts = plugin.split(":");
337 
338                 switch (pluginParts.length) {
339                     case 1:
340                         pi.setPrefix(pluginParts[0]);
341                         break;
342                     case 2:
343                         pi.setGroupId(pluginParts[0]);
344                         pi.setArtifactId(pluginParts[1]);
345                         break;
346                     case 3:
347                         pi.setGroupId(pluginParts[0]);
348                         pi.setArtifactId(pluginParts[1]);
349                         pi.setVersion(pluginParts[2]);
350                         break;
351                     default:
352                         throw new MojoFailureException("plugin parameter must be a plugin prefix,"
353                                 + " or conform to: 'groupId:artifactId[:version]'.");
354                 }
355             } else {
356                 pi.setPrefix(plugin);
357             }
358         } else {
359             pi.setGroupId(groupId);
360             pi.setArtifactId(artifactId);
361             pi.setVersion(version);
362         }
363         return pi;
364     }
365 
366     /**
367      * Method for retrieving the plugin description
368      *
369      * @param pd     contains the plugin description
370      * @param buffer contains the information to be displayed or printed
371      * @throws MojoFailureException   if any reflection exceptions occur.
372      * @throws MojoExecutionException if any
373      */
374     private void describePlugin(PluginDescriptor pd, StringBuilder buffer)
375             throws MojoFailureException, MojoExecutionException {
376         append(buffer, pd.getId(), 0);
377         buffer.append(LS);
378 
379         String name = pd.getName();
380         if (name == null) {
381             // Can be null because of MPLUGIN-137 (and descriptors generated with maven-plugin-tools-api <= 2.4.3)
382             Artifact aetherArtifact = new DefaultArtifact(pd.getGroupId(), pd.getArtifactId(), "jar", pd.getVersion());
383             ProjectBuildingRequest pbr = new DefaultProjectBuildingRequest(session.getProjectBuildingRequest());
384             pbr.setRemoteRepositories(project.getRemoteArtifactRepositories());
385             pbr.setPluginArtifactRepositories(project.getPluginArtifactRepositories());
386             pbr.setProject(null);
387             pbr.setValidationLevel(ModelBuildingRequest.VALIDATION_LEVEL_MINIMAL);
388             try {
389                 Artifact artifactCopy = resolveArtifact(aetherArtifact).getArtifact();
390                 name = projectBuilder
391                         .build(RepositoryUtils.toArtifact(artifactCopy), pbr)
392                         .getProject()
393                         .getName();
394             } catch (Exception e) {
395                 // oh well, we tried our best.
396                 getLog().warn("Unable to get the name of the plugin " + pd.getId() + ": " + e.getMessage());
397                 name = pd.getId();
398             }
399         }
400         append(buffer, "Name", MessageUtils.buffer().strong(name).toString(), 0);
401         appendAsParagraph(buffer, "Description", toDescription(pd.getDescription()), 0);
402         append(buffer, "Group Id", pd.getGroupId(), 0);
403         append(buffer, "Artifact Id", pd.getArtifactId(), 0);
404         append(buffer, "Version", pd.getVersion(), 0);
405         append(
406                 buffer,
407                 "Goal Prefix",
408                 MessageUtils.buffer().strong(pd.getGoalPrefix()).toString(),
409                 0);
410         buffer.append(LS);
411 
412         List<MojoDescriptor> mojos = pd.getMojos();
413 
414         if (mojos == null) {
415             append(buffer, "This plugin has no goals.", 0);
416             return;
417         }
418 
419         if (!minimal) {
420             append(buffer, "This plugin has " + mojos.size() + " goal" + (mojos.size() > 1 ? "s" : "") + ":", 0);
421             buffer.append(LS);
422 
423             mojos = mojos.stream()
424                     .sorted((m1, m2) -> m1.getGoal().compareToIgnoreCase(m2.getGoal()))
425                     .collect(Collectors.toList());
426 
427             for (MojoDescriptor md : mojos) {
428                 describeMojoGuts(md, buffer, detail);
429                 buffer.append(LS);
430             }
431         }
432 
433         if (!detail) {
434             buffer.append("For more information, run 'mvn help:describe [...] -Ddetail'");
435             buffer.append(LS);
436         }
437     }
438 
439     /**
440      * Displays information about the Plugin Mojo
441      *
442      * @param md     contains the description of the Plugin Mojo
443      * @param buffer the displayed output
444      * @throws MojoFailureException   if any reflection exceptions occur.
445      * @throws MojoExecutionException if any
446      */
447     private void describeMojo(MojoDescriptor md, StringBuilder buffer)
448             throws MojoFailureException, MojoExecutionException {
449         buffer.append("Mojo: '").append(md.getFullGoalName()).append("'");
450         buffer.append(LS);
451 
452         describeMojoGuts(md, buffer, detail);
453         buffer.append(LS);
454 
455         if (!detail) {
456             buffer.append("For more information, run 'mvn help:describe [...] -Ddetail'");
457             buffer.append(LS);
458         }
459     }
460 
461     /**
462      * Displays detailed information about the Plugin Mojo
463      *
464      * @param md              contains the description of the Plugin Mojo
465      * @param buffer          contains information to be printed or displayed
466      * @param fullDescription specifies whether all the details about the Plugin Mojo is to  be displayed
467      * @throws MojoFailureException   if any reflection exceptions occur.
468      * @throws MojoExecutionException if any
469      */
470     private void describeMojoGuts(MojoDescriptor md, StringBuilder buffer, boolean fullDescription)
471             throws MojoFailureException, MojoExecutionException {
472         append(buffer, MessageUtils.buffer().strong(md.getFullGoalName()).toString(), 0);
473 
474         // indent 1
475         appendAsParagraph(buffer, "Description", toDescription(md.getDescription()), 1);
476 
477         String deprecation = md.getDeprecated();
478         if (deprecation != null && deprecation.length() <= 0) {
479             deprecation = NO_REASON;
480         }
481 
482         if (StringUtils.isNotEmpty(deprecation)) {
483             append(
484                     buffer,
485                     MessageUtils.buffer().warning("Deprecated. " + deprecation).toString(),
486                     1);
487         }
488 
489         if (isReportGoal(md)) {
490             append(buffer, "Note", "This goal should be used as a Maven report.", 1);
491         }
492 
493         if (!fullDescription) {
494             return;
495         }
496 
497         append(buffer, "Implementation", md.getImplementation(), 1);
498         append(buffer, "Language", md.getLanguage(), 1);
499 
500         String phase = md.getPhase();
501         if (StringUtils.isNotEmpty(phase)) {
502             append(buffer, "Bound to phase", phase, 1);
503         }
504 
505         String eGoal = md.getExecuteGoal();
506         String eLife = md.getExecuteLifecycle();
507         String ePhase = md.getExecutePhase();
508 
509         if (StringUtils.isNotEmpty(eGoal) || StringUtils.isNotEmpty(ePhase)) {
510             append(buffer, "Before this goal executes, it will call:", 1);
511 
512             if (StringUtils.isNotEmpty(eGoal)) {
513                 append(buffer, "Single goal", "'" + eGoal + "'", 2);
514             }
515 
516             if (StringUtils.isNotEmpty(ePhase)) {
517                 String s = "Phase: '" + ePhase + "'";
518 
519                 if (StringUtils.isNotEmpty(eLife)) {
520                     s += " in Lifecycle Overlay: '" + eLife + "'";
521                 }
522 
523                 append(buffer, s, 2);
524             }
525         }
526 
527         buffer.append(LS);
528 
529         describeMojoParameters(md, buffer);
530     }
531 
532     /**
533      * Displays parameter information of the Plugin Mojo
534      *
535      * @param md     contains the description of the Plugin Mojo
536      * @param buffer contains information to be printed or displayed
537      * @throws MojoFailureException   if any reflection exceptions occur.
538      * @throws MojoExecutionException if any
539      */
540     private void describeMojoParameters(MojoDescriptor md, StringBuilder buffer)
541             throws MojoFailureException, MojoExecutionException {
542         List<Parameter> params = md.getParameters();
543 
544         if (params == null || params.isEmpty()) {
545             append(buffer, "This mojo doesn't use any parameters.", 1);
546             return;
547         }
548 
549         params = params.stream()
550                 .sorted((p1, p2) -> p1.getName().compareToIgnoreCase(p2.getName()))
551                 .collect(Collectors.toList());
552 
553         append(buffer, "Available parameters:", 1);
554 
555         // indent 2
556         for (Parameter parameter : params) {
557             if (!parameter.isEditable()) {
558                 continue;
559             }
560 
561             buffer.append(LS);
562 
563             // DGF wouldn't it be nice if this worked?
564             String defaultVal = parameter.getDefaultValue();
565             if (defaultVal == null) {
566                 // defaultVal is ALWAYS null, this is a bug in PluginDescriptorBuilder (cf. MNG-4941)
567                 defaultVal =
568                         md.getMojoConfiguration().getChild(parameter.getName()).getAttribute("default-value", null);
569             }
570 
571             if (StringUtils.isNotEmpty(defaultVal)) {
572                 defaultVal = " (Default: " + MessageUtils.buffer().strong(defaultVal) + ")";
573             } else {
574                 defaultVal = "";
575             }
576             append(buffer, MessageUtils.buffer().strong(parameter.getName()) + defaultVal, 2);
577 
578             String alias = parameter.getAlias();
579             if (!StringUtils.isEmpty(alias)) {
580                 append(buffer, "Alias", alias, 3);
581             }
582 
583             if (parameter.isRequired()) {
584                 append(buffer, "Required", "true", 3);
585             }
586 
587             String expression = parameter.getExpression();
588             if (StringUtils.isEmpty(expression)) {
589                 // expression is ALWAYS null, this is a bug in PluginDescriptorBuilder (cf. MNG-4941).
590                 // Fixed with Maven-3.0.1
591                 expression =
592                         md.getMojoConfiguration().getChild(parameter.getName()).getValue(null);
593             }
594             if (StringUtils.isNotEmpty(expression)) {
595                 Matcher matcher = EXPRESSION.matcher(expression);
596                 if (matcher.matches()) {
597                     append(buffer, "User property", matcher.group(1), 3);
598                 } else {
599                     append(buffer, "Expression", expression, 3);
600                 }
601             }
602 
603             append(buffer, toDescription(parameter.getDescription()), 3);
604 
605             String deprecation = parameter.getDeprecated();
606             if (deprecation != null && deprecation.length() <= 0) {
607                 deprecation = NO_REASON;
608             }
609 
610             if (StringUtils.isNotEmpty(deprecation)) {
611                 append(
612                         buffer,
613                         MessageUtils.buffer()
614                                 .warning("Deprecated. " + deprecation)
615                                 .toString(),
616                         3);
617             }
618         }
619     }
620 
621     /**
622      * Describe the <code>cmd</code> parameter
623      *
624      * @param descriptionBuffer not null
625      * @return <code>true</code> if it implies to describe a plugin, <code>false</code> otherwise.
626      * @throws MojoExecutionException if any
627      */
628     private boolean describeCommand(StringBuilder descriptionBuffer) throws MojoExecutionException {
629         if (cmd.indexOf(':') == -1) {
630             // phase
631             Lifecycle lifecycle = defaultLifecycles.getPhaseToLifecycleMap().get(cmd);
632             if (lifecycle == null) {
633                 throw new MojoExecutionException("The given phase '" + cmd + "' is an unknown phase.");
634             }
635 
636             Map<String, String> defaultLifecyclePhases = lifecycleMappings
637                     .get(project.getPackaging())
638                     .getLifecycles()
639                     .get("default")
640                     .getPhases();
641             List<String> phases = lifecycle.getPhases();
642 
643             if (lifecycle.getDefaultPhases() == null) {
644                 descriptionBuffer.append("'").append(cmd);
645                 descriptionBuffer
646                         .append("' is a phase corresponding to this plugin:")
647                         .append(LS);
648                 for (String key : phases) {
649                     if (!key.equals(cmd)) {
650                         continue;
651                     }
652                     if (defaultLifecyclePhases.get(key) != null) {
653                         descriptionBuffer.append(defaultLifecyclePhases.get(key));
654                         descriptionBuffer.append(LS);
655                     }
656                 }
657 
658                 descriptionBuffer.append(LS);
659                 descriptionBuffer.append("It is a part of the lifecycle for the POM packaging '");
660                 descriptionBuffer.append(project.getPackaging());
661                 descriptionBuffer.append("'. This lifecycle includes the following phases:");
662                 descriptionBuffer.append(LS);
663                 for (String key : phases) {
664                     descriptionBuffer.append("* ").append(key).append(": ");
665                     String value = defaultLifecyclePhases.get(key);
666                     if (StringUtils.isNotEmpty(value)) {
667                         for (StringTokenizer tok = new StringTokenizer(value, ","); tok.hasMoreTokens(); ) {
668                             descriptionBuffer.append(tok.nextToken().trim());
669 
670                             if (!tok.hasMoreTokens()) {
671                                 descriptionBuffer.append(LS);
672                             } else {
673                                 descriptionBuffer.append(", ");
674                             }
675                         }
676                     } else {
677                         descriptionBuffer.append(NOT_DEFINED).append(LS);
678                     }
679                 }
680             } else {
681                 descriptionBuffer.append("'").append(cmd);
682                 descriptionBuffer.append("' is a phase within the '").append(lifecycle.getId());
683                 descriptionBuffer.append("' lifecycle, which has the following phases: ");
684                 descriptionBuffer.append(LS);
685 
686                 for (String key : phases) {
687                     descriptionBuffer.append("* ").append(key).append(": ");
688                     if (lifecycle.getDefaultPhases().get(key) != null) {
689                         descriptionBuffer
690                                 .append(lifecycle.getDefaultPhases().get(key))
691                                 .append(LS);
692                     } else {
693                         descriptionBuffer.append(NOT_DEFINED).append(LS);
694                     }
695                 }
696             }
697             return false;
698         }
699 
700         // goals
701         MojoDescriptor mojoDescriptor;
702         try {
703             mojoDescriptor = mojoDescriptorCreator.getMojoDescriptor(cmd, session, project);
704         } catch (Exception e) {
705             throw new MojoExecutionException("Unable to get descriptor for " + cmd, e);
706         }
707         descriptionBuffer
708                 .append("'")
709                 .append(cmd)
710                 .append("' is a plugin goal (aka mojo)")
711                 .append(".");
712         descriptionBuffer.append(LS);
713         plugin = mojoDescriptor.getPluginDescriptor().getId();
714         goal = mojoDescriptor.getGoal();
715 
716         return true;
717     }
718 
719     /**
720      * Invoke the following private method
721      * <code>HelpMojo#toLines(String, int, int, int)</code>
722      *
723      * @param text       The text to split into lines, must not be <code>null</code>.
724      * @param indent     The base indentation level of each line, must not be negative.
725      * @param indentSize The size of each indentation, must not be negative.
726      * @param lineLength The length of the line, must not be negative.
727      * @return The sequence of display lines, never <code>null</code>.
728      * @throws MojoFailureException   if any can not invoke the method
729      * @throws MojoExecutionException if no line was found for <code>text</code>
730      */
731     private static List<String> toLines(String text, int indent, int indentSize, int lineLength)
732             throws MojoFailureException, MojoExecutionException {
733         try {
734             Method m =
735                     HelpMojo.class.getDeclaredMethod("toLines", String.class, Integer.TYPE, Integer.TYPE, Integer.TYPE);
736             m.setAccessible(true);
737             @SuppressWarnings("unchecked")
738             List<String> output = (List<String>) m.invoke(HelpMojo.class, text, indent, indentSize, lineLength);
739 
740             if (output == null) {
741                 throw new MojoExecutionException("No output was specified.");
742             }
743 
744             return output;
745         } catch (SecurityException e) {
746             throw new MojoFailureException("SecurityException: " + e.getMessage());
747         } catch (IllegalArgumentException e) {
748             throw new MojoFailureException("IllegalArgumentException: " + e.getMessage());
749         } catch (NoSuchMethodException e) {
750             throw new MojoFailureException("NoSuchMethodException: " + e.getMessage());
751         } catch (IllegalAccessException e) {
752             throw new MojoFailureException("IllegalAccessException: " + e.getMessage());
753         } catch (InvocationTargetException e) {
754             Throwable cause = e.getCause();
755 
756             if (cause instanceof NegativeArraySizeException) {
757                 throw new MojoFailureException("NegativeArraySizeException: " + cause.getMessage());
758             }
759 
760             throw new MojoFailureException("InvocationTargetException: " + e.getMessage());
761         }
762     }
763 
764     /**
765      * Append a description to the buffer by respecting the indentSize and lineLength parameters.
766      * <b>Note</b>: The last character is always a new line.
767      *
768      * @param sb          The buffer to append the description, not <code>null</code>.
769      * @param description The description, not <code>null</code>.
770      * @param indent      The base indentation level of each line, must not be negative.
771      * @throws MojoFailureException   if any reflection exceptions occur.
772      * @throws MojoExecutionException if any
773      * @see #toLines(String, int, int, int)
774      */
775     private static void append(StringBuilder sb, String description, int indent)
776             throws MojoFailureException, MojoExecutionException {
777         if (StringUtils.isEmpty(description)) {
778             sb.append(UNKNOWN).append(LS);
779             return;
780         }
781 
782         for (String line : toLines(description, indent, INDENT_SIZE, LINE_LENGTH)) {
783             sb.append(line).append(LS);
784         }
785     }
786 
787     /**
788      * Append a description to the buffer by respecting the indentSize and lineLength parameters.
789      * <b>Note</b>: The last character is always a new line.
790      *
791      * @param sb     The buffer to append the description, not <code>null</code>.
792      * @param key    The key, not <code>null</code>.
793      * @param value  The value associated to the key, could be <code>null</code>.
794      * @param indent The base indentation level of each line, must not be negative.
795      * @throws MojoFailureException   if any reflection exceptions occur.
796      * @throws MojoExecutionException if any
797      * @see #toLines(String, int, int, int)
798      */
799     private static void append(StringBuilder sb, String key, String value, int indent)
800             throws MojoFailureException, MojoExecutionException {
801         if (StringUtils.isEmpty(key)) {
802             throw new IllegalArgumentException("Key is required!");
803         }
804 
805         if (StringUtils.isEmpty(value)) {
806             value = UNKNOWN;
807         }
808 
809         String description = key + ": " + value;
810         for (String line : toLines(description, indent, INDENT_SIZE, LINE_LENGTH)) {
811             sb.append(line).append(LS);
812         }
813     }
814 
815     /**
816      * Append a description to the buffer by respecting the indentSize and lineLength parameters for the first line,
817      * and append the next lines with <code>indent + 1</code> like a paragraph.
818      * <b>Note</b>: The last character is always a new line.
819      *
820      * @param sb     The buffer to append the description, not <code>null</code>.
821      * @param key    The key, not <code>null</code>.
822      * @param value  The value, could be <code>null</code>.
823      * @param indent The base indentation level of each line, must not be negative.
824      * @throws MojoFailureException   if any reflection exceptions occur.
825      * @throws MojoExecutionException if any
826      * @see #toLines(String, int, int, int)
827      */
828     private static void appendAsParagraph(StringBuilder sb, String key, String value, int indent)
829             throws MojoFailureException, MojoExecutionException {
830         if (StringUtils.isEmpty(value)) {
831             value = UNKNOWN;
832         }
833 
834         String description;
835         if (key == null) {
836             description = value;
837         } else {
838             description = key + ": " + value;
839         }
840 
841         List<String> l1 = toLines(description, indent, INDENT_SIZE, LINE_LENGTH - INDENT_SIZE);
842         List<String> l2 = toLines(description, indent + 1, INDENT_SIZE, LINE_LENGTH);
843         l2.set(0, l1.get(0));
844         for (String line : l2) {
845             sb.append(line).append(LS);
846         }
847     }
848 
849     /**
850      * Determines if this Mojo should be used as a report or not. This resolves the plugin project along with all of its
851      * transitive dependencies to determine if the Java class of this goal implements <code>MavenReport</code>.
852      *
853      * @param md Mojo descriptor
854      * @return Whether or not this goal should be used as a report.
855      */
856     private boolean isReportGoal(MojoDescriptor md) {
857         PluginDescriptor pd = md.getPluginDescriptor();
858         List<URL> urls = new ArrayList<>();
859         ProjectBuildingRequest pbr = new DefaultProjectBuildingRequest(session.getProjectBuildingRequest());
860         pbr.setRemoteRepositories(project.getRemoteArtifactRepositories());
861         pbr.setPluginArtifactRepositories(project.getPluginArtifactRepositories());
862         pbr.setResolveDependencies(true);
863         pbr.setProject(null);
864         pbr.setValidationLevel(ModelBuildingRequest.VALIDATION_LEVEL_MINIMAL);
865         try {
866             Artifact jar = resolveArtifact(
867                             new DefaultArtifact(pd.getGroupId(), pd.getArtifactId(), "jar", pd.getVersion()))
868                     .getArtifact();
869             Artifact pom = resolveArtifact(
870                             new DefaultArtifact(pd.getGroupId(), pd.getArtifactId(), "pom", pd.getVersion()))
871                     .getArtifact();
872             MavenProject mavenProject = projectBuilder.build(pom.getFile(), pbr).getProject();
873             urls.add(jar.getFile().toURI().toURL());
874             for (String artifact : mavenProject.getCompileClasspathElements()) {
875                 urls.add(new File(artifact).toURI().toURL());
876             }
877             try (URLClassLoader classLoader =
878                     new URLClassLoader(urls.toArray(new URL[0]), getClass().getClassLoader())) {
879                 return MavenReport.class.isAssignableFrom(Class.forName(md.getImplementation(), false, classLoader));
880             }
881         } catch (Exception e) {
882             getLog().warn("Couldn't identify if this goal is a report goal: " + e.getMessage());
883             return false;
884         }
885     }
886 
887     /**
888      * Gets the effective string to use for the plugin/mojo/parameter description.
889      *
890      * @param description The description of the element, may be <code>null</code>.
891      * @return The effective description string, never <code>null</code>.
892      */
893     private static String toDescription(String description) {
894         if (StringUtils.isNotEmpty(description)) {
895             return new HtmlToPlainTextConverter().convert(description);
896         }
897 
898         return "(no description available)";
899     }
900 
901     /**
902      * Class to wrap Plugin information.
903      */
904     static class PluginInfo {
905         private String prefix;
906 
907         private String groupId;
908 
909         private String artifactId;
910 
911         private String version;
912 
913         /**
914          * @return the prefix
915          */
916         public String getPrefix() {
917             return prefix;
918         }
919 
920         /**
921          * @param prefix the prefix to set
922          */
923         public void setPrefix(String prefix) {
924             this.prefix = prefix;
925         }
926 
927         /**
928          * @return the groupId
929          */
930         public String getGroupId() {
931             return groupId;
932         }
933 
934         /**
935          * @param groupId the groupId to set
936          */
937         public void setGroupId(String groupId) {
938             this.groupId = groupId;
939         }
940 
941         /**
942          * @return the artifactId
943          */
944         public String getArtifactId() {
945             return artifactId;
946         }
947 
948         /**
949          * @param artifactId the artifactId to set
950          */
951         public void setArtifactId(String artifactId) {
952             this.artifactId = artifactId;
953         }
954 
955         /**
956          * @return the version
957          */
958         public String getVersion() {
959             return version;
960         }
961 
962         /**
963          * @param version the version to set
964          */
965         public void setVersion(String version) {
966             this.version = version;
967         }
968     }
969 }