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      *
183      * @throws MojoExecutionException if an error occurs when generating the report
184      * @see org.apache.maven.plugin.Mojo#execute()
185      */
186     @Override
187     public void execute() throws MojoExecutionException {
188         try {
189             if (!canGenerateReport()) {
190                 return;
191             }
192         } catch (MavenReportException e) {
193             throw new MojoExecutionException("Failed to determine whether report can be generated", e);
194         }
195 
196         if (outputFormat != null) {
197             reportToMarkup();
198         } else {
199             reportToSite();
200         }
201     }
202 
203     private void reportToMarkup() throws MojoExecutionException {
204         getLog().info("Rendering to " + outputFormat + " markup");
205 
206         if (!isExternalReport()) {
207             String filename = getOutputName() + '.' + outputFormat;
208             try {
209                 sinkFactory = container.lookup(SinkFactory.class, outputFormat);
210                 sink = sinkFactory.createSink(outputDirectory, filename);
211             } catch (ComponentLookupException cle) {
212                 throw new MojoExecutionException(
213                         "Cannot find SinkFactory for Doxia output format: " + outputFormat, cle);
214             } catch (IOException ioe) {
215                 throw new MojoExecutionException("Cannot create sink to " + new File(outputDirectory, filename), ioe);
216             }
217         }
218 
219         try {
220             Locale locale = getLocale();
221             generate(sink, sinkFactory, locale);
222         } catch (MavenReportException e) {
223             throw new MojoExecutionException(
224                     "An error has occurred in " + getName(Locale.ENGLISH) + " report generation.", e);
225         } finally {
226             if (sink != null) {
227                 sink.close();
228             }
229         }
230     }
231 
232     private void reportToSite() throws MojoExecutionException {
233         File outputDirectory = new File(getOutputDirectory());
234 
235         String filename = getOutputName() + ".html";
236 
237         Locale locale = getLocale();
238 
239         try {
240             SiteRenderingContext siteContext = createSiteRenderingContext(locale);
241 
242             // copy resources
243             getSiteRenderer().copyResources(siteContext, outputDirectory);
244 
245             String reportMojoInfo = mojoExecution.getPlugin().getId() + ":" + mojoExecution.getGoal();
246             DocumentRenderingContext docRenderingContext =
247                     new DocumentRenderingContext(outputDirectory, getOutputName(), reportMojoInfo);
248 
249             SiteRendererSink sink = new SiteRendererSink(docRenderingContext);
250 
251             // TODO Compared to Maven Site Plugin multipage reports will not work and fail with a NPE
252             generate(sink, null, locale);
253 
254             if (!isExternalReport()) // MSHARED-204: only render Doxia sink if not an external report
255             {
256                 outputDirectory.mkdirs();
257 
258                 try (Writer writer = new OutputStreamWriter(
259                         new FileOutputStream(new File(outputDirectory, filename)), getOutputEncoding())) {
260                     // render report
261                     getSiteRenderer().mergeDocumentIntoSite(writer, sink, siteContext);
262                 }
263             }
264 
265             // copy generated resources also
266             getSiteRenderer().copyResources(siteContext, outputDirectory);
267         } catch (RendererException | IOException | MavenReportException | SiteToolException e) {
268             throw new MojoExecutionException(
269                     "An error has occurred in " + getName(Locale.ENGLISH) + " report generation.", e);
270         }
271     }
272 
273     private SiteRenderingContext createSiteRenderingContext(Locale locale)
274             throws MavenReportException, IOException, SiteToolException {
275         SiteModel siteModel = siteTool.getSiteModel(
276                 siteDirectory, locale, project, reactorProjects, repoSession, remoteProjectRepositories);
277 
278         Map<String, Object> templateProperties = new HashMap<>();
279         // We tell the skin that we are rendering in standalone mode
280         templateProperties.put("standalone", Boolean.TRUE);
281         templateProperties.put("project", getProject());
282         templateProperties.put("inputEncoding", getInputEncoding());
283         templateProperties.put("outputEncoding", getOutputEncoding());
284         // Put any of the properties in directly into the Velocity context
285         for (Map.Entry<Object, Object> entry : getProject().getProperties().entrySet()) {
286             templateProperties.put((String) entry.getKey(), entry.getValue());
287         }
288 
289         SiteRenderingContext context;
290         try {
291             Artifact skinArtifact =
292                     siteTool.getSkinArtifactFromRepository(repoSession, remoteProjectRepositories, siteModel.getSkin());
293 
294             getLog().info(buffer().a("Rendering content with ")
295                     .strong(skinArtifact.getId() + " skin")
296                     .toString());
297 
298             context = siteRenderer.createContextForSkin(
299                     skinArtifact, templateProperties, siteModel, project.getName(), locale);
300         } catch (SiteToolException e) {
301             throw new MavenReportException("Failed to retrieve skin artifact", e);
302         } catch (RendererException e) {
303             throw new MavenReportException("Failed to create context for skin", e);
304         }
305 
306         // Add publish date
307         MavenArchiver.parseBuildOutputTimestamp(outputTimestamp).ifPresent(v -> {
308             context.setPublishDate(Date.from(v));
309         });
310 
311         // Generate static site
312         context.setRootDirectory(project.getBasedir());
313 
314         return context;
315     }
316 
317     /**
318      * Generate a report.
319      *
320      * @param sink the sink to use for the generation.
321      * @param locale the wanted locale to generate the report, could be null.
322      * @throws MavenReportException if any
323      * @deprecated use {@link #generate(Sink, SinkFactory, Locale)} instead.
324      */
325     @Deprecated
326     @Override
327     public void generate(Sink sink, Locale locale) throws MavenReportException {
328         generate(sink, null, locale);
329     }
330 
331     /**
332      * This method is called when the report generation is invoked by maven-site-plugin.
333      *
334      * @param sink
335      * @param sinkFactory
336      * @param locale
337      * @throws MavenReportException
338      */
339     @Override
340     public void generate(Sink sink, SinkFactory sinkFactory, Locale locale) throws MavenReportException {
341         this.sink = sink;
342         this.sinkFactory = sinkFactory;
343 
344         executeReport(locale);
345         closeReport();
346     }
347 
348     /**
349      * @return CATEGORY_PROJECT_REPORTS
350      */
351     @Override
352     public String getCategoryName() {
353         return CATEGORY_PROJECT_REPORTS;
354     }
355 
356     @Override
357     public File getReportOutputDirectory() {
358         if (reportOutputDirectory == null) {
359             reportOutputDirectory = new File(getOutputDirectory());
360         }
361 
362         return reportOutputDirectory;
363     }
364 
365     @Override
366     public void setReportOutputDirectory(File reportOutputDirectory) {
367         this.reportOutputDirectory = reportOutputDirectory;
368         this.outputDirectory = reportOutputDirectory;
369     }
370 
371     protected String getOutputDirectory() {
372         return outputDirectory.getAbsolutePath();
373     }
374 
375     protected MavenProject getProject() {
376         return project;
377     }
378 
379     protected Renderer getSiteRenderer() {
380         return siteRenderer;
381     }
382 
383     /**
384      * Gets the input files encoding.
385      *
386      * @return The input files encoding, never <code>null</code>.
387      */
388     protected String getInputEncoding() {
389         return (inputEncoding == null) ? ReaderFactory.FILE_ENCODING : inputEncoding;
390     }
391 
392     /**
393      * Gets the effective reporting output files encoding.
394      *
395      * @return The effective reporting output file encoding, never <code>null</code>.
396      */
397     protected String getOutputEncoding() {
398         return (outputEncoding == null) ? WriterFactory.UTF_8 : outputEncoding;
399     }
400 
401     /**
402      * Gets the locale
403      *
404      * @return the locale for this standalone report
405      */
406     protected Locale getLocale() {
407         return siteTool.getSiteLocales(locale).get(0);
408     }
409 
410     /**
411      * Actions when closing the report.
412      */
413     protected void closeReport() {
414         if (getSink() != null) {
415             getSink().close();
416         }
417     }
418 
419     /**
420      * @return the sink used
421      */
422     public Sink getSink() {
423         return sink;
424     }
425 
426     /**
427      * @return the sink factory used
428      */
429     public SinkFactory getSinkFactory() {
430         return sinkFactory;
431     }
432 
433     /**
434      * @see org.apache.maven.reporting.MavenReport#isExternalReport()
435      * @return {@code false} by default.
436      */
437     @Override
438     public boolean isExternalReport() {
439         return false;
440     }
441 
442     @Override
443     public boolean canGenerateReport() throws MavenReportException {
444         return true;
445     }
446 
447     /**
448      * Execute the generation of the report.
449      *
450      * @param locale the wanted locale to return the report's description, could be <code>null</code>.
451      * @throws MavenReportException if any
452      */
453     protected abstract void executeReport(Locale locale) throws MavenReportException;
454 }