View Javadoc
1   package org.apache.maven.plugin.plugin.report;
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.Reader;
25  import java.nio.file.Files;
26  import java.util.ArrayList;
27  import java.util.Iterator;
28  import java.util.List;
29  import java.util.Locale;
30  import java.util.Map;
31  import java.util.Optional;
32  import java.util.ResourceBundle;
33  
34  import org.apache.maven.doxia.sink.Sink;
35  import org.apache.maven.model.Plugin;
36  import org.apache.maven.model.Prerequisites;
37  import org.apache.maven.plugin.descriptor.MojoDescriptor;
38  import org.apache.maven.plugin.descriptor.PluginDescriptor;
39  import org.apache.maven.plugin.descriptor.PluginDescriptorBuilder;
40  import org.apache.maven.plugins.annotations.Component;
41  import org.apache.maven.plugins.annotations.Execute;
42  import org.apache.maven.plugins.annotations.LifecyclePhase;
43  import org.apache.maven.plugins.annotations.Mojo;
44  import org.apache.maven.plugins.annotations.Parameter;
45  import org.apache.maven.plugins.plugin.descriptor.EnhancedPluginDescriptorBuilder;
46  import org.apache.maven.project.MavenProject;
47  import org.apache.maven.reporting.AbstractMavenReport;
48  import org.apache.maven.reporting.AbstractMavenReportRenderer;
49  import org.apache.maven.reporting.MavenReportException;
50  import org.apache.maven.rtinfo.RuntimeInformation;
51  import org.apache.maven.tools.plugin.DefaultPluginToolsRequest;
52  import org.apache.maven.tools.plugin.PluginToolsRequest;
53  import org.apache.maven.tools.plugin.generator.GeneratorException;
54  import org.apache.maven.tools.plugin.generator.GeneratorUtils;
55  import org.apache.maven.tools.plugin.generator.PluginXdocGenerator;
56  import org.apache.maven.tools.plugin.util.PluginUtils;
57  import org.codehaus.plexus.configuration.PlexusConfigurationException;
58  import org.codehaus.plexus.util.StringUtils;
59  import org.codehaus.plexus.util.xml.XmlStreamReader;
60  import org.codehaus.plexus.util.xml.Xpp3Dom;
61  
62  /**
63   * Generates the Plugin's documentation report: <code>plugin-info.html</code> plugin overview page,
64   * and one <code><i>goal</i>-mojo.html</code> per goal.
65   * Relies on one output file from <a href="../maven-plugin-plugin/descriptor-mojo.html">plugin:descriptor</a>.
66   *
67   * @author <a href="snicoll@apache.org">Stephane Nicoll</a>
68   * @author <a href="mailto:vincent.siveton@gmail.com">Vincent Siveton</a>
69   * @since 3.7.0
70   */
71  @Mojo( name = "report", threadSafe = true )
72  @Execute( phase = LifecyclePhase.PROCESS_CLASSES )
73  public class PluginReport
74      extends AbstractMavenReport
75  {
76      /**
77       * Report output directory for mojos' documentation.
78       *
79       * @since 3.7.0
80       */
81      @Parameter( defaultValue = "${project.build.directory}/generated-site/xdoc" )
82      private File outputDirectory;
83  
84      /**
85       * Set this to "true" to skip generating the report.
86       *
87       * @since 3.7.0
88       */
89      @Parameter( defaultValue = "false", property = "maven.plugin.report.skip" )
90      private boolean skip;
91  
92      /**
93       * Set this to "true" to generate the usage section for "plugin-info.html" with
94       * {@code <extensions>true</extensions>}.
95       *
96       * @since 3.7.0
97       */
98      @Parameter( defaultValue = "false", property = "maven.plugin.report.hasExtensionsToLoad" )
99      private boolean hasExtensionsToLoad;
100 
101     /**
102      * The Plugin requirements history list.
103      * <p>
104      * Can be specified as list of <code>requirementsHistory</code>:
105      *
106      * <pre>
107      * &lt;requirementsHistories&gt;
108      *   &lt;requirementsHistory&gt;
109      *     &lt;version&gt;plugin version&lt;/version&gt;
110      *     &lt;maven&gt;maven version&lt;/maven&gt;
111      *     &lt;jdk&gt;jdk version&lt;/jdk&gt;
112      *   &lt;/requirementsHistory&gt;
113      * &lt;/requirementsHistories&gt;
114      * </pre>
115      *
116      * @since 3.7.0
117      */
118     @Parameter
119     private List<RequirementsHistory> requirementsHistories = new ArrayList<>();
120 
121     @Component
122     private RuntimeInformation rtInfo;
123 
124     /**
125      * Path to enhanced plugin descriptor to generate the report from (must contain some XHTML values)
126      *
127      * @since 3.7.0
128      */
129     @Parameter( defaultValue = "${project.build.directory}/plugin-enhanced.xml", required = true,
130                 readonly = true )
131     private File enhancedPluginXmlFile;
132 
133     /**
134      * In case the internal javadoc site has not been generated when running this report goal
135      * (e.g. when using an aggregator javadoc report) link validation needs to be disabled by setting
136      * this value to {@code true}.
137      * This might have the drawback that some links being generated in the report might be broken
138      * in case not all parameter types and javadoc link references are resolvable through the sites being given to
139      * goal {@code plugin:descriptor}.
140      * 
141      * @since 3.7.0
142      */
143     @Parameter( property = "maven.plugin.report.disableInternalJavadocLinkValidation" )
144     private boolean disableInternalJavadocLinkValidation;
145 
146     /**
147      * {@inheritDoc}
148      */
149     @Override
150     protected String getOutputDirectory()
151     {
152         // PLUGIN-191: output directory of plugin.html, not *-mojo.xml
153         return project.getReporting().getOutputDirectory();
154     }
155 
156     /**
157      * {@inheritDoc}
158      */
159     @Override
160     public boolean canGenerateReport()
161     {
162         return enhancedPluginXmlFile != null && enhancedPluginXmlFile.isFile() && enhancedPluginXmlFile.canRead();
163     }
164 
165     /**
166      * {@inheritDoc}
167      */
168     @Override
169     protected void executeReport( Locale locale )
170         throws MavenReportException
171     {
172         if ( skip  )
173         {
174             getLog().info( "Maven Plugin Plugin Report generation skipped." );
175             return;
176         }
177 
178         PluginDescriptor pluginDescriptor = extractPluginDescriptor();
179 
180         // Generate the mojos' documentation
181         generateMojosDocumentation( pluginDescriptor, locale );
182 
183         // Write the overview
184         PluginOverviewRenderer r =
185             new PluginOverviewRenderer( getProject(), requirementsHistories, getSink(),
186                                         pluginDescriptor, locale, hasExtensionsToLoad );
187         r.render();
188     }
189 
190     private PluginDescriptor extractPluginDescriptor()
191         throws MavenReportException
192     {
193         PluginDescriptorBuilder builder = new EnhancedPluginDescriptorBuilder( rtInfo );
194 
195         try ( Reader input = new XmlStreamReader( Files.newInputStream( enhancedPluginXmlFile.toPath() ) ) )
196         {
197             return builder.build( input );
198         }
199         catch ( IOException | PlexusConfigurationException e )
200         {
201             throw new MavenReportException( "Error extracting plugin descriptor from " + enhancedPluginXmlFile, e );
202         }
203 
204     }
205 
206     /**
207      * {@inheritDoc}
208      */
209     @Override
210     public String getDescription( Locale locale )
211     {
212         return getBundle( locale ).getString( "report.plugin.description" );
213     }
214 
215     /**
216      * {@inheritDoc}
217      */
218     @Override
219     public String getName( Locale locale )
220     {
221         return getBundle( locale ).getString( "report.plugin.name" );
222     }
223 
224     /**
225      * {@inheritDoc}
226      */
227     @Override
228     public String getOutputName()
229     {
230         return "plugin-info";
231     }
232 
233     /**
234      * Generate the mojos documentation, as xdoc files.
235      *
236      * @param pluginDescriptor not null
237      * @param locale           not null
238      * @throws MavenReportException if any
239      */
240     private void generateMojosDocumentation( PluginDescriptor pluginDescriptor, Locale locale )
241         throws MavenReportException
242     {
243         try
244         {
245             File outputDir = outputDirectory;
246             outputDir.mkdirs();
247 
248             PluginXdocGenerator generator = new PluginXdocGenerator( getProject(), locale, getReportOutputDirectory(),
249                                                                      disableInternalJavadocLinkValidation );
250             PluginToolsRequest pluginToolsRequest = new DefaultPluginToolsRequest( getProject(), pluginDescriptor );
251             generator.execute( outputDir, pluginToolsRequest );
252         }
253         catch ( GeneratorException e )
254         {
255             throw new MavenReportException( "Error writing plugin documentation", e );
256         }
257 
258     }
259 
260     /**
261      * @param locale not null
262      * @return the bundle for this report
263      */
264     protected static ResourceBundle getBundle( Locale locale )
265     {
266         return ResourceBundle.getBundle( "plugin-report", locale, PluginReport.class.getClassLoader() );
267     }
268 
269     /**
270      * Generates an overview page with the list of goals
271      * and a link to the goal's page.
272      */
273     static class PluginOverviewRenderer
274         extends AbstractMavenReportRenderer
275     {
276         private final MavenProject project;
277 
278         private final List<RequirementsHistory> requirementsHistories;
279 
280         private final PluginDescriptor pluginDescriptor;
281 
282         private final Locale locale;
283 
284         private final boolean hasExtensionsToLoad;
285 
286         /**
287          * @param project               not null
288          * @param requirementsHistories not null
289          * @param sink                  not null
290          * @param pluginDescriptor      not null
291          * @param locale                not null
292          */
293         PluginOverviewRenderer( MavenProject project,
294                                 List<RequirementsHistory> requirementsHistories, Sink sink,
295                                 PluginDescriptor pluginDescriptor, Locale locale, boolean hasExtensionsToLoad )
296         {
297             super( sink );
298 
299             this.project = project;
300 
301             this.requirementsHistories = requirementsHistories;
302 
303             this.pluginDescriptor = pluginDescriptor;
304 
305             this.locale = locale;
306 
307             this.hasExtensionsToLoad = hasExtensionsToLoad;
308         }
309 
310         /**
311          * {@inheritDoc}
312          */
313         @Override
314         public String getTitle()
315         {
316             return getBundle( locale ).getString( "report.plugin.title" );
317         }
318 
319         /**
320          * {@inheritDoc}
321          */
322         @Override
323         public void renderBody()
324         {
325             startSection( getTitle() );
326 
327             if ( !( pluginDescriptor.getMojos() != null && pluginDescriptor.getMojos().size() > 0 ) )
328             {
329                 paragraph( getBundle( locale ).getString( "report.plugin.goals.nogoal" ) );
330                 endSection();
331                 return;
332             }
333 
334             paragraph( getBundle( locale ).getString( "report.plugin.goals.intro" ) );
335 
336             boolean hasMavenReport = false;
337             for ( Iterator<MojoDescriptor> i = pluginDescriptor.getMojos().iterator(); i.hasNext(); )
338             {
339                 MojoDescriptor mojo = i.next();
340 
341                 if ( GeneratorUtils.isMavenReport( mojo.getImplementation(), project ) )
342                 {
343                     hasMavenReport = true;
344                 }
345             }
346 
347             startTable();
348 
349             String goalColumnName = getBundle( locale ).getString( "report.plugin.goals.column.goal" );
350             String isMavenReport = getBundle( locale ).getString( "report.plugin.goals.column.isMavenReport" );
351             String descriptionColumnName = getBundle( locale ).getString( "report.plugin.goals.column.description" );
352             if ( hasMavenReport )
353             {
354                 tableHeader( new String[] {goalColumnName, isMavenReport, descriptionColumnName} );
355             }
356             else
357             {
358                 tableHeader( new String[] {goalColumnName, descriptionColumnName} );
359             }
360 
361             List<MojoDescriptor> mojos = new ArrayList<>();
362             mojos.addAll( pluginDescriptor.getMojos() );
363             PluginUtils.sortMojos( mojos );
364             for ( MojoDescriptor mojo : mojos )
365             {
366                 String goalName = mojo.getFullGoalName();
367 
368                 /*
369                  * Added ./ to define a relative path
370                  * @see AbstractMavenReportRenderer#getValidHref(java.lang.String)
371                  */
372                 String goalDocumentationLink = "./" + mojo.getGoal() + "-mojo.html";
373 
374                 String description;
375                 if ( StringUtils.isNotEmpty( mojo.getDeprecated() ) )
376                 {
377                     description =
378                         "<strong>" + getBundle( locale ).getString( "report.plugin.goal.deprecated" ) + "</strong> "
379                             + mojo.getDeprecated();
380                 }
381                 else if ( StringUtils.isNotEmpty( mojo.getDescription() ) )
382                 {
383                     description = mojo.getDescription();
384                 }
385                 else
386                 {
387                     description = getBundle( locale ).getString( "report.plugin.goal.nodescription" );
388                 }
389 
390                 sink.tableRow();
391                 tableCell( createLinkPatternedText( goalName, goalDocumentationLink ) );
392                 if ( hasMavenReport )
393                 {
394                     if ( GeneratorUtils.isMavenReport( mojo.getImplementation(), project ) )
395                     {
396                         sink.tableCell();
397                         sink.text( getBundle( locale ).getString( "report.plugin.isReport" ) );
398                         sink.tableCell_();
399                     }
400                     else
401                     {
402                         sink.tableCell();
403                         sink.text( getBundle( locale ).getString( "report.plugin.isNotReport" ) );
404                         sink.tableCell_();
405                     }
406                 }
407                 tableCell( description, true );
408                 sink.tableRow_();
409             }
410 
411             endTable();
412 
413             startSection( getBundle( locale ).getString( "report.plugin.systemrequirements" ) );
414 
415             paragraph( getBundle( locale ).getString( "report.plugin.systemrequirements.intro" ) );
416 
417             startTable();
418 
419             String maven = discoverMavenRequirement( project );
420             sink.tableRow();
421             tableCell( getBundle( locale ).getString( "report.plugin.systemrequirements.maven" ) );
422             tableCell( ( maven != null
423                 ? maven
424                 : getBundle( locale ).getString( "report.plugin.systemrequirements.nominimum" ) ) );
425             sink.tableRow_();
426 
427             String jdk = discoverJdkRequirement( project );
428             sink.tableRow();
429             tableCell( getBundle( locale ).getString( "report.plugin.systemrequirements.jdk" ) );
430             tableCell(
431                 ( jdk != null ? jdk : getBundle( locale ).getString( "report.plugin.systemrequirements.nominimum" ) ) );
432             sink.tableRow_();
433 
434             endTable();
435 
436             endSection();
437 
438             renderRequirementsHistories();
439 
440             renderUsageSection( hasMavenReport );
441 
442             endSection();
443         }
444 
445         private void renderRequirementsHistories()
446         {
447             if ( requirementsHistories.isEmpty() )
448             {
449                 return;
450             }
451 
452             startSection( getBundle( locale ).getString( "report.plugin.systemrequirements.history" ) );
453             paragraph( getBundle( locale ).getString( "report.plugin.systemrequirements.history.intro" ) );
454 
455             startTable();
456             tableHeader( new String[] {
457                 getBundle( locale ).getString( "report.plugin.systemrequirements.history.version" ),
458                 getBundle( locale ).getString( "report.plugin.systemrequirements.history.maven" ),
459                 getBundle( locale ).getString( "report.plugin.systemrequirements.history.jdk" )
460             } );
461 
462             requirementsHistories.forEach(
463                 requirementsHistory ->
464                 {
465                     sink.tableRow();
466                     tableCell( requirementsHistory.getVersion() );
467                     tableCell( requirementsHistory.getMaven() );
468                     tableCell( requirementsHistory.getJdk() );
469                     sink.tableRow_();
470                 } );
471             endTable();
472 
473             endSection();
474         }
475 
476         /**
477          * Render the section about the usage of the plugin.
478          *
479          * @param hasMavenReport If the plugin has a report or not
480          */
481         private void renderUsageSection( boolean hasMavenReport )
482         {
483             startSection( getBundle( locale ).getString( "report.plugin.usage" ) );
484 
485             // Configuration
486             sink.paragraph();
487             text( getBundle( locale ).getString( "report.plugin.usage.intro" ) );
488             sink.paragraph_();
489 
490             StringBuilder sb = new StringBuilder();
491             sb.append( "<project>" ).append( '\n' );
492             sb.append( "  ..." ).append( '\n' );
493             sb.append( "  <build>" ).append( '\n' );
494             sb.append(
495                 "    <!-- " + getBundle( locale ).getString( "report.plugin.usage.pluginManagement" ) + " -->" ).append(
496                 '\n' );
497             sb.append( "    <pluginManagement>" ).append( '\n' );
498             sb.append( "      <plugins>" ).append( '\n' );
499             sb.append( "        <plugin>" ).append( '\n' );
500             sb.append( "          <groupId>" ).append( pluginDescriptor.getGroupId() ).append( "</groupId>" ).append(
501                 '\n' );
502             sb.append( "          <artifactId>" ).append( pluginDescriptor.getArtifactId() ).append(
503                 "</artifactId>" ).append( '\n' );
504             sb.append( "          <version>" ).append( pluginDescriptor.getVersion() ).append( "</version>" ).append(
505                 '\n' );
506             if ( hasExtensionsToLoad )
507             {
508                 sb.append( "          <extensions>true</extensions>" ).append(
509                     '\n' );
510             }
511             sb.append( "        </plugin>" ).append( '\n' );
512             sb.append( "        ..." ).append( '\n' );
513             sb.append( "      </plugins>" ).append( '\n' );
514             sb.append( "    </pluginManagement>" ).append( '\n' );
515             sb.append( "    <!-- " + getBundle( locale ).getString( "report.plugin.usage.plugins" ) + " -->" ).append(
516                 '\n' );
517             sb.append( "    <plugins>" ).append( '\n' );
518             sb.append( "      <plugin>" ).append( '\n' );
519             sb.append( "        <groupId>" ).append( pluginDescriptor.getGroupId() ).append( "</groupId>" ).append(
520                 '\n' );
521             sb.append( "        <artifactId>" ).append( pluginDescriptor.getArtifactId() ).append(
522                 "</artifactId>" ).append( '\n' );
523             sb.append( "      </plugin>" ).append( '\n' );
524             sb.append( "      ..." ).append( '\n' );
525             sb.append( "    </plugins>" ).append( '\n' );
526             sb.append( "  </build>" ).append( '\n' );
527 
528             if ( hasMavenReport )
529             {
530                 sb.append( "  ..." ).append( '\n' );
531                 sb.append(
532                     "  <!-- " + getBundle( locale ).getString( "report.plugin.usage.reporting" ) + " -->" ).append(
533                     '\n' );
534                 sb.append( "  <reporting>" ).append( '\n' );
535                 sb.append( "    <plugins>" ).append( '\n' );
536                 sb.append( "      <plugin>" ).append( '\n' );
537                 sb.append( "        <groupId>" ).append( pluginDescriptor.getGroupId() ).append( "</groupId>" ).append(
538                     '\n' );
539                 sb.append( "        <artifactId>" ).append( pluginDescriptor.getArtifactId() ).append(
540                     "</artifactId>" ).append( '\n' );
541                 sb.append( "        <version>" ).append( pluginDescriptor.getVersion() ).append( "</version>" ).append(
542                     '\n' );
543                 sb.append( "      </plugin>" ).append( '\n' );
544                 sb.append( "      ..." ).append( '\n' );
545                 sb.append( "    </plugins>" ).append( '\n' );
546                 sb.append( "  </reporting>" ).append( '\n' );
547             }
548 
549             sb.append( "  ..." ).append( '\n' );
550             sb.append( "</project>" ).append( '\n' );
551 
552             verbatimText( sb.toString() );
553 
554             sink.paragraph();
555             linkPatternedText( getBundle( locale ).getString( "report.plugin.configuration.end" ) );
556             sink.paragraph_();
557 
558             endSection();
559         }
560 
561         /**
562          * Try to lookup on the Maven prerequisites property.
563          *
564          * @param project      not null
565          * @return the Maven version or null if not specified
566          */
567         private static String discoverMavenRequirement( MavenProject project )
568         {
569             return Optional.ofNullable( project.getPrerequisites() )
570                 .map( Prerequisites::getMaven )
571                 .orElse( null );
572         }
573 
574         /**
575          * <ol>
576          * <li>use configured jdk requirement</li>
577          * <li>use <code>target</code> configuration of <code>org.apache.maven.plugins:maven-compiler-plugin</code></li>
578          * <li>use <code>target</code> configuration of <code>org.apache.maven.plugins:maven-compiler-plugin</code> in
579          * <code>pluginManagement</code></li>
580          * <li>use <code>maven.compiler.target</code> property</li>
581          * </ol>
582          *
583          * @param project      not null
584          * @return the JDK version
585          */
586         private static String discoverJdkRequirement( MavenProject project )
587         {
588 
589             Plugin compiler = getCompilerPlugin( project.getBuild().getPluginsAsMap() );
590             if ( compiler == null )
591             {
592                 compiler = getCompilerPlugin( project.getPluginManagement().getPluginsAsMap() );
593             }
594 
595             String jdk = getPluginParameter( compiler, "release" );
596             if ( jdk != null )
597             {
598                 return jdk;
599             }
600 
601             jdk = project.getProperties().getProperty( "maven.compiler.release" );
602             if ( jdk != null )
603             {
604                 return jdk;
605             }
606 
607             jdk = getPluginParameter( compiler, "target" );
608             if ( jdk != null )
609             {
610                 return jdk;
611             }
612 
613             // default value
614             jdk = project.getProperties().getProperty( "maven.compiler.target" );
615             if ( jdk != null )
616             {
617                 return jdk;
618             }
619 
620             String version = ( compiler == null ) ? null : compiler.getVersion();
621 
622             if ( version != null )
623             {
624                 return "Default target for maven-compiler-plugin version " + version;
625             }
626 
627             return null;
628         }
629 
630         private static Plugin getCompilerPlugin( Map<String, Plugin> pluginsAsMap )
631         {
632             return pluginsAsMap.get( "org.apache.maven.plugins:maven-compiler-plugin" );
633         }
634 
635         private static String getPluginParameter( Plugin plugin, String parameter )
636         {
637             if ( plugin != null )
638             {
639                 Xpp3Dom pluginConf = (Xpp3Dom) plugin.getConfiguration();
640 
641                 if ( pluginConf != null )
642                 {
643                     Xpp3Dom target = pluginConf.getChild( parameter );
644 
645                     if ( target != null )
646                     {
647                         return target.getValue();
648                     }
649                 }
650             }
651 
652             return null;
653         }
654     }
655 }