View Javadoc
1   package org.apache.maven.plugins.pdf;
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.io.StringReader;
26  import java.io.StringWriter;
27  import java.io.Writer;
28  import java.util.ArrayList;
29  import java.util.HashMap;
30  import java.util.List;
31  import java.util.Locale;
32  import java.util.Map;
33  
34  import org.apache.commons.io.input.XmlStreamReader;
35  import org.apache.maven.artifact.Artifact;
36  import org.apache.maven.artifact.repository.ArtifactRepository;
37  import org.apache.maven.doxia.Doxia;
38  import org.apache.maven.doxia.docrenderer.AbstractDocumentRenderer;
39  import org.apache.maven.doxia.docrenderer.DocumentRenderer;
40  import org.apache.maven.doxia.docrenderer.DocumentRendererContext;
41  import org.apache.maven.doxia.docrenderer.DocumentRendererException;
42  import org.apache.maven.doxia.docrenderer.pdf.PdfRenderer;
43  import org.apache.maven.doxia.document.DocumentMeta;
44  import org.apache.maven.doxia.document.DocumentModel;
45  import org.apache.maven.doxia.document.DocumentTOC;
46  import org.apache.maven.doxia.document.DocumentTOCItem;
47  import org.apache.maven.doxia.document.io.xpp3.DocumentXpp3Writer;
48  import org.apache.maven.doxia.index.IndexEntry;
49  import org.apache.maven.doxia.index.IndexingSink;
50  import org.apache.maven.doxia.module.xdoc.XdocSink;
51  import org.apache.maven.doxia.parser.ParseException;
52  import org.apache.maven.doxia.parser.manager.ParserNotFoundException;
53  import org.apache.maven.doxia.sink.impl.SinkAdapter;
54  import org.apache.maven.doxia.site.decoration.DecorationModel;
55  import org.apache.maven.doxia.site.decoration.io.xpp3.DecorationXpp3Reader;
56  import org.apache.maven.doxia.siterenderer.Renderer;
57  import org.apache.maven.doxia.siterenderer.RendererException;
58  import org.apache.maven.doxia.siterenderer.SiteRenderingContext;
59  import org.apache.maven.doxia.tools.SiteTool;
60  import org.apache.maven.doxia.tools.SiteToolException;
61  import org.apache.maven.execution.MavenSession;
62  import org.apache.maven.model.ReportPlugin;
63  import org.apache.maven.model.Reporting;
64  import org.apache.maven.plugin.MojoExecutionException;
65  import org.apache.maven.plugins.annotations.Component;
66  import org.apache.maven.plugins.annotations.Mojo;
67  import org.apache.maven.plugins.annotations.Parameter;
68  import org.apache.maven.plugins.annotations.ResolutionScope;
69  import org.apache.maven.project.MavenProject;
70  import org.apache.maven.reporting.MavenReport;
71  import org.apache.maven.reporting.MavenReportException;
72  import org.apache.maven.reporting.exec.MavenReportExecution;
73  import org.apache.maven.reporting.exec.MavenReportExecutor;
74  import org.apache.maven.reporting.exec.MavenReportExecutorRequest;
75  import org.apache.maven.settings.Settings;
76  import org.codehaus.plexus.PlexusConstants;
77  import org.codehaus.plexus.PlexusContainer;
78  import org.codehaus.plexus.component.repository.exception.ComponentLookupException;
79  import org.codehaus.plexus.context.Context;
80  import org.codehaus.plexus.context.ContextException;
81  import org.codehaus.plexus.i18n.I18N;
82  import org.codehaus.plexus.personality.plexus.lifecycle.phase.Contextualizable;
83  import org.codehaus.plexus.util.FileUtils;
84  import org.codehaus.plexus.util.IOUtil;
85  import org.codehaus.plexus.util.PathTool;
86  import org.codehaus.plexus.util.ReaderFactory;
87  import org.codehaus.plexus.util.StringUtils;
88  import org.codehaus.plexus.util.WriterFactory;
89  import org.codehaus.plexus.util.xml.pull.XmlPullParserException;
90  
91  /**
92   * Generates a PDF document for a project documentation usually published as web site (with maven-site-plugin).
93   *
94   * @author ltheussl
95   */
96  @Mojo( name = "pdf", requiresDependencyResolution = ResolutionScope.TEST, threadSafe = true )
97  public class PdfMojo
98      extends AbstractPdfMojo implements Contextualizable
99  {
100 
101     /**
102      * The vm line separator
103      */
104     private static final String EOL = System.getProperty( "line.separator" );
105 
106     /**
107      * FO Document Renderer.
108      */
109     @Component( hint = "fo" )
110     private PdfRenderer foRenderer;
111 
112     /**
113      * Internationalization.
114      */
115     @Component
116     private I18N i18n;
117 
118     /**
119      * IText Document Renderer.
120      */
121     @Component( hint = "itext" )
122     private PdfRenderer itextRenderer;
123 
124     /**
125      * A comma separated list of locales supported by Maven.
126      * The first valid token will be the default Locale for this instance of the Java Virtual Machine.
127      */
128     @Parameter( property = "locales" )
129     private String locales;
130 
131     /**
132      * Site renderer.
133      */
134     @Component
135     private Renderer siteRenderer;
136 
137     /**
138      * SiteTool.
139      */
140     @Component
141     private SiteTool siteTool;
142 
143     /**
144      * Doxia.
145      *
146      * @since 1.1
147      */
148     @Component
149     private Doxia doxia;
150 
151     /**
152      * The Maven Project Object.
153      */
154     @Parameter( defaultValue = "${project}", readonly = true, required = true )
155     protected MavenProject project;
156 
157     /**
158      * The Maven Settings.
159      *
160      * @since 1.1
161      */
162     @Parameter( defaultValue = "${settings}", readonly = true, required = true )
163     private Settings settings;
164 
165     /**
166      * The current build session instance.
167      *
168      * @since 1.1
169      */
170     @Parameter( defaultValue = "${session}", readonly = true, required = true )
171     private MavenSession session;
172 
173     /**
174      * Directory containing source for apt, fml and xdoc docs.
175      */
176     @Parameter( defaultValue = "${basedir}/src/site", required = true )
177     private File siteDirectory;
178 
179     /**
180      * Directory containing generated sources for apt, fml and xdoc docs.
181      *
182      * @since 1.1
183      */
184     @Parameter( defaultValue = "${project.build.directory}/generated-site", required = true )
185     private File generatedSiteDirectory;
186 
187     /**
188      * Output directory where PDF files should be created.
189      */
190     @Parameter( defaultValue = "${project.build.directory}/pdf", required = true )
191     private File outputDirectory;
192 
193     /**
194      * Working directory for working files like temp files/resources.
195      */
196     @Parameter( defaultValue = "${project.build.directory}/pdf", required = true )
197     private File workingDirectory;
198 
199     /**
200      * File that contains the DocumentModel of the PDF to generate.
201      */
202     @Parameter( defaultValue = "src/site/pdf.xml" )
203     private File docDescriptor;
204 
205     /**
206      * Identifies the framework to use for pdf generation: either "fo" (default) or "itext".
207      */
208     @Parameter( property = "implementation", defaultValue = "fo", required = true )
209     private String implementation;
210 
211     /**
212      * The local repository.
213      */
214     @Parameter( defaultValue = "${localRepository}", required = true, readonly = true )
215     private ArtifactRepository localRepository;
216 
217     /**
218      * The remote repositories where artifacts are located.
219      *
220      * @since 1.1
221      */
222     @Parameter( defaultValue = "${project.remoteArtifactRepositories}"  )
223     private List<ArtifactRepository> remoteRepositories;
224 
225     /**
226      * If <code>true</false>, aggregate all source documents in one pdf, otherwise generate one pdf for each
227      * source document.
228      */
229     @Parameter( property = "aggregate", defaultValue = "true" )
230     private boolean aggregate;
231 
232     /**
233      * The current version of this plugin.
234      */
235     @Parameter( defaultValue = "${plugin.version}", readonly = true )
236     private String pluginVersion;
237 
238     /**
239      * If <code>true</false>, generate all Maven reports defined in <code>${project.reporting}</code> and append
240      * them as a new entry in the TOC (Table Of Contents).
241      * <b>Note</b>: Including the report generation could fail the PDF generation or increase the build time.
242      *
243      * @since 1.1
244      */
245     @Parameter( property = "includeReports", defaultValue = "true" )
246     private boolean includeReports;
247 
248     /**
249      * Generate a TOC (Table Of Content) for all items defined in the &lt;toc/&gt; element from the document descriptor.
250      * <br>
251      * Possible values are: 'none', 'start' and 'end'.
252      *
253      * @since 1.1
254      */
255     @Parameter( property = "generateTOC", defaultValue = "start" )
256     private String generateTOC;
257 
258     /**
259      * Whether to validate xml input documents.
260      * If set to true, <strong>all</strong> input documents in xml format
261      * (in particular xdoc and fml) will be validated and any error will
262      * lead to a build failure.
263      *
264      * @since 1.2
265      */
266     @Parameter( property = "validate", defaultValue = "false" )
267     private boolean validate;
268 
269     /**
270      * Reports (Maven 2).
271      *
272      * @since 1.3
273      */
274     @Parameter( defaultValue = "${reports}", required = true, readonly = true )
275     private MavenReport[] reports;
276 
277     /**
278      * Reports (Maven 3).
279      *
280      * @since 1.5
281      */
282     @Parameter( defaultValue = "${project.reporting}", readonly = true )
283     private Reporting reporting;
284 
285     /**
286      * The current document Renderer.
287      * @see #implementation
288      */
289     private DocumentRenderer docRenderer;
290 
291     /**
292      * The default locale.
293      */
294     private Locale defaultLocale;
295 
296     /**
297      * The available locales list.
298      */
299     private List<Locale> localesList;
300 
301     /**
302      * The default decoration model.
303      */
304     private DecorationModel defaultDecorationModel;
305 
306     /**
307      * The temp Generated Site dir to have generated reports by this plugin.
308      *
309      * @since 1.1
310      */
311     private File generatedSiteDirectoryTmp;
312 
313     /**
314      * A map of generated MavenReport list using locale as key.
315      *
316      * @since 1.1
317      */
318     private Map<Locale, List<MavenReport>> generatedMavenReports;
319 
320     /**
321      * @since 1.3
322      */
323     private PlexusContainer container;
324 
325     /** {@inheritDoc} */
326     public void execute()
327         throws MojoExecutionException
328     {
329         init();
330 
331         try
332         {
333             generatePdf();
334         }
335         catch ( IOException e )
336         {
337             debugLogGeneratedModel( getDocumentModel( Locale.ENGLISH ) );
338 
339             throw new MojoExecutionException( "Error during document generation: " + e.getMessage(), e );
340         }
341 
342         try
343         {
344             copyGeneratedPdf();
345         }
346         catch ( IOException e )
347         {
348             throw new MojoExecutionException( "Error copying generated PDF: " + e.getMessage(), e );
349         }
350     }
351 
352     /** {@inheritDoc} */
353     public void contextualize( Context context )
354         throws ContextException
355     {
356         container = (PlexusContainer) context.get( PlexusConstants.PLEXUS_KEY );
357     }
358 
359     protected File getOutputDirectory()
360     {
361         return outputDirectory;
362     }
363 
364     protected File getWorkingDirectory()
365     {
366         return workingDirectory;
367     }
368 
369     protected boolean isIncludeReports()
370     {
371         return includeReports;
372     }
373 
374     /**
375      * Init and validate parameters
376      */
377     private void init()
378     {
379         if ( "fo".equalsIgnoreCase( implementation ) )
380         {
381             this.docRenderer = foRenderer;
382         }
383         else if ( "itext".equalsIgnoreCase( implementation ) )
384         {
385             this.docRenderer = itextRenderer;
386         }
387         else
388         {
389             getLog().warn( "Invalid 'implementation' parameter: '" + implementation
390                     + "', using 'fo' as default." );
391 
392             this.docRenderer = foRenderer;
393         }
394 
395         if ( !( "none".equalsIgnoreCase( generateTOC )
396                 || "start".equalsIgnoreCase( generateTOC ) || "end".equalsIgnoreCase( generateTOC ) ) )
397         {
398             getLog().warn( "Invalid 'generateTOC' parameter: '" + generateTOC
399                     + "', using 'start' as default." );
400 
401             this.generateTOC = "start";
402         }
403     }
404 
405     /**
406      * Copy the generated PDF to outputDirectory.
407      *
408      * @throws MojoExecutionException if any
409      * @throws IOException if any
410      * @since 1.1
411      */
412     private void copyGeneratedPdf()
413         throws MojoExecutionException, IOException
414     {
415         boolean requireCopy =
416             !getOutputDirectory().getCanonicalPath().equals( getWorkingDirectory().getCanonicalPath() );
417 
418         String outputName = getDocumentModel( getDefaultLocale() ).getOutputName().trim();
419         if ( !outputName.endsWith( ".pdf" ) )
420         {
421             outputName = outputName.concat( ".pdf" );
422         }
423 
424         for ( final Locale locale : getAvailableLocales() )
425         {
426             File generatedPdfSource = new File( getLocaleDirectory( getWorkingDirectory(), locale ), outputName );
427 
428             if ( !generatedPdfSource.exists() )
429             {
430                 getLog().warn( "Unable to find the generated pdf: " + generatedPdfSource.getAbsolutePath() );
431                 continue;
432             }
433 
434             File generatedPdfDest = new File( getLocaleDirectory( getOutputDirectory(), locale ), outputName );
435 
436             if ( requireCopy )
437             {
438                 FileUtils.copyFile( generatedPdfSource, generatedPdfDest );
439                 generatedPdfSource.delete();
440             }
441 
442             getLog().info( "pdf generated: " + generatedPdfDest );
443         }
444     }
445 
446     /**
447      * Generate the PDF.
448      *
449      * @throws MojoExecutionException if any
450      * @throws IOException if any
451      * @since 1.1
452      */
453     private void generatePdf()
454         throws MojoExecutionException, IOException
455     {
456         Locale.setDefault( getDefaultLocale() );
457 
458         for ( final Locale locale : getAvailableLocales() )
459         {
460             final File workingDir = getLocaleDirectory( getWorkingDirectory(), locale );
461 
462             File siteDirectoryFile = getLocaleDirectory( getSiteDirectoryTmp(), locale );
463 
464             copyResources( locale );
465 
466             // generated xdoc sources for reports
467             generateMavenReports( locale );
468 
469             // render all Doxia source files to pdf (were handwritten or generated by reports)
470             DocumentRendererContext context = new DocumentRendererContext();
471             context.put( "project", project );
472             context.put( "settings", settings );
473             context.put( "PathTool", new PathTool() );
474             context.put( "FileUtils", new FileUtils() );
475             context.put( "StringUtils", new StringUtils() );
476             context.put( "i18n", i18n );
477             context.put( "generateTOC", generateTOC );
478             context.put( "validate", validate );
479 
480             // Put any of the properties in directly into the Velocity context
481             for ( Map.Entry<Object, Object> entry : project.getProperties().entrySet() )
482             {
483                 context.put( (String) entry.getKey(), entry.getValue() );
484             }
485 
486             final DocumentModel model = aggregate ? getDocumentModel( locale ) : null;
487 
488             try
489             {
490                 // TODO use interface see DOXIASITETOOLS-30
491                 ( (AbstractDocumentRenderer) docRenderer ).render( siteDirectoryFile, workingDir, model, context );
492             }
493             catch ( DocumentRendererException e )
494             {
495                 throw new MojoExecutionException( "Error during document generation: " + e.getMessage(), e );
496             }
497         }
498     }
499 
500     /**
501      * @return the default tmpGeneratedSiteDirectory when report will be created.
502      * @since 1.1
503      */
504     private File getGeneratedSiteDirectoryTmp()
505     {
506         if ( this.generatedSiteDirectoryTmp == null )
507         {
508             this.generatedSiteDirectoryTmp = new File( getWorkingDirectory(), "generated-site.tmp" );
509         }
510 
511         return this.generatedSiteDirectoryTmp;
512     }
513 
514     /**
515      * Copy all site and generated-site files in the tmpSiteDirectory.
516      * <br>
517      * <b>Note</b>: ignore copying of <code>generated-site</code> files if they already exist in the
518      * <code>site</code> dir.
519      *
520      * @param tmpSiteDir not null
521      * @throws IOException if any
522      * @since 1.1
523      */
524     protected void prepareTempSiteDirectory( final File tmpSiteDir )
525         throws IOException
526     {
527         // safety
528         tmpSiteDir.mkdirs();
529 
530         // copy site
531         if ( siteDirectory.exists() )
532         {
533             FileUtils.copyDirectoryStructure( siteDirectory, tmpSiteDir );
534         }
535 
536         // Remove SCM files
537         List<String> files =
538             FileUtils.getFileAndDirectoryNames( tmpSiteDir, FileUtils.getDefaultExcludesAsString(), null, true,
539                                                 true, true, true );
540         for ( final String fileName : files )
541         {
542             final File file = new File( fileName );
543 
544             if ( file.isDirectory() )
545             {
546                 FileUtils.deleteDirectory( file );
547             }
548             else
549             {
550                 file.delete();
551             }
552         }
553 
554         copySiteDir( generatedSiteDirectory, tmpSiteDir );
555     }
556 
557     /**
558      * Copy the from site dir to the to dir.
559      *
560      * @param from not null
561      * @param to not null
562      * @throws IOException if any
563      * @since 1.1
564      */
565     private void copySiteDir( final File from, final File to )
566         throws IOException
567     {
568         if ( from == null || !from.exists() )
569         {
570             return;
571         }
572 
573         // copy generated-site
574         for ( final Locale locale : getAvailableLocales() )
575         {
576             String excludes = getDefaultExcludesWithLocales( getAvailableLocales(), getDefaultLocale() );
577             List<String> siteFiles =
578                 siteDirectory.exists() ? FileUtils.getFileNames( siteDirectory, "**/*", excludes, false )
579                                 : new ArrayList<>();
580             File siteDirectoryLocale = new File( siteDirectory, locale.getLanguage() );
581             if ( !locale.getLanguage().equals( getDefaultLocale().getLanguage() ) && siteDirectoryLocale.exists() )
582             {
583                 siteFiles = FileUtils.getFileNames( siteDirectoryLocale, "**/*", excludes, false );
584             }
585 
586             List<String> generatedSiteFiles = FileUtils.getFileNames( from, "**/*", excludes, false );
587             File fromLocale = new File( from, locale.getLanguage() );
588             if ( !locale.getLanguage().equals( getDefaultLocale().getLanguage() ) && fromLocale.exists() )
589             {
590                 generatedSiteFiles = FileUtils.getFileNames( fromLocale, "**/*", excludes, false );
591             }
592 
593             for ( final String generatedSiteFile : generatedSiteFiles )
594             {
595                 if ( siteFiles.contains( generatedSiteFile ) )
596                 {
597                     getLog().warn( "Generated-site already contains a file in site: " + generatedSiteFile
598                                        + ". Ignoring copying it!" );
599                     continue;
600                 }
601 
602                 if ( !locale.getLanguage().equals( getDefaultLocale().getLanguage() ) )
603                 {
604                     if ( fromLocale.exists() )
605                     {
606                         File in = new File( fromLocale, generatedSiteFile );
607                         File out = new File( new File( to, locale.getLanguage() ), generatedSiteFile );
608                         out.getParentFile().mkdirs();
609                         FileUtils.copyFile( in, out );
610                     }
611                 }
612                 else
613                 {
614                     File in = new File( from, generatedSiteFile );
615                     File out = new File( to, generatedSiteFile );
616                     out.getParentFile().mkdirs();
617                     FileUtils.copyFile( in, out );
618                 }
619             }
620         }
621     }
622 
623     /**
624      * Constructs a DocumentModel for the current project. The model is either read from
625      * a descriptor file, if it exists, or constructed from information in the pom and site.xml.
626      *
627      * @param locale not null
628      * @return DocumentModel.
629      * @throws MojoExecutionException if any
630      * @see #appendGeneratedReports(DocumentModel, Locale)
631      */
632     private DocumentModel getDocumentModel( Locale locale )
633         throws MojoExecutionException
634     {
635         if ( docDescriptor.exists() )
636         {
637             DocumentModel doc = getDocumentModelFromDescriptor( locale );
638             // TODO: descriptor model should get merged into default model, see MODELLO-63
639 
640             appendGeneratedReports( doc, locale );
641 
642             saveTOC( doc.getToc(), locale );
643 
644             return doc;
645         }
646 
647         DocumentModel model = new DocumentModelBuilder( project, getDefaultDecorationModel() ).getDocumentModel();
648 
649         model.getMeta().setGenerator( getDefaultGenerator() );
650         model.getMeta().setLanguage( locale.getLanguage() );
651         model.getCover().setCoverType( i18n.getString( "pdf-plugin", getDefaultLocale(), "toc.type" ) );
652         model.getToc().setName( i18n.getString( "pdf-plugin", getDefaultLocale(), "toc.title" ) );
653 
654         appendGeneratedReports( model, locale );
655 
656         saveTOC( model.getToc(), locale );
657 
658         debugLogGeneratedModel( model );
659 
660         return model;
661     }
662 
663     /**
664      * Read a DocumentModel from a file.
665      *
666      * @param locale used to set the language.
667      * @return the DocumentModel read from the configured document descriptor.
668      * @throws org.apache.maven.plugin.MojoExecutionException if the model could not be read.
669      */
670     private DocumentModel getDocumentModelFromDescriptor( Locale locale )
671         throws MojoExecutionException
672     {
673         DocumentModel model;
674 
675         try
676         {
677             model = new DocumentDescriptorReader( project, getLog(),
678                                                   locale ).readAndFilterDocumentDescriptor( docDescriptor );
679         }
680         catch ( XmlPullParserException ex )
681         {
682             throw new MojoExecutionException( "Error reading DocumentDescriptor!", ex );
683         }
684         catch ( IOException io )
685         {
686             throw new MojoExecutionException( "Error opening DocumentDescriptor!", io );
687         }
688 
689         if ( model.getMeta() == null )
690         {
691             model.setMeta( new DocumentMeta() );
692         }
693 
694         if ( StringUtils.isEmpty( model.getMeta().getLanguage() ) )
695         {
696             model.getMeta().setLanguage( locale.getLanguage() );
697         }
698 
699         if ( StringUtils.isEmpty( model.getMeta().getGenerator() ) )
700         {
701             model.getMeta().setGenerator( getDefaultGenerator() );
702         }
703 
704         return model;
705     }
706 
707     /**
708      * Return the directory for a given Locale and the current default Locale.
709      *
710      * @param basedir the base directory
711      * @param locale a Locale.
712      * @return File.
713      */
714     private File getLocaleDirectory( File basedir, Locale locale )
715     {
716         if ( locale.getLanguage().equals( getDefaultLocale().getLanguage() ) )
717         {
718             return basedir;
719         }
720 
721         return new File( basedir, locale.getLanguage() );
722     }
723 
724     /**
725      * @return the default locale from <code>siteTool</code>.
726      * @see #getAvailableLocales()
727      */
728     private Locale getDefaultLocale()
729     {
730         if ( this.defaultLocale == null )
731         {
732             this.defaultLocale = getAvailableLocales().get( 0 );
733         }
734 
735         return this.defaultLocale;
736     }
737 
738     /**
739      * @return the available locales from <code>siteTool</code>.
740      */
741     private List<Locale> getAvailableLocales()
742     {
743         if ( this.localesList == null )
744         {
745             this.localesList = siteTool.getSiteLocales( locales );
746         }
747 
748         return this.localesList;
749     }
750 
751     /**
752      * @return the DecorationModel instance from <code>site.xml</code>
753      * @throws MojoExecutionException if any
754      */
755     private DecorationModel getDefaultDecorationModel()
756         throws MojoExecutionException
757     {
758         if ( this.defaultDecorationModel == null )
759         {
760             final Locale locale = getDefaultLocale();
761 
762             final File descriptorFile = siteTool.getSiteDescriptor( siteDirectory, locale );
763             DecorationModel decoration = null;
764 
765             if ( descriptorFile.exists() )
766             {
767                 try ( XmlStreamReader reader = new XmlStreamReader( descriptorFile ) )
768                 {
769                     String siteDescriptorContent = IOUtil.toString( reader );
770 
771                     siteDescriptorContent =
772                         siteTool.getInterpolatedSiteDescriptorContent( new HashMap<>( 2 ), project,
773                                                                        siteDescriptorContent );
774 
775                     decoration = new DecorationXpp3Reader().read( new StringReader( siteDescriptorContent ) );
776                 }
777                 catch ( XmlPullParserException e )
778                 {
779                     throw new MojoExecutionException( "Error parsing site descriptor", e );
780                 }
781                 catch ( IOException e )
782                 {
783                     throw new MojoExecutionException( "Error reading site descriptor", e );
784                 }
785                 catch ( SiteToolException e )
786                 {
787                     throw new MojoExecutionException( "Error when interpoling site descriptor", e );
788                 }
789             }
790 
791             this.defaultDecorationModel = decoration;
792         }
793 
794         return this.defaultDecorationModel;
795     }
796 
797     /**
798      * Parse the decoration model to find the skin artifact and copy its resources to the output dir.
799      *
800      * @param locale not null
801      * @throws MojoExecutionException if any
802      * @see #getDefaultDecorationModel()
803      */
804     private void copyResources( Locale locale )
805         throws MojoExecutionException
806     {
807         final DecorationModel decorationModel = getDefaultDecorationModel();
808         if ( decorationModel == null )
809         {
810             return;
811         }
812 
813         Artifact skinArtifact;
814         try
815         {
816             skinArtifact =
817                 siteTool.getSkinArtifactFromRepository( localRepository, project.getRemoteArtifactRepositories(),
818                                                         decorationModel );
819         }
820         catch ( SiteToolException e )
821         {
822             throw new MojoExecutionException( "SiteToolException: " + e.getMessage(), e );
823         }
824 
825         if ( skinArtifact == null )
826         {
827             return;
828         }
829 
830         if ( getLog().isDebugEnabled() )
831         {
832             getLog().debug( "Copy resources from skin artifact: '" + skinArtifact.getId() + "'..." );
833         }
834 
835         try
836         {
837             final SiteRenderingContext context =
838                 siteRenderer.createContextForSkin( skinArtifact, new HashMap<>( 2 ), decorationModel,
839                                                    project.getName(), locale );
840             context.addSiteDirectory( new File( siteDirectory, locale.getLanguage() ) );
841 
842             siteRenderer.copyResources( context, getWorkingDirectory() );
843         }
844         catch ( IOException e )
845         {
846             throw new MojoExecutionException( "IOException: " + e.getMessage(), e );
847         }
848         catch ( RendererException e )
849         {
850             throw new MojoExecutionException( "RendererException: " + e.getMessage(), e );
851         }
852     }
853 
854     /**
855      * Construct a default producer.
856      *
857      * @return A String in the form <code>Maven PDF Plugin v. 1.1.1, 'fo' implementation</code>.
858      */
859     private String getDefaultGenerator()
860     {
861         return "Maven PDF Plugin v. " + pluginVersion + ", '" + implementation + "' implementation.";
862     }
863 
864     /**
865      * Write the auto-generated model to disc.
866      *
867      * @param docModel the model to write.
868      */
869     private void debugLogGeneratedModel( final DocumentModel docModel )
870     {
871         if ( getLog().isDebugEnabled() && project != null )
872         {
873             final File outputDir = new File( project.getBuild().getDirectory(), "pdf" );
874 
875             if ( !outputDir.exists() )
876             {
877                 outputDir.mkdirs();
878             }
879 
880             final File doc = FileUtils.createTempFile( "pdf", ".xml", outputDir );
881             final DocumentXpp3Writer xpp3 = new DocumentXpp3Writer();
882 
883             try ( Writer writer = WriterFactory.newXmlWriter( doc ) )
884             {
885                 xpp3.write( writer, docModel );
886                 getLog().debug( "Generated a default document model: " + doc.getAbsolutePath() );
887             }
888             catch ( IOException e )
889             {
890                 getLog().error( "Failed to write document model: " + e.getMessage() );
891                 getLog().debug( e );
892             }
893         }
894     }
895 
896     /**
897      * Generate all Maven reports defined in <code>${project.reporting}</code> to <code>xdoc</code> source
898      * only if <code>generateReports</code> is enabled.
899      *
900      * @param locale not null
901      * @throws MojoExecutionException if any
902      * @throws IOException if any
903      * @since 1.1
904      */
905     private void generateMavenReports( Locale locale )
906         throws MojoExecutionException, IOException
907     {
908         if ( !isIncludeReports() )
909         {
910             getLog().info( "Skipped report generation." );
911             return;
912         }
913 
914         if ( project.getReporting() == null )
915         {
916             getLog().info( "No report was specified." );
917             return;
918         }
919 
920         List<MavenReportExecution> reportExecutions = getReports();
921         for ( MavenReportExecution reportExecution : reportExecutions )
922         {
923             generateMavenReport( reportExecution, locale );
924         }
925 
926         // copy generated site
927         copySiteDir( getGeneratedSiteDirectoryTmp(), getSiteDirectoryTmp() );
928         copySiteDir( generatedSiteDirectory, getSiteDirectoryTmp() );
929     }
930 
931     /**
932      * Generate the given Maven report to an xdoc source file,
933      * only if it is not an external report and the report could be generated.
934      *
935      * @param reportExecution not null
936      * @param locale not null
937      * @throws IOException if any
938      * @throws MojoExecutionException if any
939      * @since 1.1
940      */
941     private void generateMavenReport( MavenReportExecution reportExecution, Locale locale )
942         throws IOException, MojoExecutionException
943     {
944         MavenReport report = reportExecution.getMavenReport();
945 
946         String localReportName = report.getName( locale );
947 
948         if ( !reportExecution.canGenerateReport() )
949         {
950             getLog().info( "Skipped \"" + localReportName + "\" report." );
951             getLog().debug( "canGenerateReport() was false." );
952 
953             return;
954         }
955 
956         if ( report.isExternalReport() )
957         {
958             getLog().info( "Skipped external \"" + localReportName + "\" report (not supported by pdf plugin)." );
959             getLog().debug( "isExternalReport() was false." );
960 
961             return;
962         }
963 
964         for ( final MavenReport generatedReport : getGeneratedMavenReports( locale ) )
965         {
966             if ( report.getName( locale ).equals( generatedReport.getName( locale ) ) )
967             {
968                 if ( getLog().isDebugEnabled() )
969                 {
970                     getLog().debug( report.getName( locale ) + " was already generated." );
971                 }
972                 return;
973             }
974         }
975 
976         File outDir = new File( getGeneratedSiteDirectoryTmp(), "xdoc" );
977         if ( !locale.getLanguage().equals( defaultLocale.getLanguage() ) )
978         {
979             outDir = new File( new File( getGeneratedSiteDirectoryTmp(), locale.getLanguage() ), "xdoc" );
980         }
981         outDir.mkdirs();
982 
983         File generatedReport = new File( outDir, report.getOutputName() + ".xml" );
984 
985         if ( siteDirectory.exists() )
986         {
987             String excludes = getDefaultExcludesWithLocales( getAvailableLocales(), getDefaultLocale() );
988             List<String> files =
989                 FileUtils.getFileNames( siteDirectory, "*/" + report.getOutputName() + ".*", excludes, false );
990             if ( !locale.getLanguage().equals( defaultLocale.getLanguage() ) )
991             {
992                 files =
993                     FileUtils.getFileNames( new File( siteDirectory, locale.getLanguage() ), "*/"
994                         + report.getOutputName() + ".*", excludes, false );
995             }
996 
997             if ( files.size() != 0 )
998             {
999                 String displayLanguage = locale.getDisplayLanguage( Locale.ENGLISH );
1000 
1001                 if ( getLog().isInfoEnabled() )
1002                 {
1003                     getLog().info(
1004                                    "Skipped \"" + report.getName( locale ) + "\" report, file \""
1005                                        + report.getOutputName() + "\" already exists for the " + displayLanguage
1006                                        + " version." );
1007                 }
1008 
1009                 return;
1010             }
1011         }
1012 
1013         if ( getLog().isInfoEnabled() )
1014         {
1015             getLog().info( "Generating \"" + localReportName + "\" report." );
1016         }
1017 
1018         // The report will eventually generate output by itself, so we set its output directory anyway.
1019         report.setReportOutputDirectory( outDir );
1020 
1021         StringWriter sw = new StringWriter();
1022 
1023         PdfXdocSink pdfXdocSink = null;
1024         try
1025         {
1026             pdfXdocSink = new PdfXdocSink( sw );
1027             renderReportToSink( reportExecution, locale, pdfXdocSink );
1028         }
1029         catch ( MavenReportException e )
1030         {
1031             String goal = reportExecution.getPlugin().getArtifactId() + ':' + reportExecution.getPlugin().getVersion()
1032                 + ':' + reportExecution.getGoal();
1033             throw new MojoExecutionException( "Error generating " + goal + " report", e );
1034         }
1035         finally
1036         {
1037             if ( pdfXdocSink != null )
1038             {
1039                 pdfXdocSink.close();
1040             }
1041         }
1042 
1043         if ( getLog().isDebugEnabled() )
1044         {
1045             getLog().debug( "Writing generated xdoc to " + generatedReport );
1046         }
1047         writeGeneratedReport( sw.toString(), generatedReport );
1048 
1049         // keep generated report xdoc only if it is valid
1050         if ( isValidGeneratedReportXdoc( reportExecution.getPlugin().getId() + ':' + reportExecution.getGoal(),
1051                                          generatedReport, localReportName ) )
1052         {
1053             getGeneratedMavenReports( locale ).add( report );
1054         }
1055     }
1056 
1057     /**
1058      * see org.apache.maven.plugins.site.render.ReportDocumentRenderer#renderDocument(...)
1059      *
1060      * @param reportExec
1061      * @param locale
1062      * @param sink
1063      * @throws MavenReportException
1064      */
1065     private void renderReportToSink( MavenReportExecution reportExec, Locale locale, PdfXdocSink sink )
1066         throws MavenReportException
1067     {
1068         ClassLoader originalClassLoader = Thread.currentThread().getContextClassLoader();
1069         try
1070         {
1071             if ( reportExec.getClassLoader() != null )
1072             {
1073                 Thread.currentThread().setContextClassLoader( reportExec.getClassLoader() );
1074             }
1075 
1076             MavenReport report = reportExec.getMavenReport();
1077 
1078             /*if ( report instanceof MavenMultiPageReport )
1079             {
1080                 // extended multi-page API
1081                 ( (MavenMultiPageReport) report ).generate( mainSink, multiPageSinkFactory, locale );
1082             }
1083             else if ( generateMultiPage( locale, multiPageSinkFactory, mainSink ) )
1084             {
1085                 // extended multi-page API for Maven 2.2, only accessible by reflection API
1086             }
1087             else
1088             {*/
1089             // old single-page-only API
1090             report.generate( sink, locale );
1091             //}
1092         }
1093         finally
1094         {
1095             if ( reportExec.getClassLoader() != null )
1096             {
1097                 Thread.currentThread().setContextClassLoader( originalClassLoader );
1098             }
1099         }
1100     }
1101 
1102     /**
1103      * @param locale not null
1104      * @return the generated reports
1105      * @since 1.1
1106      */
1107     private List<MavenReport> getGeneratedMavenReports( Locale locale )
1108     {
1109         if ( this.generatedMavenReports == null )
1110         {
1111             this.generatedMavenReports = new HashMap<>( 2 );
1112         }
1113 
1114         this.generatedMavenReports.computeIfAbsent( locale, k -> new ArrayList<>( 2 ) );
1115 
1116         return this.generatedMavenReports.get( locale );
1117     }
1118 
1119     /**
1120      * Append generated reports to the toc only if <code>generateReports</code> is enabled, for instance:
1121      * <pre>
1122      * &lt;item name="Project Reports" ref="project-info"&gt;
1123      * &nbsp;&nbsp;&lt;item name="Project License" ref="license" /&gt;
1124      * &nbsp;&nbsp;&lt;item name="Project Team" ref="team-list" /&gt;
1125      * &nbsp;&nbsp;&lt;item name="Continuous Integration" ref="integration" /&gt;
1126      * &nbsp;&nbsp;...
1127      * &lt;/item&gt;
1128      * </pre>
1129      *
1130      * @param model not null
1131      * @param locale not null
1132      * @see #generateMavenReports(Locale)
1133      * @since 1.1
1134      */
1135     protected void appendGeneratedReports( DocumentModel model, Locale locale )
1136     {
1137         if ( !isIncludeReports() )
1138         {
1139             return;
1140         }
1141         if ( getGeneratedMavenReports( locale ).isEmpty() )
1142         {
1143             return;
1144         }
1145 
1146         final DocumentTOCItem documentTOCItem = new DocumentTOCItem();
1147         documentTOCItem.setName( i18n.getString( "pdf-plugin", locale, "toc.project-info.item" ) );
1148         documentTOCItem.setRef( "project-info" ); // see #generateMavenReports(Locale)
1149 
1150         List<String> addedRef = new ArrayList<>( 4 );
1151 
1152         List<DocumentTOCItem> items = new ArrayList<>( 4 );
1153 
1154         // append generated report defined as MavenReport
1155         for ( final MavenReport report : getGeneratedMavenReports( locale ) )
1156         {
1157             final DocumentTOCItem reportItem = new DocumentTOCItem();
1158             reportItem.setName( report.getName( locale ) );
1159             reportItem.setRef( report.getOutputName() );
1160 
1161             items.add( reportItem );
1162 
1163             addedRef.add( report.getOutputName() );
1164         }
1165 
1166         // append all generated reports from generated-site
1167         try
1168         {
1169             if ( generatedSiteDirectory.exists() )
1170             {
1171                 String excludes = getDefaultExcludesWithLocales( getAvailableLocales(), getDefaultLocale() );
1172                 List<String> generatedDirs = FileUtils.getDirectoryNames( generatedSiteDirectory, "*", excludes,
1173                                                                           true );
1174                 if ( !locale.getLanguage().equals( getDefaultLocale().getLanguage() ) )
1175                 {
1176                     generatedDirs =
1177                         FileUtils.getFileNames( new File( generatedSiteDirectory, locale.getLanguage() ), "*",
1178                                                 excludes, true );
1179                 }
1180 
1181                 for ( final String generatedDir : generatedDirs )
1182                 {
1183                     List<String> generatedFiles =
1184                         FileUtils.getFileNames( new File( generatedDir ), "**.*", excludes, false );
1185 
1186                     for ( final String generatedFile : generatedFiles )
1187                     {
1188                         final String ref = generatedFile.substring( 0, generatedFile.lastIndexOf( '.' ) );
1189 
1190                         if ( !addedRef.contains( ref ) )
1191                         {
1192                             final String title =
1193                                 getGeneratedDocumentTitle( new File( generatedDir, generatedFile ) );
1194 
1195                             if ( title != null )
1196                             {
1197                                 final DocumentTOCItem reportItem = new DocumentTOCItem();
1198                                 reportItem.setName( title );
1199                                 reportItem.setRef( ref );
1200 
1201                                 items.add( reportItem );
1202                             }
1203                         }
1204                     }
1205                 }
1206             }
1207         }
1208         catch ( IOException e )
1209         {
1210             getLog().error( "IOException: " + e.getMessage() );
1211             getLog().debug( e );
1212         }
1213 
1214         // append to Toc
1215         documentTOCItem.setItems( items );
1216         model.getToc().addItem( documentTOCItem );
1217     }
1218 
1219     private void saveTOC( DocumentTOC toc, Locale locale )
1220     {
1221         try
1222         {
1223             TocFileHelper.saveTOC( getWorkingDirectory(), toc, locale );
1224         }
1225         catch ( IOException e )
1226         {
1227             getLog().error( "Error while writing table of contents", e );
1228         }
1229     }
1230 
1231     /**
1232      * Parse a generated Doxia file and returns its title.
1233      *
1234      * @param f not null
1235      * @return the xdoc file title or null if an error occurs.
1236      * @throws IOException if any
1237      * @since 1.1
1238      */
1239     private String getGeneratedDocumentTitle( final File f )
1240         throws IOException
1241     {
1242         final IndexEntry entry = new IndexEntry( "index" );
1243         final IndexingSink titleSink = new IndexingSink( entry );
1244 
1245         try ( Reader reader = ReaderFactory.newXmlReader( f ) )
1246         {
1247             doxia.parse( reader, f.getParentFile().getName(), titleSink );
1248         }
1249         catch ( ParseException e )
1250         {
1251             getLog().error( "ParseException: " + e.getMessage() );
1252             getLog().debug( e );
1253             return null;
1254         }
1255         catch ( ParserNotFoundException e )
1256         {
1257             getLog().error( "ParserNotFoundException: " + e.getMessage() );
1258             getLog().debug( e );
1259             return null;
1260         }
1261 
1262         return titleSink.getTitle();
1263     }
1264 
1265     /**
1266      * Parsing the generated report to see if it is correct or not. Log the error for the user.
1267      *
1268      * @param fullGoal not null
1269      * @param generatedReport not null
1270      * @param localReportName not null
1271      * @return <code>true</code> if Doxia is able to parse the generated report, <code>false</code> otherwise.
1272      * @since 1.1
1273      */
1274     private boolean isValidGeneratedReportXdoc( String fullGoal, File generatedReport, String localReportName )
1275     {
1276         SinkAdapter sinkAdapter = new SinkAdapter();
1277         try ( Reader reader = ReaderFactory.newXmlReader( generatedReport ) )
1278         {
1279             doxia.parse( reader, "xdoc", sinkAdapter );
1280         }
1281         catch ( ParseException e )
1282         {
1283             String sb = EOL
1284                     + "Error when parsing the generated report xdoc file: "
1285                     + generatedReport.getAbsolutePath() + EOL
1286                     + e.getMessage() + EOL
1287                     + "You could:" + EOL
1288                     + "  * exclude all reports using -DincludeReports=false" + EOL
1289                     + "  * remove the "
1290                     + fullGoal
1291                     + " from the <reporting/> part. To not affect the site generation, "
1292                     + "you could create a PDF profile." + EOL
1293                     + "Ignoring the \"" + localReportName + "\" report in the PDF." + EOL;
1294             getLog().error( sb );
1295             getLog().debug( e );
1296 
1297             return false;
1298         }
1299         catch ( ParserNotFoundException e )
1300         {
1301             getLog().error( "ParserNotFoundException: " + e.getMessage() );
1302             getLog().debug( e );
1303 
1304             return false;
1305         }
1306         catch ( IOException e )
1307         {
1308             getLog().error( "IOException: " + e.getMessage() );
1309             getLog().debug( e );
1310 
1311             return false;
1312         }
1313 
1314         return true;
1315     }
1316 
1317     protected List<MavenReportExecution> getReports()
1318         throws MojoExecutionException
1319     {
1320         MavenReportExecutorRequest mavenReportExecutorRequest = new MavenReportExecutorRequest();
1321         mavenReportExecutorRequest.setLocalRepository( localRepository );
1322         mavenReportExecutorRequest.setMavenSession( session );
1323         mavenReportExecutorRequest.setProject( project );
1324         mavenReportExecutorRequest.setReportPlugins( getReportingPlugins() );
1325 
1326         MavenReportExecutor mavenReportExecutor;
1327         try
1328         {
1329             mavenReportExecutor = (MavenReportExecutor) container.lookup( MavenReportExecutor.class.getName() );
1330         }
1331         catch ( ComponentLookupException e )
1332         {
1333             throw new MojoExecutionException( "could not get MavenReportExecutor component", e );
1334         }
1335         return mavenReportExecutor.buildMavenReports( mavenReportExecutorRequest );
1336     }
1337 
1338     /**
1339      * Get the report plugins from reporting section, adding if necessary (i.e. not excluded)
1340      * default reports (i.e. maven-project-info-reports)
1341      *
1342      * @return the effective list of reports
1343      * @since 1.5
1344      */
1345     private ReportPlugin[] getReportingPlugins()
1346     {
1347         List<ReportPlugin> reportingPlugins = reporting.getPlugins();
1348 
1349         // MSITE-806: add default report plugin like done in maven-model-builder DefaultReportingConverter
1350         boolean hasMavenProjectInfoReportsPlugin = false;
1351         for ( ReportPlugin plugin : reportingPlugins )
1352         {
1353             if ( "org.apache.maven.plugins".equals( plugin.getGroupId() )
1354                 && "maven-project-info-reports-plugin".equals( plugin.getArtifactId() ) )
1355             {
1356                 hasMavenProjectInfoReportsPlugin = true;
1357                 break;
1358             }
1359         }
1360 
1361         if ( !reporting.isExcludeDefaults() && !hasMavenProjectInfoReportsPlugin )
1362         {
1363             ReportPlugin mpir = new ReportPlugin();
1364             mpir.setArtifactId( "maven-project-info-reports-plugin" );
1365             reportingPlugins.add( mpir );
1366         }
1367         return reportingPlugins.toArray( new ReportPlugin[0] );
1368     }
1369 
1370     /**
1371      * Write the given content to the given file.
1372      * <br>
1373      * <b>Note</b>: try also to fix the content due to some issues in
1374      * {@link org.apache.maven.reporting.AbstractMavenReport}.
1375      *
1376      * @param content the given content
1377      * @param toFile the report file
1378      * @throws IOException if any
1379      * @since 1.1
1380      */
1381     private static void writeGeneratedReport( String content, File toFile )
1382         throws IOException
1383     {
1384         if ( StringUtils.isEmpty( content ) )
1385         {
1386             return;
1387         }
1388 
1389         try ( Writer writer = WriterFactory.newXmlWriter( toFile ) )
1390         {
1391             // see PdfSink#table()
1392             writer.write( StringUtils.replace( content, "<table><table", "<table" ) );
1393         }
1394     }
1395 
1396     /**
1397      * @param locales the list of locales dir to exclude
1398      * @param defaultLocale the default locale.
1399      * @return the comma separated list of default excludes and locales dir.
1400      * @see FileUtils#getDefaultExcludesAsString()
1401      * @since 1.1
1402      */
1403     private static String getDefaultExcludesWithLocales( List<Locale> locales, Locale defaultLocale )
1404     {
1405         StringBuilder excludesLocales = new StringBuilder( FileUtils.getDefaultExcludesAsString() );
1406         for ( final Locale locale : locales )
1407         {
1408             if ( !locale.getLanguage().equals( defaultLocale.getLanguage() ) )
1409             {
1410                 excludesLocales.append( ",**/" ).append( locale.getLanguage() ).append( "/*" );
1411             }
1412         }
1413 
1414         return excludesLocales.toString();
1415     }
1416 
1417     /**
1418      * A sink to render a Maven report as a generated xdoc file, with some known workarounds.
1419      *
1420      * @since 1.1
1421      */
1422     private static class PdfXdocSink
1423         extends XdocSink
1424         implements org.codehaus.doxia.sink.Sink
1425     {
1426         protected PdfXdocSink( Writer writer )
1427         {
1428             super( writer );
1429         }
1430 
1431         /** {@inheritDoc} */
1432         public void text( String text )
1433         {
1434             // workaround to fix quotes introduced with MPIR-59 (then removed in MPIR-136)
1435             super.text( StringUtils.replace( text, "\u0092", "'" ) );
1436         }
1437 
1438         public void tableRow()
1439         {
1440             // To be backward compatible: TODO add to XdocSink
1441             if ( !this.tableRows )
1442             {
1443                 tableRows( null, false );
1444             }
1445             super.tableRow( null );
1446         }
1447     }
1448 
1449 }