Joaquín Reñé
2025-10-07 146a0fb8b0e90f9196e569152f649baf60d6cc8f
securis/src/main/java/net/curisit/securis/utils/GZipServletResponseWrapper.java
....@@ -1,3 +1,6 @@
1
+/*
2
+ * Copyright @ 2013 CurisTEC, S.A.S. All Rights Reserved.
3
+ */
14 package net.curisit.securis.utils;
25
36 import java.io.IOException;
....@@ -10,38 +13,92 @@
1013 import jakarta.servlet.http.HttpServletResponse;
1114 import jakarta.servlet.http.HttpServletResponseWrapper;
1215
16
+/**
17
+ * GZipServletResponseWrapper
18
+ * <p>
19
+ * {@link HttpServletResponseWrapper} that transparently compresses the response
20
+ * body using GZIP. Intended for use in filters/servlets where the caller wants
21
+ * to wrap the original response and write compressed bytes to the client.
22
+ * <p>
23
+ * How it works:
24
+ * <ul>
25
+ * <li>Overrides {@link #getOutputStream()} and {@link #getWriter()} to route output through a {@link GZIPOutputStream}.</li>
26
+ * <li>Ensures mutual exclusivity between OutputStream and Writer access per Servlet API requirements.</li>
27
+ * <li>Ignores {@link #setContentLength(int)} since compressed size differs from uncompressed.</li>
28
+ * </ul>
29
+ *
30
+ * Usage:
31
+ * <pre>
32
+ * GZipServletResponseWrapper gz = new GZipServletResponseWrapper(resp);
33
+ * chain.doFilter(request, gz);
34
+ * gz.close(); // important: finish compression and flush buffers
35
+ * </pre>
36
+ *
37
+ * Thread-safety:
38
+ * <p>
39
+ * Instances are per-request and not shared.
40
+ *
41
+ * Limitations:
42
+ * <p>
43
+ * Caller is responsible for setting "Content-Encoding: gzip" and for avoiding
44
+ * double-compression scenarios.
45
+ *
46
+ * @author JRA
47
+ * Last reviewed by JRA on Oct 6, 2025.
48
+ */
1349 public class GZipServletResponseWrapper extends HttpServletResponseWrapper {
1450
1551 private GZIPServletOutputStream gzipOutputStream = null;
1652 private PrintWriter printWriter = null;
1753
54
+ // ---------------------------------------------------------------------
55
+ // Constructors
56
+ // ---------------------------------------------------------------------
57
+
58
+ /**
59
+ * GZipServletResponseWrapper
60
+ * <p>
61
+ * Wraps the given response. Actual GZIP streams are lazily created on first write.
62
+ *
63
+ * @param response the original {@link HttpServletResponse} to wrap
64
+ * @throws IOException if the underlying response streams cannot be accessed
65
+ */
1866 public GZipServletResponseWrapper(HttpServletResponse response) throws IOException {
1967 super(response);
2068 }
2169
22
- public void close() throws IOException {
70
+ // ---------------------------------------------------------------------
71
+ // Lifecycle
72
+ // ---------------------------------------------------------------------
2373
24
- //PrintWriter.close does not throw exceptions.
25
- //Hence no try-catch block.
74
+ /**
75
+ * close<p>
76
+ * Closes any open writer or output stream and finalizes the GZIP stream.
77
+ * Must be called once all response content has been written.
78
+ *
79
+ * @throws IOException if closing the underlying streams fails
80
+ */
81
+ public void close() throws IOException {
82
+ // PrintWriter.close does not throw exceptions. Hence no try-catch block.
2683 if (this.printWriter != null) {
2784 this.printWriter.close();
2885 }
29
-
3086 if (this.gzipOutputStream != null) {
3187 this.gzipOutputStream.close();
3288 }
3389 }
3490
3591 /**
36
- * Flush OutputStream or PrintWriter
92
+ * flushBuffer<p>
93
+ * Flushes the writer and the GZIP output stream, then delegates to the wrapped response.
94
+ * If multiple exceptions occur, the first encountered is thrown (typical servlet practice).
3795 *
38
- * @throws IOException
96
+ * @throws IOException if flushing any of the streams fails
3997 */
40
-
4198 @Override
4299 public void flushBuffer() throws IOException {
43100
44
- //PrintWriter.flush() does not throw exception
101
+ // PrintWriter.flush() does not throw exception
45102 if (this.printWriter != null) {
46103 this.printWriter.flush();
47104 }
....@@ -62,12 +119,23 @@
62119 exception2 = e;
63120 }
64121
65
- if (exception1 != null)
66
- throw exception1;
67
- if (exception2 != null)
68
- throw exception2;
122
+ if (exception1 != null) throw exception1;
123
+ if (exception2 != null) throw exception2;
69124 }
70125
126
+ // ---------------------------------------------------------------------
127
+ // Output acquisition
128
+ // ---------------------------------------------------------------------
129
+
130
+ /**
131
+ * getOutputStream<p>
132
+ * Returns a {@link ServletOutputStream} that writes compressed data.
133
+ * Mutually exclusive with {@link #getWriter()} as per Servlet API.
134
+ *
135
+ * @return compressed {@link ServletOutputStream}
136
+ * @throws IOException if the underlying output stream cannot be obtained
137
+ * @throws IllegalStateException if the writer has been already acquired
138
+ */
71139 @Override
72140 public ServletOutputStream getOutputStream() throws IOException {
73141 if (this.printWriter != null) {
....@@ -79,6 +147,15 @@
79147 return this.gzipOutputStream;
80148 }
81149
150
+ /**
151
+ * getWriter<p>
152
+ * Returns a {@link PrintWriter} that writes compressed data (UTF-8 by default, inherited from response).
153
+ * Mutually exclusive with {@link #getOutputStream()} as per Servlet API.
154
+ *
155
+ * @return compressed {@link PrintWriter}
156
+ * @throws IOException if streams cannot be allocated
157
+ * @throws IllegalStateException if the output stream has been already acquired
158
+ */
82159 @Override
83160 public PrintWriter getWriter() throws IOException {
84161 if (this.printWriter == null && this.gzipOutputStream != null) {
....@@ -91,50 +168,118 @@
91168 return this.printWriter;
92169 }
93170
171
+ /**
172
+ * setContentLength<p>
173
+ * No-op. The content length of the zipped content is not known a priori and
174
+ * will not match the uncompressed length; therefore we do not set it here.
175
+ *
176
+ * @param len ignored
177
+ */
94178 @Override
95179 public void setContentLength(int len) {
96
- //ignore, since content length of zipped content
97
- //does not match content length of unzipped content.
180
+ // ignore, since content length of zipped content does not match content length of unzipped content.
98181 }
99182
183
+ // ---------------------------------------------------------------------
184
+ // Inner compressed stream
185
+ // ---------------------------------------------------------------------
186
+
187
+ /**
188
+ * GZIPServletOutputStream
189
+ * <p>
190
+ * Decorates the original {@link ServletOutputStream} with a {@link GZIPOutputStream}.
191
+ * Delegates readiness and listener to the underlying (container) stream.
192
+ *
193
+ * @author JRA
194
+ * Last reviewed by JRA on Oct 5, 2025.
195
+ */
100196 private static class GZIPServletOutputStream extends ServletOutputStream {
101197 private final ServletOutputStream servletOutputStream;
102198 private final GZIPOutputStream gzipStream;
103199
200
+ /**
201
+ * GZIPServletOutputStream<p>
202
+ * Creates a new compressed stream wrapper.
203
+ *
204
+ * @param servletOutputStream underlying (container-provided) output stream
205
+ * @throws IOException if the GZIP stream cannot be created
206
+ */
104207 public GZIPServletOutputStream(ServletOutputStream servletOutputStream) throws IOException {
105208 this.servletOutputStream = servletOutputStream;
106209 this.gzipStream = new GZIPOutputStream(servletOutputStream);
107210 }
108211
212
+ /**
213
+ * isReady<p>
214
+ * Check if the output stream is ready
215
+ * {@inheritDoc}
216
+ *
217
+ * @return isReady
218
+ */
109219 @Override
110220 public boolean isReady() {
111221 return this.servletOutputStream.isReady();
112222 }
113223
224
+ /**
225
+ * setWriteListener<p>
226
+ * Set the write listener for the output stream
227
+ * {@inheritDoc}
228
+ *
229
+ * @param writeListener
230
+ */
114231 @Override
115232 public void setWriteListener(WriteListener writeListener) {
116233 this.servletOutputStream.setWriteListener(writeListener);
117234 }
118235
236
+ /**
237
+ * write<p>
238
+ * Write on the gzip stream
239
+ * {@inheritDoc}
240
+ *
241
+ * @param b
242
+ * @throws IOException
243
+ */
119244 @Override
120245 public void write(int b) throws IOException {
121246 this.gzipStream.write(b);
122247 }
123248
249
+ /**
250
+ * close<p>
251
+ * Close the gzip stream
252
+ * {@inheritDoc}
253
+ *
254
+ * @throws IOException
255
+ */
124256 @Override
125257 public void close() throws IOException {
126258 this.gzipStream.close();
127259 }
128260
261
+ /**
262
+ * flush<p>
263
+ * Flush the gzip stream
264
+ * {@inheritDoc}
265
+ *
266
+ * @throws IOException
267
+ */
129268 @Override
130269 public void flush() throws IOException {
131270 this.gzipStream.flush();
132271 }
133272
273
+ /**
274
+ * finish<p>
275
+ * Explicitly finishes writing of the GZIP stream, without closing the underlying stream.
276
+ * Not used by the wrapper but available for completeness.
277
+ *
278
+ * @throws IOException if finishing fails
279
+ */
134280 @SuppressWarnings("unused")
135281 public void finish() throws IOException {
136282 this.gzipStream.finish();
137283 }
138284 }
139
-
140285 }