NF14 Extensible Report Content Generation
IDEMPIERE-7070 introduces UI-independent and report-engine-independent extension points for generating and post-processing report content.
The feature allows plug-ins to provide alternative report renderers and to enhance generated documents without depending on the ZK report viewer or modifying iDempiere core classes.
Purpose
Before this change, the report extension mechanism was located in the ZK report viewer and exposed ZK- and Jasper-specific types.
This caused several limitations:
- The extension was not available to headless or server-side consumers.
- Report-generation paths calling
ReportEnginedirectly bypassed plug-in-provided output. - The mechanism was effectively tied to Jasper Reports.
- Multiple independent document enhancements could not be combined cleanly because only one renderer could handle the request.
- Preview and download names could expose the random suffix of an internal temporary Jasper file.
A report may need several consecutive operations, for example:
- Generate the initial PDF.
- Add ZUGFeRD/Factur-X data.
- Convert the document to PDF/A.
- Add PDF attachments.
- Add a watermark.
- Digitally sign or encrypt the final document.
IDEMPIERE-7070 separates initial report rendering from subsequent content processing, allowing these operations to be implemented by independent OSGi services and executed in a defined order.
Architecture
Report content generation consists of two extension stages:
ReportContentRequest
|
v
ranked IReportContentRendererFactory services
|
+-- first applicable renderer
|
+-- ReportEngine fallback
|
v
initial report content
|
v
all applicable IReportContentProcessor services
in descending OSGi service-ranking order
|
v
final processed content
Report content request
ReportContentRequest is the common input for rendering and post-processing:
public record ReportContentRequest(
ReportEngine reportEngine,
ProcessInfo processInfo,
String title,
boolean applyPostProcessing) {
}
The three-argument constructor enables post-processing by default:
ReportContentRequest request =
new ReportContentRequest(reportEngine, processInfo, reportTitle);
Post-processing can be disabled explicitly when required:
ReportContentRequest request =
new ReportContentRequest(reportEngine, processInfo, reportTitle, false);
The report context preserves the relevant process information, including:
- process parameters
- record ID and record UUID
- record selections
- language
- user and tenant context
- transaction context
- requested PDF filename
Stage 1: report content rendering
A plug-in can register an IReportContentRendererFactory OSGi service:
public interface IReportContentRendererFactory {
IReportContentRenderer createRenderer(ReportContentRequest request);
}
Factories are evaluated in descending OSGi service.ranking order. A factory returns:
- an
IReportContentRendererwhen it supports the request, or nullwhen it is not applicable.
The first applicable renderer is selected.
A renderer implements:
public interface IReportContentRenderer {
File getContent(String contentType, String fileExtension);
ReportContentType[] getSupportedContentTypes();
default int getRowCount() {
return -1;
}
}
Supported output types are described without any dependency on a UI toolkit:
public record ReportContentType(
String name,
String fileExtension,
String contentType) {
}
An example Declarative Services registration is:
@Component(
service = IReportContentRendererFactory.class,
immediate = true,
property = "service.ranking:Integer=100"
)
public class CustomReportContentRendererFactory
implements IReportContentRendererFactory {
@Override
public IReportContentRenderer createRenderer(
ReportContentRequest request) {
if (!supports(request))
return null;
return new CustomReportContentRenderer(request);
}
}
The default Jasper renderer is supplied by the org.adempiere.report.jasper bundle with service ranking 0. It is one provider of the generic API; the extension point is not limited to Jasper Reports.
The Jasper renderer supports the existing report viewer formats, including:
- HTML
- CSV
- XLS
- XLSX
- SSV
If no registered factory handles the request, Core uses the standard ReportEngine as a fallback. The fallback supports PDF, HTML, CSV, XLS and XLSX.
Stage 2: report content post-processing
After rendering, the generated content is passed through every applicable IReportContentProcessor service:
public interface IReportContentProcessor {
boolean isApplicable(
ReportContentRequest request,
String contentType,
String fileExtension);
File process(
ReportContentRequest request,
String contentType,
String fileExtension,
File input);
}
Processors are executed in descending OSGi service.ranking order. Each processor receives the result of the previous processor.
A processor may:
- modify the input file and return the same file, or
- create and return a new file.
It must not return null. A null result is treated as an error.
Typical processor use cases include:
- ZUGFeRD/Factur-X data
- PDF/A conversion
- PDF attachments
- watermarks
- digital signatures
- encryption
- other document transformations
An example registration is:
@Component(
service = IReportContentProcessor.class,
immediate = true,
property = "service.ranking:Integer=100"
)
public class CustomPDFProcessor
implements IReportContentProcessor {
@Override
public boolean isApplicable(
ReportContentRequest request,
String contentType,
String fileExtension) {
return "application/pdf".equals(contentType)
|| "pdf".equalsIgnoreCase(fileExtension);
}
@Override
public File process(
ReportContentRequest request,
String contentType,
String fileExtension,
File input) {
// Modify input or create a new output file.
return input;
}
}
The processor chain is applied both to custom renderer output and to content generated by the standard ReportEngine fallback.
Central Core API
The common API is provided through org.adempiere.base.Core.
The main entry points are:
IReportContentRenderer getReportContentRenderer(
ReportContentRequest request);
File getReportContent(
ReportContentRequest request,
String contentType,
String fileExtension);
File getReportContent(
ReportContentRequest request,
String contentType,
String fileExtension,
File outputFile);
File processReportContent(
ReportContentRequest request,
String contentType,
String fileExtension,
File content);
When an output file is supplied, only the final processed content is copied to that target. Intermediate renderer or processor files are not copied to it.
Failures during report generation are propagated instead of silently returning an incomplete result.
Report-generation paths using the API
The pull request routes the following report-generation paths through the common API:
- ZK report preview and export
- the legacy direct Jasper viewer
- report email attachments
- report archiving
- workflow reports
- server-side exports
- document PDF generation
- batch-print processes
- payment and remittance printing
- dunning and invoice printing
- relevant ZK report and print processes
Document generation for orders, invoices, shipments, distribution orders and RFQ responses is included in the affected server-side paths.
The existing direct-printer path remains unchanged.
Report viewer behavior
The ZK report viewer uses the common content renderer and processor chain.
Processed content is cached for each combination of MIME type and file extension. Preview, export, email and archive actions can therefore reuse the same final result.
This is important for processors such as:
- document signing
- attachment generation
- ZUGFeRD/Factur-X generation
- PDF/A conversion
Without caching, such processors could be executed more than once for the same viewer output.
The report viewer also:
- shows only formats supported by the active renderer or report engine;
- preserves the original
ProcessInfo; - preserves record UUIDs;
- refreshes native print data after changing the print format;
- uses the logical report name for preview and download media;
- retains unique filenames for internal temporary files.
Consequently, users no longer see the random suffix generated for an internal Jasper temporary file in the preview or download filename.
Compatibility
- Existing Jasper reports are handled by the default renderer in the Jasper bundle.
- The API is not limited to Jasper Reports.
- Other report engines can register their own renderer factory.
- Requests without a matching renderer fall back to
ReportEngine. - Installations without an
IReportContentProcessorreceive the original generated content unchanged. - Headless installations can use the renderer and processor APIs without the ZK bundle.
- Existing report viewer export formats are retained.
- Legacy
ZkJRVieweradapters are retained. - Direct-printer behavior is unchanged.
- No database migration is required.
Example use cases
ZUGFeRD/Factur-X
A processor can embed ZUGFeRD/Factur-X XML into a PDF produced by any renderer.
This integrates electronic invoice generation into the normal workflow:
Print button
-> Preview
-> E-mail
-> Archive
The same processor can be applied to a PDF produced by:
- the native report engine
- Jasper Reports
- a FreeMarker-based renderer
- another custom report engine
This complements the existing ZUGFeRD/XInvoice plug-in by allowing its result to participate in the standard report workflow.
PDF attachments
A processor can attach additional PDF documents to the generated report. For example, a drawing attached to an order can be included in the final order PDF.
Alternative report engines
The API was tested conceptually and practically with a FreeMarker-based print-template engine. Such a plug-in can examine the print format in ReportContentRequest, return its own renderer and use the same post-processing pipeline as Jasper or the native report engine.
Validation
Automated validation
The pull request contains integration coverage for:
- renderer selection by OSGi service ranking
- fallback to the next renderer factory
- fallback to the standard report engine
- default Jasper renderer registration
- supported output formats
- processor ordering
- processor applicability
- invalid null processor results
- enabling and disabling post-processing
- processed-content caching
- propagation of report-generation failures
After fixes made during review, the reported validation results were:
ServerReportCtlTest: 1 test, 0 failures, 0 errorsReportViewerContentRendererFactoryTest: 9 tests, 0 failures, 0 errors- complete 63-module Maven
verifyreactor: BUILD SUCCESS git diff --check: passed
Earlier failures concerning the requested PDF filename and missing server-side print data were fixed before the successful final validation.
Integration testing
The reported integration-test environment was:
| Component | Value |
|---|---|
| iDempiere | Version 14 development workspace |
| Java | OpenJDK 17.0.19 |
| Runtime | Eclipse PDE |
| Database | PostgreSQL |
| Database migration level | 202607261257_IDEMPIERE-6552.sql
|
| ZK | 10.3.0.1 |
| Locale | de_DE
|
The integration test used:
- a FreeMarker report renderer;
- a ZUGFeRD/Factur-X content processor;
- invoice
C_Invoice_ID=103.
The following flow was tested:
- Generate an invoice using the normal Print button.
- Render the invoice through the new report content pipeline.
- Post-process the generated PDF through
IReportContentProcessor. - Embed Factur-X/ZUGFeRD XML into the PDF.
- Combine the FreeMarker renderer with the ZUGFeRD processor.
Reported result:
Invoice_Header.pdfwas generated successfully.- The PDF contained an embedded
factur-x.xmlattachment. - The embedded XML was 9,711 bytes and began with a valid
CrossIndustryInvoicedocument. - The ZUGFeRD processor worked independently of the selected renderer.
- No remaining ZUGFeRD, PostgreSQL or PDFBox errors were reported in the server log after the final test.
Further review and broader testing of the affected report-generation paths and representative processor combinations are still recommended before merge.
Known limitation
Multi-record Jasper report handling is intentionally outside the scope of this change and is tracked separately in IDEMPIERE-7073.
