Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

how modify the response body with java filter?

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/

like image 335
JaimeRG Avatar asked Aug 13 '18 16:08

JaimeRG


People also ask

How does filters work in Java?

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.

What is filter in Java with example?

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.


2 Answers

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);
like image 121
JaimeRG Avatar answered Oct 22 '22 15:10

JaimeRG


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();
    }       
like image 1
Gilles Gaido Avatar answered Oct 22 '22 14:10

Gilles Gaido