diff --git a/org.springframework.web/src/main/java/org/springframework/http/client/AbstractBufferingClientHttpRequest.java b/org.springframework.web/src/main/java/org/springframework/http/client/AbstractBufferingClientHttpRequest.java new file mode 100644 index 0000000000..3048b76009 --- /dev/null +++ b/org.springframework.web/src/main/java/org/springframework/http/client/AbstractBufferingClientHttpRequest.java @@ -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; + + +} diff --git a/org.springframework.web/src/main/java/org/springframework/http/client/AbstractClientHttpRequest.java b/org.springframework.web/src/main/java/org/springframework/http/client/AbstractClientHttpRequest.java index 60afb7d4b9..9af76020ae 100644 --- a/org.springframework.web/src/main/java/org/springframework/http/client/AbstractClientHttpRequest.java +++ b/org.springframework.web/src/main/java/org/springframework/http/client/AbstractClientHttpRequest.java @@ -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"); * you may not use this file except in compliance with the License. @@ -16,7 +16,6 @@ package org.springframework.http.client; -import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.OutputStream; @@ -29,27 +28,32 @@ import org.springframework.util.Assert; * @author Arjen Poutsma * @since 3.0 */ -public abstract class AbstractClientHttpRequest implements ClientHttpRequest { +abstract class AbstractClientHttpRequest implements ClientHttpRequest { private boolean executed = false; private final HttpHeaders headers = new HttpHeaders(); - private final ByteArrayOutputStream bufferedOutput = new ByteArrayOutputStream(); - - public final HttpHeaders getHeaders() { return executed ? HttpHeaders.readOnlyHttpHeaders(headers) : this.headers; } public final OutputStream getBody() throws IOException { 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 { checkExecuted(); - ClientHttpResponse result = executeInternal(this.headers, this.bufferedOutput.toByteArray()); + ClientHttpResponse result = executeInternal(this.headers); this.executed = true; return result; } @@ -58,14 +62,13 @@ public abstract class AbstractClientHttpRequest implements ClientHttpRequest { Assert.state(!this.executed, "ClientHttpRequest already executed"); } - /** * 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; + protected abstract ClientHttpResponse executeInternal(HttpHeaders headers) throws IOException; + } diff --git a/org.springframework.web/src/main/java/org/springframework/http/client/SimpleClientHttpRequest.java b/org.springframework.web/src/main/java/org/springframework/http/client/BufferingSimpleClientHttpRequest.java similarity index 89% rename from org.springframework.web/src/main/java/org/springframework/http/client/SimpleClientHttpRequest.java rename to org.springframework.web/src/main/java/org/springframework/http/client/BufferingSimpleClientHttpRequest.java index 5efc1cc6e2..aea688719b 100644 --- a/org.springframework.web/src/main/java/org/springframework/http/client/SimpleClientHttpRequest.java +++ b/org.springframework.web/src/main/java/org/springframework/http/client/BufferingSimpleClientHttpRequest.java @@ -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"); * 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; /** - * {@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}. * * @author Arjen Poutsma * @since 3.0 * @see SimpleClientHttpRequestFactory#createRequest(java.net.URI, HttpMethod) */ -final class SimpleClientHttpRequest extends AbstractClientHttpRequest { +final class BufferingSimpleClientHttpRequest extends AbstractBufferingClientHttpRequest { private final HttpURLConnection connection; - - SimpleClientHttpRequest(HttpURLConnection connection) { + BufferingSimpleClientHttpRequest(HttpURLConnection connection) { this.connection = connection; } diff --git a/org.springframework.web/src/main/java/org/springframework/http/client/CommonsClientHttpRequest.java b/org.springframework.web/src/main/java/org/springframework/http/client/CommonsClientHttpRequest.java index 268b435994..26c98aa592 100644 --- a/org.springframework.web/src/main/java/org/springframework/http/client/CommonsClientHttpRequest.java +++ b/org.springframework.web/src/main/java/org/springframework/http/client/CommonsClientHttpRequest.java @@ -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"); * you may not use this file except in compliance with the License. @@ -41,7 +41,7 @@ import org.springframework.http.HttpMethod; * @since 3.0 * @see CommonsClientHttpRequestFactory#createRequest(java.net.URI, HttpMethod) */ -final class CommonsClientHttpRequest extends AbstractClientHttpRequest { +final class CommonsClientHttpRequest extends AbstractBufferingClientHttpRequest { private final HttpClient httpClient; diff --git a/org.springframework.web/src/main/java/org/springframework/http/client/SimpleClientHttpRequestFactory.java b/org.springframework.web/src/main/java/org/springframework/http/client/SimpleClientHttpRequestFactory.java index cf864fcfc0..8416d91610 100644 --- a/org.springframework.web/src/main/java/org/springframework/http/client/SimpleClientHttpRequestFactory.java +++ b/org.springframework.web/src/main/java/org/springframework/http/client/SimpleClientHttpRequestFactory.java @@ -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"); * 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 { + private static final int DEFAULT_CHUNK_SIZE = 4096; + private Proxy proxy; + private boolean bufferRequestBody = true; + + private int chunkSize = DEFAULT_CHUNK_SIZE; + /** * Sets the {@link Proxy} to use for this request factory. */ @@ -45,16 +51,48 @@ public class SimpleClientHttpRequestFactory implements ClientHttpRequestFactory this.proxy = proxy; } + /** + * Indicates whether this request factory should buffer the {@linkplain ClientHttpRequest#getBody() request body} + * internally. + *

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; + } + + /** + * Sets the number of bytes to write in each chunk when not buffering request bodies locally. + *

Note that this parameter is only used when {@link #setBufferRequestBody(boolean) bufferRequestBody} is set + * 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()); - return new SimpleClientHttpRequest(connection); + if (bufferRequestBody) { + return new BufferingSimpleClientHttpRequest(connection); + } + else { + return new StreamingSimpleClientHttpRequest(connection, chunkSize); + } } /** - * Opens and returns a connection to the given URL. - *

The default implementation uses the given {@linkplain #setProxy(java.net.Proxy) proxy} - if any - to open a - * connection. + * Opens and returns a connection to the given URL.

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 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}. - *

The default implementation prepares the connection for input and output, and sets the HTTP method. + * Template method for preparing the given {@link HttpURLConnection}.

The default implementation prepares the + * connection for input and output, and sets the HTTP method. * * @param connection the connection to prepare * @param httpMethod the HTTP request method ({@code GET}, {@code POST}, etc.) diff --git a/org.springframework.web/src/main/java/org/springframework/http/client/SimpleClientHttpResponse.java b/org.springframework.web/src/main/java/org/springframework/http/client/SimpleClientHttpResponse.java index 8b7195a7ab..56f23a3d85 100644 --- a/org.springframework.web/src/main/java/org/springframework/http/client/SimpleClientHttpResponse.java +++ b/org.springframework.web/src/main/java/org/springframework/http/client/SimpleClientHttpResponse.java @@ -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"); * 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. - * Obtained via the {@link SimpleClientHttpRequest#execute()}. + * Obtained via {@link BufferingSimpleClientHttpRequest#execute()} and + * {@link StreamingSimpleClientHttpRequest#execute()}. * * @author Arjen Poutsma * @since 3.0 diff --git a/org.springframework.web/src/main/java/org/springframework/http/client/StreamingSimpleClientHttpRequest.java b/org.springframework.web/src/main/java/org/springframework/http/client/StreamingSimpleClientHttpRequest.java new file mode 100644 index 0000000000..2888fea4e7 --- /dev/null +++ b/org.springframework.web/src/main/java/org/springframework/http/client/StreamingSimpleClientHttpRequest.java @@ -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> 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 { + } + } + + +} diff --git a/org.springframework.web/src/main/java/org/springframework/http/converter/FormHttpMessageConverter.java b/org.springframework.web/src/main/java/org/springframework/http/converter/FormHttpMessageConverter.java index bc42350c36..36d4bfc11b 100644 --- a/org.springframework.web/src/main/java/org/springframework/http/converter/FormHttpMessageConverter.java +++ b/org.springframework.web/src/main/java/org/springframework/http/converter/FormHttpMessageConverter.java @@ -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"); * 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.InputStreamReader; import java.io.OutputStream; -import java.io.OutputStreamWriter; import java.io.UnsupportedEncodingException; import java.net.URLDecoder; import java.net.URLEncoder; @@ -94,19 +93,25 @@ public class FormHttpMessageConverter implements HttpMessageConverter partConverter) { Assert.notNull(partConverter, "'partConverter' must not be NULL"); 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> partConverters) { Assert.notEmpty(partConverters, "'partConverters' must not be empty"); 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) { this.charset = charset; } @@ -196,7 +201,8 @@ public class FormHttpMessageConverter implements HttpMessageConverter parts, HttpOutputMessage outputMessage) @@ -328,7 +336,9 @@ public class FormHttpMessageConverter implements HttpMessageConverter result = (MultiValueMap) converter.read(null, inputMessage); + MultiValueMap result = converter.read(null, inputMessage); assertEquals("Invalid result", 3, result.size()); assertEquals("Invalid result", "value 1", result.getFirst("name 1")); @@ -97,19 +96,21 @@ public class FormHttpMessageConverterTests { body.add("name 3", null); MockHttpOutputMessage outputMessage = new MockHttpOutputMessage(); 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", - outputMessage.getBodyAsString(iso88591)); + outputMessage.getBodyAsString(Charset.forName("UTF-8"))); assertEquals("Invalid content-type", new MediaType("application", "x-www-form-urlencoded"), outputMessage.getHeaders().getContentType()); + assertEquals("Invalid content-length", outputMessage.getBodyAsBytes().length, + outputMessage.getHeaders().getContentLength()); } - + @Test public void writeMultipart() throws Exception { MultiValueMap parts = new LinkedMultiValueMap(); parts.add("name 1", "value 1"); parts.add("name 2", "value 2+1"); parts.add("name 2", "value 2+2"); + Resource logo = new ClassPathResource("/org/springframework/http/converter/logo.jpg"); parts.add("logo", logo); Source xml = new StreamSource(new StringReader("")); @@ -122,7 +123,7 @@ public class FormHttpMessageConverterTests { converter.write(parts, MediaType.MULTIPART_FORM_DATA, outputMessage); 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 FileItemFactory fileItemFactory = new DiskFileItemFactory(); @@ -157,6 +158,7 @@ public class FormHttpMessageConverterTests { } private static class MockHttpOutputMessageRequestContext implements RequestContext { + private final MockHttpOutputMessage outputMessage; private MockHttpOutputMessageRequestContext(MockHttpOutputMessage outputMessage) {