@rashidee/co2 1.2.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +25449 -0
- package/drizzle/0000_remarkable_charles_xavier.sql +210 -0
- package/drizzle/0001_wise_wolf_cub.sql +13 -0
- package/drizzle/0002_secret_spyke.sql +16 -0
- package/drizzle/0003_workspace_config_singleton.sql +16 -0
- package/drizzle/0004_terminal_claim_v120.sql +11 -0
- package/drizzle/0005_brainstorm_v124.sql +10 -0
- package/drizzle/0006_project_v140.sql +6 -0
- package/drizzle/0007_prd_wizard_v124.sql +14 -0
- package/drizzle/0008_workflow_artifact_v126.sql +24 -0
- package/drizzle/0009_project_import_v150.sql +7 -0
- package/drizzle/0010_quality_v151.sql +3 -0
- package/drizzle/meta/0000_snapshot.json +1335 -0
- package/drizzle/meta/0001_snapshot.json +1416 -0
- package/drizzle/meta/0002_snapshot.json +1510 -0
- package/drizzle/meta/_journal.json +83 -0
- package/package.json +41 -0
- package/plugin/.claude-plugin/marketplace.json +36 -0
- package/plugin/.claude-plugin/plugin.json +22 -0
- package/plugin/README.md +625 -0
- package/plugin/SKILLS.md +350 -0
- package/plugin/co2.svg +5 -0
- package/plugin/commands/brainstorm-loop.md +129 -0
- package/plugin/skills/conductor-defect/SKILL.md +898 -0
- package/plugin/skills/conductor-feature-develop/SKILL.md +1383 -0
- package/plugin/skills/conductor-feature-develop/references/implementation-domain-template.md +131 -0
- package/plugin/skills/conductor-feature-develop/references/implementation-master-template.md +89 -0
- package/plugin/skills/conductor-feature-develop/references/playwright-setup.md +224 -0
- package/plugin/skills/conductor-feature-prepare/SKILL.md +604 -0
- package/plugin/skills/depgen-k8s/SKILL.md +468 -0
- package/plugin/skills/depgen-k8s/references/dockerfile-laravel.md +197 -0
- package/plugin/skills/depgen-k8s/references/dockerfile-nodejs.md +163 -0
- package/plugin/skills/depgen-k8s/references/dockerfile-spring-boot.md +174 -0
- package/plugin/skills/depgen-k8s/references/k8s-patterns.md +869 -0
- package/plugin/skills/mockgen-shadcn/SKILL.md +1067 -0
- package/plugin/skills/mockgen-shadcn/references/admin-layout-template.md +1467 -0
- package/plugin/skills/mockgen-shadcn/references/mockup-hub-template.md +498 -0
- package/plugin/skills/mockgen-shadcn/references/mockup-index-template.md +216 -0
- package/plugin/skills/mockgen-tailwind/SKILL.md +904 -0
- package/plugin/skills/mockgen-tailwind/references/admin-layout-template.md +720 -0
- package/plugin/skills/mockgen-tailwind/references/mockup-hub-template.md +498 -0
- package/plugin/skills/mockgen-tailwind/references/mockup-index-template.md +190 -0
- package/plugin/skills/modelgen-nosql/SKILL.md +617 -0
- package/plugin/skills/modelgen-nosql/references/model-extraction-methodology.md +497 -0
- package/plugin/skills/modelgen-nosql/references/nosql-design-guide.md +258 -0
- package/plugin/skills/modelgen-relational/SKILL.md +707 -0
- package/plugin/skills/modelgen-relational/references/model-extraction-methodology.md +499 -0
- package/plugin/skills/specgen-flutter-riverpod/SKILL.md +898 -0
- package/plugin/skills/specgen-flutter-riverpod/references/component-patterns.md +971 -0
- package/plugin/skills/specgen-flutter-riverpod/references/notification-patterns.md +457 -0
- package/plugin/skills/specgen-flutter-riverpod/references/routing-patterns.md +362 -0
- package/plugin/skills/specgen-flutter-riverpod/references/security-patterns.md +709 -0
- package/plugin/skills/specgen-flutter-riverpod/references/spec-template.md +1566 -0
- package/plugin/skills/specgen-flutter-riverpod/references/state-patterns.md +518 -0
- package/plugin/skills/specgen-flutter-riverpod/references/storage-patterns.md +370 -0
- package/plugin/skills/specgen-laravel-eloquent-bladehtmx/SKILL.md +984 -0
- package/plugin/skills/specgen-laravel-eloquent-bladehtmx/references/batch-patterns.md +359 -0
- package/plugin/skills/specgen-laravel-eloquent-bladehtmx/references/messaging-patterns.md +468 -0
- package/plugin/skills/specgen-laravel-eloquent-bladehtmx/references/modulith-patterns.md +461 -0
- package/plugin/skills/specgen-laravel-eloquent-bladehtmx/references/reporting-patterns.md +803 -0
- package/plugin/skills/specgen-laravel-eloquent-bladehtmx/references/security-patterns.md +501 -0
- package/plugin/skills/specgen-laravel-eloquent-bladehtmx/references/spec-template.md +1268 -0
- package/plugin/skills/specgen-laravel-eloquent-bladehtmx/references/web-patterns.md +301 -0
- package/plugin/skills/specgen-node-cli-web/SKILL.md +561 -0
- package/plugin/skills/specgen-node-cli-web/references/auth-patterns.md +293 -0
- package/plugin/skills/specgen-node-cli-web/references/cli-packaging-patterns.md +250 -0
- package/plugin/skills/specgen-node-cli-web/references/server-patterns.md +247 -0
- package/plugin/skills/specgen-node-cli-web/references/spec-template.md +401 -0
- package/plugin/skills/specgen-node-cli-web/references/testing-patterns.md +174 -0
- package/plugin/skills/specgen-react-mui/SKILL.md +789 -0
- package/plugin/skills/specgen-react-mui/references/component-patterns.md +1077 -0
- package/plugin/skills/specgen-react-mui/references/reporting-patterns.md +1998 -0
- package/plugin/skills/specgen-react-mui/references/routing-patterns.md +461 -0
- package/plugin/skills/specgen-react-mui/references/security-patterns.md +599 -0
- package/plugin/skills/specgen-react-mui/references/spec-template.md +1030 -0
- package/plugin/skills/specgen-react-mui/references/state-patterns.md +327 -0
- package/plugin/skills/specgen-react-tailwind/SKILL.md +859 -0
- package/plugin/skills/specgen-react-tailwind/references/component-patterns.md +1212 -0
- package/plugin/skills/specgen-react-tailwind/references/reporting-patterns.md +1529 -0
- package/plugin/skills/specgen-react-tailwind/references/routing-patterns.md +669 -0
- package/plugin/skills/specgen-react-tailwind/references/security-patterns.md +656 -0
- package/plugin/skills/specgen-react-tailwind/references/spec-template.md +1237 -0
- package/plugin/skills/specgen-react-tailwind/references/state-patterns.md +327 -0
- package/plugin/skills/specgen-sdk-java/SKILL.md +933 -0
- package/plugin/skills/specgen-sdk-java/references/api-extraction-patterns.md +247 -0
- package/plugin/skills/specgen-sdk-java/references/http-client-patterns.md +495 -0
- package/plugin/skills/specgen-sdk-java/references/packaging-patterns.md +366 -0
- package/plugin/skills/specgen-sdk-java/references/spec-template.md +956 -0
- package/plugin/skills/specgen-spring-jpa-jtehtmx/SKILL.md +1284 -0
- package/plugin/skills/specgen-spring-jpa-jtehtmx/references/batch-patterns.md +1224 -0
- package/plugin/skills/specgen-spring-jpa-jtehtmx/references/jasper-patterns.md +1638 -0
- package/plugin/skills/specgen-spring-jpa-jtehtmx/references/messaging-patterns.md +512 -0
- package/plugin/skills/specgen-spring-jpa-jtehtmx/references/modulith-patterns.md +354 -0
- package/plugin/skills/specgen-spring-jpa-jtehtmx/references/security-patterns.md +551 -0
- package/plugin/skills/specgen-spring-jpa-jtehtmx/references/spec-template.md +2502 -0
- package/plugin/skills/specgen-spring-jpa-jtehtmx/references/web-patterns.md +503 -0
- package/plugin/skills/specgen-spring-jpa-restapi/SKILL.md +990 -0
- package/plugin/skills/specgen-spring-jpa-restapi/references/batch-patterns.md +1224 -0
- package/plugin/skills/specgen-spring-jpa-restapi/references/messaging-patterns.md +512 -0
- package/plugin/skills/specgen-spring-jpa-restapi/references/modulith-patterns.md +279 -0
- package/plugin/skills/specgen-spring-jpa-restapi/references/reporting-patterns.md +1536 -0
- package/plugin/skills/specgen-spring-jpa-restapi/references/restapi-patterns.md +722 -0
- package/plugin/skills/specgen-spring-jpa-restapi/references/security-patterns.md +351 -0
- package/plugin/skills/specgen-spring-jpa-restapi/references/spec-template.md +553 -0
- package/plugin/skills/specgen-ts-cli/SKILL.md +861 -0
- package/plugin/skills/specgen-ts-cli/references/async-patterns.md +1101 -0
- package/plugin/skills/specgen-ts-cli/references/command-patterns.md +383 -0
- package/plugin/skills/specgen-ts-cli/references/config-patterns.md +430 -0
- package/plugin/skills/specgen-ts-cli/references/database-patterns.md +750 -0
- package/plugin/skills/specgen-ts-cli/references/packaging-patterns.md +426 -0
- package/plugin/skills/specgen-ts-cli/references/spec-template.md +1391 -0
- package/plugin/skills/specgen-ts-cli/references/testing-patterns.md +909 -0
- package/plugin/skills/testgen-functional/SKILL.md +1381 -0
- package/plugin/skills/tracegen-matrix/SKILL.md +243 -0
- package/plugin/skills/util-preparek8senv/SKILL.md +422 -0
- package/plugin/skills/util-projectinit/SKILL.md +307 -0
- package/plugin/skills/util-projectinit/reference/brainstorm-loop.md +129 -0
- package/plugin/skills/util-projectinit/reference/brainstorm-protocol.md +144 -0
- package/plugin/skills/util-projectinit/reference/brainstorm-setup-guide.md +153 -0
- package/plugin/skills/util-projectinit/reference/brainstorm-template-appdev.md +87 -0
- package/plugin/skills/util-projectinit/reference/co2-context-generation-guide.md +761 -0
- package/plugin/skills/util-projectsync/SKILL.md +501 -0
- package/plugin/skills/util-updprd/SKILL.md +324 -0
- package/plugin/skills/util-usanalyzer/SKILL.md +307 -0
- package/plugin/skills/util-ustagger/SKILL.md +154 -0
- package/static/assets/abnfDiagram-VRR7QNED-DvPuwyCV.js +1 -0
- package/static/assets/arc-DRKw7sjs.js +1 -0
- package/static/assets/architectureDiagram-ZJ3FMSHR-CaNcxUbC.js +36 -0
- package/static/assets/blockDiagram-677ZJIJ3-Demd4AOK.js +132 -0
- package/static/assets/c4Diagram-LMCZKHZV-PVKUfc2x.js +10 -0
- package/static/assets/channel-C2nVbuwv.js +1 -0
- package/static/assets/chunk-2Q5K7J3B-CExf8ww_.js +1 -0
- package/static/assets/chunk-32BRIVSS-CFPaoONF.js +1 -0
- package/static/assets/chunk-5VM5RSS4-DE2esM67.js +15 -0
- package/static/assets/chunk-EX3LRPZG-SON_YdDG.js +231 -0
- package/static/assets/chunk-JWPE2WC7-Qxah8LNr.js +1 -0
- package/static/assets/chunk-MOJQB5TN-BcWGQV3j.js +88 -0
- package/static/assets/chunk-RYQCIY6F-DkDj-OR5.js +1 -0
- package/static/assets/chunk-V7JOEXUC-BlI1Mrah.js +206 -0
- package/static/assets/chunk-VR4S4FIN-Agf__duj.js +1 -0
- package/static/assets/chunk-XXDRQBXY-B5CfAh85.js +1 -0
- package/static/assets/classDiagram-OUVF2IWQ-DXJIljsR.js +1 -0
- package/static/assets/classDiagram-v2-EOCWNBFH-DXJIljsR.js +1 -0
- package/static/assets/cose-bilkent-JH36ORCC-DO0cQkcy.js +1 -0
- package/static/assets/cynefin-VYW2F7L2-CfOnsnXc.js +178 -0
- package/static/assets/cynefinDiagram-TSTJHNR4-BJ8tULAv.js +62 -0
- package/static/assets/cytoscape.esm-DTSO7Bv0.js +331 -0
- package/static/assets/dagre-VKFMJZFB-DfjwzHqP.js +4 -0
- package/static/assets/defaultLocale-DX6XiGOO.js +1 -0
- package/static/assets/diagram-FQU43EPY-CndSOIIM.js +3 -0
- package/static/assets/diagram-G47NLZAW-Czf5hN0Z.js +24 -0
- package/static/assets/diagram-NH7WQ7WH-COAyojGh.js +24 -0
- package/static/assets/diagram-OA4YK3LP-H8PoiN2m.js +30 -0
- package/static/assets/diagram-WEI45ONY-DzcaVzjp.js +41 -0
- package/static/assets/ebnfDiagram-CCIWWBDH-DPazGYLJ.js +1 -0
- package/static/assets/erDiagram-Q63AITRT-Bo-fubgF.js +85 -0
- package/static/assets/flowDiagram-23GEKE2U-CSvxUDwh.js +156 -0
- package/static/assets/ganttDiagram-NO4QXBWP-DnBveWX6.js +292 -0
- package/static/assets/gitGraphDiagram-IHSO6WYX-B3rVL8c-.js +106 -0
- package/static/assets/graph-C9eacEi8.js +1 -0
- package/static/assets/index-LZSQkLE2.css +41 -0
- package/static/assets/index-y7XDSgMG.js +495 -0
- package/static/assets/infoDiagram-FWYZ7A6U-CvWV9yy2.js +2 -0
- package/static/assets/init-Gi6I4Gst.js +1 -0
- package/static/assets/ishikawaDiagram-FXEZZL3T-DF28a1N6.js +70 -0
- package/static/assets/journeyDiagram-5HDEW3XC-CB6CugYk.js +139 -0
- package/static/assets/kanban-definition-HUTT4EX6-ResSLF6d.js +89 -0
- package/static/assets/katex-C5jXJg4s.js +257 -0
- package/static/assets/layout-DEXfKzaS.js +1 -0
- package/static/assets/linear-CLO--ryk.js +1 -0
- package/static/assets/map-Czzmt4hB.js +1 -0
- package/static/assets/mindmap-definition-LN4V7U3C-BvW3EbxX.js +96 -0
- package/static/assets/ordinal-Cboi1Yqb.js +1 -0
- package/static/assets/pegDiagram-2B236MQR-BHCgqrrM.js +1 -0
- package/static/assets/pieDiagram-ENE6RG2P-DVmCQ5FA.js +39 -0
- package/static/assets/quadrantDiagram-ABIIQ3AL-B3e5KR52.js +7 -0
- package/static/assets/railroadDiagram-RFXS5EU6-Csv5ysLc.js +1 -0
- package/static/assets/requirementDiagram-TGXJPOKE-DOMVzvnB.js +84 -0
- package/static/assets/sankeyDiagram-HTMAVEWB-B1dcTOvy.js +40 -0
- package/static/assets/sequenceDiagram-DBY2YBRQ-BY414yeO.js +162 -0
- package/static/assets/sizeCapture-X5ZJPWSS-CuJfrfBN.js +1 -0
- package/static/assets/stateDiagram-2N3HPSRC-FX4rJkzV.js +1 -0
- package/static/assets/stateDiagram-v2-6OUMAXLB-BVufbO_H.js +1 -0
- package/static/assets/swimlanes-5IMT3BWC-Sfy58mmv.js +2 -0
- package/static/assets/swimlanesDiagram-G3AALYLV-wDCDw08H.js +8 -0
- package/static/assets/timeline-definition-FHXFAJF6-CmV7by8p.js +120 -0
- package/static/assets/vennDiagram-L72KCM5P-FqIyFZFt.js +34 -0
- package/static/assets/wardleyDiagram-EHGQE667-BZ06O45i.js +78 -0
- package/static/assets/xychartDiagram-FW5EYKEG-BOkffR06.js +7 -0
- package/static/index.html +24 -0
|
@@ -0,0 +1,1536 @@
|
|
|
1
|
+
# Reporting Patterns — JasperReports with JRDesign API (Programmatic Layout)
|
|
2
|
+
|
|
3
|
+
This reference describes the reporting infrastructure for the spec. Include this content
|
|
4
|
+
in the Reporting section of the generated specification. All code samples must be reproduced
|
|
5
|
+
in the generated spec with full import statements and constructor injection.
|
|
6
|
+
|
|
7
|
+
The reporting feature provides:
|
|
8
|
+
- **JRDesign API** for building report layouts 100% programmatically — no `.jrxml` XML files
|
|
9
|
+
- **DTO-based data source** via `JRBeanCollectionDataSource` — no direct DB queries in reports
|
|
10
|
+
- **Multi-format export** — PDF, XLSX, CSV
|
|
11
|
+
- **Report registry** persisted in the database for discoverability
|
|
12
|
+
- **REST API endpoints** for listing reports, retrieving report metadata, and generating downloads
|
|
13
|
+
- **OpenAPI annotations** on all endpoints for Swagger documentation
|
|
14
|
+
|
|
15
|
+
> **Why JRDesign API instead of `.jrxml` templates?**
|
|
16
|
+
> Programmatic report design via `JRDesign*` classes is AI-agent-friendly: no XML parsing,
|
|
17
|
+
> compile-time type safety, dynamic layout construction, and no separate template files to
|
|
18
|
+
> manage. The entire report definition lives in Java code, making it easy to review, test,
|
|
19
|
+
> and refactor.
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## Architecture Overview
|
|
24
|
+
|
|
25
|
+
```
|
|
26
|
+
Module Shared Reporting Infrastructure
|
|
27
|
+
┌─────────────────────┐ ┌─────────────────────────────────────────┐
|
|
28
|
+
│ StaffReport │ │ ReportService │
|
|
29
|
+
│ implements │──►│ compile(JasperDesign → JasperReport) │
|
|
30
|
+
│ ReportDefinition │ │ fill(JasperReport + DTO Collection) │
|
|
31
|
+
│ │ │ export(JasperPrint → PDF/XLSX/CSV) │
|
|
32
|
+
│ buildDesign() │ │ │
|
|
33
|
+
│ getParameters() │ │ ReportRegistry │
|
|
34
|
+
│ generateData(...) │ │ discovers @Component ReportDefinition│
|
|
35
|
+
│ │ │ persists to report table │
|
|
36
|
+
│ DTO: StaffReportDTO│ │ │
|
|
37
|
+
│ (data source bean) │ │ ReportController (@RestController) │
|
|
38
|
+
│ │ │ GET /api/v1/reports — list reports │
|
|
39
|
+
└─────────────────────┘ │ GET /api/v1/reports/{id} — details │
|
|
40
|
+
│ POST /api/v1/reports/{id}/generate │
|
|
41
|
+
└─────────────────────────────────────────┘
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
**Data flow:**
|
|
45
|
+
1. Client calls `GET /api/v1/reports` to list available reports (JSON response)
|
|
46
|
+
2. Client calls `GET /api/v1/reports/{reportId}` to retrieve report metadata and parameter descriptors (JSON response)
|
|
47
|
+
3. Client calls `POST /api/v1/reports/{reportId}/generate` with parameters in the request body and desired format as a query parameter
|
|
48
|
+
4. `ReportController` dispatches to the matching `ReportDefinition`
|
|
49
|
+
5. `ReportDefinition.buildDesign(params)` constructs a `JasperDesign` programmatically using JRDesign API
|
|
50
|
+
6. `ReportDefinition.generateData(params)` calls module services to produce a `List<DTO>`
|
|
51
|
+
7. `ReportService` compiles the `JasperDesign` into a `JasperReport`, fills it with `JRBeanCollectionDataSource`, and exports
|
|
52
|
+
8. The exported file (PDF/XLSX/CSV) is returned as a binary download with `Content-Disposition: attachment`
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## Maven Dependencies
|
|
57
|
+
|
|
58
|
+
```xml
|
|
59
|
+
<!-- Reporting (included when Reporting = yes) -->
|
|
60
|
+
<dependency>
|
|
61
|
+
<groupId>net.sf.jasperreports</groupId>
|
|
62
|
+
<artifactId>jasperreports</artifactId>
|
|
63
|
+
<version>7.0.3</version>
|
|
64
|
+
</dependency>
|
|
65
|
+
<dependency>
|
|
66
|
+
<groupId>net.sf.jasperreports</groupId>
|
|
67
|
+
<artifactId>jasperreports-fonts</artifactId>
|
|
68
|
+
<version>7.0.3</version>
|
|
69
|
+
</dependency>
|
|
70
|
+
<!-- PDF export via OpenPDF (JasperReports 7.x default) -->
|
|
71
|
+
<dependency>
|
|
72
|
+
<groupId>com.github.librepdf</groupId>
|
|
73
|
+
<artifactId>openpdf</artifactId>
|
|
74
|
+
<version>2.0.4</version>
|
|
75
|
+
</dependency>
|
|
76
|
+
<!-- XLSX export -->
|
|
77
|
+
<dependency>
|
|
78
|
+
<groupId>org.apache.poi</groupId>
|
|
79
|
+
<artifactId>poi-ooxml</artifactId>
|
|
80
|
+
<version>5.4.1</version>
|
|
81
|
+
</dependency>
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
> **Note:** JasperReports 7.x uses OpenPDF instead of iText for PDF generation. The
|
|
85
|
+
> `jasperreports` artifact pulls in transitive dependencies, but `openpdf` and `poi-ooxml`
|
|
86
|
+
> should be declared explicitly for version control.
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
## Application Configuration
|
|
91
|
+
|
|
92
|
+
```yaml
|
|
93
|
+
# [If Reporting = yes] add to application.yml:
|
|
94
|
+
app:
|
|
95
|
+
reporting:
|
|
96
|
+
temp-path: ${java.io.tmpdir}/reports # temp directory for export files
|
|
97
|
+
default-format: PDF # PDF, XLSX, CSV
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### application-prod.yml overrides
|
|
101
|
+
|
|
102
|
+
```yaml
|
|
103
|
+
app:
|
|
104
|
+
reporting:
|
|
105
|
+
temp-path: ${REPORT_TEMP_PATH:/tmp/reports}
|
|
106
|
+
default-format: PDF
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
> **Note:** No `template-path` or `compiled-path` configuration is needed. Report layouts
|
|
110
|
+
> are built programmatically via `JRDesign*` API and compiled in memory. Only a temp path
|
|
111
|
+
> for intermediate export files is required.
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
## Package Structure
|
|
116
|
+
|
|
117
|
+
```
|
|
118
|
+
shared/
|
|
119
|
+
└── reporting/
|
|
120
|
+
├── ReportDefinition.java # Interface with buildDesign() for programmatic layout
|
|
121
|
+
├── ReportParameter.java # Report parameter descriptor record
|
|
122
|
+
├── ReportFormat.java # Enum: PDF, XLSX, CSV
|
|
123
|
+
├── ReportResult.java # Holds generated bytes + metadata
|
|
124
|
+
├── ReportDesignHelper.java # Static builder for common JRDesign patterns
|
|
125
|
+
├── ReportService.java # Compile, fill, export orchestrator
|
|
126
|
+
├── ReportRegistry.java # Auto-discovers and persists report definitions
|
|
127
|
+
├── ReportConfig.java # Configuration properties binding
|
|
128
|
+
├── ReportEntity.java # [If PostgreSQL/MySQL] Database entity for report registry
|
|
129
|
+
├── ReportRepository.java # [If PostgreSQL/MySQL] Spring Data JPA repository
|
|
130
|
+
├── ReportDocument.java # [If MongoDB] MongoDB document for report registry
|
|
131
|
+
├── ReportGenerationException.java # Runtime exception for report failures
|
|
132
|
+
├── ReportInfoDTO.java # REST response DTO for report metadata
|
|
133
|
+
└── ReportController.java # @RestController for report API endpoints
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
---
|
|
137
|
+
|
|
138
|
+
## Report Definition Interface
|
|
139
|
+
|
|
140
|
+
Modules implement this interface to define their reports. Each implementation is
|
|
141
|
+
a Spring `@Component` automatically discovered by the `ReportRegistry`.
|
|
142
|
+
|
|
143
|
+
The key difference from template-based reporting is the `buildDesign()` method, which
|
|
144
|
+
returns a `JasperDesign` built programmatically using JRDesign API classes.
|
|
145
|
+
|
|
146
|
+
```java
|
|
147
|
+
package {{BASE_PACKAGE}}.shared.reporting;
|
|
148
|
+
|
|
149
|
+
import net.sf.jasperreports.engine.design.JasperDesign;
|
|
150
|
+
|
|
151
|
+
import java.util.Collection;
|
|
152
|
+
import java.util.List;
|
|
153
|
+
import java.util.Map;
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Contract for domain-defined reports. Each domain module creates one or more
|
|
157
|
+
* implementations as Spring @Component beans. The ReportRegistry auto-discovers
|
|
158
|
+
* them at startup and persists their metadata to the database.
|
|
159
|
+
*
|
|
160
|
+
* Report layouts are built programmatically via JRDesign API — no .jrxml files.
|
|
161
|
+
*/
|
|
162
|
+
public interface ReportDefinition {
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Unique identifier for this report (e.g., "staff-allocation-summary").
|
|
166
|
+
* Used as the report ID in URLs and database records.
|
|
167
|
+
*/
|
|
168
|
+
String getReportId();
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Human-readable report name displayed in the report list.
|
|
172
|
+
*/
|
|
173
|
+
String getName();
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Brief description of what this report contains.
|
|
177
|
+
*/
|
|
178
|
+
String getDescription();
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* The domain this report belongs to (e.g., "Staff", "Project").
|
|
182
|
+
* Used for grouping reports in the API responses.
|
|
183
|
+
*/
|
|
184
|
+
String getDomain();
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Defines the parameters this report accepts (date range, filters, etc.).
|
|
188
|
+
* The API returns these descriptors so clients can build parameter forms.
|
|
189
|
+
* Return an empty list if the report takes no parameters.
|
|
190
|
+
*/
|
|
191
|
+
List<ReportParameter> getParameters();
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Builds the report layout programmatically using JRDesign API.
|
|
195
|
+
* Called by ReportService before compilation. The returned JasperDesign
|
|
196
|
+
* is compiled into a JasperReport and cached by reportId.
|
|
197
|
+
*
|
|
198
|
+
* @param parameters user-supplied parameter values (may influence layout)
|
|
199
|
+
* @return a fully constructed JasperDesign ready for compilation
|
|
200
|
+
*/
|
|
201
|
+
JasperDesign buildDesign(Map<String, Object> parameters);
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Executes domain-specific logic to produce the report data as a collection
|
|
205
|
+
* of DTOs. The returned collection is wrapped in JRBeanCollectionDataSource
|
|
206
|
+
* by the ReportService.
|
|
207
|
+
*
|
|
208
|
+
* @param parameters user-supplied parameter values keyed by parameter name
|
|
209
|
+
* @return collection of DTO beans to be used as the Jasper data source
|
|
210
|
+
*/
|
|
211
|
+
Collection<?> generateData(Map<String, Object> parameters);
|
|
212
|
+
}
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
---
|
|
216
|
+
|
|
217
|
+
## Report Parameter Descriptor
|
|
218
|
+
|
|
219
|
+
```java
|
|
220
|
+
package {{BASE_PACKAGE}}.shared.reporting;
|
|
221
|
+
|
|
222
|
+
import java.util.List;
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Describes a single parameter that a report accepts.
|
|
226
|
+
* Used by the REST API to expose parameter metadata so clients can build forms.
|
|
227
|
+
*
|
|
228
|
+
* @param name parameter key (e.g., "startDate", "departmentId")
|
|
229
|
+
* @param label human-readable label for the form field
|
|
230
|
+
* @param type input type: TEXT, DATE, NUMBER, SELECT, BOOLEAN
|
|
231
|
+
* @param required whether the parameter is mandatory
|
|
232
|
+
* @param options for SELECT type: list of allowed values (label-value pairs)
|
|
233
|
+
* @param defaultValue default value if user doesn't provide one
|
|
234
|
+
*/
|
|
235
|
+
public record ReportParameter(
|
|
236
|
+
String name,
|
|
237
|
+
String label,
|
|
238
|
+
ParameterType type,
|
|
239
|
+
boolean required,
|
|
240
|
+
List<Option> options,
|
|
241
|
+
String defaultValue
|
|
242
|
+
) {
|
|
243
|
+
public enum ParameterType {
|
|
244
|
+
TEXT, DATE, NUMBER, SELECT, BOOLEAN
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
public record Option(String label, String value) {}
|
|
248
|
+
|
|
249
|
+
public static ReportParameter text(String name, String label, boolean required) {
|
|
250
|
+
return new ReportParameter(name, label, ParameterType.TEXT, required, List.of(), null);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
public static ReportParameter date(String name, String label, boolean required) {
|
|
254
|
+
return new ReportParameter(name, label, ParameterType.DATE, required, List.of(), null);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
public static ReportParameter number(String name, String label, boolean required) {
|
|
258
|
+
return new ReportParameter(name, label, ParameterType.NUMBER, required, List.of(), null);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
public static ReportParameter select(String name, String label, boolean required, List<Option> options) {
|
|
262
|
+
return new ReportParameter(name, label, ParameterType.SELECT, required, options, null);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
public static ReportParameter bool(String name, String label, String defaultValue) {
|
|
266
|
+
return new ReportParameter(name, label, ParameterType.BOOLEAN, false, List.of(), defaultValue);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
---
|
|
272
|
+
|
|
273
|
+
## Report Format Enum
|
|
274
|
+
|
|
275
|
+
```java
|
|
276
|
+
package {{BASE_PACKAGE}}.shared.reporting;
|
|
277
|
+
|
|
278
|
+
public enum ReportFormat {
|
|
279
|
+
PDF("application/pdf", ".pdf"),
|
|
280
|
+
XLSX("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", ".xlsx"),
|
|
281
|
+
CSV("text/csv", ".csv");
|
|
282
|
+
|
|
283
|
+
private final String contentType;
|
|
284
|
+
private final String extension;
|
|
285
|
+
|
|
286
|
+
ReportFormat(String contentType, String extension) {
|
|
287
|
+
this.contentType = contentType;
|
|
288
|
+
this.extension = extension;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
public String getContentType() { return contentType; }
|
|
292
|
+
public String getExtension() { return extension; }
|
|
293
|
+
}
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
---
|
|
297
|
+
|
|
298
|
+
## Report Result Record
|
|
299
|
+
|
|
300
|
+
```java
|
|
301
|
+
package {{BASE_PACKAGE}}.shared.reporting;
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Holds the generated report output.
|
|
305
|
+
*
|
|
306
|
+
* @param data the exported report bytes
|
|
307
|
+
* @param format the export format used
|
|
308
|
+
* @param fileName suggested file name for download
|
|
309
|
+
* @param contentType MIME type for the HTTP response
|
|
310
|
+
*/
|
|
311
|
+
public record ReportResult(
|
|
312
|
+
byte[] data,
|
|
313
|
+
ReportFormat format,
|
|
314
|
+
String fileName,
|
|
315
|
+
String contentType
|
|
316
|
+
) {
|
|
317
|
+
public static ReportResult of(byte[] data, ReportFormat format, String reportId) {
|
|
318
|
+
String fileName = reportId + format.getExtension();
|
|
319
|
+
return new ReportResult(data, format, fileName, format.getContentType());
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
---
|
|
325
|
+
|
|
326
|
+
## Report Design Helper
|
|
327
|
+
|
|
328
|
+
Static utility class providing common JRDesign building blocks. Modules use these helpers
|
|
329
|
+
inside their `buildDesign()` implementations to avoid repetitive low-level JRDesign API calls.
|
|
330
|
+
|
|
331
|
+
```java
|
|
332
|
+
package {{BASE_PACKAGE}}.shared.reporting;
|
|
333
|
+
|
|
334
|
+
import net.sf.jasperreports.engine.design.JRDesignBand;
|
|
335
|
+
import net.sf.jasperreports.engine.design.JRDesignExpression;
|
|
336
|
+
import net.sf.jasperreports.engine.design.JRDesignField;
|
|
337
|
+
import net.sf.jasperreports.engine.design.JRDesignStaticText;
|
|
338
|
+
import net.sf.jasperreports.engine.design.JRDesignStyle;
|
|
339
|
+
import net.sf.jasperreports.engine.design.JRDesignTextField;
|
|
340
|
+
import net.sf.jasperreports.engine.design.JasperDesign;
|
|
341
|
+
import net.sf.jasperreports.engine.type.HorizontalTextAlignEnum;
|
|
342
|
+
import net.sf.jasperreports.engine.type.VerticalTextAlignEnum;
|
|
343
|
+
|
|
344
|
+
import java.awt.Color;
|
|
345
|
+
import java.util.List;
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Static helpers for common JRDesign report building patterns.
|
|
349
|
+
* All methods return JRDesign objects that can be added to a JasperDesign.
|
|
350
|
+
*/
|
|
351
|
+
public final class ReportDesignHelper {
|
|
352
|
+
|
|
353
|
+
private ReportDesignHelper() {
|
|
354
|
+
// utility class
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// ── Standard page dimensions ──
|
|
358
|
+
|
|
359
|
+
/** A4 Portrait: 595 x 842 points (72 dpi), margins 20 each side. */
|
|
360
|
+
public static final int A4_PORTRAIT_WIDTH = 595;
|
|
361
|
+
public static final int A4_PORTRAIT_HEIGHT = 842;
|
|
362
|
+
|
|
363
|
+
/** A4 Landscape: 842 x 595 points (72 dpi), margins 20 each side. */
|
|
364
|
+
public static final int A4_LANDSCAPE_WIDTH = 842;
|
|
365
|
+
public static final int A4_LANDSCAPE_HEIGHT = 595;
|
|
366
|
+
|
|
367
|
+
private static final int DEFAULT_MARGIN = 20;
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Column definition for table-style reports.
|
|
371
|
+
*
|
|
372
|
+
* @param label the column header label
|
|
373
|
+
* @param fieldName the DTO field name (must match JRDesignField name)
|
|
374
|
+
* @param width the column width in points
|
|
375
|
+
*/
|
|
376
|
+
public record ColumnDef(String label, String fieldName, int width) {}
|
|
377
|
+
|
|
378
|
+
// ── Design factory methods ──
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Create a new A4 Portrait JasperDesign with standard margins.
|
|
382
|
+
*
|
|
383
|
+
* @param name the report name (used as the design name)
|
|
384
|
+
* @return a JasperDesign configured for A4 portrait orientation
|
|
385
|
+
*/
|
|
386
|
+
public static JasperDesign createA4PortraitDesign(String name) {
|
|
387
|
+
JasperDesign design = new JasperDesign();
|
|
388
|
+
design.setName(name);
|
|
389
|
+
design.setPageWidth(A4_PORTRAIT_WIDTH);
|
|
390
|
+
design.setPageHeight(A4_PORTRAIT_HEIGHT);
|
|
391
|
+
design.setLeftMargin(DEFAULT_MARGIN);
|
|
392
|
+
design.setRightMargin(DEFAULT_MARGIN);
|
|
393
|
+
design.setTopMargin(DEFAULT_MARGIN);
|
|
394
|
+
design.setBottomMargin(DEFAULT_MARGIN);
|
|
395
|
+
design.setColumnWidth(A4_PORTRAIT_WIDTH - DEFAULT_MARGIN * 2);
|
|
396
|
+
return design;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Create a new A4 Landscape JasperDesign with standard margins.
|
|
401
|
+
*
|
|
402
|
+
* @param name the report name (used as the design name)
|
|
403
|
+
* @return a JasperDesign configured for A4 landscape orientation
|
|
404
|
+
*/
|
|
405
|
+
public static JasperDesign createA4LandscapeDesign(String name) {
|
|
406
|
+
JasperDesign design = new JasperDesign();
|
|
407
|
+
design.setName(name);
|
|
408
|
+
design.setPageWidth(A4_LANDSCAPE_WIDTH);
|
|
409
|
+
design.setPageHeight(A4_LANDSCAPE_HEIGHT);
|
|
410
|
+
design.setLeftMargin(DEFAULT_MARGIN);
|
|
411
|
+
design.setRightMargin(DEFAULT_MARGIN);
|
|
412
|
+
design.setTopMargin(DEFAULT_MARGIN);
|
|
413
|
+
design.setBottomMargin(DEFAULT_MARGIN);
|
|
414
|
+
design.setColumnWidth(A4_LANDSCAPE_WIDTH - DEFAULT_MARGIN * 2);
|
|
415
|
+
return design;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// ── Field helpers ──
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* Add a field declaration to the JasperDesign. Fields map to DTO properties
|
|
422
|
+
* and are referenced in detail band text fields via $F{fieldName} expressions.
|
|
423
|
+
*
|
|
424
|
+
* @param design the JasperDesign to add the field to
|
|
425
|
+
* @param name the field name (must match the DTO property name)
|
|
426
|
+
* @param type the field Java type (e.g., String.class, Integer.class)
|
|
427
|
+
*/
|
|
428
|
+
public static void addField(JasperDesign design, String name, Class<?> type) {
|
|
429
|
+
try {
|
|
430
|
+
JRDesignField field = new JRDesignField();
|
|
431
|
+
field.setName(name);
|
|
432
|
+
field.setValueClass(type);
|
|
433
|
+
design.addField(field);
|
|
434
|
+
} catch (Exception e) {
|
|
435
|
+
throw new ReportGenerationException("Failed to add field '" + name + "' to design", e);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// ── Band builders ──
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* Create a title band with a centered, bold heading.
|
|
443
|
+
*
|
|
444
|
+
* @param title the report title text
|
|
445
|
+
* @param height the band height in points
|
|
446
|
+
* @return a JRDesignBand configured as a title band
|
|
447
|
+
*/
|
|
448
|
+
public static JRDesignBand createTitleBand(String title, int height) {
|
|
449
|
+
JRDesignBand band = new JRDesignBand();
|
|
450
|
+
band.setHeight(height);
|
|
451
|
+
|
|
452
|
+
JRDesignStaticText titleText = new JRDesignStaticText();
|
|
453
|
+
titleText.setX(0);
|
|
454
|
+
titleText.setY(0);
|
|
455
|
+
titleText.setWidth(802); // full column width for landscape; adjusted by caller if needed
|
|
456
|
+
titleText.setHeight(height);
|
|
457
|
+
titleText.setText(title);
|
|
458
|
+
titleText.setFontSize(18f);
|
|
459
|
+
titleText.setBold(true);
|
|
460
|
+
titleText.setHorizontalTextAlign(HorizontalTextAlignEnum.CENTER);
|
|
461
|
+
titleText.setVerticalTextAlign(VerticalTextAlignEnum.MIDDLE);
|
|
462
|
+
|
|
463
|
+
band.addElement(titleText);
|
|
464
|
+
return band;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/**
|
|
468
|
+
* Create a column header band with styled header labels based on column definitions.
|
|
469
|
+
* Each column header is a static text element with bold font and a light gray background.
|
|
470
|
+
*
|
|
471
|
+
* @param columns the list of column definitions
|
|
472
|
+
* @return a JRDesignBand configured as a column header band
|
|
473
|
+
*/
|
|
474
|
+
public static JRDesignBand createColumnHeaderBand(List<ColumnDef> columns) {
|
|
475
|
+
JRDesignBand band = new JRDesignBand();
|
|
476
|
+
band.setHeight(30);
|
|
477
|
+
|
|
478
|
+
int xOffset = 0;
|
|
479
|
+
for (ColumnDef col : columns) {
|
|
480
|
+
JRDesignStaticText header = new JRDesignStaticText();
|
|
481
|
+
header.setX(xOffset);
|
|
482
|
+
header.setY(0);
|
|
483
|
+
header.setWidth(col.width());
|
|
484
|
+
header.setHeight(30);
|
|
485
|
+
header.setText(col.label());
|
|
486
|
+
header.setFontSize(10f);
|
|
487
|
+
header.setBold(true);
|
|
488
|
+
header.setVerticalTextAlign(VerticalTextAlignEnum.MIDDLE);
|
|
489
|
+
header.setMode(net.sf.jasperreports.engine.type.ModeEnum.OPAQUE);
|
|
490
|
+
header.setBackcolor(new Color(232, 232, 232)); // light gray #E8E8E8
|
|
491
|
+
|
|
492
|
+
band.addElement(header);
|
|
493
|
+
xOffset += col.width();
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
return band;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
/**
|
|
500
|
+
* Create a detail band with text fields mapped to DTO properties via $F{fieldName} expressions.
|
|
501
|
+
* Each text field is positioned according to the corresponding column definition.
|
|
502
|
+
*
|
|
503
|
+
* @param columns the list of column definitions (fieldName must match declared JRDesignField names)
|
|
504
|
+
* @return a JRDesignBand configured as a detail band
|
|
505
|
+
*/
|
|
506
|
+
public static JRDesignBand createDetailBand(List<ColumnDef> columns) {
|
|
507
|
+
JRDesignBand band = new JRDesignBand();
|
|
508
|
+
band.setHeight(25);
|
|
509
|
+
|
|
510
|
+
int xOffset = 0;
|
|
511
|
+
for (ColumnDef col : columns) {
|
|
512
|
+
JRDesignTextField textField = new JRDesignTextField();
|
|
513
|
+
textField.setX(xOffset);
|
|
514
|
+
textField.setY(0);
|
|
515
|
+
textField.setWidth(col.width());
|
|
516
|
+
textField.setHeight(25);
|
|
517
|
+
textField.setFontSize(9f);
|
|
518
|
+
textField.setVerticalTextAlign(VerticalTextAlignEnum.MIDDLE);
|
|
519
|
+
textField.setBlankWhenNull(true);
|
|
520
|
+
|
|
521
|
+
JRDesignExpression expression = new JRDesignExpression();
|
|
522
|
+
expression.setText("$F{" + col.fieldName() + "}");
|
|
523
|
+
textField.setExpression(expression);
|
|
524
|
+
|
|
525
|
+
band.addElement(textField);
|
|
526
|
+
xOffset += col.width();
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
return band;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
/**
|
|
533
|
+
* Create a page footer band with page number display ("Page X of Y").
|
|
534
|
+
* Uses built-in JasperReports variables $V{PAGE_NUMBER}.
|
|
535
|
+
*
|
|
536
|
+
* @return a JRDesignBand configured as a page footer band
|
|
537
|
+
*/
|
|
538
|
+
public static JRDesignBand createPageFooterBand() {
|
|
539
|
+
JRDesignBand band = new JRDesignBand();
|
|
540
|
+
band.setHeight(30);
|
|
541
|
+
|
|
542
|
+
// "Page X" — evaluates at page time
|
|
543
|
+
JRDesignTextField pageNumberField = new JRDesignTextField();
|
|
544
|
+
pageNumberField.setX(0);
|
|
545
|
+
pageNumberField.setY(5);
|
|
546
|
+
pageNumberField.setWidth(100);
|
|
547
|
+
pageNumberField.setHeight(20);
|
|
548
|
+
pageNumberField.setFontSize(8f);
|
|
549
|
+
pageNumberField.setHorizontalTextAlign(HorizontalTextAlignEnum.LEFT);
|
|
550
|
+
|
|
551
|
+
JRDesignExpression pageExpr = new JRDesignExpression();
|
|
552
|
+
pageExpr.setText("\"Page \" + $V{PAGE_NUMBER}");
|
|
553
|
+
pageNumberField.setExpression(pageExpr);
|
|
554
|
+
|
|
555
|
+
band.addElement(pageNumberField);
|
|
556
|
+
|
|
557
|
+
// "of Y" — evaluates at report time (after all pages rendered)
|
|
558
|
+
JRDesignTextField totalPagesField = new JRDesignTextField();
|
|
559
|
+
totalPagesField.setX(100);
|
|
560
|
+
totalPagesField.setY(5);
|
|
561
|
+
totalPagesField.setWidth(60);
|
|
562
|
+
totalPagesField.setHeight(20);
|
|
563
|
+
totalPagesField.setFontSize(8f);
|
|
564
|
+
totalPagesField.setHorizontalTextAlign(HorizontalTextAlignEnum.LEFT);
|
|
565
|
+
totalPagesField.setEvaluationTime(net.sf.jasperreports.engine.type.EvaluationTimeEnum.REPORT);
|
|
566
|
+
|
|
567
|
+
JRDesignExpression totalExpr = new JRDesignExpression();
|
|
568
|
+
totalExpr.setText("\" of \" + $V{PAGE_NUMBER}");
|
|
569
|
+
totalPagesField.setExpression(totalExpr);
|
|
570
|
+
|
|
571
|
+
band.addElement(totalPagesField);
|
|
572
|
+
|
|
573
|
+
return band;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
// ── Style helpers ──
|
|
577
|
+
|
|
578
|
+
/**
|
|
579
|
+
* Create a JRDesignStyle with the specified font properties.
|
|
580
|
+
*
|
|
581
|
+
* @param name the style name (used to reference the style in design elements)
|
|
582
|
+
* @param fontName the font family name (e.g., "SansSerif", "DejaVu Sans")
|
|
583
|
+
* @param fontSize the font size in points
|
|
584
|
+
* @param bold whether the font is bold
|
|
585
|
+
* @return a JRDesignStyle configured with the specified properties
|
|
586
|
+
*/
|
|
587
|
+
public static JRDesignStyle createStyle(String name, String fontName, int fontSize, boolean bold) {
|
|
588
|
+
JRDesignStyle style = new JRDesignStyle();
|
|
589
|
+
style.setName(name);
|
|
590
|
+
style.setFontName(fontName);
|
|
591
|
+
style.setFontSize((float) fontSize);
|
|
592
|
+
style.setBold(bold);
|
|
593
|
+
return style;
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
```
|
|
597
|
+
|
|
598
|
+
---
|
|
599
|
+
|
|
600
|
+
## Report Configuration
|
|
601
|
+
|
|
602
|
+
```java
|
|
603
|
+
package {{BASE_PACKAGE}}.shared.reporting;
|
|
604
|
+
|
|
605
|
+
import lombok.Getter;
|
|
606
|
+
import lombok.Setter;
|
|
607
|
+
import org.springframework.boot.context.properties.ConfigurationProperties;
|
|
608
|
+
import org.springframework.context.annotation.Configuration;
|
|
609
|
+
|
|
610
|
+
@Configuration
|
|
611
|
+
@ConfigurationProperties(prefix = "app.reporting")
|
|
612
|
+
@Getter
|
|
613
|
+
@Setter
|
|
614
|
+
public class ReportConfig {
|
|
615
|
+
|
|
616
|
+
private String tempPath = System.getProperty("java.io.tmpdir") + "/reports";
|
|
617
|
+
private ReportFormat defaultFormat = ReportFormat.PDF;
|
|
618
|
+
}
|
|
619
|
+
```
|
|
620
|
+
|
|
621
|
+
> **Note:** Unlike the template-based variant, there are no `templatePath` or `compiledPath`
|
|
622
|
+
> properties. Report layouts are built in memory via `JRDesign*` API and compiled on the fly.
|
|
623
|
+
> Only `tempPath` is retained for any temporary file operations during export.
|
|
624
|
+
|
|
625
|
+
---
|
|
626
|
+
|
|
627
|
+
## Report Service
|
|
628
|
+
|
|
629
|
+
The central orchestrator that compiles `JasperDesign` objects returned by `ReportDefinition.buildDesign()`,
|
|
630
|
+
fills them with DTO collections via `JRBeanCollectionDataSource`, and exports to the requested format.
|
|
631
|
+
|
|
632
|
+
Compiled `JasperReport` objects are cached by `reportId` to avoid re-compilation on every request.
|
|
633
|
+
|
|
634
|
+
```java
|
|
635
|
+
package {{BASE_PACKAGE}}.shared.reporting;
|
|
636
|
+
|
|
637
|
+
import lombok.RequiredArgsConstructor;
|
|
638
|
+
import lombok.extern.slf4j.Slf4j;
|
|
639
|
+
import net.sf.jasperreports.engine.JRException;
|
|
640
|
+
import net.sf.jasperreports.engine.JasperCompileManager;
|
|
641
|
+
import net.sf.jasperreports.engine.JasperFillManager;
|
|
642
|
+
import net.sf.jasperreports.engine.JasperPrint;
|
|
643
|
+
import net.sf.jasperreports.engine.JasperReport;
|
|
644
|
+
import net.sf.jasperreports.engine.data.JRBeanCollectionDataSource;
|
|
645
|
+
import net.sf.jasperreports.engine.design.JasperDesign;
|
|
646
|
+
import net.sf.jasperreports.engine.export.JRCsvExporter;
|
|
647
|
+
import net.sf.jasperreports.engine.export.ooxml.JRXlsxExporter;
|
|
648
|
+
import net.sf.jasperreports.export.SimpleCsvExporterConfiguration;
|
|
649
|
+
import net.sf.jasperreports.export.SimpleExporterInput;
|
|
650
|
+
import net.sf.jasperreports.export.SimpleOutputStreamExporterOutput;
|
|
651
|
+
import net.sf.jasperreports.export.SimpleXlsxReportConfiguration;
|
|
652
|
+
import net.sf.jasperreports.pdf.JRPdfExporter;
|
|
653
|
+
import org.springframework.stereotype.Service;
|
|
654
|
+
|
|
655
|
+
import java.io.ByteArrayOutputStream;
|
|
656
|
+
import java.util.Collection;
|
|
657
|
+
import java.util.HashMap;
|
|
658
|
+
import java.util.Map;
|
|
659
|
+
import java.util.concurrent.ConcurrentHashMap;
|
|
660
|
+
|
|
661
|
+
@Service
|
|
662
|
+
@RequiredArgsConstructor
|
|
663
|
+
@Slf4j
|
|
664
|
+
public class ReportService {
|
|
665
|
+
|
|
666
|
+
private final ReportConfig config;
|
|
667
|
+
|
|
668
|
+
// Cache compiled JasperReport objects by reportId to avoid re-compilation
|
|
669
|
+
private final Map<String, JasperReport> compiledCache = new ConcurrentHashMap<>();
|
|
670
|
+
|
|
671
|
+
/**
|
|
672
|
+
* Generate a report using the specified ReportDefinition, user parameters, and format.
|
|
673
|
+
*
|
|
674
|
+
* @param definition the report definition (provides design builder and data generation)
|
|
675
|
+
* @param parameters user-supplied parameter values
|
|
676
|
+
* @param format desired export format (PDF, XLSX, CSV)
|
|
677
|
+
* @return ReportResult containing the exported bytes and metadata
|
|
678
|
+
*/
|
|
679
|
+
public ReportResult generate(ReportDefinition definition,
|
|
680
|
+
Map<String, Object> parameters,
|
|
681
|
+
ReportFormat format) {
|
|
682
|
+
log.info("Generating report [{}] in format [{}]", definition.getReportId(), format);
|
|
683
|
+
|
|
684
|
+
// 1. Generate the data from the domain module
|
|
685
|
+
Collection<?> data = definition.generateData(parameters);
|
|
686
|
+
log.debug("Report [{}] produced {} data records", definition.getReportId(), data.size());
|
|
687
|
+
|
|
688
|
+
// 2. Build and compile the JasperDesign (cached by reportId)
|
|
689
|
+
JasperReport jasperReport = compiledCache.computeIfAbsent(
|
|
690
|
+
definition.getReportId(),
|
|
691
|
+
id -> compileDesign(definition, parameters)
|
|
692
|
+
);
|
|
693
|
+
|
|
694
|
+
// 3. Create the DTO-based data source
|
|
695
|
+
JRBeanCollectionDataSource dataSource = new JRBeanCollectionDataSource(data);
|
|
696
|
+
|
|
697
|
+
// 4. Build Jasper parameters (pass user params + any report-level params)
|
|
698
|
+
Map<String, Object> jasperParams = new HashMap<>(parameters);
|
|
699
|
+
|
|
700
|
+
// 5. Fill the report
|
|
701
|
+
JasperPrint jasperPrint = fillReport(jasperReport, jasperParams, dataSource);
|
|
702
|
+
|
|
703
|
+
// 6. Export to the requested format
|
|
704
|
+
byte[] exportedBytes = exportReport(jasperPrint, format);
|
|
705
|
+
|
|
706
|
+
log.info("Report [{}] generated successfully: {} bytes", definition.getReportId(), exportedBytes.length);
|
|
707
|
+
return ReportResult.of(exportedBytes, format, definition.getReportId());
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
/**
|
|
711
|
+
* Build and compile a JasperDesign from a ReportDefinition.
|
|
712
|
+
* The definition's buildDesign() method constructs the layout programmatically.
|
|
713
|
+
*/
|
|
714
|
+
private JasperReport compileDesign(ReportDefinition definition, Map<String, Object> parameters) {
|
|
715
|
+
try {
|
|
716
|
+
log.debug("Building and compiling design for report: {}", definition.getReportId());
|
|
717
|
+
JasperDesign design = definition.buildDesign(parameters);
|
|
718
|
+
return JasperCompileManager.compileReport(design);
|
|
719
|
+
} catch (JRException e) {
|
|
720
|
+
throw new ReportGenerationException(
|
|
721
|
+
"Failed to compile design for report: " + definition.getReportId(), e);
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
/**
|
|
726
|
+
* Fill a compiled JasperReport with parameters and data source.
|
|
727
|
+
*/
|
|
728
|
+
private JasperPrint fillReport(JasperReport report,
|
|
729
|
+
Map<String, Object> parameters,
|
|
730
|
+
JRBeanCollectionDataSource dataSource) {
|
|
731
|
+
try {
|
|
732
|
+
return JasperFillManager.fillReport(report, parameters, dataSource);
|
|
733
|
+
} catch (JRException e) {
|
|
734
|
+
throw new ReportGenerationException("Failed to fill report", e);
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
/**
|
|
739
|
+
* Export JasperPrint to the specified format.
|
|
740
|
+
*/
|
|
741
|
+
private byte[] exportReport(JasperPrint jasperPrint, ReportFormat format) {
|
|
742
|
+
try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
|
|
743
|
+
switch (format) {
|
|
744
|
+
case PDF -> exportToPdf(jasperPrint, outputStream);
|
|
745
|
+
case XLSX -> exportToXlsx(jasperPrint, outputStream);
|
|
746
|
+
case CSV -> exportToCsv(jasperPrint, outputStream);
|
|
747
|
+
}
|
|
748
|
+
return outputStream.toByteArray();
|
|
749
|
+
} catch (Exception e) {
|
|
750
|
+
throw new ReportGenerationException("Failed to export report as " + format, e);
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
private void exportToPdf(JasperPrint print, ByteArrayOutputStream out) throws JRException {
|
|
755
|
+
JRPdfExporter exporter = new JRPdfExporter();
|
|
756
|
+
exporter.setExporterInput(new SimpleExporterInput(print));
|
|
757
|
+
exporter.setExporterOutput(new SimpleOutputStreamExporterOutput(out));
|
|
758
|
+
exporter.exportReport();
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
private void exportToXlsx(JasperPrint print, ByteArrayOutputStream out) throws JRException {
|
|
762
|
+
JRXlsxExporter exporter = new JRXlsxExporter();
|
|
763
|
+
exporter.setExporterInput(new SimpleExporterInput(print));
|
|
764
|
+
exporter.setExporterOutput(new SimpleOutputStreamExporterOutput(out));
|
|
765
|
+
|
|
766
|
+
SimpleXlsxReportConfiguration xlsxConfig = new SimpleXlsxReportConfiguration();
|
|
767
|
+
xlsxConfig.setOnePagePerSheet(false);
|
|
768
|
+
xlsxConfig.setRemoveEmptySpaceBetweenRows(true);
|
|
769
|
+
xlsxConfig.setDetectCellType(true);
|
|
770
|
+
xlsxConfig.setWhitePageBackground(false);
|
|
771
|
+
exporter.setConfiguration(xlsxConfig);
|
|
772
|
+
|
|
773
|
+
exporter.exportReport();
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
private void exportToCsv(JasperPrint print, ByteArrayOutputStream out) throws JRException {
|
|
777
|
+
JRCsvExporter exporter = new JRCsvExporter();
|
|
778
|
+
exporter.setExporterInput(new SimpleExporterInput(print));
|
|
779
|
+
exporter.setExporterOutput(new SimpleOutputStreamExporterOutput(out));
|
|
780
|
+
|
|
781
|
+
SimpleCsvExporterConfiguration csvConfig = new SimpleCsvExporterConfiguration();
|
|
782
|
+
csvConfig.setFieldDelimiter(",");
|
|
783
|
+
exporter.setConfiguration(csvConfig);
|
|
784
|
+
|
|
785
|
+
exporter.exportReport();
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
/**
|
|
789
|
+
* Evict a cached compiled report (useful when the report design changes at runtime).
|
|
790
|
+
*
|
|
791
|
+
* @param reportId the report ID whose cached compilation should be removed
|
|
792
|
+
*/
|
|
793
|
+
public void evictReport(String reportId) {
|
|
794
|
+
compiledCache.remove(reportId);
|
|
795
|
+
log.info("Evicted cached compiled report: {}", reportId);
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
```
|
|
799
|
+
|
|
800
|
+
---
|
|
801
|
+
|
|
802
|
+
## Report Generation Exception
|
|
803
|
+
|
|
804
|
+
```java
|
|
805
|
+
package {{BASE_PACKAGE}}.shared.reporting;
|
|
806
|
+
|
|
807
|
+
public class ReportGenerationException extends RuntimeException {
|
|
808
|
+
|
|
809
|
+
public ReportGenerationException(String message, Throwable cause) {
|
|
810
|
+
super(message, cause);
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
public ReportGenerationException(String message) {
|
|
814
|
+
super(message);
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
```
|
|
818
|
+
|
|
819
|
+
---
|
|
820
|
+
|
|
821
|
+
## Report Registry
|
|
822
|
+
|
|
823
|
+
The registry auto-discovers all `ReportDefinition` beans at startup and persists their
|
|
824
|
+
metadata to the database. This allows the report list API to return all available reports
|
|
825
|
+
without scanning components at runtime.
|
|
826
|
+
|
|
827
|
+
### [If PostgreSQL/MySQL] Report Entity
|
|
828
|
+
|
|
829
|
+
```java
|
|
830
|
+
package {{BASE_PACKAGE}}.shared.reporting;
|
|
831
|
+
|
|
832
|
+
import jakarta.persistence.Column;
|
|
833
|
+
import jakarta.persistence.Entity;
|
|
834
|
+
import jakarta.persistence.Id;
|
|
835
|
+
import jakarta.persistence.Table;
|
|
836
|
+
import lombok.Getter;
|
|
837
|
+
import lombok.Setter;
|
|
838
|
+
|
|
839
|
+
@Entity
|
|
840
|
+
@Table(name = "report")
|
|
841
|
+
@Getter
|
|
842
|
+
@Setter
|
|
843
|
+
public class ReportEntity {
|
|
844
|
+
|
|
845
|
+
@Id
|
|
846
|
+
@Column(name = "report_id", length = 100)
|
|
847
|
+
private String reportId;
|
|
848
|
+
|
|
849
|
+
@Column(name = "name", nullable = false, length = 255)
|
|
850
|
+
private String name;
|
|
851
|
+
|
|
852
|
+
@Column(name = "description", length = 500)
|
|
853
|
+
private String description;
|
|
854
|
+
|
|
855
|
+
@Column(name = "domain", nullable = false, length = 100)
|
|
856
|
+
private String domain;
|
|
857
|
+
|
|
858
|
+
@Column(name = "active", nullable = false)
|
|
859
|
+
private boolean active = true;
|
|
860
|
+
}
|
|
861
|
+
```
|
|
862
|
+
|
|
863
|
+
### [If PostgreSQL/MySQL] Flyway Migration
|
|
864
|
+
|
|
865
|
+
```sql
|
|
866
|
+
-- V{N}__create_report_table.sql
|
|
867
|
+
CREATE TABLE report (
|
|
868
|
+
report_id VARCHAR(100) PRIMARY KEY,
|
|
869
|
+
name VARCHAR(255) NOT NULL,
|
|
870
|
+
description VARCHAR(500),
|
|
871
|
+
domain VARCHAR(100) NOT NULL,
|
|
872
|
+
active BOOLEAN NOT NULL DEFAULT TRUE
|
|
873
|
+
);
|
|
874
|
+
```
|
|
875
|
+
|
|
876
|
+
### [If PostgreSQL/MySQL] Report Repository
|
|
877
|
+
|
|
878
|
+
```java
|
|
879
|
+
package {{BASE_PACKAGE}}.shared.reporting;
|
|
880
|
+
|
|
881
|
+
import org.springframework.data.jpa.repository.JpaRepository;
|
|
882
|
+
|
|
883
|
+
import java.util.List;
|
|
884
|
+
|
|
885
|
+
public interface ReportRepository extends JpaRepository<ReportEntity, String> {
|
|
886
|
+
|
|
887
|
+
List<ReportEntity> findByActiveTrue();
|
|
888
|
+
|
|
889
|
+
List<ReportEntity> findByDomainAndActiveTrue(String domain);
|
|
890
|
+
}
|
|
891
|
+
```
|
|
892
|
+
|
|
893
|
+
### [If MongoDB] Report Document
|
|
894
|
+
|
|
895
|
+
```java
|
|
896
|
+
package {{BASE_PACKAGE}}.shared.reporting;
|
|
897
|
+
|
|
898
|
+
import lombok.Getter;
|
|
899
|
+
import lombok.Setter;
|
|
900
|
+
import org.springframework.data.annotation.Id;
|
|
901
|
+
import org.springframework.data.mongodb.core.mapping.Document;
|
|
902
|
+
|
|
903
|
+
@Document(collection = "report")
|
|
904
|
+
@Getter
|
|
905
|
+
@Setter
|
|
906
|
+
public class ReportDocument {
|
|
907
|
+
|
|
908
|
+
@Id
|
|
909
|
+
private String reportId;
|
|
910
|
+
private String name;
|
|
911
|
+
private String description;
|
|
912
|
+
private String domain;
|
|
913
|
+
private boolean active = true;
|
|
914
|
+
}
|
|
915
|
+
```
|
|
916
|
+
|
|
917
|
+
### [If MongoDB] Report Repository
|
|
918
|
+
|
|
919
|
+
```java
|
|
920
|
+
package {{BASE_PACKAGE}}.shared.reporting;
|
|
921
|
+
|
|
922
|
+
import org.springframework.data.mongodb.repository.MongoRepository;
|
|
923
|
+
|
|
924
|
+
import java.util.List;
|
|
925
|
+
|
|
926
|
+
public interface ReportRepository extends MongoRepository<ReportDocument, String> {
|
|
927
|
+
|
|
928
|
+
List<ReportDocument> findByActiveTrue();
|
|
929
|
+
|
|
930
|
+
List<ReportDocument> findByDomainAndActiveTrue(String domain);
|
|
931
|
+
}
|
|
932
|
+
```
|
|
933
|
+
|
|
934
|
+
### Registry Service
|
|
935
|
+
|
|
936
|
+
> **Note for [If PostgreSQL/MySQL]:** The code below references `ReportEntity` and
|
|
937
|
+
> `ReportRepository` from the JPA variant. For [If MongoDB], replace `ReportEntity` with
|
|
938
|
+
> `ReportDocument` and use the MongoDB `ReportRepository`.
|
|
939
|
+
|
|
940
|
+
```java
|
|
941
|
+
package {{BASE_PACKAGE}}.shared.reporting;
|
|
942
|
+
|
|
943
|
+
import lombok.RequiredArgsConstructor;
|
|
944
|
+
import lombok.extern.slf4j.Slf4j;
|
|
945
|
+
import org.springframework.boot.context.event.ApplicationReadyEvent;
|
|
946
|
+
import org.springframework.context.event.EventListener;
|
|
947
|
+
import org.springframework.stereotype.Service;
|
|
948
|
+
|
|
949
|
+
import java.util.List;
|
|
950
|
+
import java.util.Map;
|
|
951
|
+
import java.util.Optional;
|
|
952
|
+
import java.util.concurrent.ConcurrentHashMap;
|
|
953
|
+
|
|
954
|
+
@Service
|
|
955
|
+
@RequiredArgsConstructor
|
|
956
|
+
@Slf4j
|
|
957
|
+
public class ReportRegistry {
|
|
958
|
+
|
|
959
|
+
private final List<ReportDefinition> reportDefinitions;
|
|
960
|
+
private final ReportRepository reportRepository;
|
|
961
|
+
|
|
962
|
+
// In-memory lookup for fast dispatch during report generation
|
|
963
|
+
private final Map<String, ReportDefinition> definitionMap = new ConcurrentHashMap<>();
|
|
964
|
+
|
|
965
|
+
/**
|
|
966
|
+
* On application startup, register all ReportDefinition beans in the database
|
|
967
|
+
* and build the in-memory lookup map.
|
|
968
|
+
*/
|
|
969
|
+
@EventListener(ApplicationReadyEvent.class)
|
|
970
|
+
public void registerReports() {
|
|
971
|
+
log.info("Registering {} report definitions", reportDefinitions.size());
|
|
972
|
+
|
|
973
|
+
for (ReportDefinition definition : reportDefinitions) {
|
|
974
|
+
definitionMap.put(definition.getReportId(), definition);
|
|
975
|
+
|
|
976
|
+
// [If PostgreSQL/MySQL] Upsert to database
|
|
977
|
+
ReportEntity entity = reportRepository.findById(definition.getReportId())
|
|
978
|
+
.orElseGet(ReportEntity::new);
|
|
979
|
+
entity.setReportId(definition.getReportId());
|
|
980
|
+
entity.setName(definition.getName());
|
|
981
|
+
entity.setDescription(definition.getDescription());
|
|
982
|
+
entity.setDomain(definition.getDomain());
|
|
983
|
+
entity.setActive(true);
|
|
984
|
+
reportRepository.save(entity);
|
|
985
|
+
|
|
986
|
+
// [If MongoDB] Upsert to database
|
|
987
|
+
// ReportDocument doc = reportRepository.findById(definition.getReportId())
|
|
988
|
+
// .orElseGet(ReportDocument::new);
|
|
989
|
+
// doc.setReportId(definition.getReportId());
|
|
990
|
+
// doc.setName(definition.getName());
|
|
991
|
+
// doc.setDescription(definition.getDescription());
|
|
992
|
+
// doc.setDomain(definition.getDomain());
|
|
993
|
+
// doc.setActive(true);
|
|
994
|
+
// reportRepository.save(doc);
|
|
995
|
+
|
|
996
|
+
log.info("Registered report: [{}] {} ({})",
|
|
997
|
+
definition.getReportId(), definition.getName(), definition.getDomain());
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
// Deactivate reports that no longer have a definition
|
|
1001
|
+
List<?> allReports = reportRepository.findAll();
|
|
1002
|
+
for (Object reportObj : allReports) {
|
|
1003
|
+
// [If PostgreSQL/MySQL]
|
|
1004
|
+
ReportEntity entity = (ReportEntity) reportObj;
|
|
1005
|
+
if (!definitionMap.containsKey(entity.getReportId())) {
|
|
1006
|
+
entity.setActive(false);
|
|
1007
|
+
reportRepository.save(entity);
|
|
1008
|
+
log.info("Deactivated report: [{}] (no definition found)", entity.getReportId());
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
// [If MongoDB]
|
|
1012
|
+
// ReportDocument doc = (ReportDocument) reportObj;
|
|
1013
|
+
// if (!definitionMap.containsKey(doc.getReportId())) {
|
|
1014
|
+
// doc.setActive(false);
|
|
1015
|
+
// reportRepository.save(doc);
|
|
1016
|
+
// log.info("Deactivated report: [{}] (no definition found)", doc.getReportId());
|
|
1017
|
+
// }
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
/**
|
|
1022
|
+
* Get a ReportDefinition by its ID for report generation.
|
|
1023
|
+
*/
|
|
1024
|
+
public Optional<ReportDefinition> getDefinition(String reportId) {
|
|
1025
|
+
return Optional.ofNullable(definitionMap.get(reportId));
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
/**
|
|
1029
|
+
* List all active reports from the database.
|
|
1030
|
+
*/
|
|
1031
|
+
public List<?> listActiveReports() {
|
|
1032
|
+
return reportRepository.findByActiveTrue();
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
/**
|
|
1036
|
+
* List active reports for a specific domain.
|
|
1037
|
+
*/
|
|
1038
|
+
public List<?> listReportsByDomain(String domain) {
|
|
1039
|
+
return reportRepository.findByDomainAndActiveTrue(domain);
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
```
|
|
1043
|
+
|
|
1044
|
+
---
|
|
1045
|
+
|
|
1046
|
+
## Report DTO for REST Responses
|
|
1047
|
+
|
|
1048
|
+
This DTO is returned by the REST controller for report listing and detail endpoints.
|
|
1049
|
+
It exposes report metadata including parameter descriptors so API clients can build
|
|
1050
|
+
dynamic parameter forms.
|
|
1051
|
+
|
|
1052
|
+
```java
|
|
1053
|
+
package {{BASE_PACKAGE}}.shared.reporting;
|
|
1054
|
+
|
|
1055
|
+
import java.util.List;
|
|
1056
|
+
|
|
1057
|
+
/**
|
|
1058
|
+
* REST response DTO for report metadata.
|
|
1059
|
+
* Returned by GET /api/v1/reports and GET /api/v1/reports/{reportId}.
|
|
1060
|
+
*
|
|
1061
|
+
* @param reportId unique report identifier
|
|
1062
|
+
* @param name human-readable report name
|
|
1063
|
+
* @param description brief description of the report contents
|
|
1064
|
+
* @param domain the domain this report belongs to (for grouping)
|
|
1065
|
+
* @param parameters list of parameter descriptors for building input forms
|
|
1066
|
+
*/
|
|
1067
|
+
public record ReportInfoDTO(
|
|
1068
|
+
String reportId,
|
|
1069
|
+
String name,
|
|
1070
|
+
String description,
|
|
1071
|
+
String domain,
|
|
1072
|
+
List<ReportParameter> parameters
|
|
1073
|
+
) {
|
|
1074
|
+
|
|
1075
|
+
/**
|
|
1076
|
+
* Create from a ReportDefinition bean.
|
|
1077
|
+
*/
|
|
1078
|
+
public static ReportInfoDTO from(ReportDefinition definition) {
|
|
1079
|
+
return new ReportInfoDTO(
|
|
1080
|
+
definition.getReportId(),
|
|
1081
|
+
definition.getName(),
|
|
1082
|
+
definition.getDescription(),
|
|
1083
|
+
definition.getDomain(),
|
|
1084
|
+
definition.getParameters()
|
|
1085
|
+
);
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
/**
|
|
1089
|
+
* [If PostgreSQL/MySQL] Create from a ReportEntity (without parameters — used for list view).
|
|
1090
|
+
*/
|
|
1091
|
+
public static ReportInfoDTO fromEntity(ReportEntity entity) {
|
|
1092
|
+
return new ReportInfoDTO(
|
|
1093
|
+
entity.getReportId(),
|
|
1094
|
+
entity.getName(),
|
|
1095
|
+
entity.getDescription(),
|
|
1096
|
+
entity.getDomain(),
|
|
1097
|
+
List.of()
|
|
1098
|
+
);
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
/**
|
|
1102
|
+
* [If MongoDB] Create from a ReportDocument (without parameters — used for list view).
|
|
1103
|
+
*/
|
|
1104
|
+
public static ReportInfoDTO fromDocument(ReportDocument document) {
|
|
1105
|
+
return new ReportInfoDTO(
|
|
1106
|
+
document.getReportId(),
|
|
1107
|
+
document.getName(),
|
|
1108
|
+
document.getDescription(),
|
|
1109
|
+
document.getDomain(),
|
|
1110
|
+
List.of()
|
|
1111
|
+
);
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
```
|
|
1115
|
+
|
|
1116
|
+
---
|
|
1117
|
+
|
|
1118
|
+
## Report Controller
|
|
1119
|
+
|
|
1120
|
+
REST controller exposing report management and generation endpoints. All responses are JSON
|
|
1121
|
+
except the generate endpoint which returns binary file downloads.
|
|
1122
|
+
|
|
1123
|
+
```java
|
|
1124
|
+
package {{BASE_PACKAGE}}.shared.reporting;
|
|
1125
|
+
|
|
1126
|
+
import io.swagger.v3.oas.annotations.Operation;
|
|
1127
|
+
import io.swagger.v3.oas.annotations.Parameter;
|
|
1128
|
+
import io.swagger.v3.oas.annotations.media.Content;
|
|
1129
|
+
import io.swagger.v3.oas.annotations.media.Schema;
|
|
1130
|
+
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
|
1131
|
+
import io.swagger.v3.oas.annotations.tags.Tag;
|
|
1132
|
+
import lombok.RequiredArgsConstructor;
|
|
1133
|
+
import lombok.extern.slf4j.Slf4j;
|
|
1134
|
+
import org.springframework.http.HttpHeaders;
|
|
1135
|
+
import org.springframework.http.MediaType;
|
|
1136
|
+
import org.springframework.http.ResponseEntity;
|
|
1137
|
+
import org.springframework.web.bind.annotation.GetMapping;
|
|
1138
|
+
import org.springframework.web.bind.annotation.PathVariable;
|
|
1139
|
+
import org.springframework.web.bind.annotation.PostMapping;
|
|
1140
|
+
import org.springframework.web.bind.annotation.RequestBody;
|
|
1141
|
+
import org.springframework.web.bind.annotation.RequestMapping;
|
|
1142
|
+
import org.springframework.web.bind.annotation.RequestParam;
|
|
1143
|
+
import org.springframework.web.bind.annotation.RestController;
|
|
1144
|
+
|
|
1145
|
+
import java.util.List;
|
|
1146
|
+
import java.util.Map;
|
|
1147
|
+
|
|
1148
|
+
@RestController
|
|
1149
|
+
@RequestMapping("/api/v1/reports")
|
|
1150
|
+
@RequiredArgsConstructor
|
|
1151
|
+
@Slf4j
|
|
1152
|
+
@Tag(name = "Reports", description = "Report generation and management")
|
|
1153
|
+
class ReportController {
|
|
1154
|
+
|
|
1155
|
+
private final ReportRegistry reportRegistry;
|
|
1156
|
+
private final ReportService reportService;
|
|
1157
|
+
|
|
1158
|
+
/**
|
|
1159
|
+
* List all available reports. Returns report metadata without parameter details.
|
|
1160
|
+
* Use GET /api/v1/reports/{reportId} to retrieve full parameter descriptors.
|
|
1161
|
+
*/
|
|
1162
|
+
@GetMapping
|
|
1163
|
+
@Operation(
|
|
1164
|
+
summary = "List all available reports",
|
|
1165
|
+
description = "Returns a list of all active reports with basic metadata. "
|
|
1166
|
+
+ "Parameter details are omitted — use the detail endpoint to get parameters."
|
|
1167
|
+
)
|
|
1168
|
+
@ApiResponse(
|
|
1169
|
+
responseCode = "200",
|
|
1170
|
+
description = "List of available reports",
|
|
1171
|
+
content = @Content(
|
|
1172
|
+
mediaType = "application/json",
|
|
1173
|
+
schema = @Schema(implementation = ReportInfoDTO.class)
|
|
1174
|
+
)
|
|
1175
|
+
)
|
|
1176
|
+
public ResponseEntity<List<ReportInfoDTO>> listReports() {
|
|
1177
|
+
// [If PostgreSQL/MySQL]
|
|
1178
|
+
List<ReportInfoDTO> reports = reportRegistry.listActiveReports().stream()
|
|
1179
|
+
.map(r -> ReportInfoDTO.fromEntity((ReportEntity) r))
|
|
1180
|
+
.toList();
|
|
1181
|
+
|
|
1182
|
+
// [If MongoDB]
|
|
1183
|
+
// List<ReportInfoDTO> reports = reportRegistry.listActiveReports().stream()
|
|
1184
|
+
// .map(r -> ReportInfoDTO.fromDocument((ReportDocument) r))
|
|
1185
|
+
// .toList();
|
|
1186
|
+
|
|
1187
|
+
return ResponseEntity.ok(reports);
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
/**
|
|
1191
|
+
* Get report details including parameter descriptors.
|
|
1192
|
+
* Clients use the returned parameter metadata to build dynamic input forms.
|
|
1193
|
+
*/
|
|
1194
|
+
@GetMapping("/{reportId}")
|
|
1195
|
+
@Operation(
|
|
1196
|
+
summary = "Get report details by ID",
|
|
1197
|
+
description = "Returns full report metadata including parameter descriptors. "
|
|
1198
|
+
+ "Clients should use the parameter list to build a dynamic input form."
|
|
1199
|
+
)
|
|
1200
|
+
@ApiResponse(
|
|
1201
|
+
responseCode = "200",
|
|
1202
|
+
description = "Report details with parameters",
|
|
1203
|
+
content = @Content(
|
|
1204
|
+
mediaType = "application/json",
|
|
1205
|
+
schema = @Schema(implementation = ReportInfoDTO.class)
|
|
1206
|
+
)
|
|
1207
|
+
)
|
|
1208
|
+
@ApiResponse(
|
|
1209
|
+
responseCode = "404",
|
|
1210
|
+
description = "Report not found"
|
|
1211
|
+
)
|
|
1212
|
+
public ResponseEntity<ReportInfoDTO> getReport(
|
|
1213
|
+
@Parameter(description = "Unique report identifier")
|
|
1214
|
+
@PathVariable String reportId) {
|
|
1215
|
+
|
|
1216
|
+
ReportDefinition definition = reportRegistry.getDefinition(reportId)
|
|
1217
|
+
.orElseThrow(() -> new ReportGenerationException("Report not found: " + reportId));
|
|
1218
|
+
|
|
1219
|
+
ReportInfoDTO dto = ReportInfoDTO.from(definition);
|
|
1220
|
+
return ResponseEntity.ok(dto);
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
/**
|
|
1224
|
+
* Generate and download a report.
|
|
1225
|
+
* Accepts report parameters in the request body and returns the generated file
|
|
1226
|
+
* as a binary download with appropriate Content-Type and Content-Disposition headers.
|
|
1227
|
+
*/
|
|
1228
|
+
@PostMapping("/{reportId}/generate")
|
|
1229
|
+
@Operation(
|
|
1230
|
+
summary = "Generate and download a report",
|
|
1231
|
+
description = "Generates the specified report with the provided parameters and returns "
|
|
1232
|
+
+ "the file as a binary download. Supported formats: PDF, XLSX, CSV."
|
|
1233
|
+
)
|
|
1234
|
+
@ApiResponse(
|
|
1235
|
+
responseCode = "200",
|
|
1236
|
+
description = "Report generated successfully — binary file download",
|
|
1237
|
+
content = {
|
|
1238
|
+
@Content(mediaType = "application/pdf"),
|
|
1239
|
+
@Content(mediaType = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"),
|
|
1240
|
+
@Content(mediaType = "text/csv")
|
|
1241
|
+
}
|
|
1242
|
+
)
|
|
1243
|
+
@ApiResponse(
|
|
1244
|
+
responseCode = "400",
|
|
1245
|
+
description = "Invalid parameters or report generation failed"
|
|
1246
|
+
)
|
|
1247
|
+
@ApiResponse(
|
|
1248
|
+
responseCode = "404",
|
|
1249
|
+
description = "Report not found"
|
|
1250
|
+
)
|
|
1251
|
+
public ResponseEntity<byte[]> generateReport(
|
|
1252
|
+
@Parameter(description = "Unique report identifier")
|
|
1253
|
+
@PathVariable String reportId,
|
|
1254
|
+
@Parameter(description = "Export format: PDF (default), XLSX, or CSV")
|
|
1255
|
+
@RequestParam(defaultValue = "PDF") ReportFormat format,
|
|
1256
|
+
@io.swagger.v3.oas.annotations.parameters.RequestBody(
|
|
1257
|
+
description = "Report parameters as key-value pairs",
|
|
1258
|
+
content = @Content(schema = @Schema(implementation = Map.class))
|
|
1259
|
+
)
|
|
1260
|
+
@RequestBody(required = false) Map<String, Object> parameters) {
|
|
1261
|
+
|
|
1262
|
+
Map<String, Object> params = parameters != null ? parameters : Map.of();
|
|
1263
|
+
|
|
1264
|
+
ReportDefinition definition = reportRegistry.getDefinition(reportId)
|
|
1265
|
+
.orElseThrow(() -> new ReportGenerationException("Report not found: " + reportId));
|
|
1266
|
+
|
|
1267
|
+
log.info("Generating report [{}] with format [{}] and {} parameters",
|
|
1268
|
+
reportId, format, params.size());
|
|
1269
|
+
|
|
1270
|
+
ReportResult result = reportService.generate(definition, params, format);
|
|
1271
|
+
|
|
1272
|
+
return ResponseEntity.ok()
|
|
1273
|
+
.contentType(MediaType.parseMediaType(result.contentType()))
|
|
1274
|
+
.header(HttpHeaders.CONTENT_DISPOSITION,
|
|
1275
|
+
"attachment; filename=\"" + result.fileName() + "\"")
|
|
1276
|
+
.header(HttpHeaders.CONTENT_LENGTH, String.valueOf(result.data().length))
|
|
1277
|
+
.body(result.data());
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
```
|
|
1281
|
+
|
|
1282
|
+
---
|
|
1283
|
+
|
|
1284
|
+
## Module Integration — Sample Report Implementation
|
|
1285
|
+
|
|
1286
|
+
Each module creates `@Component` classes implementing `ReportDefinition` to register
|
|
1287
|
+
their reports. The implementation calls module services (never repositories directly) to
|
|
1288
|
+
produce DTO collections, and builds the report layout programmatically using `JRDesign*` API
|
|
1289
|
+
with helpers from `ReportDesignHelper`.
|
|
1290
|
+
|
|
1291
|
+
### Sample: Staff Allocation Summary Report
|
|
1292
|
+
|
|
1293
|
+
```java
|
|
1294
|
+
package {{BASE_PACKAGE}}.staff.internal;
|
|
1295
|
+
|
|
1296
|
+
import {{BASE_PACKAGE}}.shared.reporting.ReportDefinition;
|
|
1297
|
+
import {{BASE_PACKAGE}}.shared.reporting.ReportDesignHelper;
|
|
1298
|
+
import {{BASE_PACKAGE}}.shared.reporting.ReportDesignHelper.ColumnDef;
|
|
1299
|
+
import {{BASE_PACKAGE}}.shared.reporting.ReportGenerationException;
|
|
1300
|
+
import {{BASE_PACKAGE}}.shared.reporting.ReportParameter;
|
|
1301
|
+
import {{BASE_PACKAGE}}.staff.StaffService;
|
|
1302
|
+
import lombok.RequiredArgsConstructor;
|
|
1303
|
+
import net.sf.jasperreports.engine.design.JRDesignBand;
|
|
1304
|
+
import net.sf.jasperreports.engine.design.JasperDesign;
|
|
1305
|
+
import net.sf.jasperreports.engine.type.SectionTypeEnum;
|
|
1306
|
+
import org.springframework.stereotype.Component;
|
|
1307
|
+
|
|
1308
|
+
import java.util.Collection;
|
|
1309
|
+
import java.util.List;
|
|
1310
|
+
import java.util.Map;
|
|
1311
|
+
|
|
1312
|
+
/**
|
|
1313
|
+
* Staff Allocation Summary report — lists all staff with their current
|
|
1314
|
+
* allocation status, department, and position.
|
|
1315
|
+
*
|
|
1316
|
+
* Data source: StaffReportDTO (flat DTO projected from Staff entity/document).
|
|
1317
|
+
* Layout: A4 landscape, built programmatically via JRDesign API.
|
|
1318
|
+
*/
|
|
1319
|
+
@Component
|
|
1320
|
+
@RequiredArgsConstructor
|
|
1321
|
+
public class StaffAllocationSummaryReport implements ReportDefinition {
|
|
1322
|
+
|
|
1323
|
+
private final StaffService staffService;
|
|
1324
|
+
|
|
1325
|
+
@Override
|
|
1326
|
+
public String getReportId() {
|
|
1327
|
+
return "staff-allocation-summary";
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
@Override
|
|
1331
|
+
public String getName() {
|
|
1332
|
+
return "Staff Allocation Summary";
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
@Override
|
|
1336
|
+
public String getDescription() {
|
|
1337
|
+
return "Lists all staff members with their department, position, and current allocation status.";
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
@Override
|
|
1341
|
+
public String getDomain() {
|
|
1342
|
+
return "Staff";
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
@Override
|
|
1346
|
+
public List<ReportParameter> getParameters() {
|
|
1347
|
+
return List.of(
|
|
1348
|
+
ReportParameter.select("departmentId", "Department", false,
|
|
1349
|
+
List.of(
|
|
1350
|
+
new ReportParameter.Option("All Departments", ""),
|
|
1351
|
+
new ReportParameter.Option("Software Engineering", "dept-se"),
|
|
1352
|
+
new ReportParameter.Option("Quality Assurance", "dept-qa")
|
|
1353
|
+
)),
|
|
1354
|
+
ReportParameter.select("allocationStatus", "Allocation Status", false,
|
|
1355
|
+
List.of(
|
|
1356
|
+
new ReportParameter.Option("All", ""),
|
|
1357
|
+
new ReportParameter.Option("Available", "AVAILABLE"),
|
|
1358
|
+
new ReportParameter.Option("Allocated", "ALLOCATED")
|
|
1359
|
+
))
|
|
1360
|
+
);
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
@Override
|
|
1364
|
+
public JasperDesign buildDesign(Map<String, Object> parameters) {
|
|
1365
|
+
try {
|
|
1366
|
+
// 1. Create A4 landscape design
|
|
1367
|
+
JasperDesign design = ReportDesignHelper.createA4LandscapeDesign("staff-allocation-summary");
|
|
1368
|
+
|
|
1369
|
+
// 2. Declare fields matching StaffReportDTO properties
|
|
1370
|
+
ReportDesignHelper.addField(design, "staffNumber", String.class);
|
|
1371
|
+
ReportDesignHelper.addField(design, "fullName", String.class);
|
|
1372
|
+
ReportDesignHelper.addField(design, "email", String.class);
|
|
1373
|
+
ReportDesignHelper.addField(design, "departmentName", String.class);
|
|
1374
|
+
ReportDesignHelper.addField(design, "positionName", String.class);
|
|
1375
|
+
ReportDesignHelper.addField(design, "allocationStatus", String.class);
|
|
1376
|
+
ReportDesignHelper.addField(design, "allocationPercentage", String.class);
|
|
1377
|
+
|
|
1378
|
+
// 3. Define column layout
|
|
1379
|
+
List<ColumnDef> columns = List.of(
|
|
1380
|
+
new ColumnDef("Staff No.", "staffNumber", 80),
|
|
1381
|
+
new ColumnDef("Full Name", "fullName", 150),
|
|
1382
|
+
new ColumnDef("Email", "email", 170),
|
|
1383
|
+
new ColumnDef("Department", "departmentName", 130),
|
|
1384
|
+
new ColumnDef("Position", "positionName", 120),
|
|
1385
|
+
new ColumnDef("Status", "allocationStatus", 80),
|
|
1386
|
+
new ColumnDef("Allocation %", "allocationPercentage", 72)
|
|
1387
|
+
);
|
|
1388
|
+
|
|
1389
|
+
// 4. Title band
|
|
1390
|
+
JRDesignBand titleBand = ReportDesignHelper.createTitleBand("Staff Allocation Summary", 50);
|
|
1391
|
+
design.setTitle(titleBand);
|
|
1392
|
+
|
|
1393
|
+
// 5. Column header band
|
|
1394
|
+
JRDesignBand columnHeaderBand = ReportDesignHelper.createColumnHeaderBand(columns);
|
|
1395
|
+
design.setColumnHeader(columnHeaderBand);
|
|
1396
|
+
|
|
1397
|
+
// 6. Detail band
|
|
1398
|
+
JRDesignBand detailBand = ReportDesignHelper.createDetailBand(columns);
|
|
1399
|
+
((net.sf.jasperreports.engine.design.JRDesignSection) design.getDetailSection())
|
|
1400
|
+
.addBand(detailBand);
|
|
1401
|
+
|
|
1402
|
+
// 7. Page footer band with page numbers
|
|
1403
|
+
JRDesignBand pageFooterBand = ReportDesignHelper.createPageFooterBand();
|
|
1404
|
+
design.setPageFooter(pageFooterBand);
|
|
1405
|
+
|
|
1406
|
+
return design;
|
|
1407
|
+
} catch (Exception e) {
|
|
1408
|
+
throw new ReportGenerationException(
|
|
1409
|
+
"Failed to build design for report: " + getReportId(), e);
|
|
1410
|
+
}
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1413
|
+
@Override
|
|
1414
|
+
public Collection<?> generateData(Map<String, Object> parameters) {
|
|
1415
|
+
String departmentId = (String) parameters.getOrDefault("departmentId", "");
|
|
1416
|
+
String allocationStatus = (String) parameters.getOrDefault("allocationStatus", "");
|
|
1417
|
+
|
|
1418
|
+
// Call domain service to get filtered data as DTOs
|
|
1419
|
+
// The service returns List<StaffReportDTO> — flat DTO suitable for Jasper
|
|
1420
|
+
return staffService.getStaffForReport(departmentId, allocationStatus);
|
|
1421
|
+
}
|
|
1422
|
+
}
|
|
1423
|
+
```
|
|
1424
|
+
|
|
1425
|
+
---
|
|
1426
|
+
|
|
1427
|
+
## Sample Report DTO (Jasper Data Source Bean)
|
|
1428
|
+
|
|
1429
|
+
```java
|
|
1430
|
+
package {{BASE_PACKAGE}}.staff.internal;
|
|
1431
|
+
|
|
1432
|
+
import lombok.Getter;
|
|
1433
|
+
import lombok.Setter;
|
|
1434
|
+
|
|
1435
|
+
/**
|
|
1436
|
+
* Flat DTO used as the data source bean for JasperReports.
|
|
1437
|
+
* Each field maps to a JRDesignField declared in the report's buildDesign() method.
|
|
1438
|
+
* Field names must match exactly between this DTO and the JRDesignField declarations.
|
|
1439
|
+
*
|
|
1440
|
+
* Uses @Getter/@Setter (Lombok) so that JRBeanCollectionDataSource can read
|
|
1441
|
+
* properties via standard JavaBean getters.
|
|
1442
|
+
*/
|
|
1443
|
+
@Getter
|
|
1444
|
+
@Setter
|
|
1445
|
+
public class StaffReportDTO {
|
|
1446
|
+
|
|
1447
|
+
private String staffNumber;
|
|
1448
|
+
private String fullName;
|
|
1449
|
+
private String email;
|
|
1450
|
+
private String departmentName;
|
|
1451
|
+
private String positionName;
|
|
1452
|
+
private String allocationStatus;
|
|
1453
|
+
private String allocationPercentage;
|
|
1454
|
+
}
|
|
1455
|
+
```
|
|
1456
|
+
|
|
1457
|
+
---
|
|
1458
|
+
|
|
1459
|
+
## Key Design Decisions
|
|
1460
|
+
|
|
1461
|
+
### Why JRDesign API (Programmatic Layout) Instead of .jrxml Templates
|
|
1462
|
+
|
|
1463
|
+
| Concern | .jrxml Templates | JRDesign API (Programmatic) |
|
|
1464
|
+
|--------------------------|------------------------------------------|--------------------------------------------|
|
|
1465
|
+
| AI-agent friendliness | XML is verbose, error-prone for AI | Pure Java — AI agents excel at Java code |
|
|
1466
|
+
| Compile-time safety | Errors caught at runtime only | Field/type mismatches caught at compile |
|
|
1467
|
+
| Dynamic layouts | Requires JRXML manipulation or Scriptlets| Natural Java logic (conditionals, loops) |
|
|
1468
|
+
| Template management | Separate .jrxml files under resources/ | No extra files — layout lives in Java code |
|
|
1469
|
+
| Versioning | Must track XML changes separately | Layout changes appear in standard diffs |
|
|
1470
|
+
| Testability | Hard to unit test XML templates | buildDesign() returns testable objects |
|
|
1471
|
+
|
|
1472
|
+
### Why DTO as Data Source
|
|
1473
|
+
|
|
1474
|
+
- **Module boundary**: Reports consume DTOs produced by domain services, not raw entities. This prevents
|
|
1475
|
+
reports from coupling to persistence layer details (table structure, lazy loading, etc.).
|
|
1476
|
+
- **Testability**: Report data generation can be unit tested by mocking the service layer and verifying
|
|
1477
|
+
the returned DTO collection, independent of the database.
|
|
1478
|
+
- **No SQL in reports**: All data access goes through the module's service layer, maintaining the
|
|
1479
|
+
single-responsibility principle and keeping business logic centralized.
|
|
1480
|
+
|
|
1481
|
+
---
|
|
1482
|
+
|
|
1483
|
+
## Operational Notes
|
|
1484
|
+
|
|
1485
|
+
### Memory Management
|
|
1486
|
+
|
|
1487
|
+
- **Report compilation caching**: The `ReportService` caches compiled `JasperReport` objects in a
|
|
1488
|
+
`ConcurrentHashMap` keyed by `reportId`. For applications with many reports, monitor heap usage.
|
|
1489
|
+
The `evictReport()` method allows clearing individual cached compilations.
|
|
1490
|
+
- **Large data sets**: For reports generating tens of thousands of rows, consider paginating the data
|
|
1491
|
+
in the `generateData()` method or using JasperReports virtualizers to offload pages to disk:
|
|
1492
|
+
```java
|
|
1493
|
+
JRSwapFileVirtualizer virtualizer = new JRSwapFileVirtualizer(
|
|
1494
|
+
50, new JRSwapFile(config.getTempPath(), 2048, 1024));
|
|
1495
|
+
jasperParams.put(JRParameter.REPORT_VIRTUALIZER, virtualizer);
|
|
1496
|
+
```
|
|
1497
|
+
- **ByteArrayOutputStream**: The entire exported report is held in memory before being written to
|
|
1498
|
+
the HTTP response. For very large reports (> 50 MB), consider streaming directly to the response
|
|
1499
|
+
output stream.
|
|
1500
|
+
|
|
1501
|
+
### Concurrency
|
|
1502
|
+
|
|
1503
|
+
- **Thread safety**: `ReportService` is a singleton Spring bean. The `ConcurrentHashMap` for compilation
|
|
1504
|
+
cache is thread-safe. `JasperFillManager.fillReport()` is stateless and safe to call concurrently
|
|
1505
|
+
with different parameters.
|
|
1506
|
+
- **Rate limiting**: For CPU-intensive reports, consider adding `@RateLimiter` (Resilience4j) or a
|
|
1507
|
+
semaphore to limit concurrent report generation:
|
|
1508
|
+
```java
|
|
1509
|
+
private final Semaphore reportSemaphore = new Semaphore(4); // max 4 concurrent reports
|
|
1510
|
+
```
|
|
1511
|
+
|
|
1512
|
+
### Timeouts
|
|
1513
|
+
|
|
1514
|
+
- Report generation requests can be slow for large datasets. Configure a generous timeout in the
|
|
1515
|
+
REST client and consider async generation for reports exceeding 30 seconds:
|
|
1516
|
+
```yaml
|
|
1517
|
+
spring:
|
|
1518
|
+
mvc:
|
|
1519
|
+
async:
|
|
1520
|
+
request-timeout: 120000 # 2 minutes for report generation
|
|
1521
|
+
```
|
|
1522
|
+
|
|
1523
|
+
### Font Embedding
|
|
1524
|
+
|
|
1525
|
+
- JasperReports requires fonts to be available at runtime for PDF export. The `jasperreports-fonts`
|
|
1526
|
+
dependency bundles DejaVu Sans (the default). For custom fonts:
|
|
1527
|
+
1. Place `.ttf` files under `src/main/resources/fonts/`
|
|
1528
|
+
2. Create `jasperreports_extension.properties` with font family definitions
|
|
1529
|
+
3. Reference the font name in `ReportDesignHelper.createStyle()` calls
|
|
1530
|
+
- When using programmatic design, set fonts explicitly on text elements:
|
|
1531
|
+
```java
|
|
1532
|
+
textField.setFontName("DejaVu Sans");
|
|
1533
|
+
textField.setFontSize(10f);
|
|
1534
|
+
textField.setPdfEncoding("Identity-H");
|
|
1535
|
+
textField.setPdfEmbedded(true);
|
|
1536
|
+
```
|