From dd8fb4cbea1550e2a8f39d80e203675ca07b92a6 Mon Sep 17 00:00:00 2001 From: Sylwester Lachiewicz Date: Sat, 8 Aug 2026 16:21:52 +0200 Subject: [PATCH] Support multipage reports when the goal is invoked directly reportToSite() passed a null SinkFactory to generate(), so any report that creates sub-sinks for additional pages failed with an NPE as soon as its goal was run from the command line rather than through the site. The maven-plugin-report-plugin report is one such report. Provide the same MultiPageSinkFactory that Maven Site Plugin's ReportDocumentRenderer provides, collect the sub-sinks it hands out and merge each of them into the site next to the main document. The integration test for this was already written and disabled with a pointer to the issue, so it is simply enabled again. --- src/it/use-as-direct-mojo/invoker.properties | 2 +- src/it/use-as-direct-mojo/verify.groovy | 8 +- .../maven/reporting/AbstractMavenReport.java | 120 +++++++++++++++++- 3 files changed, 124 insertions(+), 6 deletions(-) diff --git a/src/it/use-as-direct-mojo/invoker.properties b/src/it/use-as-direct-mojo/invoker.properties index f26b482..b57113c 100644 --- a/src/it/use-as-direct-mojo/invoker.properties +++ b/src/it/use-as-direct-mojo/invoker.properties @@ -18,4 +18,4 @@ invoker.goals.1 = custom-reporting:custom invoker.goals.2 = custom-reporting:custom-renderer invoker.goals.3 = custom-reporting:external -#invoker.goals.4 = custom-reporting:multi-page # https://github.com/apache/maven-reporting-impl/issues/217 +invoker.goals.4 = custom-reporting:multi-page diff --git a/src/it/use-as-direct-mojo/verify.groovy b/src/it/use-as-direct-mojo/verify.groovy index 3db9394..da36707 100644 --- a/src/it/use-as-direct-mojo/verify.groovy +++ b/src/it/use-as-direct-mojo/verify.groovy @@ -34,10 +34,12 @@ f = new File( outputDir, 'external/report.html' ); assert f.exists(); assert f.text.contains( '

External Report

' ); -// https://github.com/apache/maven-reporting-impl/issues/217 -/*f = new File( outputDir, 'multi-page.html' ); +f = new File( outputDir, 'multi-page.html' ); assert f.exists(); +assert f.text.contains( 'Custom Maven Report with Renderer content.' ); + f = new File( outputDir, 'multi-second.html' ); -assert f.exists();*/ +assert f.exists(); +assert f.text.contains( 'Custom Maven Report with Renderer content.' ); return true; diff --git a/src/main/java/org/apache/maven/reporting/AbstractMavenReport.java b/src/main/java/org/apache/maven/reporting/AbstractMavenReport.java index c2dd8b5..86f2531 100644 --- a/src/main/java/org/apache/maven/reporting/AbstractMavenReport.java +++ b/src/main/java/org/apache/maven/reporting/AbstractMavenReport.java @@ -23,9 +23,11 @@ import java.io.File; import java.io.FileOutputStream; import java.io.IOException; +import java.io.OutputStream; import java.io.OutputStreamWriter; import java.io.Writer; import java.nio.file.Path; +import java.util.ArrayList; import java.util.Collections; import java.util.Date; import java.util.HashMap; @@ -55,6 +57,7 @@ import org.apache.maven.shared.utils.WriterFactory; import org.codehaus.plexus.PlexusContainer; import org.codehaus.plexus.component.repository.exception.ComponentLookupException; +import org.codehaus.plexus.util.PathTool; import org.codehaus.plexus.util.ReaderFactory; import org.eclipse.aether.RepositorySystemSession; import org.eclipse.aether.repository.RemoteRepository; @@ -280,9 +283,10 @@ private void reportToSite() throws MojoExecutionException { new DocumentRenderingContext(outputDirectory, getOutputPath(), reportMojoInfo); SiteRendererSink sink = new SiteRendererSink(docRenderingContext); + // sink factory, for multipage reports that need sub-sinks + MultiPageSinkFactory multiPageSinkFactory = new MultiPageSinkFactory(this, docRenderingContext); - // TODO Compared to Maven Site Plugin multipage reports will not work and fail with an NPE - generate(sink, null, locale); + generate(sink, multiPageSinkFactory, locale); if (!isExternalReport()) { // MSHARED-204: only render Doxia sink if not an external report outputDirectory.mkdirs(); @@ -292,6 +296,27 @@ private void reportToSite() throws MojoExecutionException { // render report getSiteRenderer().mergeDocumentIntoSite(writer, sink, siteContext); } + + // render the subpages eventually created by a multipage report + for (MultiPageSubSink subSink : multiPageSinkFactory.sinks()) { + File subOutputDirectory = subSink.getOutputDirectory(); + subOutputDirectory.mkdirs(); + + // the directory comes from the report, so make sure it is absolute before relativizing it + File subOutputFile = new File(subOutputDirectory, subSink.getOutputName()); + getLog().info("Rendering report to " + + getProject() + .getBasedir() + .toPath() + .relativize(subOutputFile.toPath().toAbsolutePath())); + + try (Writer writer = + new OutputStreamWriter(new FileOutputStream(subOutputFile), getOutputEncoding())) { + getSiteRenderer().mergeDocumentIntoSite(writer, subSink, siteContext); + } finally { + subSink.close(); + } + } } // copy generated resources also @@ -302,6 +327,97 @@ private void reportToSite() throws MojoExecutionException { } } + /** + * A sink for one subpage of a multipage report, remembering where it is meant to be written to. + */ + private static class MultiPageSubSink extends SiteRendererSink { + private final File outputDirectory; + + private final String outputName; + + MultiPageSubSink(File outputDirectory, String outputName, DocumentRenderingContext docRenderingContext) { + super(docRenderingContext); + this.outputDirectory = outputDirectory; + this.outputName = outputName; + } + + String getOutputName() { + return outputName; + } + + File getOutputDirectory() { + return outputDirectory; + } + } + + /** + * The sink factory handed to {@link #generate(Sink, SinkFactory, Locale)}, mirroring what Maven Site Plugin + * provides so that a multipage report behaves the same when its goal is invoked directly. + */ + private static class MultiPageSinkFactory implements SinkFactory { + /** + * The report that is (maybe) generating multiple pages + */ + private final MavenReport report; + + /** + * The main DocumentRenderingContext, which is the base for the DocumentRenderingContext of subpages + */ + private final DocumentRenderingContext docRenderingContext; + + /** + * List of sinks (subpages) associated to this report + */ + private final List sinks = new ArrayList<>(); + + MultiPageSinkFactory(MavenReport report, DocumentRenderingContext docRenderingContext) { + this.report = report; + this.docRenderingContext = docRenderingContext; + } + + @Override + public Sink createSink(File outputDirectory, String outputName) { + // Create a new document rendering context, similar to the main one, but with a different output name + String document = PathTool.getRelativeFilePath( + report.getReportOutputDirectory().getPath(), new File(outputDirectory, outputName).getPath()); + // Remove .html suffix since we know that we are in Site Renderer context + document = document.substring(0, document.lastIndexOf('.')); + + DocumentRenderingContext subSinkContext = new DocumentRenderingContext( + docRenderingContext.getBasedir(), document, docRenderingContext.getGenerator()); + + // Create a sink for this subpage, based on this new document rendering context + MultiPageSubSink sink = new MultiPageSubSink(outputDirectory, outputName, subSinkContext); + + // Add it to the list of sinks associated to this report + sinks.add(sink); + + return sink; + } + + @Override + public Sink createSink(File outputDir, String outputName, String encoding) { + throw new UnsupportedOperationException( + "Only createSink(File, String) is supported by MultiPageSinkFactory. The encoding is always determined by the site rendering context."); + } + + @Override + public Sink createSink(OutputStream out) { + throw new UnsupportedOperationException( + "Only createSink(File, String) is supported by MultiPageSinkFactory. OutputStream based sinks are not supported."); + } + + @Override + public Sink createSink(OutputStream out, String encoding) { + throw new UnsupportedOperationException( + "Only createSink(File, String) is supported by MultiPageSinkFactory. OutputStream based sinks are not supported."); + } + + List sinks() { + return sinks; + } + } + private SiteRenderingContext createSiteRenderingContext(Locale locale) throws MavenReportException, IOException, SiteToolException { SiteModel siteModel = siteTool.getSiteModel(