diff --git a/org.springframework.web/src/main/java/org/springframework/web/filter/AbstractRequestLoggingFilter.java b/org.springframework.web/src/main/java/org/springframework/web/filter/AbstractRequestLoggingFilter.java
index b294e7cd22d..c36bc87332f 100644
--- a/org.springframework.web/src/main/java/org/springframework/web/filter/AbstractRequestLoggingFilter.java
+++ b/org.springframework.web/src/main/java/org/springframework/web/filter/AbstractRequestLoggingFilter.java
@@ -16,38 +16,44 @@
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;
/**
- * Base class for Filters that perform logging operations before and after a
- * request is processed.
+ * Base class for Filters that perform logging operations before and after a request is processed.
*
- *
Subclasses should override the beforeRequest(HttpServletRequest, String)
- * and afterRequest(HttpServletRequest, String) methods to perform the actual
- * logging around the request.
+ *
Subclasses should override the beforeRequest(HttpServletRequest, String) and
+ * afterRequest(HttpServletRequest, String) methods to perform the actual logging around the request.
*
- *
Subclasses are passed the message to write to the log in the beforeRequest
- * and afterRequest methods. By default, only the URI of the request is logged.
- * However, setting the includeQueryString property to true will
- * cause the query string of the request to be included also.
+ *
Subclasses are passed the message to write to the log in the beforeRequest and
+ * afterRequest methods. By default, only the URI of the request is logged. However, setting the
+ * includeQueryString property to true will cause the query string of the request to be
+ * included also. The payload (body) of the request can be logged via the 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 beforeMessagePrefix, afterMessagePrefix,
- * beforeMessageSuffix and afterMessageSuffix properties,
+ *
Prefixes and suffixes for the before and after messages can be configured using the
+ * beforeMessagePrefix, afterMessagePrefix, beforeMessageSuffix and
+ * afterMessageSuffix properties,
*
* @author Rob Harrop
* @author Juergen Hoeller
- * @since 1.2.5
* @see #beforeRequest
* @see #afterRequest
+ * @since 1.2.5
*/
public abstract class AbstractRequestLoggingFilter extends OncePerRequestFilter {
@@ -59,11 +65,16 @@ public abstract class AbstractRequestLoggingFilter extends OncePerRequestFilter
public static final String DEFAULT_AFTER_MESSAGE_SUFFIX = "]";
+ private static final int DEFAULT_MAX_PAYLOAD_LENGTH = 50;
private boolean includeQueryString = false;
private boolean includeClientInfo = false;
+ private boolean includePayload = false;
+
+ private int maxPayloadLength = DEFAULT_MAX_PAYLOAD_LENGTH;
+
private String beforeMessagePrefix = DEFAULT_BEFORE_MESSAGE_PREFIX;
private String beforeMessageSuffix = DEFAULT_BEFORE_MESSAGE_SUFFIX;
@@ -72,11 +83,10 @@ public abstract class AbstractRequestLoggingFilter extends OncePerRequestFilter
private String afterMessageSuffix = DEFAULT_AFTER_MESSAGE_SUFFIX;
-
/**
- * Set whether or not the query string should be included in the log message.
- *
Should be configured using an <init-param> for parameter
- * name "includeQueryString" in the filter definition in web.xml.
+ * Set whether or not the query string should be included in the log message.
Should be configured using an
+ * <init-param> for parameter name "includeQueryString" in the filter definition in
+ * web.xml.
*/
public void setIncludeQueryString(boolean includeQueryString) {
this.includeQueryString = includeQueryString;
@@ -90,68 +100,94 @@ public abstract class AbstractRequestLoggingFilter extends OncePerRequestFilter
}
/**
- * Set whether or not the client address and session id should be included
- * in the log message.
- *
Should be configured using an <init-param> for parameter
- * name "includeClientInfo" in the filter definition in web.xml.
+ * Set whether or not the client address and session id should be included in the log message.
Should be configured
+ * using an <init-param> for parameter name "includeClientInfo" in the filter definition in
+ * 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 or not the client address and session id should be included in the log message.
*/
protected boolean isIncludeClientInfo() {
return this.includeClientInfo;
}
/**
- * Set the value that should be prepended to the log message written
- * before a request is processed.
+ * Set whether or not the request payload (body) should be included in the log message.
Should be configured using
+ * an <init-param> for parameter name "includePayload" in the filter definition in
+ * web.xml.
+ */
+
+ public void setIncludePayload(boolean includePayload) {
+ this.includePayload = includePayload;
+ }
+
+ /**
+ * Return whether or not the request payload (body) should be included in the log message.
+ */
+ protected boolean isIncludePayload() {
+ return includePayload;
+ }
+
+ /**
+ * 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");
+ this.maxPayloadLength = maxPayloadLength;
+ }
+
+ /**
+ * Return the maximum length of the payload body to be included in the log message.
+ */
+ protected int getMaxPayloadLength() {
+ return maxPayloadLength;
+ }
+
+ /**
+ * 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 apppended 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;
}
-
/**
- * Forwards the request to the next filter in the chain and delegates
- * down to the subclasses to perform the actual request logging both
- * before and after the request is processed.
+ * Forwards the request to the next filter in the chain and delegates down to the subclasses to perform the actual
+ * request logging both before and after the request is processed.
+ *
* @see #beforeRequest
* @see #afterRequest
*/
@Override
- protected void doFilterInternal(
- HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
+ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
-
+ if (isIncludePayload()) {
+ request = new RequestCachingRequestWrapper(request);
+ }
beforeRequest(request, getBeforeMessage(request));
try {
filterChain.doFilter(request, response);
@@ -161,9 +197,9 @@ public abstract class AbstractRequestLoggingFilter extends OncePerRequestFilter
}
}
-
/**
* Get the message to write to the log before the request.
+ *
* @see #createMessage
*/
private String getBeforeMessage(HttpServletRequest request) {
@@ -172,6 +208,7 @@ public abstract class AbstractRequestLoggingFilter extends OncePerRequestFilter
/**
* Get the message to write to the log after the request.
+ *
* @see #createMessage
*/
private String getAfterMessage(HttpServletRequest request) {
@@ -179,13 +216,10 @@ public abstract class AbstractRequestLoggingFilter extends OncePerRequestFilter
}
/**
- * Create a log message for the given request, prefix and suffix.
- *
If includeQueryString is true then
- * the inner part of the log message will take the form
- * request_uri?query_string otherwise the message will
- * simply be of the form request_uri.
- *
The final message is composed of the inner part as described - * and the supplied prefix and suffix. + * Create a log message for the given request, prefix and suffix.
If includeQueryString is
+ * true then the inner part of the log message will take the form request_uri?query_string
+ * otherwise the message will simply be of the form request_uri.
The final message is composed of the + * inner part as described and the supplied prefix and suffix. */ protected String createMessage(HttpServletRequest request, String prefix, String suffix) { StringBuilder msg = new StringBuilder(); @@ -208,25 +242,97 @@ public abstract class AbstractRequestLoggingFilter extends OncePerRequestFilter msg.append(";user=").append(user); } } + if (isIncludePayload() && request instanceof RequestCachingRequestWrapper) { + RequestCachingRequestWrapper wrapper = (RequestCachingRequestWrapper) request; + byte[] buf = wrapper.toByteArray(); + if (buf.length > 0) { + int length = Math.min(buf.length, getMaxPayloadLength()); + String payload; + try { + payload = new String(buf, 0, length, wrapper.getCharacterEncoding()); + } + catch (UnsupportedEncodingException e) { + payload = "[unknown]"; + } + msg.append(";payload=").append(payload); + } + + } msg.append(suffix); return msg.toString(); } - /** - * Concrete subclasses should implement this method to write a log message - * before the request is processed. + * Concrete subclasses should implement this method to write a log message before the request is processed. + * * @param request current HTTP request * @param message the message to log */ protected abstract void beforeRequest(HttpServletRequest request, String message); /** - * Concrete subclasses should implement this method to write a log message - * after the request is processed. + * Concrete subclasses should implement this method to write a log message after the request is processed. + * * @param request current HTTP request * @param message the message to log */ protected abstract void afterRequest(HttpServletRequest request, String message); + private static class RequestCachingRequestWrapper extends HttpServletRequestWrapper { + + private final ByteArrayOutputStream bos = new ByteArrayOutputStream(); + + 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 inputStream; + } + + @Override + public String getCharacterEncoding() { + return super.getCharacterEncoding() != null ? super.getCharacterEncoding() : + WebUtils.DEFAULT_CHARACTER_ENCODING; + } + + @Override + public BufferedReader getReader() throws IOException { + if (this.reader == null) { + this.reader = new BufferedReader(new InputStreamReader(inputStream, getCharacterEncoding())); + } + return this.reader; + } + + private byte[] toByteArray() { + return this.bos.toByteArray(); + } + + private class RequestCachingInputStream extends ServletInputStream { + + private final ServletInputStream is; + + private RequestCachingInputStream(ServletInputStream is) { + this.is = is; + } + + @Override + public int read() throws IOException { + int ch = is.read(); + if (ch != -1) { + bos.write(ch); + } + return ch; + } + + } + + } + } diff --git a/org.springframework.web/src/test/java/org/springframework/web/filter/RequestLoggingFilterTests.java b/org.springframework.web/src/test/java/org/springframework/web/filter/RequestLoggingFilterTests.java new file mode 100644 index 00000000000..b871c7a48d7 --- /dev/null +++ b/org.springframework.web/src/test/java/org/springframework/web/filter/RequestLoggingFilterTests.java @@ -0,0 +1,189 @@ +/* + * Copyright 2002-2009 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.filter; + +import java.io.IOException; +import javax.servlet.FilterChain; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpServletRequest; + +import org.junit.Test; +import org.junit.Before; +import static org.junit.Assert.*; + +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.util.FileCopyUtils; + +/** + * Test for {@link AbstractRequestLoggingFilter} and sub classes. + * + * @author Arjen Poutsma + */ +public class RequestLoggingFilterTests { + + private MyRequestLoggingFilter filter; + + @Before + public void createFilter() throws Exception { + filter = new MyRequestLoggingFilter(); + } + + @Test + public void uri() throws Exception { + final MockHttpServletRequest request = new MockHttpServletRequest("POST", "/hotels"); + MockHttpServletResponse response = new MockHttpServletResponse(); + + request.setQueryString("booking=42"); + + FilterChain filterChain = new NoopFilterChain(); + + filter.doFilter(request, response, filterChain); + + assertNotNull(filter.beforeRequestMessage); + assertTrue(filter.beforeRequestMessage.indexOf("uri=/hotel") != -1); + assertFalse(filter.beforeRequestMessage.indexOf("booking=42") != -1); + + assertNotNull(filter.afterRequestMessage); + assertTrue(filter.afterRequestMessage.indexOf("uri=/hotel") != -1); + assertFalse(filter.afterRequestMessage.indexOf("booking=42") != -1); + } + + @Test + public void queryString() throws Exception { + filter.setIncludeQueryString(true); + + final MockHttpServletRequest request = new MockHttpServletRequest("POST", "/hotels"); + MockHttpServletResponse response = new MockHttpServletResponse(); + + request.setQueryString("booking=42"); + + FilterChain filterChain = new NoopFilterChain(); + + filter.doFilter(request, response, filterChain); + + assertNotNull(filter.beforeRequestMessage); + assertTrue(filter.beforeRequestMessage.indexOf("uri=/hotels?booking=42") != -1); + + assertNotNull(filter.afterRequestMessage); + assertTrue(filter.afterRequestMessage.indexOf("uri=/hotels?booking=42") != -1); + } + + @Test + public void payloadInputStream() throws Exception { + filter.setIncludePayload(true); + + final MockHttpServletRequest request = new MockHttpServletRequest("POST", "/hotels"); + MockHttpServletResponse response = new MockHttpServletResponse(); + + final byte[] requestBody = "Hello World".getBytes("UTF-8"); + request.setContent(requestBody); + FilterChain filterChain = new FilterChain() { + + public void doFilter(ServletRequest filterRequest, ServletResponse filterResponse) + throws IOException, ServletException { + ((HttpServletResponse) filterResponse).setStatus(HttpServletResponse.SC_OK); + byte[] buf = FileCopyUtils.copyToByteArray(filterRequest.getInputStream()); + assertArrayEquals(requestBody, buf); + } + }; + + filter.doFilter(request, response, filterChain); + + assertNotNull(filter.afterRequestMessage); + assertTrue(filter.afterRequestMessage.indexOf("Hello World") != -1); + } + + @Test + public void payloadReader() throws Exception { + filter.setIncludePayload(true); + + final MockHttpServletRequest request = new MockHttpServletRequest("POST", "/hotels"); + MockHttpServletResponse response = new MockHttpServletResponse(); + + final String requestBody = "Hello World"; + request.setContent(requestBody.getBytes("UTF-8")); + FilterChain filterChain = new FilterChain() { + + public void doFilter(ServletRequest filterRequest, ServletResponse filterResponse) + throws IOException, ServletException { + ((HttpServletResponse) filterResponse).setStatus(HttpServletResponse.SC_OK); + String buf = FileCopyUtils.copyToString(filterRequest.getReader()); + assertEquals(requestBody, buf); + } + }; + + filter.doFilter(request, response, filterChain); + + assertNotNull(filter.afterRequestMessage); + assertTrue(filter.afterRequestMessage.indexOf(requestBody) != -1); + } + + @Test + public void payloadMaxLength() throws Exception { + filter.setIncludePayload(true); + filter.setMaxPayloadLength(3); + + final MockHttpServletRequest request = new MockHttpServletRequest("POST", "/hotels"); + MockHttpServletResponse response = new MockHttpServletResponse(); + + final byte[] requestBody = "Hello World".getBytes("UTF-8"); + request.setContent(requestBody); + FilterChain filterChain = new FilterChain() { + + public void doFilter(ServletRequest filterRequest, ServletResponse filterResponse) + throws IOException, ServletException { + ((HttpServletResponse) filterResponse).setStatus(HttpServletResponse.SC_OK); + byte[] buf = FileCopyUtils.copyToByteArray(filterRequest.getInputStream()); + assertArrayEquals(requestBody, buf); + } + }; + + filter.doFilter(request, response, filterChain); + + assertNotNull(filter.afterRequestMessage); + assertTrue(filter.afterRequestMessage.indexOf("Hel") != -1); + assertFalse(filter.afterRequestMessage.indexOf("Hello World") != -1); + } + + private static class MyRequestLoggingFilter extends AbstractRequestLoggingFilter { + + private String beforeRequestMessage; + + private String afterRequestMessage; + + @Override + protected void beforeRequest(HttpServletRequest request, String message) { + this.beforeRequestMessage = message; + } + + @Override + protected void afterRequest(HttpServletRequest request, String message) { + this.afterRequestMessage = message; + } + } + + private static class NoopFilterChain implements FilterChain { + + public void doFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException { + } + } + +}