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.reporting;
20  
21  import java.io.File;
22  import java.io.FileOutputStream;
23  import java.io.IOException;
24  import java.io.OutputStreamWriter;
25  import java.io.Writer;
26  import java.util.Date;
27  import java.util.HashMap;
28  import java.util.List;
29  import java.util.Locale;
30  import java.util.Map;
31  
32  import org.apache.maven.archiver.MavenArchiver;
33  import org.apache.maven.artifact.Artifact;
34  import org.apache.maven.doxia.sink.Sink;
35  import org.apache.maven.doxia.sink.SinkFactory;
36  import org.apache.maven.doxia.site.SiteModel;
37  import org.apache.maven.doxia.siterenderer.DocumentRenderingContext;
38  import org.apache.maven.doxia.siterenderer.Renderer;
39  import org.apache.maven.doxia.siterenderer.RendererException;
40  import org.apache.maven.doxia.siterenderer.SiteRenderingContext;
41  import org.apache.maven.doxia.siterenderer.sink.SiteRendererSink;
42  import org.apache.maven.doxia.tools.SiteTool;
43  import org.apache.maven.doxia.tools.SiteToolException;
44  import org.apache.maven.plugin.AbstractMojo;
45  import org.apache.maven.plugin.MojoExecution;
46  import org.apache.maven.plugin.MojoExecutionException;
47  import org.apache.maven.plugins.annotations.Component;
48  import org.apache.maven.plugins.annotations.Parameter;
49  import org.apache.maven.project.MavenProject;
50  import org.apache.maven.shared.utils.WriterFactory;
51  import org.codehaus.plexus.PlexusContainer;
52  import org.codehaus.plexus.component.repository.exception.ComponentLookupException;
53  import org.codehaus.plexus.util.ReaderFactory;
54  import org.eclipse.aether.RepositorySystemSession;
55  import org.eclipse.aether.repository.RemoteRepository;
56  
57  import static org.apache.maven.shared.utils.logging.MessageUtils.buffer;
58  
59  /**
60   * The basis for a Maven report which can be generated both as part of a site generation or
61   * as a direct standalone goal invocation.
62   * Both invocations are delegated to <code>abstract executeReport( Locale )</code> from:
63   * <ul>
64   * <li>Mojo's <code>execute()</code> method, see maven-plugin-api</li>
65   * <li>MavenMultiPageReport's <code>generate( Sink, SinkFactory, Locale )</code>, see maven-reporting-api</li>
66   * </ul>
67   *
68   * @author <a href="evenisse@apache.org">Emmanuel Venisse</a>
69   * @since 2.0
70   * @see #execute() <code>Mojo.execute()</code>, from maven-plugin-api
71   * @see #generate(Sink, SinkFactory, Locale) <code>MavenMultiPageReport.generate( Sink, SinkFactory, Locale )</code>,
72   *  from maven-reporting-api
73   * @see #executeReport(Locale) <code>abstract executeReport( Locale )</code>
74   */
75  public abstract class AbstractMavenReport extends AbstractMojo implements MavenMultiPageReport {
76      /**
77       * The output directory for the report. Note that this parameter is only evaluated if the goal is run directly from
78       * the command line. If the goal is run indirectly as part of a site generation, the output directory configured in
79       * the Maven Site Plugin is used instead.
80       */
81      @Parameter(defaultValue = "${project.reporting.outputDirectory}", readonly = true, required = true)
82      protected File outputDirectory;
83  
84      /**
85       * The Maven Project.
86       */
87      @Parameter(defaultValue = "${project}", readonly = true, required = true)
88      protected MavenProject project;
89  
90      /**
91       * The mojo execution
92       */
93      @Parameter(defaultValue = "${mojoExecution}", readonly = true, required = true)
94      protected MojoExecution mojoExecution;
95  
96      /**
97       * The reactor projects.
98       */
99      @Parameter(defaultValue = "${reactorProjects}", required = true, readonly = true)
100     protected List<MavenProject> reactorProjects;
101 
102     /**
103      * Specifies the input encoding.
104      */
105     @Parameter(property = "encoding", defaultValue = "${project.build.sourceEncoding}", readonly = true)
106     private String inputEncoding;
107 
108     /**
109      * Specifies the output encoding.
110      */
111     @Parameter(property = "outputEncoding", defaultValue = "${project.reporting.outputEncoding}", readonly = true)
112     private String outputEncoding;
113 
114     /**
115      * The repository system session.
116      */
117     @Parameter(defaultValue = "${repositorySystemSession}", readonly = true, required = true)
118     protected RepositorySystemSession repoSession;
119 
120     /**
121      * Remote project repositories used for the project.
122      */
123     @Parameter(defaultValue = "${project.remoteProjectRepositories}", readonly = true, required = true)
124     protected List<RemoteRepository> remoteProjectRepositories;
125 
126     /**
127      * Directory containing the <code>site.xml</code> file.
128      */
129     @Parameter(defaultValue = "${basedir}/src/site")
130     protected File siteDirectory;
131 
132     /**
133      * The locale to use  when the report generation is invoked directly as a standalone Mojo.
134      * <p>
135      * <b>Default value is</b>: {@link SiteTool#DEFAULT_LOCALE}
136      *
137      * @see SiteTool#getSiteLocales(String)
138      */
139     @Parameter(defaultValue = "default")
140     protected String locale;
141 
142     /**
143      * Timestamp for reproducible output archive entries, either formatted as ISO 8601
144      * <code>yyyy-MM-dd'T'HH:mm:ssXXX</code> or as an int representing seconds since the epoch (like
145      * <a href="https://reproducible-builds.org/docs/source-date-epoch/">SOURCE_DATE_EPOCH</a>).
146      */
147     @Parameter(defaultValue = "${project.build.outputTimestamp}")
148     protected String outputTimestamp;
149 
150     /**
151      * SiteTool.
152      */
153     @Component
154     protected SiteTool siteTool;
155 
156     /**
157      * Doxia Site Renderer component.
158      */
159     @Component
160     protected Renderer siteRenderer;
161 
162     /** The current sink to use */
163     private Sink sink;
164 
165     /** The sink factory to use */
166     private SinkFactory sinkFactory;
167 
168     /** The current report output directory to use */
169     private File reportOutputDirectory;
170 
171     /**
172      * The report output format: null by default, to represent a site, but can be configured to a Doxia Sink id.
173      */
174     @Parameter(property = "output.format")
175     protected String outputFormat;
176 
177     @Component
178     private PlexusContainer container;
179 
180     /**
181      * This method is called when the report generation is invoked directly as a standalone Mojo.
182      * This implementation is now marked {@code final} as it is not expected to be overridden:
183      * {@code maven-reporting-impl} provides all necessary plumbing.
184      *
185      * @throws MojoExecutionException if an error occurs when generating the report
186      * @see org.apache.maven.plugin.Mojo#execute()
187      */
188     @Override
189     public final void execute() throws MojoExecutionException {
190         try {
191             if (!canGenerateReport()) {
192                 return;
193             }
194         } catch (MavenReportException e) {
195             throw new MojoExecutionException("Failed to determine whether report can be generated", e);
196         }
197 
198         if (outputFormat != null) {
199             reportToMarkup();
200         } else {
201             reportToSite();
202         }
203     }
204 
205     private void reportToMarkup() throws MojoExecutionException {
206         getLog().info("Rendering to " + outputFormat + " markup");
207 
208         if (!isExternalReport()) {
209             String filename = getOutputName() + '.' + outputFormat;
210             try {
211                 sinkFactory = container.lookup(SinkFactory.class, outputFormat);
212                 sink = sinkFactory.createSink(outputDirectory, filename);
213             } catch (ComponentLookupException cle) {
214                 throw new MojoExecutionException(
215                         "Cannot find SinkFactory for Doxia output format: " + outputFormat, cle);
216             } catch (IOException ioe) {
217                 throw new MojoExecutionException("Cannot create sink to " + new File(outputDirectory, filename), ioe);
218             }
219         }
220 
221         try {
222             Locale locale = getLocale();
223             generate(sink, sinkFactory, locale);
224         } catch (MavenReportException e) {
225             throw new MojoExecutionException(
226                     "An error has occurred in " + getName(Locale.ENGLISH) + " report generation.", e);
227         } finally {
228             if (sink != null) {
229                 sink.close();
230             }
231         }
232     }
233 
234     private void reportToSite() throws MojoExecutionException {
235         File outputDirectory = new File(getOutputDirectory());
236 
237         String filename = getOutputName() + ".html";
238 
239         Locale locale = getLocale();
240 
241         try {
242             SiteRenderingContext siteContext = createSiteRenderingContext(locale);
243 
244             // copy resources
245             getSiteRenderer().copyResources(siteContext, outputDirectory);
246 
247             String reportMojoInfo = mojoExecution.getPlugin().getId() + ":" + mojoExecution.getGoal();
248             DocumentRenderingContext docRenderingContext =
249                     new DocumentRenderingContext(outputDirectory, getOutputName(), reportMojoInfo);
250 
251             SiteRendererSink sink = new SiteRendererSink(docRenderingContext);
252 
253             // TODO Compared to Maven Site Plugin multipage reports will not work and fail with a NPE
254             generate(sink, null, locale);
255 
256             if (!isExternalReport()) // MSHARED-204: only render Doxia sink if not an external report
257             {
258                 outputDirectory.mkdirs();
259 
260                 try (Writer writer = new OutputStreamWriter(
261                         new FileOutputStream(new File(outputDirectory, filename)), getOutputEncoding())) {
262                     // render report
263                     getSiteRenderer().mergeDocumentIntoSite(writer, sink, siteContext);
264                 }
265             }
266 
267             // copy generated resources also
268             getSiteRenderer().copyResources(siteContext, outputDirectory);
269         } catch (RendererException | IOException | MavenReportException | SiteToolException e) {
270             throw new MojoExecutionException(
271                     "An error has occurred in " + getName(Locale.ENGLISH) + " report generation.", e);
272         }
273     }
274 
275     private SiteRenderingContext createSiteRenderingContext(Locale locale)
276             throws MavenReportException, IOException, SiteToolException {
277         SiteModel siteModel = siteTool.getSiteModel(
278                 siteDirectory, locale, project, reactorProjects, repoSession, remoteProjectRepositories);
279 
280         Map<String, Object> templateProperties = new HashMap<>();
281         // We tell the skin that we are rendering in standalone mode
282         templateProperties.put("standalone", Boolean.TRUE);
283         templateProperties.put("project", getProject());
284         templateProperties.put("inputEncoding", getInputEncoding());
285         templateProperties.put("outputEncoding", getOutputEncoding());
286         // Put any of the properties in directly into the Velocity context
287         for (Map.Entry<Object, Object> entry : getProject().getProperties().entrySet()) {
288             templateProperties.put((String) entry.getKey(), entry.getValue());
289         }
290 
291         SiteRenderingContext context;
292         try {
293             Artifact skinArtifact =
294                     siteTool.getSkinArtifactFromRepository(repoSession, remoteProjectRepositories, siteModel.getSkin());
295 
296             getLog().info(buffer().a("Rendering content with ")
297                     .strong(skinArtifact.getId() + " skin")
298                     .toString());
299 
300             context = siteRenderer.createContextForSkin(
301                     skinArtifact, templateProperties, siteModel, project.getName(), locale);
302         } catch (SiteToolException e) {
303             throw new MavenReportException("Failed to retrieve skin artifact", e);
304         } catch (RendererException e) {
305             throw new MavenReportException("Failed to create context for skin", e);
306         }
307 
308         // Add publish date
309         MavenArchiver.parseBuildOutputTimestamp(outputTimestamp).ifPresent(v -> {
310             context.setPublishDate(Date.from(v));
311         });
312 
313         // Generate static site
314         context.setRootDirectory(project.getBasedir());
315 
316         return context;
317     }
318 
319     /**
320      * Generate a report.
321      *
322      * @param sink the sink to use for the generation.
323      * @param locale the wanted locale to generate the report, could be null.
324      * @throws MavenReportException if any
325      * @deprecated use {@link #generate(Sink, SinkFactory, Locale)} instead.
326      */
327     @Deprecated
328     @Override
329     public void generate(Sink sink, Locale locale) throws MavenReportException {
330         generate(sink, null, locale);
331     }
332 
333     /**
334      * This method is called when the report generation is invoked by maven-site-plugin.
335      *
336      * @param sink
337      * @param sinkFactory
338      * @param locale
339      * @throws MavenReportException
340      */
341     @Override
342     public void generate(Sink sink, SinkFactory sinkFactory, Locale locale) throws MavenReportException {
343         this.sink = sink;
344         this.sinkFactory = sinkFactory;
345 
346         executeReport(locale);
347         closeReport();
348     }
349 
350     /**
351      * @return CATEGORY_PROJECT_REPORTS
352      */
353     @Override
354     public String getCategoryName() {
355         return CATEGORY_PROJECT_REPORTS;
356     }
357 
358     @Override
359     public File getReportOutputDirectory() {
360         if (reportOutputDirectory == null) {
361             reportOutputDirectory = new File(getOutputDirectory());
362         }
363 
364         return reportOutputDirectory;
365     }
366 
367     @Override
368     public void setReportOutputDirectory(File reportOutputDirectory) {
369         this.reportOutputDirectory = reportOutputDirectory;
370         this.outputDirectory = reportOutputDirectory;
371     }
372 
373     protected String getOutputDirectory() {
374         return outputDirectory.getAbsolutePath();
375     }
376 
377     protected MavenProject getProject() {
378         return project;
379     }
380 
381     protected Renderer getSiteRenderer() {
382         return siteRenderer;
383     }
384 
385     /**
386      * Gets the input files encoding.
387      *
388      * @return The input files encoding, never <code>null</code>.
389      */
390     protected String getInputEncoding() {
391         return (inputEncoding == null) ? ReaderFactory.FILE_ENCODING : inputEncoding;
392     }
393 
394     /**
395      * Gets the effective reporting output files encoding.
396      *
397      * @return The effective reporting output file encoding, never <code>null</code>.
398      */
399     protected String getOutputEncoding() {
400         return (outputEncoding == null) ? WriterFactory.UTF_8 : outputEncoding;
401     }
402 
403     /**
404      * Gets the locale
405      *
406      * @return the locale for this standalone report
407      */
408     protected Locale getLocale() {
409         return siteTool.getSiteLocales(locale).get(0);
410     }
411 
412     /**
413      * Actions when closing the report.
414      */
415     protected void closeReport() {
416         if (getSink() != null) {
417             getSink().close();
418         }
419     }
420 
421     /**
422      * @return the sink used
423      */
424     public Sink getSink() {
425         return sink;
426     }
427 
428     /**
429      * @return the sink factory used
430      */
431     public SinkFactory getSinkFactory() {
432         return sinkFactory;
433     }
434 
435     /**
436      * @see org.apache.maven.reporting.MavenReport#isExternalReport()
437      * @return {@code false} by default.
438      */
439     @Override
440     public boolean isExternalReport() {
441         return false;
442     }
443 
444     @Override
445     public boolean canGenerateReport() throws MavenReportException {
446         return true;
447     }
448 
449     /**
450      * Execute the generation of the report.
451      *
452      * @param locale the wanted locale to return the report's description, could be <code>null</code>.
453      * @throws MavenReportException if any
454      */
455     protected abstract void executeReport(Locale locale) throws MavenReportException;
456 }