I want to perform some filter logic to HTTP responses (which are in json format).
I had successfully change the response body, but when the (string) size of the body changes: I am getting missing the last characters.
To make it simpler, I had created a simple Spring Boot application, with only Web dependency for my rest controller.
My Rest Controller
@RestController
@RequestMapping("/home/")
public class RestControllerHome {
@GetMapping (produces=MediaType.APPLICATION_JSON_VALUE)
public String home() {
return "{ \"name\" : \"Peter\" }";
}
}
My Filter
@Component
public class MyFilter implements Filter {
@Override
public void destroy() { }
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws IOException, ServletException {
HtmlResponseWrapper capturingResponseWrapper = new HtmlResponseWrapper((HttpServletResponse) response);
filterChain.doFilter(request, capturingResponseWrapper);
if (response.getContentType() != null && response.getContentType().contains("application/json")) {
String content = capturingResponseWrapper.getCaptureAsString();
// This code works fine
//response.getWriter().write(content.toUpperCase());
// This code doesn't works because the content size is changed
response.getWriter().write("{ \"name\" : \"************r\" }");
}
}
@Override
public void init(FilterConfig arg0) throws ServletException { }
}
HttpServletResponseWrapper // capture the response before it is written
public class HtmlResponseWrapper extends HttpServletResponseWrapper {
private final ByteArrayOutputStream capture;
private ServletOutputStream output;
private PrintWriter writer;
public HtmlResponseWrapper(HttpServletResponse response) {
super(response);
capture = new ByteArrayOutputStream(response.getBufferSize());
}
@Override
public ServletOutputStream getOutputStream() {
if (writer != null) {
throw new IllegalStateException("getWriter() has already been called on this response.");
}
if (output == null) {
// inner class - lets the wrapper manipulate the response
output = new ServletOutputStream() {
@Override
public void write(int b) throws IOException {
capture.write(b);
}
@Override
public void flush() throws IOException {
capture.flush();
}
@Override
public void close() throws IOException {
capture.close();
}
@Override
public boolean isReady() {
return false;
}
@Override
public void setWriteListener(WriteListener arg0) {
}
};
}
return output;
}
@Override
public PrintWriter getWriter() throws IOException {
if (output != null) {
throw new IllegalStateException("getOutputStream() has already been called on this response.");
}
if (writer == null) {
writer = new PrintWriter(new OutputStreamWriter(capture,
getCharacterEncoding()));
}
return writer;
}
@Override
public void flushBuffer() throws IOException {
super.flushBuffer();
if (writer != null) {
writer.flush();
} else if (output != null) {
output.flush();
}
}
public byte[] getCaptureAsBytes() throws IOException {
if (writer != null) {
writer.close();
} else if (output != null) {
output.close();
}
return capture.toByteArray();
}
public String getCaptureAsString() throws IOException {
return new String(getCaptureAsBytes(), getCharacterEncoding());
}
}
In my doFilter method, the following code ...
// This code works fine
response.getWriter().write(content.toUpperCase());
// This code doesn't works because the content size is changed
//response.getWriter().write("{ \"name\" : \"************r\" }");
... gives my the following output : {"NAME": "PETER"} Which tell me, that the code is working properly.
But, in reality I want to change the body content ...
// This code works fine
//response.getWriter().write(content.toUpperCase());
// This code doesn't works because the content size is changed
response.getWriter().write("{ \"name\" : \"************r\" }");
... and the previous code, is giving me an incomplete text body as output: **{ "name" : "**********
What am I doing wrong? My app have a bigger json body, and a little more complex logic in the filter. But, if I dont get this working I am not being able to make the rest of my code work. Please, help.
I took the Filter and HttpServletResponseWrapper from https://www.leveluplunch.com/java/tutorials/034-modify-html-response-using-filter/
The filter() function of the Java stream allows you to narrow down the stream's items based on a criterion. If you only want items that are even on your list, you can use the filter method to do this. This method accepts a predicate as an input and returns a list of elements that are the results of that predicate.
A filter is an object that is invoked at the preprocessing and postprocessing of a request. It is mainly used to perform filtering tasks such as conversion, logging, compression, encryption and decryption, input validation etc. The servlet filter is pluggable, i.e. its entry is defined in the web.
Thanks to the help of JBNizet, I found that the solution was to add the Content Lenght:
String newContent = "{ \"name\" : \"************r\" }";
response.setContentLength(newContent .length());
response.getWriter().write(newContent);
on my side, to modify the response I used this wrapper to capture the response's content into a byte array : ContentCachingResponseWrapper
( see also this useful general documentation from Oracle : https://www.oracle.com/java/technologies/filters.html][2] )
here is my source code :
...
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletResponse;
import org.springframework.web.util.ContentCachingResponseWrapper;
...
public class MyCustomFilter implements Filter {
private static final Logger LOGGER = Logger.getLogger( "MyCustomFilter" );
@Override
public void destroy() {
}
/**
* MODIFY THE RESPONSE
* https://www.oracle.com/java/technologies/filters.html
*/
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
// Use a SPRING-4 wrapper that caches all content read from the input stream and reader, and allows this content to be retrieved via a byte array.
final ContentCachingResponseWrapper responseWrapper = new ContentCachingResponseWrapper((HttpServletResponse) response);
// Invoke the next entity in the filter chain
chain.doFilter(request, responseWrapper);
try {
// Get the original response data
final byte[] originalData = responseWrapper.getContentAsByteArray();
final int originalLength = responseWrapper.getContentSize();
// Modify the original data
final String newData = performYourOwnBusinessLogicIntoThisMethod( originalData ) ;
final int newLength = newData.length();
// Write the data into the output stream
response.setContentLength(newData.length());
response.getWriter().write(newData);
// Commit the written data
response.getWriter().flush();
} catch (Exception e) {
LOGGER.log(Level.SEVERE, "Please contact your developer about this ERROR !" + e.getClass() + " : " + e.getMessage(), e);
response.setContentLength(responseWrapper.getContentSize());
response.getOutputStream().write(responseWrapper.getContentAsByteArray());
response.flushBuffer();
} finally {
//NOOP
}
}
}
In my case, originalData represents a JSON object (of which I do not have the classes unfortunately... I need to parse), the business logic consists in updating or removing some nodes :
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
...
public String performYourOwnBusinessLogicIntoThisMethod(byte[] jsonInput) throws IOException {
final ObjectMapper mapper = new ObjectMapper();
final ObjectNode root = (ObjectNode) mapper.readTree(jsonInput);
//update the json root
//...
return root.toString();
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With