Scriptlets, Properties & Extensions

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

Baseline date: 2026-10-09. A JasperReports template describes the layout and the engine does the rest, but sooner or later a report needs something the template language cannot say: a calculation that does not fit in an expression, a setting that must differ per report or per tenant, or a behaviour added to every report of an application. JasperReports has three mechanisms for this, and they are the subject of this page. Scriptlets are Java objects that the engine calls at fixed points of the fill. Properties are named settings that can be given globally, per context, per report or per element, and are read through JRPropertiesUtil. Extensions are objects of known types (a scriptlet factory, a components bundle, a font family) that the engine finds through a registry configured in jasperreports_extension.properties. The running example is the invoice and the monthly sales report of the Bookshelf Billing service.

All examples were compiled with Java 21 and run against JasperReports 7.0.8 in a throwaway Maven project, and the PDFs they produce were read back to check the text, the metadata and the drawn shapes. The two Maven dependencies are net.sf.jasperreports:jasperreports:7.0.8 and net.sf.jasperreports:jasperreports-pdf:7.0.8; the Java expressions were compiled with the JDK compiler, because jasperreports-jdt was not on the class path (the trade-off is explained in JasperReports Architecture & Lifecycle). Templates use the v7 JRXML syntax (<element kind="...">), described in JRXML Report Design, and expressions are described in Expressions, Parameters, Fields & Variables. Findings about behaviour are marked as observed in 7.0.8, and the one example that was not run is stated as such.

Scriptlets

A scriptlet is a Java class that extends JRDefaultScriptlet or JRAbstractScriptlet. The engine creates it for each fill and calls its methods at the events of the fill lifecycle: before and after the report, a page, a column, a group and a detail row are initialised or evaluated. Inside those methods the code can read fields, variables and parameters and can write variables. The official scriptlet sample describes the two base classes in the same way: JRAbstractScriptlet has abstract methods that must all be implemented, and JRDefaultScriptlet implements all of them as empty methods, so a subclass overrides only the events it needs.

Use a scriptlet when a calculation needs Java that does not belong in a template expression: building a list in memory while the rows are printed, calling a library, keeping state across rows, or logging. For a formatting helper or a single calculation, a static method called from an expression is simpler, and for sums, counts and averages the built-in variable calculations are simpler still and need no code.

The hooks

The ten hooks and the moment each one is called are in the table. The group hooks receive the name of the group.

Hook Called

beforeReportInit, afterReportInit

Around the initialisation of the report, before the first row is read.

beforePageInit, afterPageInit

Around the initialisation of each page.

beforeColumnInit, afterColumnInit

Around the initialisation of each column.

beforeGroupInit(String), afterGroupInit(String)

Around the initialisation of a group, each time its expression changes value; the argument is the group name.

beforeDetailEval, afterDetailEval

Around the evaluation of each detail row.

"Init" hooks are therefore about structure (a new page, a new group value) and the two "eval" hooks are about rows. The order in which they fire is not obvious from the names, so it was recorded. The report in the second example below, with a group on category and three rows, produced this sequence in 7.0.8:

beforeReportInit
beforePageInit
beforeColumnInit
beforeGroupInit CATEGORY category=Fiction
afterGroupInit CATEGORY
afterColumnInit
afterPageInit
afterReportInit
beforeDetailEval The Pragmatic Novel
afterDetailEval The Pragmatic Novel
beforeDetailEval Garden of Forking Paths
afterDetailEval Garden of Forking Paths
beforeGroupInit CATEGORY category=Science
afterGroupInit CATEGORY
beforeDetailEval Entropy for Librarians
afterDetailEval Entropy for Librarians

Three things follow from it. The page, column and group initialisation of the first page happens while the first row is being prefetched, so all of it comes before the first beforeDetailEval. When a group value changes, beforeGroupInit already sees the fields of the first row of the new group (category=Science above), because the engine has read that row to find out that the value changed. And the first call of beforePageInit happens before the page counter has a value: reading PAGE_NUMBER there returned null on the first page in the first example.

A scriptlet that computes totals

The invoice of Bookshelf Billing shows the net amount and the VAT. The scriptlet below keeps a running net total in a field while the rows are evaluated and writes two report variables from it. The variables are declared with calculation="System", which tells the engine not to calculate them itself and to keep whatever value the scriptlet sets. The VAT rate in the code is an illustrative number, not tax advice. The class also has an ordinary method, called from an expression of the template to build the invoice number; that is the other way a scriptlet is used.

The bean for the invoice lines is a plain class with getters.

src/main/java/bookshelf/InvoiceLine.java
package bookshelf;

import java.math.BigDecimal;

public class InvoiceLine {

    private final String title;
    private final int quantity;
    private final BigDecimal unitPrice;

    public InvoiceLine(String title, int quantity, BigDecimal unitPrice) {
        this.title = title;
        this.quantity = quantity;
        this.unitPrice = unitPrice;
    }

    public String getTitle() {
        return title;
    }

    public int getQuantity() {
        return quantity;
    }

    public BigDecimal getUnitPrice() {
        return unitPrice;
    }
}
src/main/java/bookshelf/InvoiceScriptlet.java
package bookshelf;

import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.ArrayList;
import java.util.List;

import net.sf.jasperreports.engine.JRDefaultScriptlet;
import net.sf.jasperreports.engine.JRScriptletException;

/**
 * Fill-time hooks for the invoice template. One instance is created for each fill.
 */
public class InvoiceScriptlet extends JRDefaultScriptlet {

    private static final BigDecimal VAT_RATE = new BigDecimal("0.04"); // illustrative rate, not tax advice

    private final List<String> events = new ArrayList<>();
    private BigDecimal net = BigDecimal.ZERO;

    @Override
    public void beforeReportInit() throws JRScriptletException {
        events.add("beforeReportInit");
    }

    @Override
    public void afterReportInit() throws JRScriptletException {
        events.add("afterReportInit");
    }

    @Override
    public void beforePageInit() throws JRScriptletException {
        events.add("beforePageInit page=" + getVariableValue("PAGE_NUMBER"));
    }

    @Override
    public void afterPageInit() throws JRScriptletException {
        events.add("afterPageInit");
    }

    @Override
    public void beforeDetailEval() throws JRScriptletException {
        // The fields already hold the row that is about to be printed.
        events.add("beforeDetailEval " + getFieldValue("title"));
    }

    @Override
    public void afterDetailEval() throws JRScriptletException {
        // Derive report-level values from the row that has just been evaluated.
        Integer quantity = (Integer) getFieldValue("quantity");
        BigDecimal unitPrice = (BigDecimal) getFieldValue("unitPrice");
        net = net.add(unitPrice.multiply(BigDecimal.valueOf(quantity)));
        setVariableValue("NET_TOTAL", net);
        setVariableValue("VAT_TOTAL", net.multiply(VAT_RATE).setScale(2, RoundingMode.HALF_UP));
        events.add("afterDetailEval net=" + net);
    }

    /** Plain methods are called from expressions as $P{REPORT_SCRIPTLET}.invoiceNumber(...). */
    public String invoiceNumber(int year, int sequence) {
        return String.format("BB-%d-%05d", year, sequence);
    }

    public List<String> getEvents() {
        return events;
    }
}

The template names the class in the scriptletClass attribute of the root element. Its expressions reach the scriptlet through the built-in parameter $P{REPORT_SCRIPTLET}, whose value is the scriptlet instance, so a cast gives access to its own methods. The variables set by the scriptlet are used like any other variable.

src/main/resources/reports/invoice-scriptlet.jrxml
<jasperReport name="invoice-scriptlet" language="java" pageWidth="595" pageHeight="842"
              columnWidth="515" leftMargin="40" rightMargin="40" topMargin="40" bottomMargin="40"
              scriptletClass="bookshelf.InvoiceScriptlet">
  <parameter name="YEAR" class="java.lang.Integer"/>
  <parameter name="SEQUENCE" class="java.lang.Integer"/>
  <field name="title" class="java.lang.String"/>
  <field name="quantity" class="java.lang.Integer"/>
  <field name="unitPrice" class="java.math.BigDecimal"/>
  <variable name="NET_TOTAL" class="java.math.BigDecimal" calculation="System"/>
  <variable name="VAT_TOTAL" class="java.math.BigDecimal" calculation="System"/>
  <title height="40">
    <element kind="textField" x="0" y="0" width="515" height="30" fontSize="18" bold="true">
      <expression><![CDATA["Invoice " + ((bookshelf.InvoiceScriptlet) $P{REPORT_SCRIPTLET}).invoiceNumber($P{YEAR}, $P{SEQUENCE})]]></expression>
    </element>
  </title>
  <detail>
    <band height="20">
      <element kind="textField" x="0" y="0" width="300" height="18">
        <expression><![CDATA[$F{title}]]></expression>
      </element>
      <element kind="textField" x="300" y="0" width="50" height="18" hTextAlign="Right">
        <expression><![CDATA[$F{quantity}]]></expression>
      </element>
      <element kind="textField" x="350" y="0" width="165" height="18" hTextAlign="Right" pattern="#,##0.00">
        <expression><![CDATA[$F{unitPrice}.multiply(new java.math.BigDecimal($F{quantity}))]]></expression>
      </element>
    </band>
  </detail>
  <summary height="50">
    <element kind="textField" x="0" y="0" width="515" height="18" hTextAlign="Right" pattern="#,##0.00">
      <expression><![CDATA["Net " + new java.text.DecimalFormat("#,##0.00").format($V{NET_TOTAL})]]></expression>
    </element>
    <element kind="textField" x="0" y="20" width="515" height="18" hTextAlign="Right">
      <expression><![CDATA["VAT " + new java.text.DecimalFormat("#,##0.00").format($V{VAT_TOTAL})]]></expression>
    </element>
  </summary>
</jasperReport>

The driver fills the report from a bean collection, prints the events the scriptlet recorded, and exports the PDF. The scriptlet was found in the parameters map after the fill, under REPORT_SCRIPTLET, which is how the events were read; that is behaviour observed in 7.0.8, and a scriptlet that must hand data out is more robust if it writes into an object the caller passes as a parameter, as the next example does.

src/main/java/bookshelf/ScriptletDemo.java
package bookshelf;

import java.io.InputStream;
import java.math.BigDecimal;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import net.sf.jasperreports.engine.JasperCompileManager;
import net.sf.jasperreports.engine.JasperExportManager;
import net.sf.jasperreports.engine.JasperFillManager;
import net.sf.jasperreports.engine.JasperPrint;
import net.sf.jasperreports.engine.JasperReport;
import net.sf.jasperreports.engine.data.JRBeanCollectionDataSource;

public class ScriptletDemo {

    public static void main(String[] args) throws Exception {
        JasperReport report;
        try (InputStream in = ScriptletDemo.class.getResourceAsStream("/reports/invoice-scriptlet.jrxml")) {
            report = JasperCompileManager.compileReport(in);
        }

        Map<String, Object> params = new HashMap<>();
        params.put("YEAR", 2026);
        params.put("SEQUENCE", 42);

        List<InvoiceLine> lines = List.of(
                new InvoiceLine("The Pragmatic Novel", 3, new BigDecimal("14.99")),
                new InvoiceLine("Entropy for Librarians", 2, new BigDecimal("29.50")));

        JasperPrint print = JasperFillManager.fillReport(report, params, new JRBeanCollectionDataSource(lines));

        // The filled report keeps no reference to the scriptlet, so read the scriptlet through the parameter
        // value the engine put in the parameters map.
        InvoiceScriptlet scriptlet = (InvoiceScriptlet) params.get("REPORT_SCRIPTLET");
        if (scriptlet != null) {
            scriptlet.getEvents().forEach(System.out::println);
        } else {
            System.out.println("REPORT_SCRIPTLET not in the parameters map");
        }

        Files.createDirectories(Path.of("target"));
        JasperExportManager.exportReportToPdfFile(print, "target/invoice-scriptlet.pdf");
        System.out.println("pages: " + print.getPages().size());
    }
}

The run printed the event list (beforeReportInit, beforePageInit, afterPageInit, afterReportInit, then a beforeDetailEval and afterDetailEval pair per line, with the net total 44.97 after the first line and 103.97 after the second) and pages: 1. The text of the PDF was Invoice BB-2026-00042, the two titles with their line amounts (44,97 and 59,00), Net 103,97 and VAT 4,16, so the scriptlet’s variables reached the summary band. The decimal separator follows the JVM locale; the run used a Spanish locale, hence the commas. Running the fill twice gave two different scriptlet instances, each with its own event list, so a scriptlet may keep per-fill state in fields; it must not keep state that should survive to the next fill.

The code derives from the JasperReports scriptlet sample.

Named scriptlets and JRAbstractScriptlet

A report can declare more than one scriptlet with the <scriptlet name="..." class="..."/> element. Each gets a parameter named after it with the suffix _SCRIPTLET, so a scriptlet called audit is available as $P{audit_SCRIPTLET}. The default scriptlet of the scriptletClass attribute stays in $P{REPORT_SCRIPTLET}. In 7.0.8 a report with only a named scriptlet and no scriptletClass had null in $P{REPORT_SCRIPTLET}; the expression $P{REPORT_SCRIPTLET}.getClass() in such a report throws, so the example uses the named parameter.

The second scriptlet extends JRAbstractScriptlet to show the contract: every hook has to be implemented. It reads a list that the caller passes as the report parameter AUDIT_LOG and appends one line per event. Passing a collaborator in a parameter is also the way to give a scriptlet access to a Spring bean, because the engine, not Spring, creates the scriptlet. getParameterValue, getFieldValue and getVariableValue take the name without the $P{}, $F{} or $V{} decoration, and setVariableValue can only write variables.

src/main/java/bookshelf/AuditScriptlet.java
package bookshelf;

import java.util.List;

import net.sf.jasperreports.engine.JRAbstractScriptlet;
import net.sf.jasperreports.engine.JRScriptletException;

/**
 * Extends JRAbstractScriptlet directly, so every hook must be implemented. The audit list is handed in as a
 * report parameter, which is also how a Spring bean would reach a scriptlet.
 */
public class AuditScriptlet extends JRAbstractScriptlet {

    @SuppressWarnings("unchecked")
    private void log(String message) throws JRScriptletException {
        List<String> audit = (List<String>) getParameterValue("AUDIT_LOG");
        audit.add(message);
    }

    @Override
    public void beforeReportInit() throws JRScriptletException {
        log("beforeReportInit");
    }

    @Override
    public void afterReportInit() throws JRScriptletException {
        log("afterReportInit");
    }

    @Override
    public void beforePageInit() throws JRScriptletException {
        log("beforePageInit");
    }

    @Override
    public void afterPageInit() throws JRScriptletException {
        log("afterPageInit");
    }

    @Override
    public void beforeColumnInit() throws JRScriptletException {
        log("beforeColumnInit");
    }

    @Override
    public void afterColumnInit() throws JRScriptletException {
        log("afterColumnInit");
    }

    @Override
    public void beforeGroupInit(String groupName) throws JRScriptletException {
        log("beforeGroupInit " + groupName + " category=" + getFieldValue("category"));
    }

    @Override
    public void afterGroupInit(String groupName) throws JRScriptletException {
        log("afterGroupInit " + groupName);
    }

    @Override
    public void beforeDetailEval() throws JRScriptletException {
        log("beforeDetailEval " + getFieldValue("title"));
    }

    @Override
    public void afterDetailEval() throws JRScriptletException {
        log("afterDetailEval " + getFieldValue("title"));
    }
}
src/main/resources/reports/grouped-audit.jrxml
<jasperReport name="grouped-audit" language="java" pageWidth="595" pageHeight="842"
              columnWidth="515" leftMargin="40" rightMargin="40" topMargin="40" bottomMargin="40">
  <scriptlet name="audit" class="bookshelf.AuditScriptlet"/>
  <parameter name="AUDIT_LOG" class="java.util.List"/>
  <field name="category" class="java.lang.String"/>
  <field name="title" class="java.lang.String"/>
  <title height="24">
    <element kind="textField" x="0" y="0" width="515" height="20">
      <expression><![CDATA["Scriptlet in use: " + $P{audit_SCRIPTLET}.getClass().getSimpleName()]]></expression>
    </element>
  </title>
  <group name="CATEGORY">
    <expression><![CDATA[$F{category}]]></expression>
    <groupHeader>
      <band height="22">
        <element kind="textField" x="0" y="0" width="515" height="20" bold="true">
          <expression><![CDATA[$F{category}]]></expression>
        </element>
      </band>
    </groupHeader>
  </group>
  <detail>
    <band height="20">
      <element kind="textField" x="0" y="0" width="515" height="18">
        <expression><![CDATA[$F{title}]]></expression>
      </element>
    </band>
  </detail>
</jasperReport>

The driver uses JRMapCollectionDataSource. The parameters are a HashMap on purpose: a Map.of map is rejected by the engine as the parameters map of a fill (a finding that applies to every example of this section), and the rows are copied into HashMap instances for the same reason of caution.

src/main/java/bookshelf/AuditDemo.java
package bookshelf;

import java.io.InputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import net.sf.jasperreports.engine.JasperCompileManager;
import net.sf.jasperreports.engine.JasperExportManager;
import net.sf.jasperreports.engine.JasperFillManager;
import net.sf.jasperreports.engine.JasperPrint;
import net.sf.jasperreports.engine.JasperReport;
import net.sf.jasperreports.engine.data.JRMapCollectionDataSource;

public class AuditDemo {

    public static void main(String[] args) throws Exception {
        JasperReport report;
        try (InputStream in = AuditDemo.class.getResourceAsStream("/reports/grouped-audit.jrxml")) {
            report = JasperCompileManager.compileReport(in);
        }
        List<String> audit = new ArrayList<>();
        Map<String, Object> params = new HashMap<>();
        params.put("AUDIT_LOG", audit);

        List<Map<String, ?>> rows = List.of(
                new HashMap<>(Map.of("category", "Fiction", "title", "The Pragmatic Novel")),
                new HashMap<>(Map.of("category", "Fiction", "title", "Garden of Forking Paths")),
                new HashMap<>(Map.of("category", "Science", "title", "Entropy for Librarians")));
        JasperPrint print = JasperFillManager.fillReport(report, params, new JRMapCollectionDataSource(rows));
        JasperExportManager.exportReportToPdfFile(print, "target/grouped-audit.pdf");
        audit.forEach(System.out::println);
    }
}

This run printed the event sequence shown at the beginning of the section, and the PDF contained Scriptlet in use: AuditScriptlet, the group headings Fiction and Science, and the three titles.

What not to do in a scriptlet

A scriptlet runs inside the fill, so slow work in a hook slows every row. A hook that throws stops the fill: in a check, a JRScriptletException thrown from beforeDetailEval came out of fillReport with its message, and so did a plain IllegalStateException, which is what a report that must not print wrong numbers should do. The scriptlet should not try to change the layout: the elements are evaluated from expressions, and a scriptlet only influences them through the variables and parameters it sets. Alternate row colours, the textbook use of a scriptlet in the official sample, are better done with a conditional style in current versions, which is covered in Styles, Templates & Markup. Finally, a scriptlet that must exist in every report of an application, such as audit logging, does not need to be added to each template: a ScriptletFactory extension, shown below, adds it from the outside.

Properties

A property in JasperReports is a string key and a string value that configures the engine. The same mechanism is used by the engine itself, by every exporter, by extensions and by the application. The key names follow the convention net.sf.jasperreports.<area>.<name> for the library and any prefix of your own for the application. The configuration reference lists several hundred properties of 7.0.8 and, for each one, its default, the version that introduced it and the scope in which it can be set.

Scopes

A scope is the place where a property can be given, and a more specific scope overrides a more general one. The scopes named in the configuration reference are the following; the counts are the numbers of properties that list the scope in the 7.0.8 reference, and show which ones matter in practice.

Scope Properties Where the value comes from

Global

396

jasperreports.properties (or the file named by a system property) on the class path, plus the defaults packaged in the jars.

Context

386

The JasperReportsContext in use: SimpleJasperReportsContext.setProperty(...), layered over the default context.

Report

321

A <property name="..." value="..."/> child of the root element of the template.

Dataset

69

The same element in a <dataset> (subdataset) of the template.

Element

150

The same element inside a <element> of the template.

Field, Parameter, Component, Chart

10 to 15 each

The <property> child of a field, a parameter, a component element or a chart.

Text Element and other element kinds, Column, Hyperlink

26 for text elements, fewer than 10 for the others

The <property> child of that kind of element, column or hyperlink.

System

1

A JVM system property; the only property of this scope is the one that names the properties file.

A property can list several scopes, and the reference says for each one what the value does at each level. The precedence is element, then report, then context, then global. In the properties file section below, the global values and the context values are not two files but two layers: the default context reads the global file, and a context that wraps the default one can set values that override it for that context only.

jasperreports.properties

The default context loads the global configuration, in the following way according to the reference and as checked in 7.0.8. If the system property net.sf.jasperreports.properties is set, the file it names is loaded; otherwise the file jasperreports.properties is looked up on the class path. Setting a library property with -D on the JVM command line is not a way to configure it: in the check, -Dnet.sf.jasperreports.allow.element.overlap=false and -Dcom.bookshelf.billing.currency=USD were ignored, while pointing net.sf.jasperreports.properties at another file replaced the class path file completely (properties that were only in the original file were then null). Defaults that ship in the jars, such as the compiler and font settings in the default.jasperreports.properties of the core jar, are loaded first and are overridden by the application’s file.

A file for Bookshelf Billing with three application settings and a family of related keys:

src/main/resources/jasperreports.properties
com.bookshelf.billing.currency=EUR
com.bookshelf.billing.pagesize=25
com.bookshelf.billing.dept.fiction=books-1
com.bookshelf.billing.dept.science=books-2

Reading properties with JRPropertiesUtil

Finding O26. The JasperReports book consulted for this section (B2, chapter 18) describes a static JRProperties class, with static methods that read and set the global properties. That class is not in the 7.0.8 core jar. Its role is played by JRPropertiesUtil, which is obtained for a context with JRPropertiesUtil.getInstance(JasperReportsContext), and the context that supplies the values is DefaultJasperReportsContext.getInstance() for the application-wide configuration or a SimpleJasperReportsContext for a private one. Code that follows the book and calls the static class does not compile; nothing from the book is reproduced here.

JRPropertiesUtil has typed getters (getProperty, getIntegerProperty, getBooleanProperty, getFloatProperty, getLongProperty, getDoubleProperty, getCharacterProperty), each with an overload that takes a default, overloads that take a JRPropertiesHolder (a report, a dataset, an element) so that the holder’s own properties are consulted before the context’s, and getProperties(prefix) to list the keys that share a prefix. The program reads the file above.

src/main/java/bookshelf/ConfigReader.java
package bookshelf;

import net.sf.jasperreports.engine.DefaultJasperReportsContext;
import net.sf.jasperreports.engine.JRPropertiesUtil;

public class ConfigReader {

    public static void main(String[] args) {
        JRPropertiesUtil util = JRPropertiesUtil.getInstance(DefaultJasperReportsContext.getInstance());

        System.out.println("currency  = " + util.getProperty("com.bookshelf.billing.currency"));
        System.out.println("missing   = " + util.getProperty("com.bookshelf.billing.missing", "none"));
        System.out.println("page size = " + util.getIntegerProperty("com.bookshelf.billing.pagesize", 10));
        System.out.println("flag      = " + util.getBooleanProperty("com.bookshelf.billing.flag", false));

        // All properties that share a prefix; the part after the prefix is the suffix.
        for (JRPropertiesUtil.PropertySuffix p : util.getProperties("com.bookshelf.billing.dept.")) {
            System.out.println(p.getSuffix() + " -> " + p.getValue());
        }
    }
}

It printed the currency EUR, the default none for the missing key, the page size 25, the flag false, and the two department entries with the suffixes fiction and science. The order of the entries from getProperties was not the order of the file in the run (science came first), so code must not depend on it.

Report and element properties, and the context

The example below uses a real property with three scopes: net.sf.jasperreports.allow.element.overlap. The reference says that, set to false globally or on a report, the compiler verifies that no two elements overlap, and that a property of the same name set to true on an element exempts that element. This is a useful check when a report goes to a grid format, and a good way to see the scopes at work, because the effect is visible at compile time.

src/main/resources/reports/overlap.jrxml
<jasperReport name="overlap" language="java" pageWidth="595" pageHeight="842"
              columnWidth="515" leftMargin="40" rightMargin="40" topMargin="40" bottomMargin="40">
  <property name="net.sf.jasperreports.allow.element.overlap" value="false"/>
  <title height="40">
    <element kind="staticText" x="0" y="0" width="300" height="30">
      <text><![CDATA[Bookshelf Billing]]></text>
    </element>
    <element kind="staticText" x="200" y="0" width="300" height="30">
      <property name="net.sf.jasperreports.allow.element.overlap" value="true"/>
      <text><![CDATA[Invoice]]></text>
    </element>
  </title>
</jasperReport>

The program loads the design into a JasperDesign, compiles it under three different configurations through a private context, and prints what the compile does. The context is a SimpleJasperReportsContext layered over the default one, so nothing global is changed.

src/main/java/bookshelf/PropertyScopes.java
package bookshelf;

import java.io.InputStream;

import net.sf.jasperreports.engine.DefaultJasperReportsContext;
import net.sf.jasperreports.engine.JRElement;
import net.sf.jasperreports.engine.JRException;
import net.sf.jasperreports.engine.JRPropertiesUtil;
import net.sf.jasperreports.engine.JasperCompileManager;
import net.sf.jasperreports.engine.SimpleJasperReportsContext;
import net.sf.jasperreports.engine.design.JasperDesign;
import net.sf.jasperreports.engine.xml.JRXmlLoader;

public class PropertyScopes {

    static final String OVERLAP = "net.sf.jasperreports.allow.element.overlap";

    static JasperDesign load(SimpleJasperReportsContext context) throws Exception {
        try (InputStream in = PropertyScopes.class.getResourceAsStream("/reports/overlap.jrxml")) {
            return new JRXmlLoader(context).loadXML(in);
        }
    }

    static String compileResult(SimpleJasperReportsContext context, JasperDesign design) {
        try {
            JasperCompileManager.getInstance(context).compile(design);
            return "compiled";
        } catch (JRException e) {
            return "rejected: " + e.getMessage().lines().findFirst().orElse("");
        }
    }

    public static void main(String[] args) throws Exception {
        // A private context layered over the default one: nothing global is changed.
        SimpleJasperReportsContext context = new SimpleJasperReportsContext(DefaultJasperReportsContext.getInstance());
        JRPropertiesUtil util = JRPropertiesUtil.getInstance(context);
        System.out.println("global/context value        : " + util.getProperty(OVERLAP));

        // 1. Report property false, second element opts in with its own property.
        JasperDesign design = load(context);
        JRElement second = design.getTitle().getElements()[1];
        System.out.println("element map value           : " + util.getProperty(second, OVERLAP));
        System.out.println("report map value            : " + util.getProperty(design, OVERLAP));
        System.out.println("1. report=false, element=true : " + compileResult(context, design));

        // 2. Same design without the element property: the overlap check now fails the compilation.
        second.getPropertiesMap().removeProperty(OVERLAP);
        System.out.println("2. report=false, element unset: " + compileResult(context, design));

        // 3. Remove the report property and set the context value instead.
        design.getPropertiesMap().removeProperty(OVERLAP);
        System.out.println("report sees (from context)  : " + util.getProperty(design, OVERLAP));
        System.out.println("3a. nothing set               : " + compileResult(context, design));
        context.setProperty(OVERLAP, "false");
        System.out.println("report sees (from context)  : " + util.getProperty(design, OVERLAP));
        System.out.println("3b. context=false             : " + compileResult(context, design));
    }
}

The output of the run in 7.0.8 was:

global/context value        : true
element map value           : true
report map value            : false
1. report=false, element=true : compiled
2. report=false, element unset: rejected: Report design not valid :
report sees (from context)  : true
3a. nothing set               : compiled
report sees (from context)  : false
3b. context=false             : rejected: Report design not valid :

So the element property made the first compile succeed, the same design without it was rejected, a report without the property used the context value (true by default, so no check), and a context value of false switched the check on for every report compiled in that context. One detail of the API: on a design or compiled object, getProperty(holder, key) consulted the holder’s own map and then the context, and did not walk from the element to its report (the element of a compiled report had no parent properties). The element, report and context precedence is applied by the engine code that reads the property, as the compile above shows, and not by that generic lookup.

Properties that change the PDF

Many report-level properties exist so that a template can carry settings for the exporter. The PDF metadata properties net.sf.jasperreports.export.pdf.metadata.title, .author, .subject, .keywords and .creator are listed with the scopes Global, Context and Report, so an invoice template can set its own title. The properties travel with the JasperPrint, and a setting made in code through an exporter configuration takes precedence for that export, which the example shows: the same print exported twice.

src/main/resources/reports/metadata.jrxml
<jasperReport name="metadata" language="java" pageWidth="595" pageHeight="842"
              columnWidth="515" leftMargin="40" rightMargin="40" topMargin="40" bottomMargin="40">
  <property name="net.sf.jasperreports.export.pdf.metadata.title" value="Invoice BB-2026-00042"/>
  <property name="net.sf.jasperreports.export.pdf.metadata.author" value="Bookshelf Billing"/>
  <title height="40">
    <element kind="staticText" x="0" y="0" width="515" height="30" fontSize="18">
      <text><![CDATA[Invoice BB-2026-00042]]></text>
    </element>
  </title>
</jasperReport>
src/main/java/bookshelf/MetadataDemo.java
package bookshelf;

import java.io.InputStream;
import java.util.HashMap;

import net.sf.jasperreports.engine.JREmptyDataSource;
import net.sf.jasperreports.engine.JasperCompileManager;
import net.sf.jasperreports.engine.JasperFillManager;
import net.sf.jasperreports.engine.JasperPrint;
import net.sf.jasperreports.engine.JasperReport;
import net.sf.jasperreports.export.SimpleExporterInput;
import net.sf.jasperreports.export.SimpleOutputStreamExporterOutput;
import net.sf.jasperreports.pdf.SimplePdfExporterConfiguration;
import net.sf.jasperreports.pdf.JRPdfExporter;

public class MetadataDemo {

    public static void main(String[] args) throws Exception {
        JasperReport report;
        try (InputStream in = MetadataDemo.class.getResourceAsStream("/reports/metadata.jrxml")) {
            report = JasperCompileManager.compileReport(in);
        }
        JasperPrint print = JasperFillManager.fillReport(report, new HashMap<>(), new JREmptyDataSource(1));

        // 1. Only report-level properties: they travel with the JasperPrint and set the PDF metadata.
        export(print, "target/metadata-report-level.pdf", null);

        // 2. An exporter configuration set in code overrides the report-level property for this export.
        SimplePdfExporterConfiguration configuration = new SimplePdfExporterConfiguration();
        configuration.setMetadataTitle("Duplicate copy of BB-2026-00042");
        export(print, "target/metadata-override.pdf", configuration);
    }

    private static void export(JasperPrint print, String file, SimplePdfExporterConfiguration configuration)
            throws Exception {
        JRPdfExporter exporter = new JRPdfExporter();
        exporter.setExporterInput(new SimpleExporterInput(print));
        exporter.setExporterOutput(new SimpleOutputStreamExporterOutput(file));
        if (configuration != null) {
            exporter.setConfiguration(configuration);
        }
        exporter.exportReport();
    }
}

Reading the two PDFs back, the first had the title Invoice BB-2026-00042 and the author Bookshelf Billing, both from the template; the second had the title Duplicate copy of BB-2026-00042 from the configuration and still the author from the template. The exporter, the metadata and the other configuration objects are covered by the export pages of this section; tagged PDF (the net.sf.jasperreports.export.pdf.tag.* properties) is a feature of the Professional edition in 7.0.8 and is not available in the open source library.

The official description of every property, its scopes and its Javadoc constant is in the configuration reference.

The extension registry

An extension is an object that implements an interface the engine knows and looks up by type, instead of being named in a template. Fonts, query executers, scriptlet factories, component bundles, repository services, export filters and the mappings used to read JRXML are all extensions. The engine asks its context for the extensions of a type with context.getExtensions(Type.class) and gets a list; objects from every registry are in the list.

Registries are created from the jasperreports_extension.properties files found on the class path. Every jar that contributes extensions has one. Each line that starts with net.sf.jasperreports.extension.registry.factory. followed by an arbitrary name gives the class of an ExtensionsRegistryFactory; the factory has a no-argument constructor and a method createRegistry(String registryId, JRPropertiesMap properties) that returns an ExtensionsRegistry, whose only method is getExtensions(Class). For a registry of a single object there is SingletonExtensionRegistry. Since 7.0.2 a numeric weight can be set per registry with net.sf.jasperreports.extension.registry.weight.<registry id> (default 1000), and extensions with a larger weight take precedence.

The distinction from the properties file matters: a key such as net.sf.jasperreports.query.executer.factory.<language> belongs in jasperreports.properties, while a registry factory belongs in jasperreports_extension.properties. In the data sources page a query executer factory registered in the extension file was not found by the compiler, which is the same distinction seen from the other side; see Data Sources & Query Executers.

A scriptlet factory

Scriptlets declared in a template apply to that report. To apply one to every report an application fills, the engine offers the ScriptletFactory extension: getScriplets(ScriptletFactoryContext) returns the scriptlets for a dataset, and the context gives the JasperReportsContext, the compiled report and the parameter values. The default factory is itself an extension and creates the scriptlets named in the templates.

The registry below adds a scriptlet that prints a line when any fill starts. It is a class with a no-argument constructor, declared in the extension file.

src/main/java/bookshelf/BookshelfExtensionsRegistryFactory.java
package bookshelf;

import java.util.List;

import net.sf.jasperreports.engine.JRAbstractScriptlet;
import net.sf.jasperreports.engine.JRDefaultScriptlet;
import net.sf.jasperreports.engine.JRException;
import net.sf.jasperreports.engine.JRPropertiesMap;
import net.sf.jasperreports.engine.JRScriptletException;
import net.sf.jasperreports.engine.scriptlets.ScriptletFactory;
import net.sf.jasperreports.engine.scriptlets.ScriptletFactoryContext;
import net.sf.jasperreports.extensions.ExtensionsRegistry;
import net.sf.jasperreports.extensions.ExtensionsRegistryFactory;
import net.sf.jasperreports.extensions.SingletonExtensionRegistry;

/** Instantiated by the engine, through a no-argument constructor, from jasperreports_extension.properties. */
public class BookshelfExtensionsRegistryFactory implements ExtensionsRegistryFactory {

    @Override
    public ExtensionsRegistry createRegistry(String registryId, JRPropertiesMap properties) {
        // registryId is the arbitrary suffix of the property name, here "bookshelf".
        return new SingletonExtensionRegistry<>(ScriptletFactory.class, new ReportLogFactory());
    }

    /** Adds a logging scriptlet to every dataset of every report filled in the JVM. */
    public static class ReportLogFactory implements ScriptletFactory {

        @Override
        public List<JRAbstractScriptlet> getScriplets(ScriptletFactoryContext context) throws JRException {
            String report = context.getJasperReport() == null ? "?" : context.getJasperReport().getName();
            return List.of(new JRDefaultScriptlet() {
                @Override
                public void beforeReportInit() throws JRScriptletException {
                    System.out.println("[registry] fill started: " + report);
                }
            });
        }
    }
}
src/main/resources/jasperreports_extension.properties (first line)
net.sf.jasperreports.extension.registry.factory.bookshelf=bookshelf.BookshelfExtensionsRegistryFactory

Filling the invoice report of the first example with this on the class path printed [registry] fill started: invoice-scriptlet in addition to the output of the invoice’s own scriptlet, and the PDF was unchanged. The new factory is added to the list of the existing ones; the template’s scriptlet kept working. The engine’s own GovernorFactory, which is also a ScriptletFactory, appeared in the list of extensions in the same run, so the engine uses this mechanism for its own features.

Registering an extension in code

When an extension needs objects the application owns, such as a Spring bean, a class instantiated by the engine from a property file is awkward. The context can hold extensions directly: SimpleJasperReportsContext.setExtensions(Class, List). The next example does that with a factory that receives a Consumer<String>, a stand-in for an audit service.

src/main/java/bookshelf/AuditScriptletFactory.java
package bookshelf;

import java.util.List;
import java.util.function.Consumer;

import net.sf.jasperreports.engine.JRAbstractScriptlet;
import net.sf.jasperreports.engine.JRDefaultScriptlet;
import net.sf.jasperreports.engine.JRException;
import net.sf.jasperreports.engine.JRScriptletException;
import net.sf.jasperreports.engine.scriptlets.ScriptletFactory;
import net.sf.jasperreports.engine.scriptlets.ScriptletFactoryContext;

/**
 * A ScriptletFactory extension: it adds one scriptlet to every dataset filled with a context that knows it.
 * The engine still asks the default factory for the scriptlets declared in the JRXML, so this one must not.
 */
public class AuditScriptletFactory implements ScriptletFactory {

    private final Consumer<String> sink;

    public AuditScriptletFactory(Consumer<String> sink) {
        this.sink = sink;
    }

    @Override
    public List<JRAbstractScriptlet> getScriplets(ScriptletFactoryContext context) throws JRException {
        String report = context.getJasperReport() == null ? "?" : context.getJasperReport().getName();
        return List.of(new JRDefaultScriptlet() {
            @Override
            public void beforeReportInit() throws JRScriptletException {
                sink.accept("fill started: " + report);
            }

            @Override
            public void afterReportInit() throws JRScriptletException {
                sink.accept("fill initialised: " + report);
            }
        });
    }
}
src/main/java/bookshelf/FactoryDemo.java
package bookshelf;

import java.io.InputStream;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import net.sf.jasperreports.engine.DefaultJasperReportsContext;
import net.sf.jasperreports.engine.JasperCompileManager;
import net.sf.jasperreports.engine.JasperFillManager;
import net.sf.jasperreports.engine.JasperPrint;
import net.sf.jasperreports.engine.JasperReport;
import net.sf.jasperreports.engine.SimpleJasperReportsContext;
import net.sf.jasperreports.engine.data.JRBeanCollectionDataSource;
import net.sf.jasperreports.engine.scriptlets.ScriptletFactory;

public class FactoryDemo {

    public static void main(String[] args) throws Exception {
        List<String> audit = new ArrayList<>();

        SimpleJasperReportsContext context = new SimpleJasperReportsContext(DefaultJasperReportsContext.getInstance());
        context.setExtensions(ScriptletFactory.class, List.of(new AuditScriptletFactory(audit::add)));

        JasperReport report;
        try (InputStream in = FactoryDemo.class.getResourceAsStream("/reports/invoice-scriptlet.jrxml")) {
            report = JasperCompileManager.getInstance(context).compile(in);
        }
        Map<String, Object> params = new HashMap<>();
        params.put("YEAR", 2026);
        params.put("SEQUENCE", 43);
        JasperPrint print = JasperFillManager.getInstance(context).fill(report, params,
                new JRBeanCollectionDataSource(List.of(new InvoiceLine("Dune", 1, new BigDecimal("12.50")))));
        System.out.println(audit);
        System.out.println("pages: " + print.getPages().size());

        // The same report filled through the default context does not see the extension.
        audit.clear();
        JasperFillManager.fillReport(report, new HashMap<>(params), new JRBeanCollectionDataSource(
                List.of(new InvoiceLine("Dune", 1, new BigDecimal("12.50")))));
        System.out.println("default context: " + audit);
    }
}

With the extension file above removed from the class path, the run printed [fill started: invoice-scriptlet, fill initialised: invoice-scriptlet], then pages: 1, then default context: []: the extension is seen by fills made with the private context and not by fills made with the default one. Two properties of setExtensions were observed that are easy to get wrong. The list given is added in front of the extensions the parent context already has, it does not replace them; the list returned by getExtensions(ScriptletFactory.class) held the new factory first, then the registered ones, then the governor factory and DefaultScriptletFactory. Because the default factory is still in the list, a custom factory must not delegate to it: an earlier version of the factory that did so made every scriptlet of the template run twice (each event appeared twice in the audit list). The same logic applies to the registry factory above, which returns only its own scriptlet.

In a Spring Boot application the private context is a bean and the services that fill reports take it as a dependency. This sketch was not run; it only connects the example to the setup of the Getting Started page, Getting Started with Spring Boot. The fill is done as in the example, with JasperFillManager.getInstance(context).

package bookshelf;

import java.util.List;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import net.sf.jasperreports.engine.DefaultJasperReportsContext;
import net.sf.jasperreports.engine.JasperReportsContext;
import net.sf.jasperreports.engine.SimpleJasperReportsContext;
import net.sf.jasperreports.engine.scriptlets.ScriptletFactory;

@Configuration
class ReportingConfiguration {

    @Bean
    JasperReportsContext billingReportsContext(AuditService auditService) {
        SimpleJasperReportsContext context = new SimpleJasperReportsContext(DefaultJasperReportsContext.getInstance());
        context.setProperty("net.sf.jasperreports.allow.element.overlap", "false");
        context.setExtensions(ScriptletFactory.class, List.of(new AuditScriptletFactory(auditService::record)));
        return context;
    }
}

AuditService is a hypothetical application class with a record(String) method. Whether one shared SimpleJasperReportsContext is safe for concurrent fills was not tested here; the example configures it once at start-up and does not change it afterwards.

The extension types are documented in the Javadoc of ScriptletFactory, ExtensionsRegistryFactory and SimpleJasperReportsContext.

Font extensions

Fonts are the extension most reports need, and they use the same registry mechanism with a ready-made factory. Two lines in jasperreports_extension.properties register an XML file that lists the font families, with the files of the regular, bold, italic and bold-italic faces, the PDF encoding and whether to embed the font:

net.sf.jasperreports.extension.registry.factory.simple.font.families=net.sf.jasperreports.engine.fonts.SimpleFontExtensionsRegistryFactory
net.sf.jasperreports.extension.simple.font.families.bookshelf=fonts/bookshelf-fonts.xml
<?xml version="1.0" encoding="UTF-8"?>
<fontFamilies>
  <fontFamily name="Bookshelf Mono">
    <normal>fonts/BookshelfMono-Regular.ttf</normal>
    <bold>fonts/BookshelfMono-Bold.ttf</bold>
    <italic>fonts/BookshelfMono-Italic.ttf</italic>
    <boldItalic>fonts/BookshelfMono-BoldItalic.ttf</boldItalic>
    <pdfEncoding>Identity-H</pdfEncoding>
    <pdfEmbedded>true</pdfEmbedded>
    <exportFonts>
      <export key="net.sf.jasperreports.html">'Bookshelf Mono', 'Courier New', monospace</export>
    </exportFonts>
  </fontFamily>
</fontFamilies>

The four .ttf files in the check were the DejaVu Sans Mono faces copied from the jasperreports-fonts jar and renamed. A template then uses fontName="Bookshelf Mono", and the generated PDF listed two embedded Type 0 fonts with the Identity-H encoding, the regular and the bold face, and contained the text including the euro sign and the dash. Use fonts whose licence allows embedding. The full treatment, including why embedding matters and the fallback rules, is in Fonts & Internationalization; the official fonts sample describes the mechanism.

Custom components

A component is the extension behind elements such as the table, the list, the barcode and the chart. In JRXML a component is an element with kind="component" that holds a child <component kind="prefix:Name" .../>; in 7.0 the kind value comes from a Jackson @JsonTypeName on the component class. Writing one takes four pieces, all of which can be seen in the jasperreports-barcode4j jar that this example follows:

  • a class that implements Component (the design-time model), annotated with the type name;

  • a ComponentCompiler, which collects expressions, verifies the settings and returns the compiled form;

  • a ComponentFillFactory that builds a FillComponent, the object that decides at fill time what is printed; and

  • a ComponentDesignConverter, used by design-time previews.

The four are bundled in a DefaultComponentsBundle that maps the component class to a DefaultComponentManager, and the bundle is registered as a ComponentsBundle extension. The example is a progress bar for the credit used by a customer, drawn as a rectangle whose width is a percentage of the element’s width. It is deliberately small; a real component would add expressions, evaluation times and a rendered image or text.

src/main/java/bookshelf/component/ProgressBarComponent.java
package bookshelf.component;

import java.io.Serializable;

import com.fasterxml.jackson.annotation.JsonTypeName;

import net.sf.jasperreports.engine.component.Component;

/** Design-time model of the component. The JsonTypeName is the value of kind="..." in the JRXML. */
@JsonTypeName("bookshelf:ProgressBar")
public class ProgressBarComponent implements Component, Serializable {

    private static final long serialVersionUID = 1L;

    private int percent;

    public int getPercent() {
        return percent;
    }

    public void setPercent(int percent) {
        this.percent = percent;
    }
}
src/main/java/bookshelf/component/ProgressBarFillComponent.java
package bookshelf.component;

import java.awt.Color;

import net.sf.jasperreports.engine.JRException;
import net.sf.jasperreports.engine.JRPrintElement;
import net.sf.jasperreports.engine.component.BaseFillComponent;
import net.sf.jasperreports.engine.component.FillPrepareResult;
import net.sf.jasperreports.engine.fill.JRTemplatePrintRectangle;
import net.sf.jasperreports.engine.fill.JRTemplateRectangle;
import net.sf.jasperreports.engine.type.ModeEnum;

/** Fill-time behaviour: turns the component into one print element, a rectangle as wide as the percentage. */
public class ProgressBarFillComponent extends BaseFillComponent {

    private final ProgressBarComponent component;

    public ProgressBarFillComponent(ProgressBarComponent component) {
        this.component = component;
    }

    ProgressBarComponent component() {
        return component;
    }

    @Override
    public void evaluate(byte evaluation) throws JRException {
        // Nothing to evaluate: the component has no expressions.
    }

    @Override
    public FillPrepareResult prepare(int availableHeight) {
        return availableHeight < fillContext.getComponentElement().getHeight()
                ? FillPrepareResult.NO_PRINT_NO_OVERFLOW
                : FillPrepareResult.PRINT_NO_STRETCH;
    }

    @Override
    public JRPrintElement fill() {
        JRTemplateRectangle template = new JRTemplateRectangle(fillContext.getElementOrigin(),
                fillContext.getDefaultStyleProvider());
        template.setMode(ModeEnum.OPAQUE);
        template.setBackcolor(new Color(0x2E7D32));
        template.setForecolor(new Color(0x2E7D32));
        template = deduplicate(template);

        JRTemplatePrintRectangle bar = new JRTemplatePrintRectangle(template, printElementOriginator);
        bar.setX(fillContext.getComponentElement().getX());
        bar.setY(fillContext.getElementPrintY());
        bar.setWidth(fillContext.getComponentElement().getWidth() * component.getPercent() / 100);
        bar.setHeight(fillContext.getComponentElement().getHeight());
        return bar;
    }
}
src/main/java/bookshelf/component/ProgressBarComponentsBundle.java
package bookshelf.component;

import java.util.Map;

import net.sf.jasperreports.engine.JRComponentElement;
import net.sf.jasperreports.engine.JRExpressionCollector;
import net.sf.jasperreports.engine.JRPrintElement;
import net.sf.jasperreports.engine.base.JRBaseObjectFactory;
import net.sf.jasperreports.engine.component.Component;
import net.sf.jasperreports.engine.component.ComponentCompiler;
import net.sf.jasperreports.engine.component.ComponentDesignConverter;
import net.sf.jasperreports.engine.component.ComponentFillFactory;
import net.sf.jasperreports.engine.component.DefaultComponentManager;
import net.sf.jasperreports.engine.component.DefaultComponentsBundle;
import net.sf.jasperreports.engine.component.FillComponent;
import net.sf.jasperreports.engine.convert.ReportConverter;
import net.sf.jasperreports.engine.design.JRVerifier;
import net.sf.jasperreports.engine.fill.JRFillCloneFactory;
import net.sf.jasperreports.engine.fill.JRFillObjectFactory;

public final class ProgressBarComponentsBundle {

    private ProgressBarComponentsBundle() {
    }

    public static DefaultComponentsBundle create() {
        DefaultComponentManager manager = new DefaultComponentManager();
        manager.setComponentCompiler(new ComponentCompiler() {
            @Override
            public void collectExpressions(Component component, JRExpressionCollector collector) {
                // no expressions
            }

            @Override
            public void verify(Component component, JRVerifier verifier) {
                // a real component would report invalid settings here
            }

            @Override
            public Component toCompiledComponent(Component component, JRBaseObjectFactory factory) {
                return component; // immutable and serializable, so it can be shared
            }
        });
        manager.setComponentFillFactory(new ComponentFillFactory() {
            @Override
            public FillComponent toFillComponent(Component component, JRFillObjectFactory factory) {
                return new ProgressBarFillComponent((ProgressBarComponent) component);
            }

            @Override
            public FillComponent cloneFillComponent(FillComponent component, JRFillCloneFactory factory) {
                return new ProgressBarFillComponent(((ProgressBarFillComponent) component).component());
            }
        });
        manager.setDesignConverter(new ComponentDesignConverter() {
            @Override
            public JRPrintElement convert(ReportConverter converter, JRComponentElement element) {
                return null; // only used by design-time previews
            }
        });
        DefaultComponentsBundle bundle = new DefaultComponentsBundle();
        bundle.setComponentManagers(Map.of(ProgressBarComponent.class, manager));
        return bundle;
    }
}
src/main/java/bookshelf/component/ProgressBarRegistryFactory.java
package bookshelf.component;

import net.sf.jasperreports.engine.JRPropertiesMap;
import net.sf.jasperreports.engine.component.ComponentsBundle;
import net.sf.jasperreports.extensions.ExtensionsRegistry;
import net.sf.jasperreports.extensions.ExtensionsRegistryFactory;
import net.sf.jasperreports.extensions.SingletonExtensionRegistry;

public class ProgressBarRegistryFactory implements ExtensionsRegistryFactory {

    @Override
    public ExtensionsRegistry createRegistry(String registryId, JRPropertiesMap properties) {
        return new SingletonExtensionRegistry<>(ComponentsBundle.class, ProgressBarComponentsBundle.create());
    }
}

The registry factory is declared in the extension file, as before. The template uses the component, with the percentage as an attribute:

net.sf.jasperreports.extension.registry.factory.progressbar=bookshelf.component.ProgressBarRegistryFactory
src/main/resources/reports/component.jrxml
<jasperReport name="component" language="java" pageWidth="595" pageHeight="842"
              columnWidth="515" leftMargin="40" rightMargin="40" topMargin="40" bottomMargin="40">
  <title height="60">
    <element kind="staticText" x="0" y="0" width="515" height="20">
      <text><![CDATA[Credit used]]></text>
    </element>
    <element kind="component" x="0" y="25" width="400" height="12">
      <component kind="bookshelf:ProgressBar" percent="35"/>
    </element>
  </title>
</jasperReport>
src/main/java/bookshelf/ComponentDemo.java
package bookshelf;

import java.io.InputStream;
import java.util.HashMap;

import net.sf.jasperreports.engine.JREmptyDataSource;
import net.sf.jasperreports.engine.JasperCompileManager;
import net.sf.jasperreports.engine.JasperExportManager;
import net.sf.jasperreports.engine.JasperFillManager;
import net.sf.jasperreports.engine.JasperPrint;
import net.sf.jasperreports.engine.JasperReport;

public class ComponentDemo {

    public static void main(String[] args) throws Exception {
        JasperReport report;
        try (InputStream in = ComponentDemo.class.getResourceAsStream("/reports/component.jrxml")) {
            report = JasperCompileManager.compileReport(in);
        }
        JasperPrint print = JasperFillManager.fillReport(report, new HashMap<>(), new JREmptyDataSource(1));
        System.out.println(print.getPages().get(0).getElements());
        JasperExportManager.exportReportToPdfFile(print, "target/component.pdf");
    }
}

The first run compiled and filled the template with no further configuration: the print had a text element and a rectangle. Reading the PDF back, the rectangle went from x=40 to x=180 points, a width of 140 points, which is 35 percent of the 400 point element, and it was filled with the green set in the fill component. So the registry, the Jackson type name and the bundle are enough to make JRXML accept the new kind.

One more step is needed if compiled reports are saved to .jasper files. JasperReports 7 filters the classes that can be deserialised (the property net.sf.jasperreports.deserialization.class.filter.enabled is true by default, since 7.0.4), and a class that is not on the whitelist is refused. In the check, saving the compiled report with JRSaver worked, and loading it with JRLoader failed with Class bookshelf.component.ProgressBarComponent is not visible to deserialization. The fix, which the barcode4j jar uses for its own classes, is to register the standard whitelist extension and to add a whitelist property with a pattern for the component classes; with the lines below, the load succeeded. Patterns accept * for part of a name and ** across packages, as the configuration reference explains.

net.sf.jasperreports.extension.registry.factory.deserialization.whitelist=net.sf.jasperreports.engine.util.StandardDeserializationClassWhitelistExtension
net.sf.jasperreports.deserialization.class.whitelist.bookshelf=bookshelf.component.*Component

Whitelisting classes is a security control, since deserialising untrusted .jasper files is a classic attack path, so the pattern should be as narrow as the component allows and the filter should stay on. What the example does not cover: the design converter returns null, which suits a headless service but not a preview in a designer; there is no Jaspersoft Studio support for the component, which would need an additional plug-in; and the exporters other than PDF were not tried. The Javadoc of ComponentsBundle describes the interfaces.

Putting it together

Need Mechanism Where it is configured

Java logic at fill time for one report

Scriptlet (JRDefaultScriptlet)

scriptletClass or <scriptlet> in the JRXML.

Java logic at fill time for every report

ScriptletFactory extension

jasperreports_extension.properties, or setExtensions on a context.

A setting for the whole application

Global property

jasperreports.properties (not JVM -D options).

A setting for one tenant or one test

Context property

SimpleJasperReportsContext.setProperty.

A setting for one report or one element

Report or element property

<property> in the JRXML.

A new font family

Font extension

jasperreports_extension.properties and a fonts XML file.

A new kind of element

Custom component

A ComponentsBundle extension, a Jackson type name and, for .jasper files, a whitelist entry.

For the monthly sales report built in Generating Reports from Databases & Services, the sensible split is a context bean with the application-wide properties, templates that carry their own PDF metadata, a scriptlet factory only if there is a cross-cutting need such as audit, and scriptlets in templates only for calculations that expressions cannot express.