diff --git a/spring-web/src/main/java/org/springframework/web/filter/AbstractRequestLoggingFilter.java b/spring-web/src/main/java/org/springframework/web/filter/AbstractRequestLoggingFilter.java index 5e4eea21d86..1e3765af6fe 100644 --- a/spring-web/src/main/java/org/springframework/web/filter/AbstractRequestLoggingFilter.java +++ b/spring-web/src/main/java/org/springframework/web/filter/AbstractRequestLoggingFilter.java @@ -16,34 +16,32 @@ package org.springframework.web.filter; -import java.io.BufferedReader; -import java.io.ByteArrayOutputStream; import java.io.IOException; -import java.io.InputStreamReader; import java.io.UnsupportedEncodingException; import javax.servlet.FilterChain; import javax.servlet.ServletException; -import javax.servlet.ServletInputStream; import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletRequestWrapper; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import org.springframework.util.Assert; import org.springframework.util.StringUtils; -import org.springframework.web.util.WebUtils; +import org.springframework.web.util.ContentCachingRequestWrapper; /** - * Base class for {@code Filter}s that perform logging operations before and after a request is processed. + * Base class for {@code Filter}s that perform logging operations before and after a request + * is processed. * *
Subclasses should override the {@code beforeRequest(HttpServletRequest, String)} and - * {@code afterRequest(HttpServletRequest, String)} methods to perform the actual logging around the request. + * {@code afterRequest(HttpServletRequest, String)} methods to perform the actual logging + * around the request. * *
Subclasses are passed the message to write to the log in the {@code beforeRequest} and - * {@code afterRequest} methods. By default, only the URI of the request is logged. However, setting the - * {@code includeQueryString} property to {@code true} will cause the query string of the request to be - * included also. The payload (body) of the request can be logged via the {@code includePayload} flag. Note that - * this will only log that which is read, which might not be the entire payload. + * {@code afterRequest} methods. By default, only the URI of the request is logged. However, + * setting the {@code includeQueryString} property to {@code true} will cause the query string + * of the request to be included also. The payload (body) of the request can be logged via the + * {@code includePayload} flag. Note that this will only log that which is read, which might + * not be the entire payload. * *
Prefixes and suffixes for the before and after messages can be configured using the * {@code beforeMessagePrefix}, {@code afterMessagePrefix}, {@code beforeMessageSuffix} and @@ -87,41 +85,43 @@ public abstract class AbstractRequestLoggingFilter extends OncePerRequestFilter /** - * Set whether or not the query string should be included in the log message.
Should be configured using an - * {@code <init-param>} for parameter name "includeQueryString" in the filter definition in - * {@code web.xml}. + * Set whether the query string should be included in the log message. + *
Should be configured using an {@code <init-param>} for parameter name + * "includeQueryString" in the filter definition in {@code web.xml}. */ public void setIncludeQueryString(boolean includeQueryString) { this.includeQueryString = includeQueryString; } /** - * Return whether or not the query string should be included in the log message. + * Return whether the query string should be included in the log message. */ protected boolean isIncludeQueryString() { return this.includeQueryString; } /** - * Set whether or not the client address and session id should be included in the log message.
Should be configured - * using an {@code <init-param>} for parameter name "includeClientInfo" in the filter definition in - * {@code web.xml}. + * Set whether the client address and session id should be included in the + * log message. + *
Should be configured using an {@code <init-param>} for parameter name + * "includeClientInfo" in the filter definition in {@code web.xml}. */ public void setIncludeClientInfo(boolean includeClientInfo) { this.includeClientInfo = includeClientInfo; } /** - * Return whether or not the client address and session id should be included in the log message. + * Return whether the client address and session id should be included in the + * log message. */ protected boolean isIncludeClientInfo() { return this.includeClientInfo; } /** - * Set whether or not the request payload (body) should be included in the log message.
Should be configured using - * an {@code <init-param>} for parameter name "includePayload" in the filter definition in - * {@code web.xml}. + * Set whether the request payload (body) should be included in the log message. + *
Should be configured using an {@code <init-param>} for parameter name + * "includePayload" in the filter definition in {@code web.xml}. */ public void setIncludePayload(boolean includePayload) { @@ -129,14 +129,15 @@ public abstract class AbstractRequestLoggingFilter extends OncePerRequestFilter } /** - * Return whether or not the request payload (body) should be included in the log message. + * Return whether the request payload (body) should be included in the log message. */ protected boolean isIncludePayload() { - return includePayload; + return this.includePayload; } /** - * Sets the maximum length of the payload body to be included in the log message. Default is 50 characters. + * Sets the maximum length of the payload body to be included in the log message. + * Default is 50 characters. */ public void setMaxPayloadLength(int maxPayloadLength) { Assert.isTrue(maxPayloadLength >= 0, "'maxPayloadLength' should be larger than or equal to 0"); @@ -147,32 +148,36 @@ public abstract class AbstractRequestLoggingFilter extends OncePerRequestFilter * Return the maximum length of the payload body to be included in the log message. */ protected int getMaxPayloadLength() { - return maxPayloadLength; + return this.maxPayloadLength; } /** - * Set the value that should be prepended to the log message written before a request is processed. + * Set the value that should be prepended to the log message written + * before a request is processed. */ public void setBeforeMessagePrefix(String beforeMessagePrefix) { this.beforeMessagePrefix = beforeMessagePrefix; } /** - * Set the value that should be apppended to the log message written before a request is processed. + * Set the value that should be appended to the log message written + * before a request is processed. */ public void setBeforeMessageSuffix(String beforeMessageSuffix) { this.beforeMessageSuffix = beforeMessageSuffix; } /** - * Set the value that should be prepended to the log message written after a request is processed. + * Set the value that should be prepended to the log message written + * after a request is processed. */ public void setAfterMessagePrefix(String afterMessagePrefix) { this.afterMessagePrefix = afterMessagePrefix; } /** - * Set the value that should be appended to the log message written after a request is processed. + * Set the value that should be appended to the log message written + * after a request is processed. */ public void setAfterMessageSuffix(String afterMessageSuffix) { this.afterMessageSuffix = afterMessageSuffix; @@ -200,22 +205,21 @@ public abstract class AbstractRequestLoggingFilter extends OncePerRequestFilter throws ServletException, IOException { boolean isFirstRequest = !isAsyncDispatch(request); + HttpServletRequest requestToUse = request; - if (isIncludePayload()) { - if (isFirstRequest) { - request = new RequestCachingRequestWrapper(request); - } + if (isIncludePayload() && isFirstRequest && !(request instanceof ContentCachingRequestWrapper)) { + requestToUse = new ContentCachingRequestWrapper(request); } if (isFirstRequest) { - beforeRequest(request, getBeforeMessage(request)); + beforeRequest(requestToUse, getBeforeMessage(requestToUse)); } try { - filterChain.doFilter(request, response); + filterChain.doFilter(requestToUse, response); } finally { - if (!isAsyncStarted(request)) { - afterRequest(request, getAfterMessage(request)); + if (!isAsyncStarted(requestToUse)) { + afterRequest(requestToUse, getAfterMessage(requestToUse)); } } } @@ -265,8 +269,8 @@ public abstract class AbstractRequestLoggingFilter extends OncePerRequestFilter msg.append(";user=").append(user); } } - if (isIncludePayload() && request instanceof RequestCachingRequestWrapper) { - RequestCachingRequestWrapper wrapper = (RequestCachingRequestWrapper) request; + if (isIncludePayload() && request instanceof ContentCachingRequestWrapper) { + ContentCachingRequestWrapper wrapper = (ContentCachingRequestWrapper) request; byte[] buf = wrapper.getContentAsByteArray(); if (buf.length > 0) { int length = Math.min(buf.length, getMaxPayloadLength()); @@ -301,63 +305,4 @@ public abstract class AbstractRequestLoggingFilter extends OncePerRequestFilter */ protected abstract void afterRequest(HttpServletRequest request, String message); - - private static class RequestCachingRequestWrapper extends HttpServletRequestWrapper { - - private final ByteArrayOutputStream cachedContent = new ByteArrayOutputStream(1024); - - private final ServletInputStream inputStream; - - private BufferedReader reader; - - private RequestCachingRequestWrapper(HttpServletRequest request) throws IOException { - super(request); - this.inputStream = new RequestCachingInputStream(request.getInputStream()); - } - - @Override - public ServletInputStream getInputStream() throws IOException { - return this.inputStream; - } - - @Override - public String getCharacterEncoding() { - String enc = super.getCharacterEncoding(); - return (enc != null ? enc : WebUtils.DEFAULT_CHARACTER_ENCODING); - } - - @Override - public BufferedReader getReader() throws IOException { - if (this.reader == null) { - this.reader = new BufferedReader(new InputStreamReader(this.inputStream, getCharacterEncoding())); - } - return this.reader; - } - - private byte[] getContentAsByteArray() { - return this.cachedContent.toByteArray(); - } - - - private class RequestCachingInputStream extends ServletInputStream { - - private final ServletInputStream is; - - public RequestCachingInputStream(ServletInputStream is) { - this.is = is; - } - - @Override - public int read() throws IOException { - int ch = this.is.read(); - if (ch != -1) { - cachedContent.write(ch); - } - return ch; - } - - } - - } - } diff --git a/spring-web/src/main/java/org/springframework/web/filter/ShallowEtagHeaderFilter.java b/spring-web/src/main/java/org/springframework/web/filter/ShallowEtagHeaderFilter.java index 738d1b87342..8e8fa9d0289 100644 --- a/spring-web/src/main/java/org/springframework/web/filter/ShallowEtagHeaderFilter.java +++ b/spring-web/src/main/java/org/springframework/web/filter/ShallowEtagHeaderFilter.java @@ -17,22 +17,17 @@ package org.springframework.web.filter; import java.io.IOException; -import java.io.OutputStreamWriter; -import java.io.PrintWriter; -import java.io.UnsupportedEncodingException; import javax.servlet.FilterChain; import javax.servlet.ServletException; -import javax.servlet.ServletOutputStream; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; -import javax.servlet.http.HttpServletResponseWrapper; import org.springframework.http.HttpMethod; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.DigestUtils; -import org.springframework.util.ResizableByteArrayOutputStream; import org.springframework.util.StreamUtils; +import org.springframework.web.util.ContentCachingResponseWrapper; import org.springframework.web.util.WebUtils; /** @@ -80,8 +75,8 @@ public class ShallowEtagHeaderFilter extends OncePerRequestFilter { throws ServletException, IOException { HttpServletResponse responseToUse = response; - if (!isAsyncDispatch(request)) { - responseToUse = new ShallowEtagResponseWrapper(response); + if (!isAsyncDispatch(request) && !(response instanceof ContentCachingResponseWrapper)) { + responseToUse = new ContentCachingResponseWrapper(response); } filterChain.doFilter(request, responseToUse); @@ -92,13 +87,13 @@ public class ShallowEtagHeaderFilter extends OncePerRequestFilter { } private void updateResponse(HttpServletRequest request, HttpServletResponse response) throws IOException { - ShallowEtagResponseWrapper responseWrapper = - WebUtils.getNativeResponse(response, ShallowEtagResponseWrapper.class); + ContentCachingResponseWrapper responseWrapper = + WebUtils.getNativeResponse(response, ContentCachingResponseWrapper.class); Assert.notNull(responseWrapper, "ShallowEtagResponseWrapper not found"); HttpServletResponse rawResponse = (HttpServletResponse) responseWrapper.getResponse(); int statusCode = responseWrapper.getStatusCode(); - byte[] body = responseWrapper.toByteArray(); + byte[] body = responseWrapper.getContentAsByteArray(); if (rawResponse.isCommitted()) { if (body.length > 0) { @@ -178,165 +173,4 @@ public class ShallowEtagHeaderFilter extends OncePerRequestFilter { return builder.toString(); } - - /** - * {@link HttpServletRequest} wrapper that buffers all content written to the - * {@linkplain #getOutputStream() output stream} and {@linkplain #getWriter() writer}, - * and allows this content to be retrieved via a {@link #toByteArray() byte array}. - */ - private static class ShallowEtagResponseWrapper extends HttpServletResponseWrapper { - - private final ResizableByteArrayOutputStream content = new ResizableByteArrayOutputStream(1024); - - private final ServletOutputStream outputStream = new ResponseServletOutputStream(); - - private PrintWriter writer; - - private int statusCode = HttpServletResponse.SC_OK; - - public ShallowEtagResponseWrapper(HttpServletResponse response) { - super(response); - } - - @Override - public void setStatus(int sc) { - super.setStatus(sc); - this.statusCode = sc; - } - - @SuppressWarnings("deprecation") - @Override - public void setStatus(int sc, String sm) { - super.setStatus(sc, sm); - this.statusCode = sc; - } - - @Override - public void sendError(int sc) throws IOException { - copyBodyToResponse(); - super.sendError(sc); - this.statusCode = sc; - } - - @Override - public void sendError(int sc, String msg) throws IOException { - copyBodyToResponse(); - super.sendError(sc, msg); - this.statusCode = sc; - } - - @Override - public void sendRedirect(String location) throws IOException { - copyBodyToResponse(); - super.sendRedirect(location); - } - - @Override - public ServletOutputStream getOutputStream() { - return this.outputStream; - } - - @Override - public PrintWriter getWriter() throws IOException { - if (this.writer == null) { - String characterEncoding = getCharacterEncoding(); - this.writer = (characterEncoding != null ? new ResponsePrintWriter(characterEncoding) : - new ResponsePrintWriter(WebUtils.DEFAULT_CHARACTER_ENCODING)); - } - return this.writer; - } - - @Override - public void setContentLength(int len) { - if (len > this.content.capacity()) { - this.content.resize(len); - } - } - - // Overrides Servlet 3.1 setContentLengthLong(long) at runtime - public void setContentLengthLong(long len) { - if (len > Integer.MAX_VALUE) { - throw new IllegalArgumentException("Content-Length exceeds ShallowEtagHeaderFilter's maximum (" + - Integer.MAX_VALUE + "): " + len); - } - if (len > this.content.capacity()) { - this.content.resize((int) len); - } - } - - @Override - public void setBufferSize(int size) { - if (size > this.content.capacity()) { - this.content.resize(size); - } - } - - @Override - public void resetBuffer() { - this.content.reset(); - } - - @Override - public void reset() { - super.reset(); - this.content.reset(); - } - - public int getStatusCode() { - return this.statusCode; - } - - public byte[] toByteArray() { - return this.content.toByteArray(); - } - - private void copyBodyToResponse() throws IOException { - if (this.content.size() > 0) { - getResponse().setContentLength(this.content.size()); - StreamUtils.copy(this.content.toByteArray(), getResponse().getOutputStream()); - this.content.reset(); - } - } - - - private class ResponseServletOutputStream extends ServletOutputStream { - - @Override - public void write(int b) throws IOException { - content.write(b); - } - - @Override - public void write(byte[] b, int off, int len) throws IOException { - content.write(b, off, len); - } - } - - - private class ResponsePrintWriter extends PrintWriter { - - public ResponsePrintWriter(String characterEncoding) throws UnsupportedEncodingException { - super(new OutputStreamWriter(content, characterEncoding)); - } - - @Override - public void write(char buf[], int off, int len) { - super.write(buf, off, len); - super.flush(); - } - - @Override - public void write(String s, int off, int len) { - super.write(s, off, len); - super.flush(); - } - - @Override - public void write(int c) { - super.write(c); - super.flush(); - } - } - } - } diff --git a/spring-web/src/main/java/org/springframework/web/util/ContentCachingRequestWrapper.java b/spring-web/src/main/java/org/springframework/web/util/ContentCachingRequestWrapper.java new file mode 100644 index 00000000000..888341c1c8c --- /dev/null +++ b/spring-web/src/main/java/org/springframework/web/util/ContentCachingRequestWrapper.java @@ -0,0 +1,105 @@ +/* + * Copyright 2002-2014 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.web.util; + +import java.io.BufferedReader; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import javax.servlet.ServletInputStream; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletRequestWrapper; + +/** + * {@link javax.servlet.http-HttpServletRequest} wrapper that caches all content read from + * the {@linkplain #getInputStream() input stream} and {@linkplain #getReader() reader}, + * and allows this content to be retrieved via a {@link #getContentAsByteArray() byte array}. + * + *
Used e.g. by {@link org.springframework.web.filter.AbstractRequestLoggingFilter}. + * + * @author Juergen Hoeller + * @since 4.1.3 + */ +public class ContentCachingRequestWrapper extends HttpServletRequestWrapper { + + private final ByteArrayOutputStream cachedContent; + + private ServletInputStream inputStream; + + private BufferedReader reader; + + + /** + * Create a new ContentCachingRequestWrapper for the given servlet request. + * @param request the original servlet request + */ + public ContentCachingRequestWrapper(HttpServletRequest request) { + super(request); + int contentLength = request.getContentLength(); + this.cachedContent = new ByteArrayOutputStream(contentLength >= 0 ? contentLength : 1024); + } + + + @Override + public ServletInputStream getInputStream() throws IOException { + if (this.inputStream == null) { + this.inputStream = new ContentCachingInputStream(getRequest().getInputStream()); + } + return this.inputStream; + } + + @Override + public String getCharacterEncoding() { + String enc = super.getCharacterEncoding(); + return (enc != null ? enc : WebUtils.DEFAULT_CHARACTER_ENCODING); + } + + @Override + public BufferedReader getReader() throws IOException { + if (this.reader == null) { + this.reader = new BufferedReader(new InputStreamReader(getInputStream(), getCharacterEncoding())); + } + return this.reader; + } + + /** + * Return the cached request content as a byte array. + */ + public byte[] getContentAsByteArray() { + return this.cachedContent.toByteArray(); + } + + + private class ContentCachingInputStream extends ServletInputStream { + + private final ServletInputStream is; + + public ContentCachingInputStream(ServletInputStream is) { + this.is = is; + } + + @Override + public int read() throws IOException { + int ch = this.is.read(); + if (ch != -1) { + cachedContent.write(ch); + } + return ch; + } + } + +} diff --git a/spring-web/src/main/java/org/springframework/web/util/ContentCachingResponseWrapper.java b/spring-web/src/main/java/org/springframework/web/util/ContentCachingResponseWrapper.java new file mode 100644 index 00000000000..3eee67e72c6 --- /dev/null +++ b/spring-web/src/main/java/org/springframework/web/util/ContentCachingResponseWrapper.java @@ -0,0 +1,206 @@ +/* + * Copyright 2002-2014 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.web.util; + +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.io.PrintWriter; +import java.io.UnsupportedEncodingException; +import javax.servlet.ServletOutputStream; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpServletResponseWrapper; + +import org.springframework.util.ResizableByteArrayOutputStream; +import org.springframework.util.StreamUtils; + +/** + * {@link javax.servlet.http-HttpServletResponse} wrapper that caches all content written to + * the {@linkplain #getOutputStream() output stream} and {@linkplain #getWriter() writer}, + * and allows this content to be retrieved via a {@link #getContentAsByteArray() byte array}. + * + *
Used e.g. by {@link org.springframework.web.filter.ShallowEtagHeaderFilter}. + * + * @author Juergen Hoeller + * @since 4.1.3 + */ +public class ContentCachingResponseWrapper extends HttpServletResponseWrapper { + + private final ResizableByteArrayOutputStream content = new ResizableByteArrayOutputStream(1024); + + private final ServletOutputStream outputStream = new ResponseServletOutputStream(); + + private PrintWriter writer; + + private int statusCode = HttpServletResponse.SC_OK; + + + /** + * Create a new ContentCachingResponseWrapper for the given servlet response. + * @param response the original servlet response + */ + public ContentCachingResponseWrapper(HttpServletResponse response) { + super(response); + } + + + @Override + public void setStatus(int sc) { + super.setStatus(sc); + this.statusCode = sc; + } + + @SuppressWarnings("deprecation") + @Override + public void setStatus(int sc, String sm) { + super.setStatus(sc, sm); + this.statusCode = sc; + } + + @Override + public void sendError(int sc) throws IOException { + copyBodyToResponse(); + super.sendError(sc); + this.statusCode = sc; + } + + @Override + public void sendError(int sc, String msg) throws IOException { + copyBodyToResponse(); + super.sendError(sc, msg); + this.statusCode = sc; + } + + @Override + public void sendRedirect(String location) throws IOException { + copyBodyToResponse(); + super.sendRedirect(location); + } + + @Override + public ServletOutputStream getOutputStream() { + return this.outputStream; + } + + @Override + public PrintWriter getWriter() throws IOException { + if (this.writer == null) { + String characterEncoding = getCharacterEncoding(); + this.writer = (characterEncoding != null ? new ResponsePrintWriter(characterEncoding) : + new ResponsePrintWriter(WebUtils.DEFAULT_CHARACTER_ENCODING)); + } + return this.writer; + } + + @Override + public void setContentLength(int len) { + if (len > this.content.capacity()) { + this.content.resize(len); + } + } + + // Overrides Servlet 3.1 setContentLengthLong(long) at runtime + public void setContentLengthLong(long len) { + if (len > Integer.MAX_VALUE) { + throw new IllegalArgumentException("Content-Length exceeds ShallowEtagHeaderFilter's maximum (" + + Integer.MAX_VALUE + "): " + len); + } + if (len > this.content.capacity()) { + this.content.resize((int) len); + } + } + + @Override + public void setBufferSize(int size) { + if (size > this.content.capacity()) { + this.content.resize(size); + } + } + + @Override + public void resetBuffer() { + this.content.reset(); + } + + @Override + public void reset() { + super.reset(); + this.content.reset(); + } + + /** + * Return the status code as specifed on the response. + */ + public int getStatusCode() { + return this.statusCode; + } + + /** + * Return the cached response content as a byte array. + */ + public byte[] getContentAsByteArray() { + return this.content.toByteArray(); + } + + private void copyBodyToResponse() throws IOException { + if (this.content.size() > 0) { + getResponse().setContentLength(this.content.size()); + StreamUtils.copy(this.content.toByteArray(), getResponse().getOutputStream()); + this.content.reset(); + } + } + + + private class ResponseServletOutputStream extends ServletOutputStream { + + @Override + public void write(int b) throws IOException { + content.write(b); + } + + @Override + public void write(byte[] b, int off, int len) throws IOException { + content.write(b, off, len); + } + } + + + private class ResponsePrintWriter extends PrintWriter { + + public ResponsePrintWriter(String characterEncoding) throws UnsupportedEncodingException { + super(new OutputStreamWriter(content, characterEncoding)); + } + + @Override + public void write(char buf[], int off, int len) { + super.write(buf, off, len); + super.flush(); + } + + @Override + public void write(String s, int off, int len) { + super.write(s, off, len); + super.flush(); + } + + @Override + public void write(int c) { + super.write(c); + super.flush(); + } + } + +}