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