NF14 Extensible Report Content Generation

From iDempiere en
Revision as of 22:48, 12 September 2026 by Mbozem (talk | contribs)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)

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 ReportEngine directly 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:

  1. Generate the initial PDF.
  2. Add ZUGFeRD/Factur-X data.
  3. Convert the document to PDF/A.
  4. Add PDF attachments.
  5. Add a watermark.
  6. 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 IReportContentRenderer when it supports the request, or
  • null when 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:

  • PDF
  • 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 IReportContentProcessor receive 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 ZkJRViewer adapters 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 errors
  • ReportViewerContentRendererFactoryTest: 9 tests, 0 failures, 0 errors
  • complete 63-module Maven verify reactor: 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:

  1. Generate an invoice using the normal Print button.
  2. Render the invoice through the new report content pipeline.
  3. Post-process the generated PDF through IReportContentProcessor.
  4. Embed Factur-X/ZUGFeRD XML into the PDF.
  5. Combine the FreeMarker renderer with the ZUGFeRD processor.

Reported result:

  • Invoice_Header.pdf was generated successfully.
  • The PDF contained an embedded factur-x.xml attachment.
  • The embedded XML was 9,711 bytes and began with a valid CrossIndustryInvoice document.
  • 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.

Related work and examples

Cookies help us deliver our services. By using our services, you agree to our use of cookies.