| .. | .. |
|---|
| 1 | +/* |
|---|
| 2 | + * Copyright @ 2013 CurisTEC, S.A.S. All Rights Reserved. |
|---|
| 3 | + */ |
|---|
| 1 | 4 | package net.curisit.securis.utils; |
|---|
| 2 | 5 | |
|---|
| 3 | 6 | import java.io.IOException; |
|---|
| .. | .. |
|---|
| 10 | 13 | import jakarta.servlet.http.HttpServletResponse; |
|---|
| 11 | 14 | import jakarta.servlet.http.HttpServletResponseWrapper; |
|---|
| 12 | 15 | |
|---|
| 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 | + */ |
|---|
| 13 | 49 | public class GZipServletResponseWrapper extends HttpServletResponseWrapper { |
|---|
| 14 | 50 | |
|---|
| 15 | 51 | private GZIPServletOutputStream gzipOutputStream = null; |
|---|
| 16 | 52 | private PrintWriter printWriter = null; |
|---|
| 17 | 53 | |
|---|
| 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 | + */ |
|---|
| 18 | 66 | public GZipServletResponseWrapper(HttpServletResponse response) throws IOException { |
|---|
| 19 | 67 | super(response); |
|---|
| 20 | 68 | } |
|---|
| 21 | 69 | |
|---|
| 22 | | - public void close() throws IOException { |
|---|
| 70 | + // --------------------------------------------------------------------- |
|---|
| 71 | + // Lifecycle |
|---|
| 72 | + // --------------------------------------------------------------------- |
|---|
| 23 | 73 | |
|---|
| 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. |
|---|
| 26 | 83 | if (this.printWriter != null) { |
|---|
| 27 | 84 | this.printWriter.close(); |
|---|
| 28 | 85 | } |
|---|
| 29 | | - |
|---|
| 30 | 86 | if (this.gzipOutputStream != null) { |
|---|
| 31 | 87 | this.gzipOutputStream.close(); |
|---|
| 32 | 88 | } |
|---|
| 33 | 89 | } |
|---|
| 34 | 90 | |
|---|
| 35 | 91 | /** |
|---|
| 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). |
|---|
| 37 | 95 | * |
|---|
| 38 | | - * @throws IOException |
|---|
| 96 | + * @throws IOException if flushing any of the streams fails |
|---|
| 39 | 97 | */ |
|---|
| 40 | | - |
|---|
| 41 | 98 | @Override |
|---|
| 42 | 99 | public void flushBuffer() throws IOException { |
|---|
| 43 | 100 | |
|---|
| 44 | | - //PrintWriter.flush() does not throw exception |
|---|
| 101 | + // PrintWriter.flush() does not throw exception |
|---|
| 45 | 102 | if (this.printWriter != null) { |
|---|
| 46 | 103 | this.printWriter.flush(); |
|---|
| 47 | 104 | } |
|---|
| .. | .. |
|---|
| 62 | 119 | exception2 = e; |
|---|
| 63 | 120 | } |
|---|
| 64 | 121 | |
|---|
| 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; |
|---|
| 69 | 124 | } |
|---|
| 70 | 125 | |
|---|
| 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 | + */ |
|---|
| 71 | 139 | @Override |
|---|
| 72 | 140 | public ServletOutputStream getOutputStream() throws IOException { |
|---|
| 73 | 141 | if (this.printWriter != null) { |
|---|
| .. | .. |
|---|
| 79 | 147 | return this.gzipOutputStream; |
|---|
| 80 | 148 | } |
|---|
| 81 | 149 | |
|---|
| 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 | + */ |
|---|
| 82 | 159 | @Override |
|---|
| 83 | 160 | public PrintWriter getWriter() throws IOException { |
|---|
| 84 | 161 | if (this.printWriter == null && this.gzipOutputStream != null) { |
|---|
| .. | .. |
|---|
| 91 | 168 | return this.printWriter; |
|---|
| 92 | 169 | } |
|---|
| 93 | 170 | |
|---|
| 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 | + */ |
|---|
| 94 | 178 | @Override |
|---|
| 95 | 179 | 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. |
|---|
| 98 | 181 | } |
|---|
| 99 | 182 | |
|---|
| 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 | + */ |
|---|
| 100 | 196 | private static class GZIPServletOutputStream extends ServletOutputStream { |
|---|
| 101 | 197 | private final ServletOutputStream servletOutputStream; |
|---|
| 102 | 198 | private final GZIPOutputStream gzipStream; |
|---|
| 103 | 199 | |
|---|
| 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 | + */ |
|---|
| 104 | 207 | public GZIPServletOutputStream(ServletOutputStream servletOutputStream) throws IOException { |
|---|
| 105 | 208 | this.servletOutputStream = servletOutputStream; |
|---|
| 106 | 209 | this.gzipStream = new GZIPOutputStream(servletOutputStream); |
|---|
| 107 | 210 | } |
|---|
| 108 | 211 | |
|---|
| 212 | + /** |
|---|
| 213 | + * isReady<p> |
|---|
| 214 | + * Check if the output stream is ready |
|---|
| 215 | + * {@inheritDoc} |
|---|
| 216 | + * |
|---|
| 217 | + * @return isReady |
|---|
| 218 | + */ |
|---|
| 109 | 219 | @Override |
|---|
| 110 | 220 | public boolean isReady() { |
|---|
| 111 | 221 | return this.servletOutputStream.isReady(); |
|---|
| 112 | 222 | } |
|---|
| 113 | 223 | |
|---|
| 224 | + /** |
|---|
| 225 | + * setWriteListener<p> |
|---|
| 226 | + * Set the write listener for the output stream |
|---|
| 227 | + * {@inheritDoc} |
|---|
| 228 | + * |
|---|
| 229 | + * @param writeListener |
|---|
| 230 | + */ |
|---|
| 114 | 231 | @Override |
|---|
| 115 | 232 | public void setWriteListener(WriteListener writeListener) { |
|---|
| 116 | 233 | this.servletOutputStream.setWriteListener(writeListener); |
|---|
| 117 | 234 | } |
|---|
| 118 | 235 | |
|---|
| 236 | + /** |
|---|
| 237 | + * write<p> |
|---|
| 238 | + * Write on the gzip stream |
|---|
| 239 | + * {@inheritDoc} |
|---|
| 240 | + * |
|---|
| 241 | + * @param b |
|---|
| 242 | + * @throws IOException |
|---|
| 243 | + */ |
|---|
| 119 | 244 | @Override |
|---|
| 120 | 245 | public void write(int b) throws IOException { |
|---|
| 121 | 246 | this.gzipStream.write(b); |
|---|
| 122 | 247 | } |
|---|
| 123 | 248 | |
|---|
| 249 | + /** |
|---|
| 250 | + * close<p> |
|---|
| 251 | + * Close the gzip stream |
|---|
| 252 | + * {@inheritDoc} |
|---|
| 253 | + * |
|---|
| 254 | + * @throws IOException |
|---|
| 255 | + */ |
|---|
| 124 | 256 | @Override |
|---|
| 125 | 257 | public void close() throws IOException { |
|---|
| 126 | 258 | this.gzipStream.close(); |
|---|
| 127 | 259 | } |
|---|
| 128 | 260 | |
|---|
| 261 | + /** |
|---|
| 262 | + * flush<p> |
|---|
| 263 | + * Flush the gzip stream |
|---|
| 264 | + * {@inheritDoc} |
|---|
| 265 | + * |
|---|
| 266 | + * @throws IOException |
|---|
| 267 | + */ |
|---|
| 129 | 268 | @Override |
|---|
| 130 | 269 | public void flush() throws IOException { |
|---|
| 131 | 270 | this.gzipStream.flush(); |
|---|
| 132 | 271 | } |
|---|
| 133 | 272 | |
|---|
| 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 | + */ |
|---|
| 134 | 280 | @SuppressWarnings("unused") |
|---|
| 135 | 281 | public void finish() throws IOException { |
|---|
| 136 | 282 | this.gzipStream.finish(); |
|---|
| 137 | 283 | } |
|---|
| 138 | 284 | } |
|---|
| 139 | | - |
|---|
| 140 | 285 | } |
|---|