Search code examples
javajerseyjersey-2.0

Jersey - Obtain the contents of an OutputStream in an Interceptor before calling context.proceed()


Using an Interceptor in Jersey I can manipulate the Output, however, I also want to add a Header to the response which value is calculated from the result of the output.

@Sha256Sum
public class Sha256SumInterceptor implements WriterInterceptor {

    public static final String SHA256_HASH_HEADER_NAME = "SHA256-SUM";

    @Override
    public void aroundWriteTo(WriterInterceptorContext context) throws IOException, WebApplicationException {
        // Retrieve the OutputStream, read the contents and calculate the hashsum.
        // Set the header value in context.
        context.proceed();
    }
}

However, the issue is that when I finally have read the entire stream, I'm unable to set the headers as when context.proceed is called and the contents written (and thus enabling me to do anything with it) I can no longer set the header.

My question in short: How do I capture the entire stream output as a byte[], calculate a result from the array of bytes and finally set the header in the response to the computed result? I do not want to deplete the output stream.


Solution

  • If you've ever worked with an AOP framework or even CDI interceptors, you'll have worked with the concept of Around-Advice or Around-Invoke, respectively. You can perform operations before and after the invocation of the advised/intercepted method. context.proceed() works the same way; it's the method invocation (or more precisely the MessageBodyWriter doing it's writing). We can perform some operations before the MessageBodyWriter does it's job, call proceed() to let the writer do it's work, then we can do some more work.

    With that said, here are the steps you can take:

    1. Hold onto the old OutputStream from the context, with context.getOutputStream()
    2. Create a ByteArrayOutputStream and set that as the OutputStream on the context, with context.setOutputStream(baos)
    3. Call context.proceed(). What this does is have the MessageBodyWriter write to the ByteArrayOutputStream.
    4. Get the byte[] from the ByteArrayOutputStream with baos.toByteArray()
    5. Checksum the byte[] and set the header
    6. Write the byte[] to the old OutputStream.
    7. Finally set the OutputStream on the context to the old OutputStream.

    Here's the basic implementation (tested and works as expected)

    @Provider
    public class ChecksumInterceptor implements WriterInterceptor {
    
        @Override
        public void aroundWriteTo(WriterInterceptorContext context)
                throws IOException, WebApplicationException {
    
            OutputStream old = context.getOutputStream();
            ByteArrayOutputStream buffer = new ByteArrayOutputStream();
            try {
    
                context.setOutputStream(buffer);
                // let MessageBodyWriter do it's job
                context.proceed();
    
                // get bytes
                byte[] entity = buffer.toByteArray();
    
                String checksum = ChecksumUtil.createChecksum(entity);
                context.getHeaders().putSingle("X-Checksum", checksum);
    
                old.write(entity);
            } finally {
                context.setOutputStream(old);
            }
        }
    }