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