diff --git a/plugins/rest/src/main/java/org/apache/struts2/rest/ContentTypeInterceptor.java b/plugins/rest/src/main/java/org/apache/struts2/rest/ContentTypeInterceptor.java
index 17ccdf2de2..e814e5dfc0 100644
--- a/plugins/rest/src/main/java/org/apache/struts2/rest/ContentTypeInterceptor.java
+++ b/plugins/rest/src/main/java/org/apache/struts2/rest/ContentTypeInterceptor.java
@@ -27,6 +27,7 @@
import org.apache.struts2.ServletActionContext;
import org.apache.struts2.rest.handler.ContentTypeHandler;
import org.apache.commons.lang3.BooleanUtils;
+import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
@@ -34,8 +35,11 @@
import java.beans.BeanInfo;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
+import java.io.FilterReader;
+import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
+import java.io.Reader;
import java.lang.reflect.Array;
import java.lang.reflect.Method;
import java.util.ArrayList;
@@ -62,9 +66,12 @@ public class ContentTypeInterceptor extends AbstractInterceptor {
private static final Logger LOG = LogManager.getLogger(ContentTypeInterceptor.class);
+ public static final int DEFAULT_MAX_LENGTH = 2_097_152;
+
private ContentTypeHandlerManager selector;
private ParameterAuthorizer parameterAuthorizer;
private boolean requireAnnotations = false;
+ private int maxLength = DEFAULT_MAX_LENGTH;
@Inject
public void setContentTypeHandlerSelector(ContentTypeHandlerManager selector) {
@@ -81,6 +88,27 @@ public void setRequireAnnotations(String requireAnnotations) {
this.requireAnnotations = BooleanUtils.toBoolean(requireAnnotations);
}
+ @Inject(value = RestConstants.REST_CONTENT_MAX_LENGTH, required = false)
+ public void setMaxLength(String maxLength) {
+ if (StringUtils.isBlank(maxLength)) {
+ return;
+ }
+ int length;
+ try {
+ length = Integer.parseInt(maxLength.trim());
+ } catch (NumberFormatException e) {
+ LOG.warn("Ignoring non-numeric {} value: {}, keeping {}",
+ RestConstants.REST_CONTENT_MAX_LENGTH, maxLength, this.maxLength);
+ return;
+ }
+ if (length < 1) {
+ LOG.warn("Ignoring out-of-range {} value: {}, expected 1 or more, keeping {}",
+ RestConstants.REST_CONTENT_MAX_LENGTH, length, this.maxLength);
+ return;
+ }
+ this.maxLength = length;
+ }
+
public String intercept(ActionInvocation invocation) throws Exception {
HttpServletRequest request = ServletActionContext.getRequest();
ContentTypeHandler handler = selector.getHandlerForRequest(request);
@@ -91,19 +119,35 @@ public String intercept(ActionInvocation invocation) throws Exception {
}
if (request.getContentLength() > 0) {
- applyRequestBody(invocation, handler, target, openBodyReader(request));
+ BoundedReader reader = new BoundedReader(openBodyReader(request), maxLength);
+ try {
+ applyRequestBody(invocation, handler, target, reader);
+ } catch (Exception e) {
+ if (reader.limitExceeded()) {
+ throw requestBodyTooLarge();
+ }
+ throw e;
+ }
+ if (reader.limitExceeded()) {
+ throw requestBodyTooLarge();
+ }
}
return invocation.invoke();
}
- private static InputStreamReader openBodyReader(HttpServletRequest request) throws java.io.IOException {
+ private RequestBodyTooLargeException requestBodyTooLarge() {
+ return new RequestBodyTooLargeException("Request body exceeds maximum allowed length ("
+ + maxLength + "). Use " + RestConstants.REST_CONTENT_MAX_LENGTH + " to increase the limit.");
+ }
+
+ private static InputStreamReader openBodyReader(HttpServletRequest request) throws IOException {
String encoding = request.getCharacterEncoding();
InputStream is = request.getInputStream();
return encoding == null ? new InputStreamReader(is) : new InputStreamReader(is, encoding);
}
private void applyRequestBody(ActionInvocation invocation, ContentTypeHandler handler, Object target,
- InputStreamReader reader) throws Exception {
+ Reader reader) throws Exception {
if (!requireAnnotations) {
// Direct deserialization (backward compat when requireAnnotations is not enabled).
handler.toObject(invocation, reader, target);
@@ -122,7 +166,7 @@ private void applyRequestBody(ActionInvocation invocation, ContentTypeHandler ha
* for the call duration.
*/
private void applyWithAuthorizationContext(ActionInvocation invocation, ContentTypeHandler handler, Object target,
- InputStreamReader reader) throws java.io.IOException {
+ Reader reader) throws IOException {
Object action = invocation.getAction();
Object resolvedTarget = parameterAuthorizer.resolveTarget(action);
org.apache.struts2.interceptor.parameter.ParameterAuthorizationContext.bind(
@@ -141,7 +185,7 @@ private void applyWithAuthorizationContext(ActionInvocation invocation, ContentT
* unauthorized property is nulled out, so skipping is the safer choice).
*/
private void applyTwoPhaseDeserialize(ActionInvocation invocation, ContentTypeHandler handler, Object target,
- InputStreamReader reader) throws Exception {
+ Reader reader) throws Exception {
Object freshInstance = createFreshInstance(target.getClass());
if (freshInstance == null) {
LOG.warn("REST body rejected: requireAnnotations=true but [{}] has no no-arg constructor; "
@@ -378,4 +422,60 @@ private boolean isNestedBeanType(Class> clazz) {
return true;
}
+ /**
+ * Stops the handler at {@code struts.rest.content.maxLength} characters. The handler may wrap the
+ * {@link IOException} thrown here in its own type, so {@link #intercept} consults
+ * {@link #limitExceeded()} afterwards rather than relying on what propagates.
+ */
+ private static final class BoundedReader extends FilterReader {
+
+ private final int limit;
+ private long consumed;
+ private boolean limitExceeded;
+
+ BoundedReader(Reader in, int limit) {
+ super(in);
+ this.limit = limit;
+ }
+
+ @Override
+ public int read() throws IOException {
+ int c = super.read();
+ if (c != -1) {
+ consumed(1);
+ }
+ return c;
+ }
+
+ @Override
+ public int read(char[] buf, int off, int len) throws IOException {
+ int n = super.read(buf, off, len);
+ if (n > 0) {
+ consumed(n);
+ }
+ return n;
+ }
+
+ @Override
+ public long skip(long n) throws IOException {
+ long skipped = super.skip(n);
+ if (skipped > 0) {
+ consumed(skipped);
+ }
+ return skipped;
+ }
+
+ private void consumed(long n) throws IOException {
+ consumed += n;
+ if (consumed > limit) {
+ limitExceeded = true;
+ throw new IOException("Request body exceeds " + limit + " characters");
+ }
+ }
+
+ boolean limitExceeded() {
+ return limitExceeded;
+ }
+ }
+
}
diff --git a/plugins/rest/src/main/java/org/apache/struts2/rest/RequestBodyTooLargeException.java b/plugins/rest/src/main/java/org/apache/struts2/rest/RequestBodyTooLargeException.java
new file mode 100644
index 0000000000..5f1e8ef5c2
--- /dev/null
+++ b/plugins/rest/src/main/java/org/apache/struts2/rest/RequestBodyTooLargeException.java
@@ -0,0 +1,32 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.apache.struts2.rest;
+
+import org.apache.struts2.StrutsException;
+
+/**
+ * Thrown by {@link ContentTypeInterceptor} when a request body exceeds
+ * {@code struts.rest.content.maxLength}.
+ */
+public class RequestBodyTooLargeException extends StrutsException {
+
+ public RequestBodyTooLargeException(String message) {
+ super(message);
+ }
+}
diff --git a/plugins/rest/src/main/java/org/apache/struts2/rest/RestConstants.java b/plugins/rest/src/main/java/org/apache/struts2/rest/RestConstants.java
index d2675ecee0..2fd7721209 100644
--- a/plugins/rest/src/main/java/org/apache/struts2/rest/RestConstants.java
+++ b/plugins/rest/src/main/java/org/apache/struts2/rest/RestConstants.java
@@ -36,4 +36,5 @@ public class RestConstants {
public static final String REST_MAPPER_PUT_CONTINUE_METHOD_NAME = "struts.mapper.putContinueMethodName";
public static final String STRUTS_REST_NAMESPACE = "struts.rest.namespace";
public static final String REST_VALIDATION_FAILURE_STATUS_CODE = "struts.rest.validationFailureStatusCode";
+ public static final String REST_CONTENT_MAX_LENGTH = "struts.rest.content.maxLength";
}
diff --git a/plugins/rest/src/main/resources/struts-plugin.xml b/plugins/rest/src/main/resources/struts-plugin.xml
index 73ad21146e..c6bb08e0eb 100644
--- a/plugins/rest/src/main/resources/struts-plugin.xml
+++ b/plugins/rest/src/main/resources/struts-plugin.xml
@@ -41,6 +41,7 @@
+
diff --git a/plugins/rest/src/test/java/org/apache/struts2/rest/ContentTypeInterceptorTest.java b/plugins/rest/src/test/java/org/apache/struts2/rest/ContentTypeInterceptorTest.java
index a14d407eef..8ed42d4c19 100644
--- a/plugins/rest/src/test/java/org/apache/struts2/rest/ContentTypeInterceptorTest.java
+++ b/plugins/rest/src/test/java/org/apache/struts2/rest/ContentTypeInterceptorTest.java
@@ -26,7 +26,11 @@
import org.apache.struts2.ActionSupport;
import junit.framework.TestCase;
-import java.io.InputStreamReader;
+import jakarta.servlet.ReadListener;
+import jakarta.servlet.ServletInputStream;
+import java.io.IOException;
+import java.io.Reader;
+import java.util.Arrays;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
@@ -74,8 +78,8 @@ public boolean matches(Object[] args) {
mockContentTypeHandler.verify();
}
- public void testRequestWithEncodingAscii() throws Exception {
- final Charset charset = StandardCharsets.US_ASCII;
+ public void testRequestWithEncodingLatin1() throws Exception {
+ final Charset charset = StandardCharsets.ISO_8859_1;
ContentTypeInterceptor interceptor = new ContentTypeInterceptor();
interceptor.setParameterAuthorizer((parameterName, target, action) -> true);
@@ -86,8 +90,7 @@ public void testRequestWithEncodingAscii() throws Exception {
Mock mockContentTypeHandler = new Mock(ContentTypeHandler.class);
mockContentTypeHandler.expect("toObject", new AnyConstraintMatcher() {
public boolean matches(Object[] args) {
- InputStreamReader in = (InputStreamReader) args[1];
- return charset.equals(Charset.forName(in.getEncoding()));
+ return "caf\u00e9".equals(readFully((Reader) args[1]));
}
});
mockActionInvocation.expectAndReturn("invoke", Action.SUCCESS);
@@ -101,7 +104,7 @@ public boolean matches(Object[] args) {
interceptor.setContentTypeHandlerSelector((ContentTypeHandlerManager) mockContentTypeHandlerManager.proxy());
MockHttpServletRequest request = new MockHttpServletRequest();
- request.setContent(new byte[] {1});
+ request.setContent("caf\u00e9".getBytes(charset));
request.setCharacterEncoding(charset.name());
ActionContext.of()
@@ -115,7 +118,7 @@ public boolean matches(Object[] args) {
mockContentTypeHandler.verify();
}
- public void testRequestWithEncodingUtf() throws Exception {
+ public void testRequestWithEncodingUtf8() throws Exception {
final Charset charset = StandardCharsets.UTF_8;
ContentTypeInterceptor interceptor = new ContentTypeInterceptor();
@@ -127,8 +130,7 @@ public void testRequestWithEncodingUtf() throws Exception {
Mock mockContentTypeHandler = new Mock(ContentTypeHandler.class);
mockContentTypeHandler.expect("toObject", new AnyConstraintMatcher() {
public boolean matches(Object[] args) {
- InputStreamReader in = (InputStreamReader) args[1];
- return charset.equals(Charset.forName(in.getEncoding()));
+ return "caf\u00e9".equals(readFully((Reader) args[1]));
}
});
mockActionInvocation.expectAndReturn("invoke", Action.SUCCESS);
@@ -142,7 +144,7 @@ public boolean matches(Object[] args) {
interceptor.setContentTypeHandlerSelector((ContentTypeHandlerManager) mockContentTypeHandlerManager.proxy());
MockHttpServletRequest request = new MockHttpServletRequest();
- request.setContent(new byte[] {1});
+ request.setContent("caf\u00e9".getBytes(charset));
request.setCharacterEncoding(charset.name());
ActionContext.of()
@@ -233,4 +235,348 @@ public boolean matches(Object[] args) {
mockActionInvocation.verify();
mockContentTypeHandler.verify();
}
+
+ public void testBodyOverLimitIsRejectedBeforeActionRuns() throws Exception {
+ ContentTypeInterceptor interceptor = new ContentTypeInterceptor();
+ interceptor.setParameterAuthorizer((parameterName, target, action) -> true);
+ interceptor.setMaxLength("8");
+
+ Mock mockActionInvocation = new Mock(ActionInvocation.class);
+ mockActionInvocation.expectAndReturn("getAction", new ActionSupport());
+ interceptor.setContentTypeHandlerSelector(selectorReturning(readingHandler()));
+
+ MockHttpServletRequest request = new MockHttpServletRequest();
+ request.setContent("123456789".getBytes(StandardCharsets.US_ASCII));
+
+ ActionContext.of()
+ .withActionMapping(new ActionMapping())
+ .withServletRequest(request)
+ .bind();
+
+ try {
+ interceptor.intercept((ActionInvocation) mockActionInvocation.proxy());
+ fail("expected " + RequestBodyTooLargeException.class.getSimpleName());
+ } catch (RequestBodyTooLargeException expected) {
+ assertTrue(expected.getMessage().contains(RestConstants.REST_CONTENT_MAX_LENGTH));
+ }
+ mockActionInvocation.verify();
+ }
+
+ public void testBodyAtLimitIsPassedToHandlerInFull() throws Exception {
+ ContentTypeInterceptor interceptor = new ContentTypeInterceptor();
+ interceptor.setParameterAuthorizer((parameterName, target, action) -> true);
+ interceptor.setMaxLength("8");
+
+ assertEquals("12345678", interceptAndCaptureBody(interceptor, new MockHttpServletRequest(),
+ "12345678".getBytes(StandardCharsets.US_ASCII)));
+ }
+
+ public void testBodyOverLimitIsNotReadToTheEnd() throws Exception {
+ ContentTypeInterceptor interceptor = new ContentTypeInterceptor();
+ interceptor.setParameterAuthorizer((parameterName, target, action) -> true);
+ interceptor.setMaxLength("8");
+
+ Mock mockActionInvocation = new Mock(ActionInvocation.class);
+ mockActionInvocation.expectAndReturn("getAction", new ActionSupport());
+ interceptor.setContentTypeHandlerSelector(selectorReturning(readingHandler()));
+
+ byte[] body = new byte[1024 * 1024];
+ Arrays.fill(body, (byte) 'x');
+ CountingRequest request = new CountingRequest();
+ request.setContent(body);
+
+ ActionContext.of()
+ .withActionMapping(new ActionMapping())
+ .withServletRequest(request)
+ .bind();
+
+ try {
+ interceptor.intercept((ActionInvocation) mockActionInvocation.proxy());
+ fail("expected " + RequestBodyTooLargeException.class.getSimpleName());
+ } catch (RequestBodyTooLargeException expected) {
+ assertTrue("read " + request.bytesRead + " of " + body.length + " bytes",
+ request.bytesRead < body.length);
+ }
+ }
+
+ public void testNonNumericMaxLengthKeepsDefault() throws Exception {
+ ContentTypeInterceptor interceptor = new ContentTypeInterceptor();
+ interceptor.setParameterAuthorizer((parameterName, target, action) -> true);
+ interceptor.setMaxLength("lots");
+
+ byte[] body = new byte[64 * 1024];
+ Arrays.fill(body, (byte) 'x');
+ assertEquals(body.length, interceptAndCaptureBody(interceptor, new MockHttpServletRequest(), body).length());
+ }
+
+ public void testMaxLengthBelowOneKeepsDefault() throws Exception {
+ ContentTypeInterceptor interceptor = new ContentTypeInterceptor();
+ interceptor.setParameterAuthorizer((parameterName, target, action) -> true);
+ interceptor.setMaxLength("0");
+
+ assertEquals("abc", interceptAndCaptureBody(interceptor, new MockHttpServletRequest(),
+ "abc".getBytes(StandardCharsets.US_ASCII)));
+ }
+
+ public void testHandlerThatIgnoresTheReaderLeavesBodyUnread() throws Exception {
+ ContentTypeInterceptor interceptor = new ContentTypeInterceptor();
+ interceptor.setParameterAuthorizer((parameterName, target, action) -> true);
+
+ Mock mockActionInvocation = new Mock(ActionInvocation.class);
+ mockActionInvocation.expectAndReturn("invoke", Action.SUCCESS);
+ mockActionInvocation.expectAndReturn("getAction", new ActionSupport());
+ Mock mockContentTypeHandler = new Mock(ContentTypeHandler.class);
+ mockContentTypeHandler.expect("toObject", new AnyConstraintMatcher() {
+ public boolean matches(Object[] args) {
+ return true;
+ }
+ });
+ Mock mockContentTypeHandlerManager = new Mock(ContentTypeHandlerManager.class);
+ mockContentTypeHandlerManager.expectAndReturn("getHandlerForRequest", new AnyConstraintMatcher() {
+ public boolean matches(Object[] args) {
+ return true;
+ }
+ }, mockContentTypeHandler.proxy());
+ interceptor.setContentTypeHandlerSelector((ContentTypeHandlerManager) mockContentTypeHandlerManager.proxy());
+
+ CountingRequest request = new CountingRequest();
+ request.setContent("raw body the action may want to read itself".getBytes(StandardCharsets.US_ASCII));
+
+ ActionContext.of()
+ .withActionMapping(new ActionMapping())
+ .withServletRequest(request)
+ .bind();
+
+ interceptor.intercept((ActionInvocation) mockActionInvocation.proxy());
+ assertEquals(0, request.bytesRead);
+ mockActionInvocation.verify();
+ }
+
+ public void testBlankMaxLengthKeepsDefault() throws Exception {
+ ContentTypeInterceptor interceptor = new ContentTypeInterceptor();
+ interceptor.setParameterAuthorizer((parameterName, target, action) -> true);
+ interceptor.setMaxLength(" ");
+
+ byte[] body = new byte[64 * 1024];
+ Arrays.fill(body, (byte) 'x');
+ assertEquals(body.length, interceptAndCaptureBody(interceptor, new MockHttpServletRequest(), body).length());
+ }
+
+ public void testHandlerThatSwallowsTheLimitIsStillRejected() throws Exception {
+ ContentTypeInterceptor interceptor = new ContentTypeInterceptor();
+ interceptor.setParameterAuthorizer((parameterName, target, action) -> true);
+ interceptor.setMaxLength("8");
+
+ Mock mockActionInvocation = new Mock(ActionInvocation.class);
+ mockActionInvocation.expectAndReturn("getAction", new ActionSupport());
+ Mock swallowingHandler = new Mock(ContentTypeHandler.class);
+ swallowingHandler.expect("toObject", new AnyConstraintMatcher() {
+ public boolean matches(Object[] args) {
+ try {
+ readFully((Reader) args[1]);
+ } catch (RuntimeException swallowed) {
+ // a handler that hides the reader's failure must not let the action run
+ }
+ return true;
+ }
+ });
+ interceptor.setContentTypeHandlerSelector(selectorReturning((ContentTypeHandler) swallowingHandler.proxy()));
+
+ MockHttpServletRequest request = new MockHttpServletRequest();
+ request.setContent("123456789".getBytes(StandardCharsets.US_ASCII));
+ ActionContext.of()
+ .withActionMapping(new ActionMapping())
+ .withServletRequest(request)
+ .bind();
+
+ try {
+ interceptor.intercept((ActionInvocation) mockActionInvocation.proxy());
+ fail("expected " + RequestBodyTooLargeException.class.getSimpleName());
+ } catch (RequestBodyTooLargeException expected) {
+ // action never invoked: no "invoke" expectation was set
+ }
+ mockActionInvocation.verify();
+ }
+
+ public void testHandlerFailureUnderTheLimitPropagatesUnchanged() throws Exception {
+ ContentTypeInterceptor interceptor = new ContentTypeInterceptor();
+ interceptor.setParameterAuthorizer((parameterName, target, action) -> true);
+ interceptor.setMaxLength("8");
+
+ Mock mockActionInvocation = new Mock(ActionInvocation.class);
+ mockActionInvocation.expectAndReturn("getAction", new ActionSupport());
+ IllegalStateException handlerFailure = new IllegalStateException("malformed");
+ Mock failingHandler = new Mock(ContentTypeHandler.class);
+ failingHandler.expect("toObject", new AnyConstraintMatcher() {
+ public boolean matches(Object[] args) {
+ throw handlerFailure;
+ }
+ });
+ interceptor.setContentTypeHandlerSelector(selectorReturning((ContentTypeHandler) failingHandler.proxy()));
+
+ MockHttpServletRequest request = new MockHttpServletRequest();
+ request.setContent("abc".getBytes(StandardCharsets.US_ASCII));
+ ActionContext.of()
+ .withActionMapping(new ActionMapping())
+ .withServletRequest(request)
+ .bind();
+
+ try {
+ interceptor.intercept((ActionInvocation) mockActionInvocation.proxy());
+ fail("expected the handler's own exception");
+ } catch (IllegalStateException e) {
+ assertSame(handlerFailure, e);
+ }
+ }
+
+ public void testSkippingPastTheLimitIsRejected() throws Exception {
+ ContentTypeInterceptor interceptor = new ContentTypeInterceptor();
+ interceptor.setParameterAuthorizer((parameterName, target, action) -> true);
+ interceptor.setMaxLength("8");
+
+ Mock mockActionInvocation = new Mock(ActionInvocation.class);
+ mockActionInvocation.expectAndReturn("getAction", new ActionSupport());
+ Mock skippingHandler = new Mock(ContentTypeHandler.class);
+ skippingHandler.expect("toObject", new AnyConstraintMatcher() {
+ public boolean matches(Object[] args) {
+ try {
+ ((Reader) args[1]).skip(Long.MAX_VALUE);
+ } catch (IOException e) {
+ throw new IllegalStateException(e);
+ }
+ return true;
+ }
+ });
+ interceptor.setContentTypeHandlerSelector(selectorReturning((ContentTypeHandler) skippingHandler.proxy()));
+
+ MockHttpServletRequest request = new MockHttpServletRequest();
+ request.setContent("123456789".getBytes(StandardCharsets.US_ASCII));
+ ActionContext.of()
+ .withActionMapping(new ActionMapping())
+ .withServletRequest(request)
+ .bind();
+
+ try {
+ interceptor.intercept((ActionInvocation) mockActionInvocation.proxy());
+ fail("expected " + RequestBodyTooLargeException.class.getSimpleName());
+ } catch (RequestBodyTooLargeException expected) {
+ // skipped input counts against the limit like read input
+ }
+ mockActionInvocation.verify();
+ }
+
+ /**
+ * A handler that reads the body the way the real ones do, and surfaces the reader's failure in its
+ * own exception type as Jackson, XStream and Juneau each do.
+ */
+ private static ContentTypeHandler readingHandler() {
+ Mock handler = new Mock(ContentTypeHandler.class);
+ handler.expect("toObject", new AnyConstraintMatcher() {
+ public boolean matches(Object[] args) {
+ readFully((Reader) args[1]);
+ return true;
+ }
+ });
+ return (ContentTypeHandler) handler.proxy();
+ }
+
+ private static ContentTypeHandlerManager selectorReturning(ContentTypeHandler handler) {
+ Mock selector = new Mock(ContentTypeHandlerManager.class);
+ selector.expectAndReturn("getHandlerForRequest", new AnyConstraintMatcher() {
+ public boolean matches(Object[] args) {
+ return true;
+ }
+ }, handler);
+ return (ContentTypeHandlerManager) selector.proxy();
+ }
+
+ private static String interceptAndCaptureBody(ContentTypeInterceptor interceptor, MockHttpServletRequest request,
+ byte[] body) throws Exception {
+ String[] captured = new String[1];
+ Mock mockActionInvocation = new Mock(ActionInvocation.class);
+ mockActionInvocation.expectAndReturn("invoke", Action.SUCCESS);
+ mockActionInvocation.expectAndReturn("getAction", new ActionSupport());
+ Mock mockContentTypeHandler = new Mock(ContentTypeHandler.class);
+ mockContentTypeHandler.expect("toObject", new AnyConstraintMatcher() {
+ public boolean matches(Object[] args) {
+ captured[0] = readFully((Reader) args[1]);
+ return true;
+ }
+ });
+ Mock mockContentTypeHandlerManager = new Mock(ContentTypeHandlerManager.class);
+ mockContentTypeHandlerManager.expectAndReturn("getHandlerForRequest", new AnyConstraintMatcher() {
+ public boolean matches(Object[] args) {
+ return true;
+ }
+ }, mockContentTypeHandler.proxy());
+ interceptor.setContentTypeHandlerSelector((ContentTypeHandlerManager) mockContentTypeHandlerManager.proxy());
+
+ request.setContent(body);
+ ActionContext.of()
+ .withActionMapping(new ActionMapping())
+ .withServletRequest(request)
+ .bind();
+
+ interceptor.intercept((ActionInvocation) mockActionInvocation.proxy());
+ mockContentTypeHandler.verify();
+ mockActionInvocation.verify();
+ return captured[0];
+ }
+
+ /** Counts the bytes the interceptor actually pulls from the request stream. */
+ private static final class CountingRequest extends MockHttpServletRequest {
+ long bytesRead;
+
+ @Override
+ public ServletInputStream getInputStream() {
+ ServletInputStream delegate = super.getInputStream();
+ return new ServletInputStream() {
+ @Override
+ public int read() throws IOException {
+ int b = delegate.read();
+ if (b != -1) {
+ bytesRead++;
+ }
+ return b;
+ }
+
+ @Override
+ public int read(byte[] buf, int off, int len) throws IOException {
+ int n = delegate.read(buf, off, len);
+ if (n > 0) {
+ bytesRead += n;
+ }
+ return n;
+ }
+
+ @Override
+ public boolean isFinished() {
+ return delegate.isFinished();
+ }
+
+ @Override
+ public boolean isReady() {
+ return delegate.isReady();
+ }
+
+ @Override
+ public void setReadListener(ReadListener readListener) {
+ delegate.setReadListener(readListener);
+ }
+ };
+ }
+ }
+
+ private static String readFully(Reader reader) {
+ try {
+ StringBuilder out = new StringBuilder();
+ int c;
+ while ((c = reader.read()) != -1) {
+ out.append((char) c);
+ }
+ return out.toString();
+ } catch (IOException e) {
+ throw new IllegalStateException(e);
+ }
+ }
}