Skip to content

Commit 85b5948

Browse files
authored
feat: compress response if service wants to (#1728)
* initial commit, compress response if service wants to Signed-off-by: achmelo <a.chmelo@gmail.com> * add unit tests Signed-off-by: achmelo <a.chmelo@gmail.com> * empty gzip body should be 20 bytes Signed-off-by: achmelo <a.chmelo@gmail.com> * test flush buffers Signed-off-by: achmelo <a.chmelo@gmail.com> * test no instances available and close buffers Signed-off-by: achmelo <a.chmelo@gmail.com> * compress per service acceptance test Signed-off-by: achmelo <a.chmelo@gmail.com> * compress per service acceptance test Signed-off-by: achmelo <a.chmelo@gmail.com> * resolve conflicts Signed-off-by: achmelo <a.chmelo@gmail.com> * provide description for filter class Signed-off-by: achmelo <a.chmelo@gmail.com>
1 parent ebcb113 commit 85b5948

File tree

14 files changed

+832
-21
lines changed

14 files changed

+832
-21
lines changed
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
/*
2+
* This program and the accompanying materials are made available under the terms of the
3+
* Eclipse Public License v2.0 which accompanies this distribution, and is available at
4+
* https://www.eclipse.org/legal/epl-v20.html
5+
*
6+
* SPDX-License-Identifier: EPL-2.0
7+
*
8+
* Copyright Contributors to the Zowe Project.
9+
*/
10+
package org.zowe.apiml.gzip;
11+
12+
public class GZipResponseException extends RuntimeException {
13+
14+
public GZipResponseException(String message) {
15+
super(message);
16+
}
17+
}
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
/*
2+
* This program and the accompanying materials are made available under the terms of the
3+
* Eclipse Public License v2.0 which accompanies this distribution, and is available at
4+
* https://www.eclipse.org/legal/epl-v20.html
5+
*
6+
* SPDX-License-Identifier: EPL-2.0
7+
*
8+
* Copyright Contributors to the Zowe Project.
9+
*/
10+
package org.zowe.apiml.gzip;
11+
12+
import javax.servlet.http.HttpServletResponse;
13+
14+
15+
public final class GZipResponseUtils {
16+
17+
/**
18+
* Gzipping an empty file or stream always results in a 20 byte output
19+
* This is in java or elsewhere.
20+
* <p/>
21+
* On a unix system to reproduce do <code>gzip -n empty_file</code>. -n tells gzip to not
22+
* include the file name. The resulting file size is 20 bytes.
23+
* <p/>
24+
* Therefore 20 bytes can be used indicate that the gzip byte[] will be empty when ungzipped.
25+
*/
26+
private static final int EMPTY_GZIPPED_CONTENT_SIZE = 20;
27+
28+
/**
29+
* Utility class. No public constructor.
30+
*/
31+
private GZipResponseUtils() {
32+
}
33+
34+
/**
35+
* Checks whether a gzipped body is actually empty and should just be zero.
36+
* When the compressedBytes is {@link #EMPTY_GZIPPED_CONTENT_SIZE} it should be zero.
37+
*
38+
* @param compressedBytes the gzipped response body
39+
* @return true if the response should be 0, even if it is isn't.
40+
*/
41+
public static boolean shouldGzippedBodyBeZero(byte[] compressedBytes) {
42+
return compressedBytes.length == EMPTY_GZIPPED_CONTENT_SIZE;
43+
}
44+
45+
/**
46+
* Performs a number of checks to ensure response saneness according to the rules of RFC2616:
47+
* <ol>
48+
* <li>If the response code is {@link javax.servlet.http.HttpServletResponse#SC_NO_CONTENT} then it is forbidden for the body
49+
* to contain anything. See http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.2.5
50+
* <li>If the response code is {@link javax.servlet.http.HttpServletResponse#SC_NOT_MODIFIED} then it is forbidden for the body
51+
* to contain anything. See http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.3.5
52+
* </ol>
53+
*
54+
* @param responseStatus the responseStatus
55+
* @return true if the response should be 0, even if it is isn't.
56+
*/
57+
public static boolean shouldBodyBeZero(int responseStatus) {
58+
return responseStatus == HttpServletResponse.SC_NO_CONTENT || responseStatus == HttpServletResponse.SC_NOT_MODIFIED;
59+
}
60+
61+
/**
62+
* Adds the gzip HTTP header to the response.
63+
* <p/>
64+
* <p>
65+
* This is need when a gzipped body is returned so that browsers can properly decompress it.
66+
* </p>
67+
*
68+
* @param response the response which will have a header added to it. I.e this method changes its parameter
69+
* @throws RuntimeException Either the response is committed or we were called using the include method
70+
* from a {@link javax.servlet.RequestDispatcher#include(javax.servlet.ServletRequest, javax.servlet.ServletResponse)}
71+
* method and the set header is ignored.
72+
*/
73+
public static void addGzipHeader(final HttpServletResponse response) throws GZipResponseException {
74+
response.setHeader("Content-Encoding", "gzip");
75+
boolean containsEncoding = response.containsHeader("Content-Encoding");
76+
if (!containsEncoding) {
77+
throw new GZipResponseException("Failure when attempting to set "
78+
+ "Content-Encoding: gzip");
79+
}
80+
}
81+
}
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
/*
2+
* This program and the accompanying materials are made available under the terms of the
3+
* Eclipse Public License v2.0 which accompanies this distribution, and is available at
4+
* https://www.eclipse.org/legal/epl-v20.html
5+
*
6+
* SPDX-License-Identifier: EPL-2.0
7+
*
8+
* Copyright Contributors to the Zowe Project.
9+
*/
10+
package org.zowe.apiml.gzip;
11+
12+
13+
import javax.servlet.ServletOutputStream;
14+
import javax.servlet.http.HttpServletResponse;
15+
import javax.servlet.http.HttpServletResponseWrapper;
16+
import java.io.IOException;
17+
import java.io.OutputStreamWriter;
18+
import java.io.PrintWriter;
19+
import java.util.zip.GZIPOutputStream;
20+
21+
public class GZipResponseWrapper extends HttpServletResponseWrapper {
22+
23+
private GZipServletOutputStream gzipOutputStream;
24+
private PrintWriter printWriter = null;
25+
private boolean disableFlushBuffer = false;
26+
27+
/**
28+
* Constructs a response adaptor wrapping the given response.
29+
*
30+
* @param response The response to be wrapped
31+
* @throws IllegalArgumentException if the response is null
32+
*/
33+
public GZipResponseWrapper(HttpServletResponse response, GZIPOutputStream stream) {
34+
super(response);
35+
gzipOutputStream = new GZipServletOutputStream(stream);
36+
}
37+
38+
public void close() throws IOException {
39+
if (this.printWriter != null) {
40+
this.printWriter.close();
41+
}
42+
43+
if (this.gzipOutputStream != null) {
44+
this.gzipOutputStream.close();
45+
}
46+
}
47+
48+
/**
49+
* Flush OutputStream or PrintWriter
50+
*
51+
* @throws IOException
52+
*/
53+
@Override
54+
public void flushBuffer() throws IOException {
55+
flush();
56+
57+
// doing this might leads to response already committed exception
58+
// when the PageInfo has not yet built but the buffer already flushed
59+
// Happens in Weblogic when a servlet forward to a JSP page and the forward
60+
// method trigger a flush before it forwarded to the JSP
61+
// disableFlushBuffer for that purpose is 'true' by default
62+
if (!disableFlushBuffer) {
63+
super.flushBuffer();
64+
}
65+
}
66+
67+
/**
68+
* Flushes all the streams for this response.
69+
*/
70+
public void flush() throws IOException {
71+
if (printWriter != null) {
72+
printWriter.flush();
73+
}
74+
75+
if (gzipOutputStream != null) {
76+
gzipOutputStream.flush();
77+
}
78+
}
79+
80+
@Override
81+
public ServletOutputStream getOutputStream() {
82+
if (this.printWriter != null) {
83+
throw new IllegalStateException(
84+
"PrintWriter obtained already - cannot get OutputStream");
85+
}
86+
87+
return this.gzipOutputStream;
88+
}
89+
90+
@Override
91+
public PrintWriter getWriter() throws IOException {
92+
if (this.printWriter == null) {
93+
this.gzipOutputStream = new GZipServletOutputStream(
94+
getResponse().getOutputStream());
95+
96+
this.printWriter = new PrintWriter(new OutputStreamWriter(
97+
this.gzipOutputStream, getResponse().getCharacterEncoding()), true);
98+
}
99+
100+
return this.printWriter;
101+
}
102+
103+
104+
@Override
105+
public void setContentLength(int length) {
106+
//ignore, since content length of zipped content
107+
//does not match content length of unzipped content.
108+
}
109+
110+
111+
/**
112+
* Set if the wrapped reponse's buffer flushing should be disabled.
113+
*
114+
* @param disableFlushBuffer true if the wrapped reponse's buffer flushing should be disabled
115+
*/
116+
public void setDisableFlushBuffer(boolean disableFlushBuffer) {
117+
this.disableFlushBuffer = disableFlushBuffer;
118+
}
119+
120+
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
/*
2+
* This program and the accompanying materials are made available under the terms of the
3+
* Eclipse Public License v2.0 which accompanies this distribution, and is available at
4+
* https://www.eclipse.org/legal/epl-v20.html
5+
*
6+
* SPDX-License-Identifier: EPL-2.0
7+
*
8+
* Copyright Contributors to the Zowe Project.
9+
*/
10+
package org.zowe.apiml.gzip;
11+
12+
import javax.servlet.ServletOutputStream;
13+
import javax.servlet.WriteListener;
14+
import java.io.IOException;
15+
import java.io.OutputStream;
16+
17+
public class GZipServletOutputStream extends ServletOutputStream {
18+
19+
private OutputStream gzipOutputStream;
20+
21+
public GZipServletOutputStream(OutputStream outputStream) {
22+
super();
23+
this.gzipOutputStream = outputStream;
24+
}
25+
26+
@Override
27+
public void close() throws IOException {
28+
this.gzipOutputStream.close();
29+
}
30+
31+
@Override
32+
public void flush() throws IOException {
33+
this.gzipOutputStream.flush();
34+
}
35+
36+
@Override
37+
public void write(byte[] b) throws IOException {
38+
this.gzipOutputStream.write(b);
39+
}
40+
41+
@Override
42+
public void write(byte[] b, int off, int len) throws IOException {
43+
this.gzipOutputStream.write(b, off, len);
44+
}
45+
46+
@Override
47+
public void write(int b) throws IOException {
48+
this.gzipOutputStream.write(b);
49+
}
50+
51+
@Override
52+
public boolean isReady() {
53+
return true;
54+
}
55+
56+
@Override
57+
public void setWriteListener(WriteListener listener) {
58+
// writer is never used in this case
59+
}
60+
61+
62+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/*
2+
* This program and the accompanying materials are made available under the terms of the
3+
* Eclipse Public License v2.0 which accompanies this distribution, and is available at
4+
* https://www.eclipse.org/legal/epl-v20.html
5+
*
6+
* SPDX-License-Identifier: EPL-2.0
7+
*
8+
* Copyright Contributors to the Zowe Project.
9+
*/
10+
11+
package org.zowe.apiml.gzip;
12+
13+
import org.junit.jupiter.api.Test;
14+
import org.springframework.mock.web.MockHttpServletResponse;
15+
16+
import javax.servlet.http.HttpServletResponse;
17+
18+
import static org.junit.jupiter.api.Assertions.*;
19+
import static org.mockito.Mockito.mock;
20+
import static org.mockito.Mockito.when;
21+
22+
class GZipResponseUtilsTest {
23+
24+
@Test
25+
void addHeader() {
26+
MockHttpServletResponse response = new MockHttpServletResponse();
27+
GZipResponseUtils.addGzipHeader(response);
28+
assertEquals("gzip", response.getHeader("Content-Encoding"));
29+
}
30+
31+
@Test
32+
void whenSetHeaderFails_thenThrowException() {
33+
HttpServletResponse response = mock(HttpServletResponse.class);
34+
when(response.containsHeader("Content-Encoding")).thenReturn(false);
35+
assertThrows(GZipResponseException.class, () -> GZipResponseUtils.addGzipHeader(response));
36+
}
37+
38+
@Test
39+
void whenContentShouldBeEmpty_thenReturnTrue() {
40+
assertFalse(GZipResponseUtils.shouldBodyBeZero(200));
41+
assertTrue(GZipResponseUtils.shouldBodyBeZero(204));
42+
assertTrue(GZipResponseUtils.shouldBodyBeZero(304));
43+
}
44+
45+
@Test
46+
void whenGZippedBodyIsEmpty_thenReturnTrue() {
47+
byte[] bytes = new byte[20];
48+
assertTrue(GZipResponseUtils.shouldGzippedBodyBeZero(bytes));
49+
}
50+
}

0 commit comments

Comments
 (0)