How to change the HTTP response content length header in Java Filter

陌路散爱 提交于 2019-12-02 07:13:32

Here's a Java sample that does this. It stores the response in a temporary file, which is deleted when the response completes. It is intended for serving static files only at this point, as it temporarily caches files by url path. Note that it stores the length of the file in memory, by url path, and uses that on subsequent requests to avoid I/O.

Note that if something writes to the response body before your filter gets called, then your Content-Length header will be ignored. This header needs to be set BEFORE any content is written out, so if you're finding it's not getting added, that's why.

Use it like so:

new ContentLengthFilter("contentLengthFilter_", new File("/tmp/fileCache"))

ContentLengthFilter.java

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponseWrapper;

import org.apache.commons.io.IOUtils;

/*
 * This filter adds a "Content-Length" header to all responses.
 * It does this by caching the response to a temporary file, which
 * is deleted immediately after the response completes.
 * 
 * It caches the size of the file to a hashmap, and uses that for
 * any matching requests that it encounters in the future, to decrease
 * the amount of I/O required. So the first request to a file is the
 * only one that does file I/O, the rest use the cache.
 * 
 * Note that it ignores queryString params when comparing responses.
 * If this is important to you, then you should override the getFilenameForUrl
 * method as required.
 */
public class ContentLengthFilter implements Filter
{
    protected ServletContext servletContext;
    protected final File tempDir;
    protected final Map<String, Long> contentLengths = new HashMap<String, Long>();
    protected final String filenamePrefix;

    public static final String CONTENT_LENGTH = "Content-Length";

    public ContentLengthFilter(String filenamePrefix, File tempDir)
    {
        this.filenamePrefix = filenamePrefix;

        this.tempDir = tempDir;
        this.tempDir.mkdirs();
    }

    private final static class BufferingOutputStreamFile extends ServletOutputStream
    {
        private FileOutputStream baos;

        public BufferingOutputStreamFile(File file)
        {
            try
            {
                baos = new FileOutputStream(file);
            }
            catch (FileNotFoundException e)
            {
                baos = null;
            }
        }

        @Override
        public void write(int b) throws IOException
        {
            baos.write(b);
        }

        @Override
        public void write(byte[] b) throws IOException
        {
            baos.write(b);
        }

        @Override
        public void write(byte[] b, int off, int len) throws IOException
        {
            baos.write(b, off, len);
        }
    }

    private final static class BufferingHttpServletResponse extends HttpServletResponseWrapper
    {
        private enum StreamType
        {
            OUTPUT_STREAM, WRITER
        }

        private final HttpServletResponse httpResponse;

        private StreamType acquired;
        private PrintWriter writer;
        private ServletOutputStream outputStream;
        private boolean savedResponseToTmpFile;
        private File file;

        public BufferingHttpServletResponse(HttpServletResponse response, File file)
        {
            super(response);
            this.file = file;
            httpResponse = response;
        }

        @Override
        public ServletOutputStream getOutputStream() throws IOException
        {
            if (acquired == StreamType.WRITER)
                throw new IllegalStateException("Character stream already acquired.");

            if (outputStream != null)
                return outputStream;

            if (alreadyHasContentLength())
            {
                outputStream = super.getOutputStream();
            }
            else
            {
                outputStream = new BufferingOutputStreamFile(file);
                savedResponseToTmpFile = true;
            }

            acquired = StreamType.OUTPUT_STREAM;
            return outputStream;
        }

        @Override
        public PrintWriter getWriter() throws IOException
        {
            if (acquired == StreamType.OUTPUT_STREAM)
                throw new IllegalStateException("Binary stream already acquired.");

            if (writer != null)
                return writer;

            if (alreadyHasContentLength())
            {
                writer = super.getWriter();
            }
            else
            {
                writer = new PrintWriter(new OutputStreamWriter(getOutputStream(), getCharacterEncoding()), false);
            }

            acquired = StreamType.WRITER;

            return writer;
        }

        private boolean alreadyHasContentLength()
        {
            return super.containsHeader(CONTENT_LENGTH);
        }

        public void copyTmpFileToOutput() throws IOException
        {
            if (!savedResponseToTmpFile)
                throw new IllegalStateException("Not saving response to temporary file.");

            // Get the file, and write it to the output stream
            FileInputStream fis = new FileInputStream(file);
            ServletOutputStream sos;
            try
            {
                long contentLength = file.length();
                httpResponse.setHeader(CONTENT_LENGTH, contentLength + "");

                sos = httpResponse.getOutputStream();
                IOUtils.copy(fis, sos);
            }
            finally
            {
                IOUtils.closeQuietly(fis);
                fis.close();
            }
        }
    }

    protected String getFilenameForUrl(HttpServletRequest request)
    {
        String result = filenamePrefix + request.getRequestURI();

        result = hashString(result);

        return result;
    }

    // Simple way to make a unique filename for an url. Note that
    // there could be collisions of course using this approach,
    // so use something better (e.g. MD5) if you want to avoid
    // collisions entirely. This approach is more readable, and
    // is why it's used.
    protected String hashString(String input)
    {
        String result = input.replaceAll("[^0-9A-Za-z]", "_");

        return result;
    }

    public void log(Object o)
    {
        System.out.println(o);
    }

    protected boolean setContentLengthUsingMap(String key, FilterChain chain, HttpServletResponse response) throws IOException, ServletException
    {
        Long contentLength = contentLengths.get(key);

        if (contentLength == null)
            return false;

        response.setHeader(CONTENT_LENGTH, contentLength + "");
        log("content-length from map:" + key + ", length:" + contentLength + ", entries:" + contentLengths.size());

        return true;
    }

    protected void writeFileToResponse(String filenameFromUrl, HttpServletRequest request, File file, BufferingHttpServletResponse wrappedResponse) throws IOException
    {
        Long contentLength = file.length();

        if (contentLength > 0)
        {
            log("Response written to temporary_file=" + filenameFromUrl + ", contentLength=" + contentLength);
            contentLengths.put(filenameFromUrl, contentLength);
        }
        else
        {
            log("Skipping caching response for temporary_file=" + filenameFromUrl + ", contentLength=" + contentLength);
        }

        wrappedResponse.copyTmpFileToOutput();

        String contentType = servletContext.getMimeType(request.getRequestURI());
        wrappedResponse.setContentType(contentType);
    }

    protected void deleteTempFileIfExists(File file)
    {
        if (file.exists())
        {
            try
            {
                file.delete();
            }
            catch (Exception e)
            {
                log(e);
            }
        }
    }

    public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException
    {
        final HttpServletResponse response = (HttpServletResponse) resp;
        final HttpServletRequest request = (HttpServletRequest) req;
        final String filenameFromUrl = getFilenameForUrl(request);

        // If we've downloaded this file before, we saved it's
        // size, so write that out and skip caching the file locally
        // as it's not required.
        if (setContentLengthUsingMap(filenameFromUrl, chain, response))
        {
            chain.doFilter(request, response);
            return;
        }

        // We've never seen this request before, so download the response
        // to a temporary file, then write that file and it's
        // file size to the response.
        final File file = new File(tempDir, filenameFromUrl + UUID.randomUUID());
        try
        {
            final BufferingHttpServletResponse wrappedResponse = new BufferingHttpServletResponse(response, file);

            chain.doFilter(req, wrappedResponse);

            if (wrappedResponse.savedResponseToTmpFile)
            {
                writeFileToResponse(filenameFromUrl, request, file, wrappedResponse);
            }
        }
        finally
        {
            deleteTempFileIfExists(file);
        }
    }

    public void destroy()
    {
        this.servletContext = null;
    }

    public void init(FilterConfig config) throws ServletException
    {
        this.servletContext = config.getServletContext();
    }
}

Another great sample filter for doing this, that can be used standalone from the project, is this ContentLengthFilter.java from the Carrot2 project on github. Note that it works great, but stores each file in memory when writing it out, so if you have large files, you'll need to consider a different approach.

This uses a response wrapper with a byte stream to solve the issue, so this also ensures that Transfer-Encoding: Chunked doesn't get set by some other Filter/code in the filter chain, and override your Content-Length header when it's set. You can verify that by testing this with larger files, as they'd normally be chunked in the response.

I'm going to copy the contents of the file here as well, to ensure it doesn't become a broken link.

/*
 * Carrot2 project.
 *
 * Copyright (C) 2002-2010, Dawid Weiss, Stanisław Osiński.
 * All rights reserved.
 *
 * Refer to the full license file "carrot2.LICENSE"
 * in the root folder of the repository checkout or at:
 * http://www.carrot2.org/carrot2.LICENSE
 */

package org.carrot2.webapp;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponseWrapper;

/**
 * Buffer the output from filters below and set accurate <code>Content-Length</code>
 * header. This header is required by flash, among others, to display progress
 * information.
 */
public class ContentLengthFilter implements Filter
{
    private final static class BufferingOutputStream extends ServletOutputStream
    {
        private final ByteArrayOutputStream baos = new ByteArrayOutputStream();

        @Override
        public void write(int b) throws IOException
        {
            baos.write(b);
        }

        @Override
        public void write(byte [] b) throws IOException
        {
            baos.write(b);
        }

        @Override
        public void write(byte [] b, int off, int len) throws IOException
        {
            baos.write(b, off, len);
        }
    }

    private final static class BufferingHttpServletResponse extends
        HttpServletResponseWrapper
    {
        private enum StreamType
        {
            OUTPUT_STREAM,
            WRITER
        }

        private final HttpServletResponse httpResponse;

        private StreamType acquired;
        private PrintWriter writer;
        private ServletOutputStream outputStream;
        private boolean buffering;

        public BufferingHttpServletResponse(HttpServletResponse response)
        {
            super(response);
            httpResponse = response;
        }

        @Override
        public ServletOutputStream getOutputStream() throws IOException
        {
            if (acquired == StreamType.WRITER)
                throw new IllegalStateException("Character stream already acquired.");

            if (outputStream != null)
                return outputStream;

            if (hasContentLength())
            {
                outputStream = super.getOutputStream();
            }
            else
            {
                outputStream = new BufferingOutputStream();
                buffering = true;
            }

            acquired = StreamType.OUTPUT_STREAM;
            return outputStream;
        }

        @Override
        public PrintWriter getWriter() throws IOException
        {
            if (acquired == StreamType.OUTPUT_STREAM)
                throw new IllegalStateException("Binary stream already acquired.");

            if (writer != null)
                return writer;

            if (hasContentLength())
            {
                writer = super.getWriter();
            }
            else
            {
                writer = new PrintWriter(new OutputStreamWriter(
                    getOutputStream(), getCharacterEncoding()), false);
            }

            acquired = StreamType.WRITER;
            return writer;
        }

        /**
         * Returns <code>true</code> if the user set <code>Content-Length</code>
         * explicitly.
         */
        private boolean hasContentLength()
        {
            return super.containsHeader("Content-Length");
        }

        /**
         * Push out the buffered data.
         */
        public void pushBuffer() throws IOException
        {
            if (!buffering)
                throw new IllegalStateException("Not buffering.");

            BufferingOutputStream bufferedStream = 
                (BufferingOutputStream) outputStream;

            byte [] buffer = bufferedStream.baos.toByteArray();
            httpResponse.setContentLength(buffer.length);
            httpResponse.getOutputStream().write(buffer);
        }
    }

    public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain)
        throws IOException, ServletException
    {
        final HttpServletResponse response = (HttpServletResponse) resp;
        final BufferingHttpServletResponse wrapped = 
            new BufferingHttpServletResponse(response);

        chain.doFilter(req, wrapped);

        if (wrapped.buffering)
        {
            wrapped.pushBuffer();
        }
    }

    public void destroy()
    {
        // Empty
    }

    public void init(FilterConfig config) throws ServletException
    {
        // Empty
    }
}
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!