package org.frameworkset.web.filter; import java.io.IOException; import java.io.InputStream; import java.io.PrintWriter; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.ServletOutputStream; import javax.servlet.ServletRequest; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.frameworkset.util.Assert; import org.frameworkset.util.ClassUtils; import org.frameworkset.util.DigestUtils; import org.frameworkset.util.annotations.HttpMethod; import org.frameworkset.web.util.ContentCachingResponseWrapper; import org.frameworkset.web.util.WebUtils; /** * {@link javax.servlet.Filter} that generates an {@code ETag} value based on the * content on the response. This ETag is compared to the {@code If-None-Match} * header of the request. If these headers are equal, the response content is * not sent, but rather a {@code 304 "Not Modified"} status instead. * * <p>Since the ETag is based on the response content, the response * (e.g. a {@link org.frameworkset.web.servlet.View}) is still rendered. * As such, this filter only saves bandwidth, not server performance. * * @author Arjen Poutsma * @author Rossen Stoyanchev * @author Juergen Hoeller * @since 3.0 */ public class ShallowEtagHeaderFilter extends OncePerRequestFilter { private static final String HEADER_ETAG = "ETag"; private static final String HEADER_IF_NONE_MATCH = "If-None-Match"; private static final String HEADER_CACHE_CONTROL = "Cache-Control"; private static final String DIRECTIVE_NO_STORE = "no-store"; private static final String STREAMING_ATTRIBUTE = ShallowEtagHeaderFilter.class.getName() + ".STREAMING"; /** Checking for Servlet 3.0+ HttpServletResponse.getHeader(String) */ private static final boolean servlet3Present = ClassUtils.hasMethod(HttpServletResponse.class, "getHeader", String.class); /** * The default value is "false" so that the filter may delay the generation of * an ETag until the last asynchronously dispatched thread. */ @Override protected boolean shouldNotFilterAsyncDispatch() { return false; } @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { HttpServletResponse responseToUse = response; if (!isAsyncDispatch(request) && !(response instanceof ContentCachingResponseWrapper)) { responseToUse = new HttpStreamingAwareContentCachingResponseWrapper(response, request); } filterChain.doFilter(request, responseToUse); if (!isAsyncStarted(request) && !isContentCachingDisabled(request)) { updateResponse(request, responseToUse); } } private void updateResponse(HttpServletRequest request, HttpServletResponse response) throws IOException { ContentCachingResponseWrapper responseWrapper = WebUtils.getNativeResponse(response, ContentCachingResponseWrapper.class); Assert.notNull(responseWrapper, "ContentCachingResponseWrapper not found"); HttpServletResponse rawResponse = (HttpServletResponse) responseWrapper.getResponse(); int statusCode = responseWrapper.getStatusCode(); if (rawResponse.isCommitted()) { responseWrapper.copyBodyToResponse(); } else if (isEligibleForEtag(request, responseWrapper, statusCode, responseWrapper.getContentInputStream())) { String responseETag = generateETagHeaderValue(responseWrapper.getContentInputStream()); rawResponse.setHeader(HEADER_ETAG, responseETag); String requestETag = request.getHeader(HEADER_IF_NONE_MATCH); if (responseETag.equals(requestETag)) { if (logger.isTraceEnabled()) { logger.trace("ETag [" + responseETag + "] equal to If-None-Match, sending 304"); } rawResponse.setStatus(HttpServletResponse.SC_NOT_MODIFIED); } else { if (logger.isTraceEnabled()) { logger.trace("ETag [" + responseETag + "] not equal to If-None-Match [" + requestETag + "], sending normal response"); } responseWrapper.copyBodyToResponse(); } } else { if (logger.isTraceEnabled()) { logger.trace("Response with status code [" + statusCode + "] not eligible for ETag"); } responseWrapper.copyBodyToResponse(); } } /** * Indicates whether the given request and response are eligible for ETag generation. * <p>The default implementation returns {@code true} if all conditions match: * <ul> * <li>response status codes in the {@code 2xx} series</li> * <li>request method is a GET</li> * <li>response Cache-Control header is not set or does not contain a "no-store" directive</li> * </ul> * @param request the HTTP request * @param response the HTTP response * @param responseStatusCode the HTTP response status code * @param inputStream the response body * @return {@code true} if eligible for ETag generation; {@code false} otherwise */ protected boolean isEligibleForEtag(HttpServletRequest request, HttpServletResponse response, int responseStatusCode, InputStream inputStream) { if (responseStatusCode >= 200 && responseStatusCode < 300 && HttpMethod.GET.matches(request.getMethod())) { String cacheControl = null; if (servlet3Present) { cacheControl = response.getHeader(HEADER_CACHE_CONTROL); } if (cacheControl == null || !cacheControl.contains(DIRECTIVE_NO_STORE)) { return true; } } return false; } /** * Generate the ETag header value from the given response body byte array. * <p>The default implementation generates an MD5 hash. * @param inputStream the response body as an InputStream * @return the ETag header value * @see org.frameworkset.util.DigestUtils */ protected String generateETagHeaderValue(InputStream inputStream) throws IOException { StringBuilder builder = new StringBuilder("\"0"); DigestUtils.appendMd5DigestAsHex(inputStream, builder); builder.append('"'); return builder.toString(); } /** * This method can be used to disable the content caching response wrapper * of the ShallowEtagHeaderFilter. This can be done before the start of HTTP * streaming for example where the response will be written to asynchronously * and not in the context of a Servlet container thread. * @since 4.2 */ public static void disableContentCaching(ServletRequest request) { Assert.notNull(request, "ServletRequest must not be null"); request.setAttribute(STREAMING_ATTRIBUTE, true); } private static boolean isContentCachingDisabled(HttpServletRequest request) { return (request.getAttribute(STREAMING_ATTRIBUTE) != null); } private static class HttpStreamingAwareContentCachingResponseWrapper extends ContentCachingResponseWrapper { private final HttpServletRequest request; public HttpStreamingAwareContentCachingResponseWrapper(HttpServletResponse response, HttpServletRequest request) { super(response); this.request = request; } @Override public ServletOutputStream getOutputStream() throws IOException { return (useRawResponse() ? getResponse().getOutputStream() : super.getOutputStream()); } @Override public PrintWriter getWriter() throws IOException { return (useRawResponse() ? getResponse().getWriter() : super.getWriter()); } private boolean useRawResponse() { return isContentCachingDisabled(this.request); } } }