|
|
||
|---|---|---|
| .gitea/workflows | ||
| db | ||
| src/main | ||
| .gitignore | ||
| Containerfile | ||
| pom.xml | ||
| README.md | ||
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) whoseQRTZ_*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 thesalestable fromwarehouseDS, renders a PDF via JasperReports 6.20, writes it to the report output directory, and records areport_runsrow. - Cleanup — a Quartz job (
CleanupJob) runs daily at 03:15 and deletes report files older thanreport.retention.days(default 7). - Web tier (
GET /reports) — lists generated reports (fromreport_runsjoined 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)
- Create the three PostgreSQL databases and roles (
warehouse,quartz_scheduler,oltp). - Apply
db/init.sqltowarehouseanddb/init-quartz.sqltoquartz_scheduler. - Put the PostgreSQL JDBC driver on Tomcat's classpath (or rely on the copy
bundled in the WAR) and place the three
<Resource>definitions incontext.xml. - Create writable host dirs:
/var/lib/report-factory/reportsand/var/log/report-factory. - Drop
report-factory.warinto$CATALINA_BASE/webapps/. - 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"