99import java .util .List ;
1010import java .util .function .Predicate ;
1111import net .sf .jasperreports .engine .JasperPrint ;
12+ import net .sf .jasperreports .engine .fill .JRSwapFileVirtualizer ;
1213import org .slf4j .Logger ;
1314import org .slf4j .LoggerFactory ;
1415import org .springframework .boot .autoconfigure .condition .ConditionalOnProperty ;
@@ -37,26 +38,29 @@ public class PrintService {
3738
3839 private final ReportService reportService ;
3940 private final MillContextService millContextService ;
41+ private final ReportVirtualizerFactory virtualizerFactory ;
4042
41- public PrintService (ReportService reportService , MillContextService millContextService ) {
43+ public PrintService (ReportService reportService , MillContextService millContextService ,
44+ ReportVirtualizerFactory virtualizerFactory ) {
4245 this .reportService = reportService ;
4346 this .millContextService = millContextService ;
47+ this .virtualizerFactory = virtualizerFactory ;
4448 }
4549
4650 /**
4751 * Render the combined Print Schedules PDF for the validated context and selection.
4852 *
4953 * @param context the validated (millId, year) pair
5054 * @param request the print selection (schedule flags + "all" + print options)
51- * @return the combined, bookmarked PDF bytes
55+ * @return the filled report, ready to stream to the response (the caller closes it after export)
5256 * @throws ScheduleNotFoundException 404 — no selected in-scope schedule has any data (ERR-005)
5357 */
5458 // Deliberately NOT @Transactional: this is six independent read-only reads, each *Service.getScheduleN
5559 // managing its own transaction and the Schedule 9 fill borrowing (and releasing) its own connection. A
5660 // method-wide readOnly transaction would pin ONE pooled connection for the whole render while the
5761 // Schedule 9 fill grabs a SECOND — two connections per /print on a maximum-pool-size of 5, so a handful
5862 // of concurrent prints would exhaust the pool and block until the 30s timeout.
59- public byte [] render (MillYearContext context , PrintRequest request ) {
63+ public RenderedReport render (MillYearContext context , PrintRequest request ) {
6064 PrintOptions options =
6165 new PrintOptions (request .printScheduleInformation (), request .printComments ());
6266 Predicate <ScheduleKey > selected = selectionOf (request );
@@ -65,6 +69,7 @@ public byte[] render(MillYearContext context, PrintRequest request) {
6569 // The ONLY requested content is the deferred Mill Information report — no in-scope schedule
6670 // and no content option. That yields no PDF for a reason unrelated to missing schedule data,
6771 // so surface the honest "not yet available" rather than the misleading "Schedule not found.".
72+ // Thrown before a virtualizer is created, so nothing to clean up.
6873 log .info ("Mill-information-report-only selection for mill {} year {} — report not yet available" ,
6974 context .millId (), context .year ());
7075 throw new MillInformationReportUnavailableException ();
@@ -74,33 +79,48 @@ public byte[] render(MillYearContext context, PrintRequest request) {
7479 // instead of re-querying it for each of the five bean sections.
7580 String millTitleBlock = millContextService .resolveMillTitleBlock (context .millId ());
7681
77- List <JasperPrint > sections = new ArrayList <>();
78- for (ScheduleKey key : ScheduleKey .values ()) {
79- if (!selected .test (key )) {
80- continue ;
82+ // One virtualizer for the whole combined fill (Story 29.2): every section's page objects spill to
83+ // the SAME swap file, so an "all schedules" print never pins the full section graph on the heap.
84+ JRSwapFileVirtualizer virtualizer = virtualizerFactory .create ();
85+ boolean ownershipTransferred = false ;
86+ try {
87+ List <JasperPrint > sections = new ArrayList <>();
88+ for (ScheduleKey key : ScheduleKey .values ()) {
89+ if (!selected .test (key )) {
90+ continue ;
91+ }
92+ // BR-08: pass the section's bookmark title so its template renders the top-level PDF outline
93+ // anchor. Every rendered schedule gets exactly one bookmark, including a single-schedule print.
94+ JasperPrint print = reportService .fillSection (
95+ key , context .millId (), context .year (), options , millTitleBlock , key .bookmarkTitle (),
96+ virtualizer );
97+ if (print == null ) {
98+ // BR-09 skip-empty: a selected schedule with no data contributes nothing and never aborts.
99+ log .info ("Skipping {} for mill {} year {} — no data" ,
100+ key , context .millId (), context .year ());
101+ continue ;
102+ }
103+ sections .add (print );
81104 }
82- // BR-08: pass the section's bookmark title so its template renders the top-level PDF outline
83- // anchor. Every rendered schedule gets exactly one bookmark, including a single-schedule print.
84- JasperPrint print = reportService .fillSection (
85- key , context .millId (), context .year (), options , millTitleBlock , key .bookmarkTitle ());
86- if (print == null ) {
87- // BR-09 skip-empty: a selected schedule with no data contributes nothing and never aborts.
88- log .info ("Skipping {} for mill {} year {} — no data" ,
89- key , context .millId (), context .year ());
90- continue ;
91- }
92- sections .add (print );
93- }
94105
95- logUnimplementedSelections (request );
106+ logUnimplementedSelections (request );
96107
97- if (sections .isEmpty ()) {
98- // All-empty (S11): no selected content produced any section → no PDF, 404 ERR-005.
99- throw new ScheduleNotFoundException ();
108+ if (sections .isEmpty ()) {
109+ // All-empty (S11): no selected content produced any section → no PDF, 404 ERR-005.
110+ throw new ScheduleNotFoundException ();
111+ }
112+ log .info ("Combining {} section(s) into one PDF for mill {} year {}" ,
113+ sections .size (), context .millId (), context .year ());
114+ RenderedReport report = new RenderedReport (sections , virtualizer );
115+ ownershipTransferred = true ;
116+ return report ;
117+ } finally {
118+ // All-empty (404) or a fill failure produces no PDF, so clean the swap file here; otherwise
119+ // ownership passes to the RenderedReport, which the streaming caller closes after export.
120+ if (!ownershipTransferred ) {
121+ virtualizer .cleanup ();
122+ }
100123 }
101- log .info ("Combining {} section(s) into one PDF for mill {} year {}" ,
102- sections .size (), context .millId (), context .year ());
103- return reportService .exportPdf (sections );
104124 }
105125
106126 /**
0 commit comments