36

I've been searching the net and stackoverflow for an example of somebody inserting content into the response using a servlet filter, but can only find examples of people capturing/compressing the output and/or changing the headers. My goal is to append a chunk of HTML just before the closing </body> of all HTML responses.

I'm working on a solution that extends the HttpServletResponseWrapper to use my own PrintWriter, then overriding the write methods thereon. Inside the write method I'm storing the last 7 characters to see if it's equal to the closing body tag, and then I write my HTML chunk plus the closing body tag, before continuing normal write operations for the rest of the document.

I feel that somebody must have solved this problem already, and probably more elegantly than I will. I'd appreciate any examples of how to use a servlet filter to insert content into a response.

UPDATED

Responding to a comment, I am also trying to implement the CharResponseWrapper from http://www.oracle.com/technetwork/java/filters-137243.html. Here is my code:

PrintWriter out = response.getWriter();
CharResponseWrapper wrappedResponse = new CharResponseWrapper(
        (HttpServletResponse)response);

chain.doFilter(wrappedRequest, wrappedResponse);
String s = wrappedResponse.toString();

if (wrappedResponse.getContentType().equals("text/html") &&
        StringUtils.isNotBlank(s)) {
    CharArrayWriter caw = new CharArrayWriter();
    caw.write(s.substring(0, s.indexOf("</body>") - 1));
    caw.write("WTF</body></html>");
    response.setContentLength(caw.toString().length());
    out.write(caw.toString());
}
else {
    out.write(wrappedResponse.toString());
}

out.close();

I am also wrapping the request, but that code works and shouldn't affect the response.

matt snider
  • 4,013
  • 4
  • 24
  • 39

4 Answers4

46

The codebase I am using, calls the getOutputStream method, instead of getWriter when it processes the response, so the examples included in the other answer doesn't help. Here is a more complete answer that works with both the OutputStream and the PrintWriter, even erroring correctly, if the writer is accessed twice. This is derived from the great example, DUMP REQUEST AND RESPONSE USING JAVAX.SERVLET.FILTER.

import javax.servlet.*;
import javax.servlet.http.*;
import java.io.*;

public class MyFilter implements Filter
{
    private FilterConfig filterConfig = null;

    private static class ByteArrayServletStream extends ServletOutputStream
    {
        ByteArrayOutputStream baos;

        ByteArrayServletStream(ByteArrayOutputStream baos)
        {
            this.baos = baos;
        }

        public void write(int param) throws IOException
        {
            baos.write(param);
        }
    }

    private static class ByteArrayPrintWriter
    {

        private ByteArrayOutputStream baos = new ByteArrayOutputStream();

        private PrintWriter pw = new PrintWriter(baos);

        private ServletOutputStream sos = new ByteArrayServletStream(baos);

        public PrintWriter getWriter()
        {
            return pw;
        }

        public ServletOutputStream getStream()
        {
            return sos;
        }

        byte[] toByteArray()
        {
            return baos.toByteArray();
        }
    }

    public class CharResponseWrapper extends HttpServletResponseWrapper
    {
        private ByteArrayPrintWriter output;
        private boolean usingWriter;

        public CharResponseWrapper(HttpServletResponse response)
        {
            super(response);
            usingWriter = false;
            output = new ByteArrayPrintWriter();
        }

        public byte[] getByteArray()
        {
            return output.toByteArray();
        }

        @Override
        public ServletOutputStream getOutputStream() throws IOException
        {
            // will error out, if in use
            if (usingWriter) {
                super.getOutputStream();
            }
            usingWriter = true;
            return output.getStream();
        }

        @Override
        public PrintWriter getWriter() throws IOException
        {
            // will error out, if in use
            if (usingWriter) {
                super.getWriter();
            }
            usingWriter = true;
            return output.getWriter();
        }

        public String toString()
        {
            return output.toString();
        }
    }

    public void init(FilterConfig filterConfig) throws ServletException
    {
        this.filterConfig = filterConfig;
    }

    public void destroy()
    {
        filterConfig = null;
    }

    public void doFilter(
            ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException
    {
        CharResponseWrapper wrappedResponse = new CharResponseWrapper(
                (HttpServletResponse)response);

        chain.doFilter(request, wrappedResponse);
        byte[] bytes = wrappedResponse.getByteArray();

        if (wrappedResponse.getContentType().contains("text/html")) {
            String out = new String(bytes);
            // DO YOUR REPLACEMENTS HERE
            out = out.replace("</head>", "WTF</head>");
            response.getOutputStream().write(out.getBytes());
        }
        else {
            response.getOutputStream().write(bytes);
        }
    }
}
Forge_7
  • 1,839
  • 2
  • 20
  • 19
matt snider
  • 4,013
  • 4
  • 24
  • 39
  • 19
    One important note: you need to use HttpServletResponse.setContentLength method to update response header according to your response body change, otherwise mismatch may lead to unknown behavior on client. – snowindy May 12 '15 at 07:34
  • @snowindy that info should definitely be added to the main answer – tanenbring Jan 20 '16 at 23:05
  • 2
    @snowindy very important note! However, I had to adjust the content length _before_ making the actual replacement (i.e. before calling response.getOutputStream().write()), otherwise it would have simply be ignored. – Ciri Jan 21 '16 at 10:39
  • @Serban obviously :) headers can't be written after response sent to client. – snowindy Jan 21 '16 at 14:03
  • @snowindy took me half a day to figure that one out. So the response is basically sent when I make the call to the write() method of the output stream? I thought the content is being buffered and sent on its merry way only after returning from the doFilter method. – Ciri Jan 21 '16 at 21:19
  • 1
    @Serban It is buffered up to some certain value, rather small one but configurable per servlet container. When buffer overflow happens a response starting chunks automatically gets sent to client. – snowindy Jan 22 '16 at 01:45
  • 2
    This solution is no longer usable (at leas in this posted form). ByteArrayServletStream requires implementation of two abstract methods and init function can no longer be owerrriden since it is final. – Pastx Oct 09 '18 at 07:28
18

You will need to implement HttpServletResponseWrapper to modify the response. See this document The Essentials of Filters, it has an example that transforms the response, which is more than what you want

Edit

I have tried a simple Servlet with response filter and it worked perfectly. The Servlet output the string Test and the response filter append to it the string filtered and finally when I run from the browser I get the response Test filtered which is what you are trying to achieve.

I did run the below code on Apache Tomcat 7 and it is working without exceptions.

Servlet:

protected void doGet(HttpServletRequest request,
        HttpServletResponse response) throws ServletException, IOException {

   response.getWriter().println("Test");

}

Filter:

public void doFilter(ServletRequest request, ServletResponse response,
        FilterChain chain) throws IOException, ServletException {

    System.out.println("BEFORE filter");
    PrintWriter out = response.getWriter();
    CharResponseWrapper responseWrapper = new CharResponseWrapper(
            (HttpServletResponse) response);

    chain.doFilter(request, responseWrapper);

    String servletResponse = new String(responseWrapper.toString());

    out.write(servletResponse + " filtered"); // Here you can change the response


    System.out.println("AFTER filter, original response: "
            + servletResponse);

}

CharResponseWrapper (exactly as the article)

public class CharResponseWrapper extends HttpServletResponseWrapper {
    private CharArrayWriter output;

    public String toString() {
        return output.toString();
    }

    public CharResponseWrapper(HttpServletResponse response) {
        super(response);
        output = new CharArrayWriter();
    }

    public PrintWriter getWriter() {
        return new PrintWriter(output);
    }
}

web.xml

<servlet>
    <servlet-name>TestServlet</servlet-name>
    <servlet-class>TestServlet</servlet-class>
</servlet>
<servlet-mapping>
    <servlet-name>TestServlet</servlet-name>
    <url-pattern>/TestServlet</url-pattern>
</servlet-mapping>

<filter>
    <filter-name>TestFilter</filter-name>
    <filter-class>MyFilter</filter-class>
</filter>

<filter-mapping>
    <filter-name>TestFilter</filter-name>
    <url-pattern>/TestServlet/*</url-pattern>
</filter-mapping>
shreyas
  • 700
  • 8
  • 10
iTech
  • 18,192
  • 4
  • 57
  • 80
  • 2
    Following that example, I get this exception - java.lang.IllegalStateException: getWriter() has already been called for this response – matt snider Feb 06 '13 at 19:48
  • It would be better if you can post the code and the stacktrace of the exception. – iTech Feb 06 '13 at 20:24
  • Added the wrapping of the response. My CharResponseWrapper is an exact copy of the one in The Essentials of Filters link. – matt snider Feb 06 '13 at 22:01
  • 3
    I'm getting the exception at chain.doFilter(wrappedRequest, wrappedResponse); It looks like the underlying code is calling getOutputStream(), which uses the same writer. – matt snider Feb 06 '13 at 22:43
  • I tested it with a simple Servlet and it is working fine, see my updated answer – iTech Feb 06 '13 at 23:35
4

The iTech answer worked partially for me and this is based on that response..

But you must notice, that it seems some web servers (and AppEngine Standard) closes the outputStream after the first call to chain.doFilter inside a Filter..

So when you need to write on the pre-saved PrintWritter, the stream is closed and you get a blank screen. (I didn't recieve even an error to realise what was happening).

So the solution for me was creating a "dummy" ServletOutputStream and returning back in the getOutputStream method of my ResponseWrapper.

These changes plus the solution of iTech allowed me to insert a fully rendered jsp response in html inside a json response (properly escaping conflictive characters like quotes).

This is my code:

Myfilter

@WebFilter({"/json/*"})    
public class Myfilter implements Filter {
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {}
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        //Save original writer
        PrintWriter out = response.getWriter(); 
        //Generate a response wrapper with a different output stream
        ResponseWrapper responseWrapper = new ResponseWrapper((HttpServletResponse) response);
        //Process all in the chain (=get the jsp response..)
        chain.doFilter(request, responseWrapper);
        //Parse the response
        out.write("BEFORE"+responseWrapper.toString()+"AFTER"); //Just + for clear display, better use a StringUtils.concat
    }
    @Override
    public void destroy() {}
}

My ResponseWrapper:

public class ResponseWrapper extends HttpServletResponseWrapper {
    private StringWriter output;
    public String toString() {
        return output.toString();
    }
    public ResponseWrapper(HttpServletResponse response) {
        super(response);
        //This creates a new writer to prevent the old one to be closed
        output = new StringWriter();
    }
    public PrintWriter getWriter() {
        return new PrintWriter(output,false);
    }
    @Override
    public ServletOutputStream getOutputStream() throws IOException {
        //This is the magic to prevent closing stream, create a "virtual" stream that does nothing..
        return new ServletOutputStream() {
            @Override
            public void write(int b) throws IOException {}
            @Override
            public void setWriteListener(WriteListener writeListener) {}
            @Override
            public boolean isReady() {
                return true;
            }
        };
    }
}
Xarly CR
  • 41
  • 2
1

Great! but please update content-length,

        String out = new String(bytes);
        // DO YOUR REPLACEMENTS HERE
        out = out.replace("</head>", "WTF</head>");
        response.setContentLength(out.length());
        response.getOutputStream().write(out.getBytes());
Adam Gong
  • 11
  • 1