SPR-7909 - Sending large payloads with RestTemplate results in an OutOfMemoryError

This commit is contained in:
Arjen Poutsma 2011-01-25 16:08:15 +00:00
parent 33674933ea
commit d0d6a07870
12 changed files with 388 additions and 65 deletions

View File

@ -0,0 +1,61 @@
/*
* Copyright 2002-2011 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.http.client;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import org.springframework.http.HttpHeaders;
/**
* Abstract base for {@link ClientHttpRequest} that buffers output in a byte array before sending it over the wire.
*
* @author Arjen Poutsma
* @since 3.0.6
*/
abstract class AbstractBufferingClientHttpRequest extends AbstractClientHttpRequest {
private ByteArrayOutputStream bufferedOutput = new ByteArrayOutputStream();
@Override
protected OutputStream getBodyInternal(HttpHeaders headers) throws IOException {
return this.bufferedOutput;
}
@Override
protected ClientHttpResponse executeInternal(HttpHeaders headers) throws IOException {
byte[] bytes = this.bufferedOutput.toByteArray();
if (headers.getContentLength() == -1) {
headers.setContentLength(bytes.length);
}
ClientHttpResponse result = executeInternal(headers, bytes);
this.bufferedOutput = null;
return result;
}
/**
* Abstract template method that writes the given headers and content to the HTTP request.
* @param headers the HTTP headers
* @param bufferedOutput the body content
* @return the response object for the executed request
*/
protected abstract ClientHttpResponse executeInternal(HttpHeaders headers, byte[] bufferedOutput)
throws IOException;
}

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2010 the original author or authors. * Copyright 2002-2011 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -16,7 +16,6 @@
package org.springframework.http.client; package org.springframework.http.client;
import java.io.ByteArrayOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.OutputStream; import java.io.OutputStream;
@ -29,27 +28,32 @@ import org.springframework.util.Assert;
* @author Arjen Poutsma * @author Arjen Poutsma
* @since 3.0 * @since 3.0
*/ */
public abstract class AbstractClientHttpRequest implements ClientHttpRequest { abstract class AbstractClientHttpRequest implements ClientHttpRequest {
private boolean executed = false; private boolean executed = false;
private final HttpHeaders headers = new HttpHeaders(); private final HttpHeaders headers = new HttpHeaders();
private final ByteArrayOutputStream bufferedOutput = new ByteArrayOutputStream();
public final HttpHeaders getHeaders() { public final HttpHeaders getHeaders() {
return executed ? HttpHeaders.readOnlyHttpHeaders(headers) : this.headers; return executed ? HttpHeaders.readOnlyHttpHeaders(headers) : this.headers;
} }
public final OutputStream getBody() throws IOException { public final OutputStream getBody() throws IOException {
checkExecuted(); checkExecuted();
return this.bufferedOutput; return getBodyInternal(this.headers);
} }
/**
* Abstract template method that returns the body.
*
* @param headers the HTTP headers
* @return the body output stream
*/
protected abstract OutputStream getBodyInternal(HttpHeaders headers) throws IOException;
public final ClientHttpResponse execute() throws IOException { public final ClientHttpResponse execute() throws IOException {
checkExecuted(); checkExecuted();
ClientHttpResponse result = executeInternal(this.headers, this.bufferedOutput.toByteArray()); ClientHttpResponse result = executeInternal(this.headers);
this.executed = true; this.executed = true;
return result; return result;
} }
@ -58,14 +62,13 @@ public abstract class AbstractClientHttpRequest implements ClientHttpRequest {
Assert.state(!this.executed, "ClientHttpRequest already executed"); Assert.state(!this.executed, "ClientHttpRequest already executed");
} }
/** /**
* Abstract template method that writes the given headers and content to the HTTP request. * Abstract template method that writes the given headers and content to the HTTP request.
*
* @param headers the HTTP headers * @param headers the HTTP headers
* @param bufferedOutput the body content
* @return the response object for the executed request * @return the response object for the executed request
*/ */
protected abstract ClientHttpResponse executeInternal(HttpHeaders headers, byte[] bufferedOutput) protected abstract ClientHttpResponse executeInternal(HttpHeaders headers) throws IOException;
throws IOException;
} }

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2010 the original author or authors. * Copyright 2002-2011 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -28,19 +28,18 @@ import org.springframework.http.HttpMethod;
import org.springframework.util.FileCopyUtils; import org.springframework.util.FileCopyUtils;
/** /**
* {@link ClientHttpRequest} implementation that uses standard J2SE facilities to execute requests. * {@link ClientHttpRequest} implementation that uses standard J2SE facilities to execute buffered requests.
* Created via the {@link SimpleClientHttpRequestFactory}. * Created via the {@link SimpleClientHttpRequestFactory}.
* *
* @author Arjen Poutsma * @author Arjen Poutsma
* @since 3.0 * @since 3.0
* @see SimpleClientHttpRequestFactory#createRequest(java.net.URI, HttpMethod) * @see SimpleClientHttpRequestFactory#createRequest(java.net.URI, HttpMethod)
*/ */
final class SimpleClientHttpRequest extends AbstractClientHttpRequest { final class BufferingSimpleClientHttpRequest extends AbstractBufferingClientHttpRequest {
private final HttpURLConnection connection; private final HttpURLConnection connection;
BufferingSimpleClientHttpRequest(HttpURLConnection connection) {
SimpleClientHttpRequest(HttpURLConnection connection) {
this.connection = connection; this.connection = connection;
} }

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2010 the original author or authors. * Copyright 2002-2011 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -41,7 +41,7 @@ import org.springframework.http.HttpMethod;
* @since 3.0 * @since 3.0
* @see CommonsClientHttpRequestFactory#createRequest(java.net.URI, HttpMethod) * @see CommonsClientHttpRequestFactory#createRequest(java.net.URI, HttpMethod)
*/ */
final class CommonsClientHttpRequest extends AbstractClientHttpRequest { final class CommonsClientHttpRequest extends AbstractBufferingClientHttpRequest {
private final HttpClient httpClient; private final HttpClient httpClient;

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2010 the original author or authors. * Copyright 2002-2011 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -36,8 +36,14 @@ import org.springframework.util.Assert;
*/ */
public class SimpleClientHttpRequestFactory implements ClientHttpRequestFactory { public class SimpleClientHttpRequestFactory implements ClientHttpRequestFactory {
private static final int DEFAULT_CHUNK_SIZE = 4096;
private Proxy proxy; private Proxy proxy;
private boolean bufferRequestBody = true;
private int chunkSize = DEFAULT_CHUNK_SIZE;
/** /**
* Sets the {@link Proxy} to use for this request factory. * Sets the {@link Proxy} to use for this request factory.
*/ */
@ -45,16 +51,48 @@ public class SimpleClientHttpRequestFactory implements ClientHttpRequestFactory
this.proxy = proxy; this.proxy = proxy;
} }
public ClientHttpRequest createRequest(URI uri, HttpMethod httpMethod) throws IOException { /**
HttpURLConnection connection = openConnection(uri.toURL(), proxy); * Indicates whether this request factory should buffer the {@linkplain ClientHttpRequest#getBody() request body}
prepareConnection(connection, httpMethod.name()); * internally.
return new SimpleClientHttpRequest(connection); * <p>Default is {@code true}. When sending large amounts of data via POST or PUT, it is recommended to change this
* property to {@code false}, so as not to run out of memory. This will result in a {@link ClientHttpRequest}
* that either streams directly to the underlying {@link HttpURLConnection} (if the
* {@link org.springframework.http.HttpHeaders#getContentLength() Content-Length} is known in advance), or that will
* use "Chunked transfer encoding" (if the {@code Content-Length} is not known in advance).
*
* @see #setChunkSize(int)
* @see HttpURLConnection#setFixedLengthStreamingMode(int)
*/
public void setBufferRequestBody(boolean bufferRequestBody) {
this.bufferRequestBody = bufferRequestBody;
} }
/** /**
* Opens and returns a connection to the given URL. * Sets the number of bytes to write in each chunk when not buffering request bodies locally.
* <p>The default implementation uses the given {@linkplain #setProxy(java.net.Proxy) proxy} - if any - to open a * <p>Note that this parameter is only used when {@link #setBufferRequestBody(boolean) bufferRequestBody} is set
* connection. * to {@code false}, and the {@link org.springframework.http.HttpHeaders#getContentLength() Content-Length}
* is not known in advance.
*
* @see #setBufferRequestBody(boolean)
*/
public void setChunkSize(int chunkSize) {
this.chunkSize = chunkSize;
}
public ClientHttpRequest createRequest(URI uri, HttpMethod httpMethod) throws IOException {
HttpURLConnection connection = openConnection(uri.toURL(), proxy);
prepareConnection(connection, httpMethod.name());
if (bufferRequestBody) {
return new BufferingSimpleClientHttpRequest(connection);
}
else {
return new StreamingSimpleClientHttpRequest(connection, chunkSize);
}
}
/**
* Opens and returns a connection to the given URL. <p>The default implementation uses the given {@linkplain
* #setProxy(java.net.Proxy) proxy} - if any - to open a connection.
* *
* @param url the URL to open a connection to * @param url the URL to open a connection to
* @param proxy the proxy to use, may be {@code null} * @param proxy the proxy to use, may be {@code null}
@ -68,8 +106,8 @@ public class SimpleClientHttpRequestFactory implements ClientHttpRequestFactory
} }
/** /**
* Template method for preparing the given {@link HttpURLConnection}. * Template method for preparing the given {@link HttpURLConnection}. <p>The default implementation prepares the
* <p>The default implementation prepares the connection for input and output, and sets the HTTP method. * connection for input and output, and sets the HTTP method.
* *
* @param connection the connection to prepare * @param connection the connection to prepare
* @param httpMethod the HTTP request method ({@code GET}, {@code POST}, etc.) * @param httpMethod the HTTP request method ({@code GET}, {@code POST}, etc.)

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2010 the original author or authors. * Copyright 2002-2011 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -26,7 +26,8 @@ import org.springframework.util.StringUtils;
/** /**
* {@link ClientHttpResponse} implementation that uses standard J2SE facilities. * {@link ClientHttpResponse} implementation that uses standard J2SE facilities.
* Obtained via the {@link SimpleClientHttpRequest#execute()}. * Obtained via {@link BufferingSimpleClientHttpRequest#execute()} and
* {@link StreamingSimpleClientHttpRequest#execute()}.
* *
* @author Arjen Poutsma * @author Arjen Poutsma
* @since 3.0 * @since 3.0

View File

@ -0,0 +1,112 @@
/*
* Copyright 2002-2011 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.http.client;
import java.io.FilterOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.List;
import java.util.Map;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
/**
* {@link ClientHttpRequest} implementation that uses standard J2SE facilities to execute streaming requests.
* Created via the {@link SimpleClientHttpRequestFactory}.
*
* @author Arjen Poutsma
* @since 3.0
* @see SimpleClientHttpRequestFactory#createRequest(java.net.URI, HttpMethod)
*/
public class StreamingSimpleClientHttpRequest extends AbstractClientHttpRequest {
private final HttpURLConnection connection;
private final int chunkSize;
private OutputStream body;
StreamingSimpleClientHttpRequest(HttpURLConnection connection, int chunkSize) {
this.connection = connection;
this.chunkSize = chunkSize;
}
public HttpMethod getMethod() {
return HttpMethod.valueOf(this.connection.getRequestMethod());
}
public URI getURI() {
try {
return this.connection.getURL().toURI();
}
catch (URISyntaxException ex) {
throw new IllegalStateException("Could not get HttpURLConnection URI: " + ex.getMessage(), ex);
}
}
@Override
protected OutputStream getBodyInternal(HttpHeaders headers) throws IOException {
if (body == null) {
int contentLength = (int) headers.getContentLength();
if (contentLength >= 0) {
this.connection.setFixedLengthStreamingMode(contentLength);
}
else {
this.connection.setChunkedStreamingMode(chunkSize);
}
for (Map.Entry<String, List<String>> entry : headers.entrySet()) {
String headerName = entry.getKey();
for (String headerValue : entry.getValue()) {
this.connection.addRequestProperty(headerName, headerValue);
}
}
this.connection.connect();
this.body = this.connection.getOutputStream();
}
return new NonClosingOutputStream(body);
}
@Override
protected ClientHttpResponse executeInternal(HttpHeaders headers) throws IOException {
try {
if (body != null) {
body.close();
}
}
catch (IOException ex) {
// ignore
}
return new SimpleClientHttpResponse(this.connection);
}
private static class NonClosingOutputStream extends FilterOutputStream {
private NonClosingOutputStream(OutputStream out) {
super(out);
}
@Override
public void close() throws IOException {
}
}
}

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2010 the original author or authors. * Copyright 2002-2011 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -19,7 +19,6 @@ package org.springframework.http.converter;
import java.io.IOException; import java.io.IOException;
import java.io.InputStreamReader; import java.io.InputStreamReader;
import java.io.OutputStream; import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.UnsupportedEncodingException; import java.io.UnsupportedEncodingException;
import java.net.URLDecoder; import java.net.URLDecoder;
import java.net.URLEncoder; import java.net.URLEncoder;
@ -94,19 +93,25 @@ public class FormHttpMessageConverter implements HttpMessageConverter<MultiValue
this.partConverters.add(new ResourceHttpMessageConverter()); this.partConverters.add(new ResourceHttpMessageConverter());
} }
/** Add a message body converter. Such a converters is used to convert objects to MIME parts. */ /**
* Add a message body converter. Such a converters is used to convert objects to MIME parts.
*/
public final void addPartConverter(HttpMessageConverter<?> partConverter) { public final void addPartConverter(HttpMessageConverter<?> partConverter) {
Assert.notNull(partConverter, "'partConverter' must not be NULL"); Assert.notNull(partConverter, "'partConverter' must not be NULL");
this.partConverters.add(partConverter); this.partConverters.add(partConverter);
} }
/** Set the message body converters to use. These converters are used to convert objects to MIME parts. */ /**
* Set the message body converters to use. These converters are used to convert objects to MIME parts.
*/
public final void setPartConverters(List<HttpMessageConverter<?>> partConverters) { public final void setPartConverters(List<HttpMessageConverter<?>> partConverters) {
Assert.notEmpty(partConverters, "'partConverters' must not be empty"); Assert.notEmpty(partConverters, "'partConverters' must not be empty");
this.partConverters = partConverters; this.partConverters = partConverters;
} }
/** Sets the character set used for writing form data. */ /**
* Sets the character set used for writing form data.
*/
public void setCharset(Charset charset) { public void setCharset(Charset charset) {
this.charset = charset; this.charset = charset;
} }
@ -196,7 +201,8 @@ public class FormHttpMessageConverter implements HttpMessageConverter<MultiValue
if (contentType != null) { if (contentType != null) {
outputMessage.getHeaders().setContentType(contentType); outputMessage.getHeaders().setContentType(contentType);
charset = contentType.getCharSet() != null ? contentType.getCharSet() : this.charset; charset = contentType.getCharSet() != null ? contentType.getCharSet() : this.charset;
} else { }
else {
outputMessage.getHeaders().setContentType(MediaType.APPLICATION_FORM_URLENCODED); outputMessage.getHeaders().setContentType(MediaType.APPLICATION_FORM_URLENCODED);
charset = this.charset; charset = this.charset;
} }
@ -218,7 +224,9 @@ public class FormHttpMessageConverter implements HttpMessageConverter<MultiValue
builder.append('&'); builder.append('&');
} }
} }
FileCopyUtils.copy(builder.toString(), new OutputStreamWriter(outputMessage.getBody(), charset)); byte[] bytes = builder.toString().getBytes(charset.name());
outputMessage.getHeaders().setContentLength(bytes.length);
FileCopyUtils.copy(bytes, outputMessage.getBody());
} }
private void writeMultipart(MultiValueMap<String, Object> parts, HttpOutputMessage outputMessage) private void writeMultipart(MultiValueMap<String, Object> parts, HttpOutputMessage outputMessage)
@ -328,7 +336,9 @@ public class FormHttpMessageConverter implements HttpMessageConverter<MultiValue
} }
} }
/** Implementation of {@link org.springframework.http.HttpOutputMessage} used for writing multipart data. */ /**
* Implementation of {@link org.springframework.http.HttpOutputMessage} used for writing multipart data.
*/
private class MultipartHttpOutputMessage implements HttpOutputMessage { private class MultipartHttpOutputMessage implements HttpOutputMessage {
private final HttpHeaders headers = new HttpHeaders(); private final HttpHeaders headers = new HttpHeaders();

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2010 the original author or authors. * Copyright 2002-2011 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -17,6 +17,7 @@
package org.springframework.http.client; package org.springframework.http.client;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream;
import java.net.URI; import java.net.URI;
import java.util.Arrays; import java.util.Arrays;
import java.util.Enumeration; import java.util.Enumeration;
@ -45,12 +46,12 @@ import static org.junit.Assert.*;
public abstract class AbstractHttpRequestFactoryTestCase { public abstract class AbstractHttpRequestFactoryTestCase {
private ClientHttpRequestFactory factory; protected ClientHttpRequestFactory factory;
protected static String baseUrl;
private static Server jettyServer; private static Server jettyServer;
private static String baseUrl;
@BeforeClass @BeforeClass
public static void startJettyServer() throws Exception { public static void startJettyServer() throws Exception {
int port = FreePortScanner.getFreePort(); int port = FreePortScanner.getFreePort();
@ -64,7 +65,7 @@ public abstract class AbstractHttpRequestFactoryTestCase {
jettyContext.addServlet(new ServletHolder(new MethodServlet("GET")), "/methods/get"); jettyContext.addServlet(new ServletHolder(new MethodServlet("GET")), "/methods/get");
jettyContext.addServlet(new ServletHolder(new MethodServlet("HEAD")), "/methods/head"); jettyContext.addServlet(new ServletHolder(new MethodServlet("HEAD")), "/methods/head");
jettyContext.addServlet(new ServletHolder(new MethodServlet("OPTIONS")), "/methods/options"); jettyContext.addServlet(new ServletHolder(new MethodServlet("OPTIONS")), "/methods/options");
jettyContext.addServlet(new ServletHolder(new MethodServlet("POST")), "/methods/post"); jettyContext.addServlet(new ServletHolder(new PostServlet()), "/methods/post");
jettyContext.addServlet(new ServletHolder(new MethodServlet("PUT")), "/methods/put"); jettyContext.addServlet(new ServletHolder(new MethodServlet("PUT")), "/methods/put");
jettyContext.addServlet(new ServletHolder(new RedirectServlet("/status/ok")), "/redirect"); jettyContext.addServlet(new ServletHolder(new RedirectServlet("/status/ok")), "/redirect");
jettyServer.start(); jettyServer.start();
@ -87,8 +88,7 @@ public abstract class AbstractHttpRequestFactoryTestCase {
@Test @Test
public void status() throws Exception { public void status() throws Exception {
URI uri = new URI(baseUrl + "/status/notfound"); URI uri = new URI(baseUrl + "/status/notfound");
ClientHttpRequest request = ClientHttpRequest request = factory.createRequest(uri, HttpMethod.GET);
factory.createRequest(uri, HttpMethod.GET);
assertEquals("Invalid HTTP method", HttpMethod.GET, request.getMethod()); assertEquals("Invalid HTTP method", HttpMethod.GET, request.getMethod());
assertEquals("Invalid HTTP URI", uri, request.getURI()); assertEquals("Invalid HTTP URI", uri, request.getURI());
ClientHttpResponse response = request.execute(); ClientHttpResponse response = request.execute();
@ -105,6 +105,7 @@ public abstract class AbstractHttpRequestFactoryTestCase {
String headerValue2 = "value2"; String headerValue2 = "value2";
request.getHeaders().add(headerName, headerValue2); request.getHeaders().add(headerName, headerValue2);
byte[] body = "Hello World".getBytes("UTF-8"); byte[] body = "Hello World".getBytes("UTF-8");
request.getHeaders().setContentLength(body.length);
FileCopyUtils.copy(body, request.getBody()); FileCopyUtils.copy(body, request.getBody());
ClientHttpResponse response = request.execute(); ClientHttpResponse response = request.execute();
assertEquals("Invalid status code", HttpStatus.OK, response.getStatusCode()); assertEquals("Invalid status code", HttpStatus.OK, response.getStatusCode());
@ -159,6 +160,7 @@ public abstract class AbstractHttpRequestFactoryTestCase {
try { try {
ClientHttpRequest request = factory.createRequest(new URI(baseUrl + "/methods/" + path), method); ClientHttpRequest request = factory.createRequest(new URI(baseUrl + "/methods/" + path), method);
response = request.execute(); response = request.execute();
assertEquals("Invalid response status", HttpStatus.OK, response.getStatusCode());
assertEquals("Invalid method", path.toUpperCase(Locale.ENGLISH), request.getMethod().name()); assertEquals("Invalid method", path.toUpperCase(Locale.ENGLISH), request.getMethod().name());
} }
finally { finally {
@ -167,16 +169,18 @@ public abstract class AbstractHttpRequestFactoryTestCase {
} }
} }
} }
@Test @Test
public void redirect() throws Exception { public void redirect() throws Exception {
ClientHttpResponse response = null; ClientHttpResponse response = null;
try { try {
ClientHttpRequest request = factory.createRequest(new URI(baseUrl + "/redirect"), HttpMethod.PUT); ClientHttpRequest request = factory.createRequest(new URI(baseUrl + "/redirect"), HttpMethod.PUT);
response = request.execute(); response = request.execute();
assertEquals("Invalid Location value", new URI(baseUrl + "/status/ok"), response.getHeaders().getLocation()); assertEquals("Invalid Location value", new URI(baseUrl + "/status/ok"),
response.getHeaders().getLocation());
} finally { }
finally {
if (response != null) { if (response != null) {
response.close(); response.close();
response = null; response = null;
@ -187,13 +191,14 @@ public abstract class AbstractHttpRequestFactoryTestCase {
response = request.execute(); response = request.execute();
assertNull("Invalid Location value", response.getHeaders().getLocation()); assertNull("Invalid Location value", response.getHeaders().getLocation());
} finally { }
finally {
if (response != null) { if (response != null) {
response.close(); response.close();
} }
} }
} }
/** Servlet that sets a given status code. */ /** Servlet that sets a given status code. */
private static class StatusServlet extends GenericServlet { private static class StatusServlet extends GenericServlet {
@ -221,6 +226,31 @@ public abstract class AbstractHttpRequestFactoryTestCase {
public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException { public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {
HttpServletRequest httpReq = (HttpServletRequest) req; HttpServletRequest httpReq = (HttpServletRequest) req;
assertEquals("Invalid HTTP method", method, httpReq.getMethod()); assertEquals("Invalid HTTP method", method, httpReq.getMethod());
res.setContentLength(0);
((HttpServletResponse) res).setStatus(200);
}
}
private static class PostServlet extends MethodServlet {
private PostServlet() {
super("POST");
}
@Override
public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {
super.service(req, res);
long contentLength = req.getContentLength();
if (contentLength != -1) {
InputStream in = req.getInputStream();
long byteCount = 0;
byte[] buffer = new byte[4096];
int bytesRead;
while ((bytesRead = in.read(buffer)) != -1) {
byteCount += bytesRead;
}
assertEquals("Invalid content-length", contentLength, byteCount);
}
} }
} }

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2009 the original author or authors. * Copyright 2002-2011 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -16,7 +16,7 @@
package org.springframework.http.client; package org.springframework.http.client;
public class SimpleHttpRequestFactoryTests extends AbstractHttpRequestFactoryTestCase { public class BufferedSimpleHttpRequestFactoryTests extends AbstractHttpRequestFactoryTestCase {
@Override @Override
protected ClientHttpRequestFactory createRequestFactory() { protected ClientHttpRequestFactory createRequestFactory() {

View File

@ -0,0 +1,67 @@
/*
* Copyright 2002-2011 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.http.client;
import java.io.OutputStream;
import java.net.URI;
import java.util.Random;
import org.junit.Ignore;
import org.junit.Test;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import static org.junit.Assert.*;
public class StreamingSimpleHttpRequestFactoryTests extends AbstractHttpRequestFactoryTestCase {
@Override
protected ClientHttpRequestFactory createRequestFactory() {
SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
factory.setStreaming(true);
return factory;
}
@Test
@Ignore
public void largeFileUpload() throws Exception {
Random rnd = new Random();
ClientHttpResponse response = null;
try {
ClientHttpRequest request = factory.createRequest(new URI(baseUrl + "/methods/post"), HttpMethod.POST);
final int BUF_SIZE = 4096;
final int ITERATIONS = Integer.MAX_VALUE / BUF_SIZE;
final int contentLength = ITERATIONS * BUF_SIZE;
// request.getHeaders().setContentLength(contentLength);
OutputStream body = request.getBody();
for (int i=0; i < ITERATIONS; i++) {
byte[] buffer = new byte[BUF_SIZE];
rnd.nextBytes(buffer);
body.write(buffer);
}
response = request.execute();
assertEquals("Invalid response status", HttpStatus.OK, response.getStatusCode());
}
finally {
if (response != null) {
response.close();
}
}
}
}

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2010 the original author or authors. * Copyright 2002-2011 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -30,7 +30,6 @@ import org.apache.commons.fileupload.FileItemFactory;
import org.apache.commons.fileupload.FileUpload; import org.apache.commons.fileupload.FileUpload;
import org.apache.commons.fileupload.RequestContext; import org.apache.commons.fileupload.RequestContext;
import org.apache.commons.fileupload.disk.DiskFileItemFactory; import org.apache.commons.fileupload.disk.DiskFileItemFactory;
import static org.junit.Assert.*;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
@ -45,9 +44,9 @@ import org.springframework.http.converter.xml.XmlAwareFormHttpMessageConverter;
import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap; import org.springframework.util.MultiValueMap;
/** import static org.junit.Assert.*;
* @author Arjen Poutsma
*/ /** @author Arjen Poutsma */
public class FormHttpMessageConverterTests { public class FormHttpMessageConverterTests {
private FormHttpMessageConverter converter; private FormHttpMessageConverter converter;
@ -60,13 +59,13 @@ public class FormHttpMessageConverterTests {
@Test @Test
public void canRead() { public void canRead() {
assertTrue(converter.canRead(MultiValueMap.class, new MediaType("application", "x-www-form-urlencoded"))); assertTrue(converter.canRead(MultiValueMap.class, new MediaType("application", "x-www-form-urlencoded")));
assertFalse(converter.canRead(MultiValueMap.class, new MediaType("multipart","form-data"))); assertFalse(converter.canRead(MultiValueMap.class, new MediaType("multipart", "form-data")));
} }
@Test @Test
public void canWrite() { public void canWrite() {
assertTrue(converter.canWrite(MultiValueMap.class, new MediaType("application", "x-www-form-urlencoded"))); assertTrue(converter.canWrite(MultiValueMap.class, new MediaType("application", "x-www-form-urlencoded")));
assertTrue(converter.canWrite(MultiValueMap.class, new MediaType("multipart","form-data"))); assertTrue(converter.canWrite(MultiValueMap.class, new MediaType("multipart", "form-data")));
assertTrue(converter.canWrite(MultiValueMap.class, MediaType.ALL)); assertTrue(converter.canWrite(MultiValueMap.class, MediaType.ALL));
} }
@ -77,7 +76,7 @@ public class FormHttpMessageConverterTests {
Charset iso88591 = Charset.forName("ISO-8859-1"); Charset iso88591 = Charset.forName("ISO-8859-1");
MockHttpInputMessage inputMessage = new MockHttpInputMessage(body.getBytes(iso88591)); MockHttpInputMessage inputMessage = new MockHttpInputMessage(body.getBytes(iso88591));
inputMessage.getHeaders().setContentType(new MediaType("application", "x-www-form-urlencoded", iso88591)); inputMessage.getHeaders().setContentType(new MediaType("application", "x-www-form-urlencoded", iso88591));
MultiValueMap<String, String> result = (MultiValueMap<String, String>) converter.read(null, inputMessage); MultiValueMap<String, String> result = converter.read(null, inputMessage);
assertEquals("Invalid result", 3, result.size()); assertEquals("Invalid result", 3, result.size());
assertEquals("Invalid result", "value 1", result.getFirst("name 1")); assertEquals("Invalid result", "value 1", result.getFirst("name 1"));
@ -97,19 +96,21 @@ public class FormHttpMessageConverterTests {
body.add("name 3", null); body.add("name 3", null);
MockHttpOutputMessage outputMessage = new MockHttpOutputMessage(); MockHttpOutputMessage outputMessage = new MockHttpOutputMessage();
converter.write(body, MediaType.APPLICATION_FORM_URLENCODED, outputMessage); converter.write(body, MediaType.APPLICATION_FORM_URLENCODED, outputMessage);
Charset iso88591 = Charset.forName("ISO-8859-1");
assertEquals("Invalid result", "name+1=value+1&name+2=value+2%2B1&name+2=value+2%2B2&name+3", assertEquals("Invalid result", "name+1=value+1&name+2=value+2%2B1&name+2=value+2%2B2&name+3",
outputMessage.getBodyAsString(iso88591)); outputMessage.getBodyAsString(Charset.forName("UTF-8")));
assertEquals("Invalid content-type", new MediaType("application", "x-www-form-urlencoded"), assertEquals("Invalid content-type", new MediaType("application", "x-www-form-urlencoded"),
outputMessage.getHeaders().getContentType()); outputMessage.getHeaders().getContentType());
assertEquals("Invalid content-length", outputMessage.getBodyAsBytes().length,
outputMessage.getHeaders().getContentLength());
} }
@Test @Test
public void writeMultipart() throws Exception { public void writeMultipart() throws Exception {
MultiValueMap<String, Object> parts = new LinkedMultiValueMap<String, Object>(); MultiValueMap<String, Object> parts = new LinkedMultiValueMap<String, Object>();
parts.add("name 1", "value 1"); parts.add("name 1", "value 1");
parts.add("name 2", "value 2+1"); parts.add("name 2", "value 2+1");
parts.add("name 2", "value 2+2"); parts.add("name 2", "value 2+2");
Resource logo = new ClassPathResource("/org/springframework/http/converter/logo.jpg"); Resource logo = new ClassPathResource("/org/springframework/http/converter/logo.jpg");
parts.add("logo", logo); parts.add("logo", logo);
Source xml = new StreamSource(new StringReader("<root><child/></root>")); Source xml = new StreamSource(new StringReader("<root><child/></root>"));
@ -122,7 +123,7 @@ public class FormHttpMessageConverterTests {
converter.write(parts, MediaType.MULTIPART_FORM_DATA, outputMessage); converter.write(parts, MediaType.MULTIPART_FORM_DATA, outputMessage);
final MediaType contentType = outputMessage.getHeaders().getContentType(); final MediaType contentType = outputMessage.getHeaders().getContentType();
assertNotNull(contentType.getParameter("boundary")); assertNotNull("No boundary found", contentType.getParameter("boundary"));
// see if Commons FileUpload can read what we wrote // see if Commons FileUpload can read what we wrote
FileItemFactory fileItemFactory = new DiskFileItemFactory(); FileItemFactory fileItemFactory = new DiskFileItemFactory();
@ -157,6 +158,7 @@ public class FormHttpMessageConverterTests {
} }
private static class MockHttpOutputMessageRequestContext implements RequestContext { private static class MockHttpOutputMessageRequestContext implements RequestContext {
private final MockHttpOutputMessage outputMessage; private final MockHttpOutputMessage outputMessage;
private MockHttpOutputMessageRequestContext(MockHttpOutputMessage outputMessage) { private MockHttpOutputMessageRequestContext(MockHttpOutputMessage outputMessage) {