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