@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.
Files changed (190) hide show
  1. package/dist/index.js +25449 -0
  2. package/drizzle/0000_remarkable_charles_xavier.sql +210 -0
  3. package/drizzle/0001_wise_wolf_cub.sql +13 -0
  4. package/drizzle/0002_secret_spyke.sql +16 -0
  5. package/drizzle/0003_workspace_config_singleton.sql +16 -0
  6. package/drizzle/0004_terminal_claim_v120.sql +11 -0
  7. package/drizzle/0005_brainstorm_v124.sql +10 -0
  8. package/drizzle/0006_project_v140.sql +6 -0
  9. package/drizzle/0007_prd_wizard_v124.sql +14 -0
  10. package/drizzle/0008_workflow_artifact_v126.sql +24 -0
  11. package/drizzle/0009_project_import_v150.sql +7 -0
  12. package/drizzle/0010_quality_v151.sql +3 -0
  13. package/drizzle/meta/0000_snapshot.json +1335 -0
  14. package/drizzle/meta/0001_snapshot.json +1416 -0
  15. package/drizzle/meta/0002_snapshot.json +1510 -0
  16. package/drizzle/meta/_journal.json +83 -0
  17. package/package.json +41 -0
  18. package/plugin/.claude-plugin/marketplace.json +36 -0
  19. package/plugin/.claude-plugin/plugin.json +22 -0
  20. package/plugin/README.md +625 -0
  21. package/plugin/SKILLS.md +350 -0
  22. package/plugin/co2.svg +5 -0
  23. package/plugin/commands/brainstorm-loop.md +129 -0
  24. package/plugin/skills/conductor-defect/SKILL.md +898 -0
  25. package/plugin/skills/conductor-feature-develop/SKILL.md +1383 -0
  26. package/plugin/skills/conductor-feature-develop/references/implementation-domain-template.md +131 -0
  27. package/plugin/skills/conductor-feature-develop/references/implementation-master-template.md +89 -0
  28. package/plugin/skills/conductor-feature-develop/references/playwright-setup.md +224 -0
  29. package/plugin/skills/conductor-feature-prepare/SKILL.md +604 -0
  30. package/plugin/skills/depgen-k8s/SKILL.md +468 -0
  31. package/plugin/skills/depgen-k8s/references/dockerfile-laravel.md +197 -0
  32. package/plugin/skills/depgen-k8s/references/dockerfile-nodejs.md +163 -0
  33. package/plugin/skills/depgen-k8s/references/dockerfile-spring-boot.md +174 -0
  34. package/plugin/skills/depgen-k8s/references/k8s-patterns.md +869 -0
  35. package/plugin/skills/mockgen-shadcn/SKILL.md +1067 -0
  36. package/plugin/skills/mockgen-shadcn/references/admin-layout-template.md +1467 -0
  37. package/plugin/skills/mockgen-shadcn/references/mockup-hub-template.md +498 -0
  38. package/plugin/skills/mockgen-shadcn/references/mockup-index-template.md +216 -0
  39. package/plugin/skills/mockgen-tailwind/SKILL.md +904 -0
  40. package/plugin/skills/mockgen-tailwind/references/admin-layout-template.md +720 -0
  41. package/plugin/skills/mockgen-tailwind/references/mockup-hub-template.md +498 -0
  42. package/plugin/skills/mockgen-tailwind/references/mockup-index-template.md +190 -0
  43. package/plugin/skills/modelgen-nosql/SKILL.md +617 -0
  44. package/plugin/skills/modelgen-nosql/references/model-extraction-methodology.md +497 -0
  45. package/plugin/skills/modelgen-nosql/references/nosql-design-guide.md +258 -0
  46. package/plugin/skills/modelgen-relational/SKILL.md +707 -0
  47. package/plugin/skills/modelgen-relational/references/model-extraction-methodology.md +499 -0
  48. package/plugin/skills/specgen-flutter-riverpod/SKILL.md +898 -0
  49. package/plugin/skills/specgen-flutter-riverpod/references/component-patterns.md +971 -0
  50. package/plugin/skills/specgen-flutter-riverpod/references/notification-patterns.md +457 -0
  51. package/plugin/skills/specgen-flutter-riverpod/references/routing-patterns.md +362 -0
  52. package/plugin/skills/specgen-flutter-riverpod/references/security-patterns.md +709 -0
  53. package/plugin/skills/specgen-flutter-riverpod/references/spec-template.md +1566 -0
  54. package/plugin/skills/specgen-flutter-riverpod/references/state-patterns.md +518 -0
  55. package/plugin/skills/specgen-flutter-riverpod/references/storage-patterns.md +370 -0
  56. package/plugin/skills/specgen-laravel-eloquent-bladehtmx/SKILL.md +984 -0
  57. package/plugin/skills/specgen-laravel-eloquent-bladehtmx/references/batch-patterns.md +359 -0
  58. package/plugin/skills/specgen-laravel-eloquent-bladehtmx/references/messaging-patterns.md +468 -0
  59. package/plugin/skills/specgen-laravel-eloquent-bladehtmx/references/modulith-patterns.md +461 -0
  60. package/plugin/skills/specgen-laravel-eloquent-bladehtmx/references/reporting-patterns.md +803 -0
  61. package/plugin/skills/specgen-laravel-eloquent-bladehtmx/references/security-patterns.md +501 -0
  62. package/plugin/skills/specgen-laravel-eloquent-bladehtmx/references/spec-template.md +1268 -0
  63. package/plugin/skills/specgen-laravel-eloquent-bladehtmx/references/web-patterns.md +301 -0
  64. package/plugin/skills/specgen-node-cli-web/SKILL.md +561 -0
  65. package/plugin/skills/specgen-node-cli-web/references/auth-patterns.md +293 -0
  66. package/plugin/skills/specgen-node-cli-web/references/cli-packaging-patterns.md +250 -0
  67. package/plugin/skills/specgen-node-cli-web/references/server-patterns.md +247 -0
  68. package/plugin/skills/specgen-node-cli-web/references/spec-template.md +401 -0
  69. package/plugin/skills/specgen-node-cli-web/references/testing-patterns.md +174 -0
  70. package/plugin/skills/specgen-react-mui/SKILL.md +789 -0
  71. package/plugin/skills/specgen-react-mui/references/component-patterns.md +1077 -0
  72. package/plugin/skills/specgen-react-mui/references/reporting-patterns.md +1998 -0
  73. package/plugin/skills/specgen-react-mui/references/routing-patterns.md +461 -0
  74. package/plugin/skills/specgen-react-mui/references/security-patterns.md +599 -0
  75. package/plugin/skills/specgen-react-mui/references/spec-template.md +1030 -0
  76. package/plugin/skills/specgen-react-mui/references/state-patterns.md +327 -0
  77. package/plugin/skills/specgen-react-tailwind/SKILL.md +859 -0
  78. package/plugin/skills/specgen-react-tailwind/references/component-patterns.md +1212 -0
  79. package/plugin/skills/specgen-react-tailwind/references/reporting-patterns.md +1529 -0
  80. package/plugin/skills/specgen-react-tailwind/references/routing-patterns.md +669 -0
  81. package/plugin/skills/specgen-react-tailwind/references/security-patterns.md +656 -0
  82. package/plugin/skills/specgen-react-tailwind/references/spec-template.md +1237 -0
  83. package/plugin/skills/specgen-react-tailwind/references/state-patterns.md +327 -0
  84. package/plugin/skills/specgen-sdk-java/SKILL.md +933 -0
  85. package/plugin/skills/specgen-sdk-java/references/api-extraction-patterns.md +247 -0
  86. package/plugin/skills/specgen-sdk-java/references/http-client-patterns.md +495 -0
  87. package/plugin/skills/specgen-sdk-java/references/packaging-patterns.md +366 -0
  88. package/plugin/skills/specgen-sdk-java/references/spec-template.md +956 -0
  89. package/plugin/skills/specgen-spring-jpa-jtehtmx/SKILL.md +1284 -0
  90. package/plugin/skills/specgen-spring-jpa-jtehtmx/references/batch-patterns.md +1224 -0
  91. package/plugin/skills/specgen-spring-jpa-jtehtmx/references/jasper-patterns.md +1638 -0
  92. package/plugin/skills/specgen-spring-jpa-jtehtmx/references/messaging-patterns.md +512 -0
  93. package/plugin/skills/specgen-spring-jpa-jtehtmx/references/modulith-patterns.md +354 -0
  94. package/plugin/skills/specgen-spring-jpa-jtehtmx/references/security-patterns.md +551 -0
  95. package/plugin/skills/specgen-spring-jpa-jtehtmx/references/spec-template.md +2502 -0
  96. package/plugin/skills/specgen-spring-jpa-jtehtmx/references/web-patterns.md +503 -0
  97. package/plugin/skills/specgen-spring-jpa-restapi/SKILL.md +990 -0
  98. package/plugin/skills/specgen-spring-jpa-restapi/references/batch-patterns.md +1224 -0
  99. package/plugin/skills/specgen-spring-jpa-restapi/references/messaging-patterns.md +512 -0
  100. package/plugin/skills/specgen-spring-jpa-restapi/references/modulith-patterns.md +279 -0
  101. package/plugin/skills/specgen-spring-jpa-restapi/references/reporting-patterns.md +1536 -0
  102. package/plugin/skills/specgen-spring-jpa-restapi/references/restapi-patterns.md +722 -0
  103. package/plugin/skills/specgen-spring-jpa-restapi/references/security-patterns.md +351 -0
  104. package/plugin/skills/specgen-spring-jpa-restapi/references/spec-template.md +553 -0
  105. package/plugin/skills/specgen-ts-cli/SKILL.md +861 -0
  106. package/plugin/skills/specgen-ts-cli/references/async-patterns.md +1101 -0
  107. package/plugin/skills/specgen-ts-cli/references/command-patterns.md +383 -0
  108. package/plugin/skills/specgen-ts-cli/references/config-patterns.md +430 -0
  109. package/plugin/skills/specgen-ts-cli/references/database-patterns.md +750 -0
  110. package/plugin/skills/specgen-ts-cli/references/packaging-patterns.md +426 -0
  111. package/plugin/skills/specgen-ts-cli/references/spec-template.md +1391 -0
  112. package/plugin/skills/specgen-ts-cli/references/testing-patterns.md +909 -0
  113. package/plugin/skills/testgen-functional/SKILL.md +1381 -0
  114. package/plugin/skills/tracegen-matrix/SKILL.md +243 -0
  115. package/plugin/skills/util-preparek8senv/SKILL.md +422 -0
  116. package/plugin/skills/util-projectinit/SKILL.md +307 -0
  117. package/plugin/skills/util-projectinit/reference/brainstorm-loop.md +129 -0
  118. package/plugin/skills/util-projectinit/reference/brainstorm-protocol.md +144 -0
  119. package/plugin/skills/util-projectinit/reference/brainstorm-setup-guide.md +153 -0
  120. package/plugin/skills/util-projectinit/reference/brainstorm-template-appdev.md +87 -0
  121. package/plugin/skills/util-projectinit/reference/co2-context-generation-guide.md +761 -0
  122. package/plugin/skills/util-projectsync/SKILL.md +501 -0
  123. package/plugin/skills/util-updprd/SKILL.md +324 -0
  124. package/plugin/skills/util-usanalyzer/SKILL.md +307 -0
  125. package/plugin/skills/util-ustagger/SKILL.md +154 -0
  126. package/static/assets/abnfDiagram-VRR7QNED-DvPuwyCV.js +1 -0
  127. package/static/assets/arc-DRKw7sjs.js +1 -0
  128. package/static/assets/architectureDiagram-ZJ3FMSHR-CaNcxUbC.js +36 -0
  129. package/static/assets/blockDiagram-677ZJIJ3-Demd4AOK.js +132 -0
  130. package/static/assets/c4Diagram-LMCZKHZV-PVKUfc2x.js +10 -0
  131. package/static/assets/channel-C2nVbuwv.js +1 -0
  132. package/static/assets/chunk-2Q5K7J3B-CExf8ww_.js +1 -0
  133. package/static/assets/chunk-32BRIVSS-CFPaoONF.js +1 -0
  134. package/static/assets/chunk-5VM5RSS4-DE2esM67.js +15 -0
  135. package/static/assets/chunk-EX3LRPZG-SON_YdDG.js +231 -0
  136. package/static/assets/chunk-JWPE2WC7-Qxah8LNr.js +1 -0
  137. package/static/assets/chunk-MOJQB5TN-BcWGQV3j.js +88 -0
  138. package/static/assets/chunk-RYQCIY6F-DkDj-OR5.js +1 -0
  139. package/static/assets/chunk-V7JOEXUC-BlI1Mrah.js +206 -0
  140. package/static/assets/chunk-VR4S4FIN-Agf__duj.js +1 -0
  141. package/static/assets/chunk-XXDRQBXY-B5CfAh85.js +1 -0
  142. package/static/assets/classDiagram-OUVF2IWQ-DXJIljsR.js +1 -0
  143. package/static/assets/classDiagram-v2-EOCWNBFH-DXJIljsR.js +1 -0
  144. package/static/assets/cose-bilkent-JH36ORCC-DO0cQkcy.js +1 -0
  145. package/static/assets/cynefin-VYW2F7L2-CfOnsnXc.js +178 -0
  146. package/static/assets/cynefinDiagram-TSTJHNR4-BJ8tULAv.js +62 -0
  147. package/static/assets/cytoscape.esm-DTSO7Bv0.js +331 -0
  148. package/static/assets/dagre-VKFMJZFB-DfjwzHqP.js +4 -0
  149. package/static/assets/defaultLocale-DX6XiGOO.js +1 -0
  150. package/static/assets/diagram-FQU43EPY-CndSOIIM.js +3 -0
  151. package/static/assets/diagram-G47NLZAW-Czf5hN0Z.js +24 -0
  152. package/static/assets/diagram-NH7WQ7WH-COAyojGh.js +24 -0
  153. package/static/assets/diagram-OA4YK3LP-H8PoiN2m.js +30 -0
  154. package/static/assets/diagram-WEI45ONY-DzcaVzjp.js +41 -0
  155. package/static/assets/ebnfDiagram-CCIWWBDH-DPazGYLJ.js +1 -0
  156. package/static/assets/erDiagram-Q63AITRT-Bo-fubgF.js +85 -0
  157. package/static/assets/flowDiagram-23GEKE2U-CSvxUDwh.js +156 -0
  158. package/static/assets/ganttDiagram-NO4QXBWP-DnBveWX6.js +292 -0
  159. package/static/assets/gitGraphDiagram-IHSO6WYX-B3rVL8c-.js +106 -0
  160. package/static/assets/graph-C9eacEi8.js +1 -0
  161. package/static/assets/index-LZSQkLE2.css +41 -0
  162. package/static/assets/index-y7XDSgMG.js +495 -0
  163. package/static/assets/infoDiagram-FWYZ7A6U-CvWV9yy2.js +2 -0
  164. package/static/assets/init-Gi6I4Gst.js +1 -0
  165. package/static/assets/ishikawaDiagram-FXEZZL3T-DF28a1N6.js +70 -0
  166. package/static/assets/journeyDiagram-5HDEW3XC-CB6CugYk.js +139 -0
  167. package/static/assets/kanban-definition-HUTT4EX6-ResSLF6d.js +89 -0
  168. package/static/assets/katex-C5jXJg4s.js +257 -0
  169. package/static/assets/layout-DEXfKzaS.js +1 -0
  170. package/static/assets/linear-CLO--ryk.js +1 -0
  171. package/static/assets/map-Czzmt4hB.js +1 -0
  172. package/static/assets/mindmap-definition-LN4V7U3C-BvW3EbxX.js +96 -0
  173. package/static/assets/ordinal-Cboi1Yqb.js +1 -0
  174. package/static/assets/pegDiagram-2B236MQR-BHCgqrrM.js +1 -0
  175. package/static/assets/pieDiagram-ENE6RG2P-DVmCQ5FA.js +39 -0
  176. package/static/assets/quadrantDiagram-ABIIQ3AL-B3e5KR52.js +7 -0
  177. package/static/assets/railroadDiagram-RFXS5EU6-Csv5ysLc.js +1 -0
  178. package/static/assets/requirementDiagram-TGXJPOKE-DOMVzvnB.js +84 -0
  179. package/static/assets/sankeyDiagram-HTMAVEWB-B1dcTOvy.js +40 -0
  180. package/static/assets/sequenceDiagram-DBY2YBRQ-BY414yeO.js +162 -0
  181. package/static/assets/sizeCapture-X5ZJPWSS-CuJfrfBN.js +1 -0
  182. package/static/assets/stateDiagram-2N3HPSRC-FX4rJkzV.js +1 -0
  183. package/static/assets/stateDiagram-v2-6OUMAXLB-BVufbO_H.js +1 -0
  184. package/static/assets/swimlanes-5IMT3BWC-Sfy58mmv.js +2 -0
  185. package/static/assets/swimlanesDiagram-G3AALYLV-wDCDw08H.js +8 -0
  186. package/static/assets/timeline-definition-FHXFAJF6-CmV7by8p.js +120 -0
  187. package/static/assets/vennDiagram-L72KCM5P-FqIyFZFt.js +34 -0
  188. package/static/assets/wardleyDiagram-EHGQE667-BZ06O45i.js +78 -0
  189. package/static/assets/xychartDiagram-FW5EYKEG-BOkffR06.js +7 -0
  190. 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
+ ```