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.tools.plugin.generator;
20  
21  import java.io.File;
22  import java.io.IOException;
23  import java.io.OutputStreamWriter;
24  import java.io.PrintWriter;
25  import java.io.Writer;
26  import java.net.URI;
27  import java.net.URISyntaxException;
28  import java.text.MessageFormat;
29  import java.util.ArrayList;
30  import java.util.Iterator;
31  import java.util.List;
32  import java.util.Locale;
33  import java.util.ResourceBundle;
34  import java.util.regex.Matcher;
35  import java.util.regex.Pattern;
36  
37  import org.apache.maven.plugin.descriptor.MojoDescriptor;
38  import org.apache.maven.plugin.descriptor.Parameter;
39  import org.apache.maven.project.MavenProject;
40  import org.apache.maven.tools.plugin.EnhancedParameterWrapper;
41  import org.apache.maven.tools.plugin.ExtendedMojoDescriptor;
42  import org.apache.maven.tools.plugin.PluginToolsRequest;
43  import org.apache.maven.tools.plugin.javadoc.JavadocLinkGenerator;
44  import org.codehaus.plexus.util.StringUtils;
45  import org.codehaus.plexus.util.io.CachingOutputStream;
46  import org.codehaus.plexus.util.xml.PrettyPrintXMLWriter;
47  import org.codehaus.plexus.util.xml.XMLWriter;
48  import org.slf4j.Logger;
49  import org.slf4j.LoggerFactory;
50  
51  import static java.nio.charset.StandardCharsets.UTF_8;
52  
53  /**
54   * Generate <a href="https://maven.apache.org/doxia/references/xdoc-format.html">xdoc documentation</a> for each mojo.
55   */
56  public class PluginXdocGenerator implements Generator {
57      /**
58       * Regular expression matching an XHTML link
59       * group 1 = link target, group 2 = link label
60       */
61      private static final Pattern HTML_LINK_PATTERN = Pattern.compile("<a href=\\\"([^\\\"]*)\\\">(.*?)</a>");
62  
63      private static final Logger LOG = LoggerFactory.getLogger(PluginXdocGenerator.class);
64  
65      /**
66       * locale
67       */
68      private final Locale locale;
69  
70      /**
71       * project
72       */
73      private final MavenProject project;
74  
75      /**
76       * The directory where the generated site is written.
77       * Used for resolving relative links to javadoc.
78       */
79      private final File reportOutputDirectory;
80  
81      private final boolean disableInternalJavadocLinkValidation;
82  
83      /**
84       * Default constructor using <code>Locale.ENGLISH</code> as locale.
85       * Used only in test cases.
86       */
87      public PluginXdocGenerator() {
88          this(null);
89      }
90  
91      /**
92       * Constructor using <code>Locale.ENGLISH</code> as locale.
93       *
94       * @param project not null Maven project.
95       */
96      public PluginXdocGenerator(MavenProject project) {
97          this(project, Locale.ENGLISH, new File("").getAbsoluteFile(), false);
98      }
99  
100     /**
101      * @param project not null.
102      * @param locale  not null.
103      */
104     public PluginXdocGenerator(
105             MavenProject project,
106             Locale locale,
107             File reportOutputDirectory,
108             boolean disableInternalJavadocLinkValidation) {
109         this.project = project;
110         this.locale = locale;
111         this.reportOutputDirectory = reportOutputDirectory;
112         this.disableInternalJavadocLinkValidation = disableInternalJavadocLinkValidation;
113     }
114 
115     /**
116      * {@inheritDoc}
117      */
118     @Override
119     public void execute(File destinationDirectory, PluginToolsRequest request) throws GeneratorException {
120         try {
121             if (request.getPluginDescriptor().getMojos() != null) {
122                 List<MojoDescriptor> mojos = request.getPluginDescriptor().getMojos();
123                 for (MojoDescriptor descriptor : mojos) {
124                     processMojoDescriptor(descriptor, destinationDirectory);
125                 }
126             }
127         } catch (IOException e) {
128             throw new GeneratorException(e.getMessage(), e);
129         }
130     }
131 
132     /**
133      * @param mojoDescriptor       not null
134      * @param destinationDirectory not null
135      * @throws IOException if any
136      */
137     protected void processMojoDescriptor(MojoDescriptor mojoDescriptor, File destinationDirectory) throws IOException {
138         File outputFile = new File(destinationDirectory, getMojoFilename(mojoDescriptor, "xml"));
139         try (Writer writer = new OutputStreamWriter(new CachingOutputStream(outputFile), UTF_8)) {
140             XMLWriter w = new PrettyPrintXMLWriter(new PrintWriter(writer), UTF_8.name(), null);
141             writeBody(mojoDescriptor, w);
142 
143             writer.flush();
144         }
145     }
146 
147     /**
148      * @param mojo not null
149      * @param ext  not null
150      * @return the output file name
151      */
152     private String getMojoFilename(MojoDescriptor mojo, String ext) {
153         return mojo.getGoal() + "-mojo." + ext;
154     }
155 
156     /**
157      * @param mojoDescriptor not null
158      * @param w              not null
159      */
160     private void writeBody(MojoDescriptor mojoDescriptor, XMLWriter w) {
161         w.startElement("document");
162         w.addAttribute("xmlns", "http://maven.apache.org/XDOC/2.0");
163         w.addAttribute("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance");
164         w.addAttribute(
165                 "xsi:schemaLocation", "http://maven.apache.org/XDOC/2.0 http://maven.apache.org/xsd/xdoc-2.0.xsd");
166 
167         // ----------------------------------------------------------------------
168         //
169         // ----------------------------------------------------------------------
170 
171         w.startElement("properties");
172 
173         w.startElement("title");
174         w.writeText(mojoDescriptor.getFullGoalName());
175         w.endElement(); // title
176 
177         w.endElement(); // properties
178 
179         // ----------------------------------------------------------------------
180         //
181         // ----------------------------------------------------------------------
182 
183         w.startElement("body");
184 
185         w.startElement("section");
186 
187         w.addAttribute("name", mojoDescriptor.getFullGoalName());
188 
189         writeReportNotice(mojoDescriptor, w);
190 
191         w.startElement("p");
192         w.writeMarkup(getString("pluginxdoc.mojodescriptor.fullname"));
193         w.endElement(); // p
194         w.startElement("p");
195         w.writeMarkup(mojoDescriptor.getPluginDescriptor().getId() + ":" + mojoDescriptor.getGoal());
196         w.endElement(); // p
197 
198         String context = "goal " + mojoDescriptor.getGoal();
199         if (StringUtils.isNotEmpty(mojoDescriptor.getDeprecated())) {
200             w.startElement("p");
201             w.writeMarkup(getString("pluginxdoc.mojodescriptor.deprecated"));
202             w.endElement(); // p
203             w.startElement("div");
204             w.writeMarkup(getXhtmlWithValidatedLinks(mojoDescriptor.getDeprecated(), context));
205             w.endElement(); // div
206         }
207 
208         w.startElement("p");
209         w.writeMarkup(getString("pluginxdoc.description"));
210         w.endElement(); // p
211         w.startElement("div");
212         if (StringUtils.isNotEmpty(mojoDescriptor.getDescription())) {
213             w.writeMarkup(getXhtmlWithValidatedLinks(mojoDescriptor.getDescription(), context));
214         } else {
215             w.writeText(getString("pluginxdoc.nodescription"));
216         }
217         w.endElement(); // div
218 
219         writeGoalAttributes(mojoDescriptor, w);
220 
221         writeGoalParameterTable(mojoDescriptor, w);
222 
223         w.endElement(); // section
224 
225         w.endElement(); // body
226 
227         w.endElement(); // document
228     }
229 
230     /**
231      * @param mojoDescriptor not null
232      * @param w              not null
233      */
234     private void writeReportNotice(MojoDescriptor mojoDescriptor, XMLWriter w) {
235         if (GeneratorUtils.isMavenReport(mojoDescriptor.getImplementation(), project)) {
236             w.startElement("p");
237             w.writeMarkup(getString("pluginxdoc.mojodescriptor.notice.note"));
238             w.writeText(getString("pluginxdoc.mojodescriptor.notice.isMavenReport"));
239             w.endElement(); // p
240         }
241     }
242 
243     /**
244      * @param mojoDescriptor not null
245      * @param w              not null
246      */
247     private void writeGoalAttributes(MojoDescriptor mojoDescriptor, XMLWriter w) {
248         w.startElement("p");
249         w.writeMarkup(getString("pluginxdoc.mojodescriptor.attributes"));
250         w.endElement(); // p
251 
252         boolean addedUl = false;
253         String value;
254         if (mojoDescriptor.isProjectRequired()) {
255             addedUl = addUl(w, addedUl);
256             w.startElement("li");
257             w.writeMarkup(getString("pluginxdoc.mojodescriptor.projectRequired"));
258             w.endElement(); // li
259         }
260 
261         if (mojoDescriptor.isRequiresReports()) {
262             addedUl = addUl(w, addedUl);
263             w.startElement("li");
264             w.writeMarkup(getString("pluginxdoc.mojodescriptor.reportingMojo"));
265             w.endElement(); // li
266         }
267 
268         if (mojoDescriptor.isAggregator()) {
269             addedUl = addUl(w, addedUl);
270             w.startElement("li");
271             w.writeMarkup(getString("pluginxdoc.mojodescriptor.aggregator"));
272             w.endElement(); // li
273         }
274 
275         if (mojoDescriptor.isDirectInvocationOnly()) {
276             addedUl = addUl(w, addedUl);
277             w.startElement("li");
278             w.writeMarkup(getString("pluginxdoc.mojodescriptor.directInvocationOnly"));
279             w.endElement(); // li
280         }
281 
282         value = mojoDescriptor.isDependencyResolutionRequired();
283         if (value != null && !value.isEmpty()) {
284             addedUl = addUl(w, addedUl);
285             w.startElement("li");
286             w.writeMarkup(format("pluginxdoc.mojodescriptor.dependencyResolutionRequired", value));
287             w.endElement(); // li
288         }
289 
290         if (mojoDescriptor instanceof ExtendedMojoDescriptor) {
291             ExtendedMojoDescriptor extendedMojoDescriptor = (ExtendedMojoDescriptor) mojoDescriptor;
292 
293             value = extendedMojoDescriptor.getDependencyCollectionRequired();
294             if (value != null && !value.isEmpty()) {
295                 addedUl = addUl(w, addedUl);
296                 w.startElement("li");
297                 w.writeMarkup(format("pluginxdoc.mojodescriptor.dependencyCollectionRequired", value));
298                 w.endElement(); // li
299             }
300         }
301 
302         addedUl = addUl(w, addedUl);
303         w.startElement("li");
304         w.writeMarkup(getString(
305                 mojoDescriptor.isThreadSafe()
306                         ? "pluginxdoc.mojodescriptor.threadSafe"
307                         : "pluginxdoc.mojodescriptor.notThreadSafe"));
308         w.endElement(); // li
309 
310         value = mojoDescriptor.getSince();
311         if (value != null && !value.isEmpty()) {
312             addedUl = addUl(w, addedUl);
313             w.startElement("li");
314             w.writeMarkup(format("pluginxdoc.mojodescriptor.since", value));
315             w.endElement(); // li
316         }
317 
318         value = mojoDescriptor.getPhase();
319         if (value != null && !value.isEmpty()) {
320             addedUl = addUl(w, addedUl);
321             w.startElement("li");
322             w.writeMarkup(format("pluginxdoc.mojodescriptor.phase", value));
323             w.endElement(); // li
324         }
325 
326         value = mojoDescriptor.getExecutePhase();
327         if (value != null && !value.isEmpty()) {
328             addedUl = addUl(w, addedUl);
329             w.startElement("li");
330             w.writeMarkup(format("pluginxdoc.mojodescriptor.executePhase", value));
331             w.endElement(); // li
332         }
333 
334         value = mojoDescriptor.getExecuteGoal();
335         if (value != null && !value.isEmpty()) {
336             addedUl = addUl(w, addedUl);
337             w.startElement("li");
338             w.writeMarkup(format("pluginxdoc.mojodescriptor.executeGoal", value));
339             w.endElement(); // li
340         }
341 
342         value = mojoDescriptor.getExecuteLifecycle();
343         if (value != null && !value.isEmpty()) {
344             addedUl = addUl(w, addedUl);
345             w.startElement("li");
346             w.writeMarkup(format("pluginxdoc.mojodescriptor.executeLifecycle", value));
347             w.endElement(); // li
348         }
349 
350         if (mojoDescriptor.isOnlineRequired()) {
351             addedUl = addUl(w, addedUl);
352             w.startElement("li");
353             w.writeMarkup(getString("pluginxdoc.mojodescriptor.onlineRequired"));
354             w.endElement(); // li
355         }
356 
357         if (!mojoDescriptor.isInheritedByDefault()) {
358             addedUl = addUl(w, addedUl);
359             w.startElement("li");
360             w.writeMarkup(getString("pluginxdoc.mojodescriptor.inheritedByDefault"));
361             w.endElement(); // li
362         }
363 
364         if (addedUl) {
365             w.endElement(); // ul
366         }
367     }
368 
369     /**
370      * @param mojoDescriptor not null
371      * @param w              not null
372      */
373     private void writeGoalParameterTable(MojoDescriptor mojoDescriptor, XMLWriter w) {
374         List<Parameter> parameterList = mojoDescriptor.getParameters();
375 
376         // remove components and read-only parameters
377         List<Parameter> list = filterParameters(parameterList);
378 
379         if (!list.isEmpty()) {
380             writeParameterSummary(list, w, mojoDescriptor.getGoal());
381             writeParameterDetails(list, w, mojoDescriptor.getGoal());
382         } else {
383             w.startElement("subsection");
384             w.addAttribute("name", getString("pluginxdoc.mojodescriptor.parameters"));
385 
386             w.startElement("p");
387             w.writeMarkup(getString("pluginxdoc.mojodescriptor.noParameter"));
388             w.endElement(); // p
389 
390             w.endElement();
391         }
392     }
393 
394     /**
395      * Filter parameters to only retain those which must be documented, i.e. neither components nor readonly.
396      *
397      * @param parameterList not null
398      * @return the parameters list without components.
399      */
400     private List<Parameter> filterParameters(List<Parameter> parameterList) {
401         List<Parameter> filtered = new ArrayList<>();
402 
403         if (parameterList != null) {
404             for (Parameter parameter : parameterList) {
405                 if (parameter.isEditable()) {
406                     String expression = parameter.getExpression();
407 
408                     if (expression == null || !expression.startsWith("${component.")) {
409                         filtered.add(parameter);
410                     }
411                 }
412             }
413         }
414 
415         return filtered;
416     }
417 
418     /**
419      * @param parameterList  not null
420      * @param w              not null
421      */
422     private void writeParameterDetails(List<Parameter> parameterList, XMLWriter w, String goal) {
423         w.startElement("subsection");
424         w.addAttribute("name", getString("pluginxdoc.mojodescriptor.parameter.details"));
425 
426         for (Iterator<Parameter> parameters = parameterList.iterator(); parameters.hasNext(); ) {
427             Parameter parameter = parameters.next();
428 
429             w.startElement("h4");
430             w.writeMarkup(format("pluginxdoc.mojodescriptor.parameter.name_internal", parameter.getName()));
431             w.endElement();
432 
433             String context = "Parameter " + parameter.getName() + " in goal " + goal;
434             if (StringUtils.isNotEmpty(parameter.getDeprecated())) {
435                 w.startElement("div");
436                 String deprecated = getXhtmlWithValidatedLinks(parameter.getDeprecated(), context);
437                 w.writeMarkup(format("pluginxdoc.mojodescriptor.parameter.deprecated", deprecated));
438                 w.endElement(); // div
439             }
440 
441             w.startElement("div");
442             if (StringUtils.isNotEmpty(parameter.getDescription())) {
443                 w.writeMarkup(getXhtmlWithValidatedLinks(parameter.getDescription(), context));
444             } else {
445                 w.writeMarkup(getString("pluginxdoc.nodescription"));
446             }
447             w.endElement(); // div
448 
449             boolean addedUl = false;
450             addedUl = addUl(w, addedUl, parameter.getType());
451             String typeValue = getLinkedType(parameter, false);
452             writeDetail(getString("pluginxdoc.mojodescriptor.parameter.type"), typeValue, w);
453 
454             if (StringUtils.isNotEmpty(parameter.getSince())) {
455                 addedUl = addUl(w, addedUl);
456                 writeDetail(getString("pluginxdoc.mojodescriptor.parameter.since"), parameter.getSince(), w);
457             }
458 
459             if (parameter.isRequired()) {
460                 addedUl = addUl(w, addedUl);
461                 writeDetail(getString("pluginxdoc.mojodescriptor.parameter.required"), getString("pluginxdoc.yes"), w);
462             } else {
463                 addedUl = addUl(w, addedUl);
464                 writeDetail(getString("pluginxdoc.mojodescriptor.parameter.required"), getString("pluginxdoc.no"), w);
465             }
466 
467             String expression = parameter.getExpression();
468             addedUl = addUl(w, addedUl, expression);
469             String property = getPropertyFromExpression(expression);
470             if (property == null) {
471                 writeDetail(getString("pluginxdoc.mojodescriptor.parameter.expression"), expression, w);
472             } else {
473                 writeDetail(getString("pluginxdoc.mojodescriptor.parameter.property"), property, w);
474             }
475 
476             addedUl = addUl(w, addedUl, parameter.getDefaultValue());
477             writeDetail(
478                     getString("pluginxdoc.mojodescriptor.parameter.default"),
479                     escapeXml(parameter.getDefaultValue()),
480                     w);
481 
482             addedUl = addUl(w, addedUl, parameter.getAlias());
483             writeDetail(getString("pluginxdoc.mojodescriptor.parameter.alias"), escapeXml(parameter.getAlias()), w);
484 
485             if (addedUl) {
486                 w.endElement(); // ul
487             }
488 
489             if (parameters.hasNext()) {
490                 w.writeMarkup("<hr/>");
491             }
492         }
493 
494         w.endElement();
495     }
496 
497     static String getShortType(String type) {
498         // split into type arguments and main type
499         int startTypeArguments = type.indexOf('<');
500         if (startTypeArguments == -1) {
501             return getShortTypeOfSimpleType(type);
502         } else {
503             StringBuilder shortType = new StringBuilder();
504             shortType.append(getShortTypeOfSimpleType(type.substring(0, startTypeArguments)));
505             shortType
506                     .append("<")
507                     .append(getShortTypeOfTypeArgument(type.substring(startTypeArguments + 1, type.lastIndexOf(">"))))
508                     .append(">");
509             return shortType.toString();
510         }
511     }
512 
513     private static String getShortTypeOfTypeArgument(String type) {
514         String[] typeArguments = type.split(",\\s*");
515         StringBuilder shortType = new StringBuilder();
516         for (int i = 0; i < typeArguments.length; i++) {
517             String typeArgument = typeArguments[i];
518             if (typeArgument.contains("<")) {
519                 // nested type arguments lead to ellipsis
520                 return "...";
521             } else {
522                 shortType.append(getShortTypeOfSimpleType(typeArgument));
523                 if (i < typeArguments.length - 1) {
524                     shortType.append(",");
525                 }
526             }
527         }
528         return shortType.toString();
529     }
530 
531     private static String getShortTypeOfSimpleType(String type) {
532         int index = type.lastIndexOf('.');
533         return type.substring(index + 1);
534     }
535 
536     private String getLinkedType(Parameter parameter, boolean isShortType) {
537         final String typeValue;
538         if (isShortType) {
539             typeValue = getShortType(parameter.getType());
540         } else {
541             typeValue = parameter.getType();
542         }
543         if (parameter instanceof EnhancedParameterWrapper) {
544             EnhancedParameterWrapper enhancedParameter = (EnhancedParameterWrapper) parameter;
545             if (enhancedParameter.getTypeJavadocUrl() != null) {
546                 URI javadocUrl = enhancedParameter.getTypeJavadocUrl();
547                 // optionally check if link is valid
548                 if (javadocUrl.isAbsolute()
549                         || disableInternalJavadocLinkValidation
550                         || JavadocLinkGenerator.isLinkValid(javadocUrl, reportOutputDirectory.toPath())) {
551                     return format(
552                             "pluginxdoc.mojodescriptor.parameter.type_link",
553                             new Object[] {escapeXml(typeValue), enhancedParameter.getTypeJavadocUrl()});
554                 }
555             }
556         }
557         return escapeXml(typeValue);
558     }
559 
560     private boolean addUl(XMLWriter w, boolean addedUl, String content) {
561         if (content != null && !content.isEmpty()) {
562             return addUl(w, addedUl);
563         }
564         return addedUl;
565     }
566 
567     private boolean addUl(XMLWriter w, boolean addedUl) {
568         if (!addedUl) {
569             w.startElement("ul");
570             addedUl = true;
571         }
572         return addedUl;
573     }
574 
575     private String getPropertyFromExpression(String expression) {
576         if ((expression != null && !expression.isEmpty())
577                 && expression.startsWith("${")
578                 && expression.endsWith("}")
579                 && !expression.substring(2).contains("${")) {
580             // expression="${xxx}" -> property="xxx"
581             return expression.substring(2, expression.length() - 1);
582         }
583         // no property can be extracted
584         return null;
585     }
586 
587     /**
588      * @param param not null
589      * @param value could be null
590      * @param w     not null
591      */
592     private void writeDetail(String param, String value, XMLWriter w) {
593         if (value != null && !value.isEmpty()) {
594             w.startElement("li");
595             w.writeMarkup(format("pluginxdoc.detail", new String[] {param, value}));
596             w.endElement(); // li
597         }
598     }
599 
600     /**
601      * @param parameterList  not null
602      * @param w              not null
603      */
604     private void writeParameterSummary(List<Parameter> parameterList, XMLWriter w, String goal) {
605         List<Parameter> requiredParams = getParametersByRequired(true, parameterList);
606         if (!requiredParams.isEmpty()) {
607             writeParameterList(getString("pluginxdoc.mojodescriptor.requiredParameters"), requiredParams, w, goal);
608         }
609 
610         List<Parameter> optionalParams = getParametersByRequired(false, parameterList);
611         if (!optionalParams.isEmpty()) {
612             writeParameterList(getString("pluginxdoc.mojodescriptor.optionalParameters"), optionalParams, w, goal);
613         }
614     }
615 
616     /**
617      * @param title          not null
618      * @param parameterList  not null
619      * @param w              not null
620      */
621     private void writeParameterList(String title, List<Parameter> parameterList, XMLWriter w, String goal) {
622         w.startElement("subsection");
623         w.addAttribute("name", title);
624 
625         w.startElement("table");
626         w.addAttribute("border", "0");
627         w.addAttribute("class", "bodyTable");
628 
629         w.startElement("tr");
630         w.startElement("th");
631         w.writeText(getString("pluginxdoc.mojodescriptor.parameter.name"));
632         w.endElement(); // th
633         w.startElement("th");
634         w.writeText(getString("pluginxdoc.mojodescriptor.parameter.type"));
635         w.endElement(); // th
636         w.startElement("th");
637         w.writeText(getString("pluginxdoc.mojodescriptor.parameter.since"));
638         w.endElement(); // th
639         w.startElement("th");
640         w.writeText(getString("pluginxdoc.mojodescriptor.parameter.description"));
641         w.endElement(); // th
642         w.endElement(); // tr
643 
644         for (Parameter parameter : parameterList) {
645             w.startElement("tr");
646 
647             // name
648             w.startElement("td");
649             w.writeMarkup(format("pluginxdoc.mojodescriptor.parameter.name_link", parameter.getName()));
650             w.endElement(); // td
651 
652             // type
653             w.startElement("td");
654             w.writeMarkup("<code>" + getLinkedType(parameter, true) + "</code>");
655             w.endElement(); // td
656 
657             // since
658             w.startElement("td");
659             if (StringUtils.isNotEmpty(parameter.getSince())) {
660                 w.writeMarkup("<code>" + parameter.getSince() + "</code>");
661             } else {
662                 w.writeMarkup("<code>-</code>");
663             }
664             w.endElement(); // td
665 
666             // description
667             w.startElement("td");
668             String description;
669             String context = "Parameter " + parameter.getName() + " in goal " + goal;
670             if (StringUtils.isNotEmpty(parameter.getDeprecated())) {
671                 String deprecated = getXhtmlWithValidatedLinks(parameter.getDescription(), context);
672                 description = format("pluginxdoc.mojodescriptor.parameter.deprecated", deprecated);
673             } else if (StringUtils.isNotEmpty(parameter.getDescription())) {
674                 description = getXhtmlWithValidatedLinks(parameter.getDescription(), context);
675             } else {
676                 description = getString("pluginxdoc.nodescription");
677             }
678             w.writeMarkup(description + "<br/>");
679 
680             if (StringUtils.isNotEmpty(parameter.getDefaultValue())) {
681                 w.writeMarkup(format(
682                         "pluginxdoc.mojodescriptor.parameter.defaultValue", escapeXml(parameter.getDefaultValue())));
683                 w.writeMarkup("<br/>");
684             }
685 
686             String property = getPropertyFromExpression(parameter.getExpression());
687             if (property != null) {
688                 w.writeMarkup(format("pluginxdoc.mojodescriptor.parameter.property.description", property));
689                 w.writeMarkup("<br/>");
690             }
691 
692             if (StringUtils.isNotEmpty(parameter.getAlias())) {
693                 w.writeMarkup(format(
694                         "pluginxdoc.mojodescriptor.parameter.alias.description", escapeXml(parameter.getAlias())));
695             }
696 
697             w.endElement(); // td
698             w.endElement(); // tr
699         }
700 
701         w.endElement(); // table
702         w.endElement(); // section
703     }
704 
705     /**
706      * @param required      <code>true</code> for required parameters, <code>false</code> otherwise.
707      * @param parameterList not null
708      * @return list of parameters depending the value of <code>required</code>
709      */
710     private List<Parameter> getParametersByRequired(boolean required, List<Parameter> parameterList) {
711         List<Parameter> list = new ArrayList<>();
712 
713         for (Parameter parameter : parameterList) {
714             if (parameter.isRequired() == required) {
715                 list.add(parameter);
716             }
717         }
718 
719         return list;
720     }
721 
722     /**
723      * Gets the resource bundle for the <code>locale</code> instance variable.
724      *
725      * @return The resource bundle for the <code>locale</code> instance variable.
726      */
727     private ResourceBundle getBundle() {
728         return ResourceBundle.getBundle("pluginxdoc", locale, getClass().getClassLoader());
729     }
730 
731     /**
732      * @param key not null
733      * @return Localized, text identified by <code>key</code>.
734      * @see #getBundle()
735      */
736     private String getString(String key) {
737         return getBundle().getString(key);
738     }
739 
740     /**
741      * Convenience method.
742      *
743      * @param key  not null
744      * @param arg1 not null
745      * @return Localized, formatted text identified by <code>key</code>.
746      * @see #format(String, Object[])
747      */
748     private String format(String key, Object arg1) {
749         return format(key, new Object[] {arg1});
750     }
751 
752     /**
753      * Looks up the value for <code>key</code> in the <code>ResourceBundle</code>,
754      * then formats that value for the specified <code>Locale</code> using <code>args</code>.
755      *
756      * @param key  not null
757      * @param args not null
758      * @return Localized, formatted text identified by <code>key</code>.
759      */
760     private String format(String key, Object[] args) {
761         String pattern = getString(key);
762         // we don't need quoting so spare us the confusion in the resource bundle to double them up in some keys
763         pattern = StringUtils.replace(pattern, "'", "''");
764 
765         MessageFormat messageFormat = new MessageFormat("");
766         messageFormat.setLocale(locale);
767         messageFormat.applyPattern(pattern);
768 
769         return messageFormat.format(args);
770     }
771 
772     /**
773      * @param text the string to escape
774      * @return A string escaped with XML entities
775      */
776     private String escapeXml(String text) {
777         if (text != null) {
778             text = text.replace("&", "&amp;");
779             text = text.replace("<", "&lt;");
780             text = text.replace(">", "&gt;");
781             text = text.replace("\"", "&quot;");
782             text = text.replace("\'", "&apos;");
783         }
784         return text;
785     }
786 
787     String getXhtmlWithValidatedLinks(String xhtmlText, String context) {
788         if (disableInternalJavadocLinkValidation) {
789             return xhtmlText;
790         }
791         StringBuffer sanitizedXhtmlText = new StringBuffer();
792         // find all links which are not absolute
793         Matcher matcher = HTML_LINK_PATTERN.matcher(xhtmlText);
794         while (matcher.find()) {
795             URI link;
796             try {
797                 link = new URI(matcher.group(1));
798                 if (!link.isAbsolute() && !JavadocLinkGenerator.isLinkValid(link, reportOutputDirectory.toPath())) {
799                     matcher.appendReplacement(sanitizedXhtmlText, matcher.group(2));
800                     LOG.debug("Removed invalid link {} in {}", link, context);
801                 } else {
802                     matcher.appendReplacement(sanitizedXhtmlText, matcher.group(0));
803                 }
804             } catch (URISyntaxException e) {
805                 LOG.warn("Invalid URI {} found in {}. Cannot validate, leave untouched", matcher.group(1), context);
806                 matcher.appendReplacement(sanitizedXhtmlText, matcher.group(0));
807             }
808         }
809         matcher.appendTail(sanitizedXhtmlText);
810         return sanitizedXhtmlText.toString();
811     }
812 }