-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA1

All,

After becoming intrigued over Youssef's recent post (see the "Record and
simulate a web app" thread from the 18th), I wrote come code.

Unfortunately, I wasn't able to get a complete response captured because
of the timing of various events during the request (in summary: the
HttpProcessor does some stuff at the last minute that we can't capture
using application code). So, I've given up on writing the
response-capture part of the code, at least for now.

Since I wrote it, I figured I'd publish it, and here is as good a place
as any (attached, hopefully). I'm sure there are some folks on the list
who would like to see how request wrappers should be written and you can
even see a few other neat things like:

- - how to wrap a servlet input stream and reader
- - how to make a copy of streaming data
- - how to figure out which request parameters are GET versus POST

I'd love comments if anyone has them.

Enjoy!
- -chris
-----BEGIN PGP SIGNATURE-----
Version: GnuPG v1.4.9 (MingW32)
Comment: Using GnuPG with Mozilla - http://enigmail.mozdev.org

iEYEARECAAYFAkme2ZcACgkQ9CaO5/Lv0PB8QQCgwGJhkc5IPr9sccpeLJZbQ7mL
hyUAnikwihV2dZsG4B1LpRA6Iz4ZV7yW
=Kug3
-----END PGP SIGNATURE-----
//
// License:
// Copyright (c) 2009 Christopher Schultz
// Free to use for any purpose for no fee. No guarantees. Credits and 
shout-outs are appreciated.
//
package net.christopherschultz.http;

import java.io.IOException;
import java.io.PrintStream;

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.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

// RequestWrapper
import java.io.InputStream;
import java.io.OutputStream;
import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
import java.io.Writer;
import java.io.StringWriter;
import java.io.BufferedReader;
import java.io.UnsupportedEncodingException;
import java.nio.CharBuffer;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.Map;
import java.util.HashMap;
import java.net.URLEncoder;
import javax.servlet.http.HttpServletRequestWrapper;
import javax.servlet.ServletInputStream;
import javax.servlet.ServletOutputStream;

/**
 * HttpConversationRecorderFilter
 *
 * Dumps the complete HTTP request to stdout after the request has
 * been processed.
 *
 * This should be the first filter in your filter chain so you can
 * capture all of the request body.
 *
 * @author Chris Schultz (ch...@christopherschultz.net)
 * @version 2009-02-20
 */
public class HttpConversationRecorderFilter
    implements Filter
{
    private static final String DEFAULT_CHARSET = "ISO-8859-1";
    private static final String DEFAULT_URI_CHARSET = "UTF-8";

    private String _defaultCharset = DEFAULT_CHARSET;
    private String _uriCharset = DEFAULT_URI_CHARSET;

    public void init(FilterConfig config)
    { 
        String charset = config.getInitParameter("default-charset");

        if(null != charset)
            _defaultCharset = charset;

        charset = config.getInitParameter("default-uri-charset");

        if(null != charset)
            _uriCharset = charset;
   }

    public void destroy()
    {
    }

    public void doFilter(ServletRequest request,
                         ServletResponse response,
                         FilterChain chain)
        throws IOException, ServletException
    {
        if(request instanceof HttpServletRequest
           && response instanceof HttpServletResponse)
        {
            Throwable ex = null;
            HttpRequestRecorderWrapper req;

            req = wrapRequest((HttpServletRequest)request);

            try
            {
                chain.doFilter(req, response);
            }
            catch (Exception e)
            {
                // Save the exception for later. We still want to record
                // the request and possibly part of the response.
                //
                // Don't worry about catching java.lang.Error.
                ex = e;
            }

            // TODO: Write the request to a file?
            req.dumpRequest(System.out);
            req.free();

            // Re-throw any exception that might have occurred
            if(null != ex)
            {
                if(ex instanceof ServletException)
                    throw (ServletException)ex;
                if(ex instanceof IOException)
                    throw (IOException)ex;
                if(ex instanceof RuntimeException)
                    throw (RuntimeException)ex;
                if(ex instanceof Error)
                    throw (Error)ex;

                // Should never get here
                throw new ServletException("Unexpected exception", ex);
            }
        }
        else
        {
            chain.doFilter(request, response);
        }
    }

    private HttpRequestRecorderWrapper wrapRequest(HttpServletRequest request)
    {
        return new HttpRequestRecorderWrapper(request);
    }

    private class HttpRequestRecorderWrapper
        extends HttpServletRequestWrapper
    {
        private ServletTeeInputStream _inputStream;
        private ByteArrayOutputStream _inputStreamBuffer;

        private BufferedTeeReader _inputReader;
        private StringWriter _inputReaderBuffer;

        private boolean _usingReader;

        public HttpRequestRecorderWrapper(HttpServletRequest request)
        {
            super(request);
        }

        public boolean usingReader()
        {
            return _usingReader;
        }

        public BufferedReader getReader()
            throws IOException
        {
            if(null == _inputReader)
            {
                BufferedReader in = super.getReader();

                _inputReaderBuffer = new StringWriter();

                _inputReader = new BufferedTeeReader(in, _inputReaderBuffer, 
true);

                _usingReader = true;
            }

            return _inputReader;
        }

        public ServletInputStream getInputStream()
            throws IOException
        {
            if(null == _inputStream)
            {
                ServletInputStream in = super.getInputStream();

                _inputStreamBuffer = new ByteArrayOutputStream();

                _inputStream = new ServletTeeInputStream(in, 
_inputStreamBuffer, true);
            }

            return _inputStream;
        }

        public byte[] getRequestBody()
        {
            if(usingReader())
            {
                String charset = getCharacterEncoding();

                if(null == charset)
                    charset = _defaultCharset;

                try
                {
                    return _inputReaderBuffer.toString().getBytes(charset);
                }
                catch (UnsupportedEncodingException uee)
                {
                    throw new InternalError("Unrecognized character set: " + 
charset);
                }
            }
            else
            {
                return _inputStreamBuffer.toByteArray();
            }
        }

        public void free()
        {
            _inputStream = null;
            _inputReader = null;

            if(null != _inputStreamBuffer)
            {
                _inputStreamBuffer.reset(); // empty buffer
                _inputStreamBuffer = null;
            }

            if(null != _inputReaderBuffer)
            {
                _inputReaderBuffer.getBuffer().setLength(0); // empty buffer
                _inputReaderBuffer = null;
            }
        }

        public void dumpRequest(PrintStream out)
            throws IOException
        {
            out.println("======> dumping HTTP request <=============");

            //
            // Wow, the first line of an HTTP request is tough to reconstruct
            //
            out.print(getMethod());
            out.print(" ");
            out.print(getRequestURI());

            String query = getQueryString();
            if(null != query)
            {
                out.print("?");
                out.print(query);
            }

            String protocol = getProtocol();
            if(null != protocol)
            {
                out.print(" ");
                out.print(protocol);
            }

            out.println();

            //
            // Now, dump all the headers
            //
            for(Enumeration i=getHeaderNames(); i.hasMoreElements(); )
            {
                String headerName = (String)i.nextElement();

                out.print(headerName);
                out.print(": ");

                for(Enumeration j = getHeaders(headerName); 
j.hasMoreElements(); )
                {
                    out.print((String)j.nextElement());

                    if(j.hasMoreElements())
                        out.print(",");
                }

                out.println();
            }

            // Print the end-of-header newline
            out.println();

            //
            // If Content-Type is application/x-www-form-urlencoded
            // then we have to reconstruct the parameters from the parameter
            // map.
            //

            // Trigger the fetching of request parameters
            getParameter("");

            // See Servlet Specification SRV.3.1.1 for details.
            if("POST".equals(getMethod())
               && "application/x-www-form-urlencoded".equals(getContentType()))
            {
                // In this case, the servlet container is supposed to
                // consume the request body and parse it as POST form data.
                //
                // We will need to reconstruct the POST string using all
                // parameters read plus the query string to figure out which
                // came from where.

                String postQueryString = RequestUtils.getPostQueryString(this, 
_uriCharset);

                out.print(postQueryString);
            }
            else
            {
                //
                // Consume any remaining request body
                //
                if(usingReader())
                {
                    int leftover = 0;

                    // Stealthily avoid reading from a closed reader
                    if(!_inputReader.isClosed())
                    {
                        try
                        {
                            while(-1 != _inputReader.read())
                                leftover++;

                            _inputReader.close();
                        }
                        catch (IOException ioe)
                        {
                            System.err.println("WARN: reader already closed?");
                        }
                    }

                    if(0 < leftover)
                        System.out.println("====> Filter read " + leftover + " 
unread characters from the request reader");
                }
                else
                {
                    int leftover = 0;

                    // Stealthily avoid reading from a closed reader
                    if(!_inputStream.isClosed())
                    {
                        try
                        {
                            while(-1 != _inputStream.read())
                                leftover++;

                            _inputStream.close();
                        }
                        catch (IOException ioe)
                        {
                            System.err.println("WARN: stream already closed? " 
+ ioe);
                        }
                    }

                    if(0 < leftover)
                        System.out.println("====> Filter read " + leftover + " 
unread bytes from the request stream");
                }

                //
                // Dump the request body
                //
                byte[] body = getRequestBody();

                String charset = getCharacterEncoding();
                if(null == charset)
                    charset = _defaultCharset;

                if(null != body && 0 < body.length)
                    out.write(body);
            }

            out.flush();
        }
    }

    private static class ServletTeeInputStream
        extends ServletInputStream
    {
        private InputStream _in;
        private OutputStream _out;
        private boolean _close;
        private boolean _isClosed = false;

        public ServletTeeInputStream(InputStream in,
                                     OutputStream out,
                                     boolean close)
        {
            _in = in;
            _out = out;
            _close = close;
        }

        public int available() throws IOException
        { return _in.available(); }
        public void close() throws IOException
        {
            try
            {
                _in.close();
            }
            finally
            {
                if(_close)
                    _out.close();
            }

            _isClosed = true;
        }

        public void mark(int readlimit) { _in.mark(readlimit); }
        public boolean markSupported() { return _in.markSupported(); }
        public void reset() throws IOException
        { _in.reset(); }

        public int read() throws IOException
        {
            int ch = _in.read();

            if(-1 != ch)
                _out.write(ch);

            return ch;
        }

        public int read(byte[] b) throws IOException
        {
            return this.read(b, 0, b.length);
        }

        public int read(byte[] b, int off, int len) throws IOException
        {
            int n = _in.read(b,off,len);

            if(-1 != n)
                _out.write(b, off, n);

            return n;
        }

        public long skip(long want) throws IOException
        {
            long total = 0;
            byte[] buffer = new byte[1024];

            while(0 < want)
            {
                int n;

                int wantThisTime = (1024 < want ? 1024 : (int)want);

                if(-1 != (n = _in.read(buffer, 0, wantThisTime)))
                {
                    _out.write(buffer, 0, n);

                    total += n;
                    want -= n;
                }
            }

            return total;
        }

        public boolean isClosed()
        {
            return _isClosed;
        }
    }

    public static class BufferedTeeReader
        extends BufferedReader
    {
        private Writer _out;
        private boolean _close;
        private boolean _isClosed = false;

        // TODO: Allow this to be overridden
        private String _lineSeparator = System.getProperty("line.separator");

        public BufferedTeeReader(BufferedReader in, Writer out, boolean close)
        {
            super(in);

            _out = out;
            _close = close;
        }

        public int read() throws IOException
        {
            int c = super.read();

            if(-1 != c)
                _out.write(c);

            return c;
        }

        public int read(char[] buffer) throws IOException
        {
            // Calling super.read(buffer) results in
            // this.read(buffer, 0, buffer.length) being called,
            // which will double the amount of output generated.

//             int n = super.read(buffer);
//
//             if(-1 != n)
//                 _out.write(buffer, 0, n);
//
//             return n;

            return this.read(buffer, 0, buffer.length);
        }

        public int read(char[] buffer, int off, int len) throws IOException
        {
            int n = super.read(buffer, off, len);

            if(-1 != n)
                _out.write(buffer, off, n);

            return n;
        }

        public int read(CharBuffer buffer) throws IOException
        {
            int n = super.read(buffer);

            if(-1 != n)
                _out.append(buffer, 0, n);

            return n;
        }

        public String readLine() throws IOException
        {
            String line = super.readLine();

            if(null != line)
            {
                _out.write(line);
                _out.write(_lineSeparator);
            }

            return line;
        }

        public long skip(long want) throws IOException
        {
            long total = 0;
            char[] buffer = new char[1024];

            while(0 < want)
            {
                int n;

                int wantThisTime = (1024 < want ? 1024 : (int)want);

                if(-1 != (n = super.read(buffer, 0, wantThisTime)))
                {
                    _out.write(buffer, 0, n);

                    total += n;
                    want -= n;
                }
            }

            return total;
        }

        public boolean ready() throws IOException
        { return super.ready(); }

        // TODO: return false?
        public boolean markSupported() { return super.markSupported(); }

        public void mark(int readAheadLimit) throws IOException
        { super.mark(readAheadLimit); }

        public void reset() throws IOException
        { super.reset(); }

        public void close() throws IOException
        {
            try
            {
                super.close();
            }
            finally
            {
                if(_close)
                    _out.close();
            }

            _isClosed = true;
        }

        public boolean isClosed()
        {
            return _isClosed;
        }
    }

    public static class RequestUtils
    {
        public static String getPostQueryString(HttpServletRequest req)
        {
            try
            {
                return getPostQueryString(req, "UTF-8");
            }
            catch (UnsupportedEncodingException uee)
            {
                throw new InternalError("UTF-8 charset is suddenly 
unsupported");
            }
        }

        public static String getPostQueryString(HttpServletRequest req,
                                                String uriCharset)
            throws UnsupportedEncodingException
        {
            if(!("POST".equals(req.getMethod())
                 && 
"application/x-www-form-urlencoded".equals(req.getContentType())))
                throw new IllegalArgumentException("Request must be both POST 
and application/x-www-urlencoded");

            StringBuffer sb = new StringBuffer();

            Map queryParameterCounts
                = countQueryParameters(req.getQueryString());

            boolean first = true;

            for(Iterator i=req.getParameterMap().entrySet().iterator();
                i.hasNext(); )
            {
                Map.Entry entry = (Map.Entry)i.next();

                String paramName = (String)entry.getKey();
                String[] values = (String[])entry.getValue();

                int skip = 0;
                Integer queryCount = (Integer)queryParameterCounts
                    .get(paramName);

                if(null != queryCount)
                    skip += queryCount.intValue();

                if(values.length > skip)
                {
                    for(int j=skip; j<values.length; ++j)
                    {
                        if(first)
                            first = false;
                        else
                            sb.append('&');

                        sb.append(URLEncoder.encode(paramName, uriCharset))
                            .append('=')
                            .append(URLEncoder.encode(values[j], uriCharset));
                    }
                }
            }

            return sb.toString();
        }

        private static Map countQueryParameters(String query)
        {
            HashMap counts = new HashMap();

            if(null != query)
            {
                String[] paramNames = query.split("=[^&]*(&)?");

                for(int i=0; i<paramNames.length; ++i)
                {
                    Integer count = (Integer)counts.get(paramNames[i]);

                    if(null == count)
                        count = new Integer(1);
                    else
                        count = new Integer(count.intValue() + 1);

                    counts.put(paramNames[i], count);
                }
            }

            return counts;
        }
    }
}
---------------------------------------------------------------------
To unsubscribe, e-mail: users-unsubscr...@tomcat.apache.org
For additional commands, e-mail: users-h...@tomcat.apache.org

Reply via email to