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