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