Legacy reporting engine: Quartz JDBC store + JasperReports + 3 JNDI datasources on Tomcat
Find a file
anaeem d648016642
Some checks failed
build-image / build (push) Has been cancelled
CI: build with docker (the runner provides docker, not podman)
2026-06-15 15:17:12 +01:00
.gitea/workflows CI: build with docker (the runner provides docker, not podman) 2026-06-15 15:17:12 +01:00
db Initial commit: report-factory legacy reporting engine (Quartz JDBC store + JasperReports + 3 JNDI datasources on Tomcat 9) 2026-06-11 08:41:53 +01:00
src/main Use Java 11-compatible instanceof (drop pattern-matching) for the OpenJDK 11 builder 2026-06-14 03:48:58 +01:00
.gitignore Initial commit: report-factory legacy reporting engine (Quartz JDBC store + JasperReports + 3 JNDI datasources on Tomcat 9) 2026-06-11 08:41:53 +01:00
Containerfile Add Containerfile + Gitea build-and-push workflow 2026-06-15 15:14:14 +01:00
pom.xml Target Java 11 to match the OpenJDK 11 JBoss Web Server S2I builder 2026-06-14 03:46:36 +01:00
README.md Initial commit: report-factory legacy reporting engine (Quartz JDBC store + JasperReports + 3 JNDI datasources on Tomcat 9) 2026-06-11 08:41:53 +01:00

Report Factory

A deliberately legacy-styled Java reporting application built as a VM-to-OpenShift migration / discovery test fixture. It is the "stateful scheduled-jobs" case for the fleet: a report-factory.war deployed on Tomcat 9 that runs Quartz with a persistent JDBC job store, reads from three JNDI datasources, renders JasperReports PDFs to disk, and logs via log4j2.

It compiles and runs on Java 17 while still using the javax.* Servlet 4 / Java EE 8 APIs, Spring 5.3 (context + MVC), and JSP views — a realistic mid-2010s enterprise app.

Why it exists (discovery surface)

A containerisation pass has to notice and externalise all of:

  • Three container-managed JNDI datasources with credentials embedded in context.xml (the multi-datasource + plaintext-secrets case).
  • A persistent Quartz JDBC job store (JobStoreTX) whose QRTZ_* schema must exist before the app boots (the stateful-prerequisite case).
  • Report files written to a host directory (default /var/lib/report-factory/reports).
  • log4j2 file logging to /var/log/report-factory/app.log (variety vs the logback-based fixtures — the log-config discovery case).
  • A PKCS12 keystore the VM carries for the TLS connector.

What it does

  • Daily Sales Report — a Quartz job (DailySalesReportJob) runs every 2 minutes (demo cadence), aggregates the sales table from warehouseDS, renders a PDF via JasperReports 6.20, writes it to the report output directory, and records a report_runs row.
  • Cleanup — a Quartz job (CleanupJob) runs daily at 03:15 and deletes report files older than report.retention.days (default 7).
  • Web tier (GET /reports) — lists generated reports (from report_runs joined with on-disk presence), lets you download a PDF, and has a button to trigger DailySalesReportJob on demand (scheduler.triggerJob).
  • Health (GET /health) — JSON health check that pings all three datasources and reports whether the scheduler is running:
    {"status":"UP","datasources":{"warehouseDS":true,"quartzDS":true,"oltpDS":true},"schedulerRunning":true}
    

Reporting engine decision: JasperReports (not PDFBox)

The build uses JasperReports 6.20.6. The .jrxml template (src/main/resources/reports/daily_sales.jrxml) is compiled at runtime via JasperCompileManager rather than through a build-time JasperReports Maven plugin — this keeps the build plain (mvn package) and avoids jrxml tooling fights, while still exercising the full Jasper compile→fill→export PDF path. No PDFBox fallback was needed.

Module layout

report-factory/
  pom.xml                                  single-module WAR (Java 17, javax.*)
  src/main/java/com/example/reports/
    config/AppConfig            report.output.dir / retention resolution
    config/DataSources          JNDI lookups for the 3 datasources
    config/WebConfig            Spring MVC (@EnableWebMvc + JSP views)
    job/SchedulerBootstrap      @WebListener: builds Quartz scheduler, schedules jobs
    job/DailySalesReportJob     every 2 min -> render PDF
    job/CleanupJob              daily -> prune old PDFs
    service/DailySalesReportService  query warehouseDS + Jasper compile/fill/export
    service/ReportRunDao        report_runs CRUD
    service/SalesRow            report row bean
    web/ReportController        /reports list, /reports/download, /reports/trigger
    servlet/HealthServlet       /health (3 DS pings + scheduler state -> JSON)
  src/main/resources/
    quartz.properties           JobStoreTX over quartzDS (NOT RAMJobStore)
    log4j2.xml                  RollingFile -> /var/log/report-factory/app.log
    reports/daily_sales.jrxml   Jasper template, compiled at runtime
  src/main/webapp/
    WEB-INF/web.xml             context-param, resource-refs, DispatcherServlet
    WEB-INF/views/reports.jsp   report list UI
    META-INF/context.xml        the 3 JNDI Resource definitions (plaintext pw)
    index.jsp                   redirect -> /reports
  db/init.sql                   warehouse schema + ~50 seed sales rows + report_runs
  db/init-quartz.sql            canonical Quartz 2.3 QRTZ_* schema (PostgreSQL)

The three datasources (context.xml snippets)

All three are PostgreSQL. oltpDS stands in for the legacy MySQL OLTP database — the original spec called for MySQL, but the fleet keeps everything PostgreSQL for simplicity. Passwords are intentionally in plaintext (discovery secrets case). These live in src/main/webapp/META-INF/context.xml; on a real VM they are typically in $CATALINA_BASE/conf/Catalina/localhost/report-factory.xml.

<!-- Warehouse: main reporting data (sales, report_runs) -->
<Resource name="jdbc/warehouseDS"
          auth="Container" type="javax.sql.DataSource"
          driverClassName="org.postgresql.Driver"
          url="jdbc:postgresql://db-warehouse.internal:5432/warehouse"
          username="warehouse_app" password="wh-pw-2019"
          maxTotal="20" maxIdle="5" maxWaitMillis="10000"
          validationQuery="SELECT 1" testOnBorrow="true"/>

<!-- Quartz: dedicated quartz_scheduler database holding the QRTZ_* tables -->
<Resource name="jdbc/quartzDS"
          auth="Container" type="javax.sql.DataSource"
          driverClassName="org.postgresql.Driver"
          url="jdbc:postgresql://db-quartz.internal:5432/quartz_scheduler"
          username="quartz_app" password="qrtz-pw-2019"
          maxTotal="10" maxIdle="3" maxWaitMillis="10000"
          validationQuery="SELECT 1" testOnBorrow="true"/>

<!-- OLTP: legacy operational DB (stands in for the old MySQL OLTP) -->
<Resource name="jdbc/oltpDS"
          auth="Container" type="javax.sql.DataSource"
          driverClassName="org.postgresql.Driver"
          url="jdbc:postgresql://db-oltp.internal:5432/oltp"
          username="oltp_app" password="oltp-pw-2019"
          maxTotal="15" maxIdle="4" maxWaitMillis="10000"
          validationQuery="SELECT 1" testOnBorrow="true"/>

⚠️ Quartz schema prerequisite

The scheduler uses org.quartz.impl.jdbcjobstore.JobStoreTX over quartzDS. The QRTZ_* tables must already exist in the quartz_scheduler database before the WAR starts. If they are missing the scheduler bootstrap throws and the context fails to initialise. Apply db/init-quartz.sql first:

psql "postgresql://quartz_app:qrtz-pw-2019@db-quartz.internal:5432/quartz_scheduler" -f db/init-quartz.sql

TLS connector / PKCS12 keystore note

The VM carries a PKCS12 keystore at conf/reports-keystore.p12 used by the Tomcat HTTPS connector. A migration pass must externalise this (e.g. into a Secret / mounted volume). Example server.xml connector:

<Connector port="8443" protocol="org.apache.coyote.http11.Http11NioProtocol"
           SSLEnabled="true" maxThreads="150" scheme="https" secure="true">
  <SSLHostConfig>
    <Certificate certificateKeystoreFile="conf/reports-keystore.p12"
                 certificateKeystoreType="PKCS12"
                 certificateKeystorePassword="changeit"/>
  </SSLHostConfig>
</Connector>

Generate a self-signed one for local testing:

keytool -genkeypair -alias reports -keyalg RSA -keysize 2048 \
  -storetype PKCS12 -keystore conf/reports-keystore.p12 \
  -storepass changeit -validity 3650 \
  -dname "CN=report-factory,OU=fleet,O=example,C=US"

Build

Java 17 + Maven. The host JDK may be newer, so build inside the UBI image:

# from the repo root
podman run --rm -v "$PWD":/src:Z -w /src \
  registry.access.redhat.com/ubi9/openjdk-17:latest \
  mvn -B -DskipTests package

If SELinux bind-mounts misbehave, copy the source inside the container first:

podman run --rm registry.access.redhat.com/ubi9/openjdk-17:latest bash -c '
  mkdir -p /tmp/build && cd /tmp/build &&
  ... (copy source in) && mvn -B -DskipTests package'

Output: target/report-factory.war (with WEB-INF/classes/quartz.properties, log4j2.xml, and the compiled web tier).

Run (on a Tomcat 9 VM)

  1. Create the three PostgreSQL databases and roles (warehouse, quartz_scheduler, oltp).
  2. Apply db/init.sql to warehouse and db/init-quartz.sql to quartz_scheduler.
  3. Put the PostgreSQL JDBC driver on Tomcat's classpath (or rely on the copy bundled in the WAR) and place the three <Resource> definitions in context.xml.
  4. Create writable host dirs: /var/lib/report-factory/reports and /var/log/report-factory.
  5. Drop report-factory.war into $CATALINA_BASE/webapps/.
  6. Browse to /report-factory/reports; check /report-factory/health.

Override the output dir or retention at boot:

export CATALINA_OPTS="-Dreport.output.dir=/data/reports -Dreport.retention.days=14"