-
Notifications
You must be signed in to change notification settings - Fork 6
Expand file tree
/
Copy pathHttpStreamingExample.java
More file actions
129 lines (121 loc) · 5.4 KB
/
Copy pathHttpStreamingExample.java
File metadata and controls
129 lines (121 loc) · 5.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
package com.demcha.examples.features.streaming;
import com.demcha.compose.GraphCompose;
import com.demcha.compose.document.api.DocumentPageSize;
import com.demcha.compose.document.api.DocumentSession;
import com.demcha.compose.document.style.DocumentInsets;
import com.demcha.compose.document.templates.api.DocumentTemplate;
import com.demcha.compose.document.templates.core.theme.BrandTheme;
import com.demcha.compose.document.templates.data.invoice.InvoiceDocumentSpec;
import com.demcha.compose.document.templates.invoice.presets.ModernInvoice;
import com.demcha.examples.support.ExampleDataFactory;
import com.demcha.examples.support.ExampleOutputPaths;
import java.io.ByteArrayOutputStream;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
/**
* Phase E.4 — runnable showcase for the canonical
* {@code DocumentSession.writePdf(OutputStream)} streaming path.
*
* <p>The example deliberately keeps Spring out of the
* {@code graphcompose-examples} module so the artifact stays
* dependency-light. Instead, it isolates the exact code that any
* server-side endpoint would call into a single
* {@link #streamInvoiceTo(InvoiceDocumentSpec, OutputStream)} method,
* then exercises that method end-to-end against
* {@link ByteArrayOutputStream} (in {@link #generate()}) so the
* example actually produces a PDF you can open.</p>
*
* <p>To wire the same code into a Spring Boot controller, copy the
* snippet below into your project — the body delegates to
* {@link #streamInvoiceTo(InvoiceDocumentSpec, OutputStream)} and the
* Servlet response stream is the {@code OutputStream} sink:</p>
*
* <pre>{@code
* @RestController
* @RequestMapping("/api/invoices")
* public class InvoiceController {
*
* @GetMapping(value = "/{id}.pdf", produces = MediaType.APPLICATION_PDF_VALUE)
* public void downloadInvoice(@PathVariable String id,
* HttpServletResponse response) throws Exception {
* InvoiceDocumentSpec invoice = invoiceService.findById(id);
*
* response.setHeader("Content-Disposition",
* "attachment; filename=invoice-" + id + ".pdf");
*
* // GraphCompose writes the PDF bytes to the stream but does
* // NOT close it — the Servlet container still owns the stream
* // lifecycle.
* HttpStreamingExample.streamInvoiceTo(invoice, response.getOutputStream());
* }
* }
* }</pre>
*
* <p>The same pattern works for any sink that exposes an
* {@link OutputStream}: AWS S3 multipart uploads, GCS resumable
* uploads, Azure block blobs, Kafka producers wrapped in a custom
* stream, etc. Because GraphCompose never holds the rendered PDF in
* memory as a single byte array, the streaming path scales to large
* documents without the {@code -Xmx} hit of {@code toPdfBytes()}.</p>
*
* <p>See {@code docs/recipes/streaming.md} for the full discussion of
* file vs. stream vs. byte-array output and the trade-offs around
* caching, retries, and error reporting.</p>
*
* @author Artem Demchyshyn
*/
public final class HttpStreamingExample {
private HttpStreamingExample() {
}
/**
* Renders the supplied invoice into the supplied output stream
* via {@code ModernInvoice} on {@link BrandTheme#invoiceModern()}.
*
* <p>The stream is intentionally <strong>not closed</strong> by
* this method — that lets the same code be used from a Servlet,
* an S3 multipart uploader, or a {@link Files#newOutputStream}
* call without forcing the caller into a particular stream
* lifecycle.</p>
*
* @param invoice invoice spec to render
* @param sink destination stream owned by the caller
* @throws Exception if PDF rendering fails
*/
public static void streamInvoiceTo(InvoiceDocumentSpec invoice, OutputStream sink) throws Exception {
BrandTheme theme = BrandTheme.invoiceModern();
DocumentTemplate<InvoiceDocumentSpec> template = ModernInvoice.create(theme);
try (DocumentSession document = GraphCompose.document()
.pageSize(DocumentPageSize.A4)
.pageBackground(theme.palette().mainFill())
.margin(DocumentInsets.of(28))
.create()) {
template.compose(document, invoice);
document.writePdf(sink);
}
// Note: we deliberately do not call sink.close().
// In a Servlet or storage upload the caller closes it.
}
/**
* Runs the streaming pipeline end-to-end against
* {@link ByteArrayOutputStream}, then writes the captured bytes
* to disk so reviewers have a real PDF to open. The byte-buffer
* acts as a stand-in for {@code response.getOutputStream()} and
* proves the streaming code path produces a complete, well-formed
* document.
*
* @return path to the generated PDF
* @throws Exception if PDF rendering or file IO fails
*/
public static Path generate() throws Exception {
Path outputFile = ExampleOutputPaths.prepare("features/streaming", "invoice-http-stream.pdf");
try (ByteArrayOutputStream buffer = new ByteArrayOutputStream(64 * 1024)) {
streamInvoiceTo(ExampleDataFactory.sampleInvoice(), buffer);
Files.write(outputFile, buffer.toByteArray());
}
return outputFile;
}
public static void main(String[] args) throws Exception {
System.out.println("Generated: " + generate());
}
}