diff --git a/spring-core/src/main/java/org/springframework/util/FileCopyUtils.java b/spring-core/src/main/java/org/springframework/util/FileCopyUtils.java index 5aac3090866..2c14da3b517 100644 --- a/spring-core/src/main/java/org/springframework/util/FileCopyUtils.java +++ b/spring-core/src/main/java/org/springframework/util/FileCopyUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-2013 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. @@ -31,19 +31,19 @@ import java.io.StringWriter; import java.io.Writer; /** - * Simple utility methods for file and stream copying. - * All copy methods use a block size of 4096 bytes, - * and close all affected streams when done. + * Simple utility methods for file and stream copying. All copy methods use a block size + * of 4096 bytes, and close all affected streams when done. A variation of the copy + * methods from this class that leave streams open can be found in {@link StreamUtils}. * - *

Mainly for use within the framework, - * but also useful for application code. + *

Mainly for use within the framework, but also useful for application code. * * @author Juergen Hoeller * @since 06.10.2003 + * @see StreamUtils */ public abstract class FileCopyUtils { - public static final int BUFFER_SIZE = 4096; + public static final int BUFFER_SIZE = StreamUtils.BUFFER_SIZE; //--------------------------------------------------------------------- @@ -106,15 +106,7 @@ public abstract class FileCopyUtils { Assert.notNull(in, "No InputStream specified"); Assert.notNull(out, "No OutputStream specified"); try { - int byteCount = 0; - byte[] buffer = new byte[BUFFER_SIZE]; - int bytesRead = -1; - while ((bytesRead = in.read(buffer)) != -1) { - out.write(buffer, 0, bytesRead); - byteCount += bytesRead; - } - out.flush(); - return byteCount; + return StreamUtils.copy(in, out); } finally { try { @@ -208,7 +200,7 @@ public abstract class FileCopyUtils { /** * Copy the contents of the given String to the given output Writer. - * Closes the write when done. + * Closes the writer when done. * @param in the String to copy from * @param out the Writer to copy to * @throws IOException in case of I/O errors diff --git a/spring-core/src/main/java/org/springframework/util/StreamUtils.java b/spring-core/src/main/java/org/springframework/util/StreamUtils.java new file mode 100644 index 00000000000..8adf07159ec --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/StreamUtils.java @@ -0,0 +1,183 @@ +/* + * Copyright 2002-2013 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.util; + +import java.io.ByteArrayOutputStream; +import java.io.FilterInputStream; +import java.io.FilterOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.Writer; +import java.nio.charset.Charset; + + +/** + * Simple utility methods for dealing with streams. The copy methods of this class are + * similar to those defined in {@link FileCopyUtils} except that all affected streams are + * left open when done. All copy methods use a block size of 4096 bytes. + * + *

Mainly for use within the framework, but also useful for application code. + * + * @author Juergen Hoeller + * @author Phillip Webb + * @since 3.2 + * @see FileCopyUtils + */ +public abstract class StreamUtils { + + public static final int BUFFER_SIZE = 4096; + + + /** + * Copy the contents of the given InputStream into a new byte array. + * Leaves the stream open when done. + * @param in the stream to copy from + * @return the new byte array that has been copied to + * @throws IOException in case of I/O errors + */ + public static byte[] copyToByteArray(InputStream in) throws IOException { + ByteArrayOutputStream out = new ByteArrayOutputStream(BUFFER_SIZE); + copy(in, out); + return out.toByteArray(); + } + + /** + * Copy the contents of the given InputStream into a String. + * Leaves the stream open when done. + * @param in the InputStream to copy from + * @return the String that has been copied to + * @throws IOException in case of I/O errors + */ + public static String copyToString(InputStream in, Charset charset) throws IOException { + Assert.notNull(in, "No InputStream specified"); + StringBuilder out = new StringBuilder(); + InputStreamReader reader = new InputStreamReader(in, charset); + char[] buffer = new char[BUFFER_SIZE]; + int bytesRead = -1; + while ((bytesRead = reader.read(buffer)) != -1) { + out.append(buffer, 0, bytesRead); + } + return out.toString(); + } + + /** + * Copy the contents of the given byte array to the given OutputStream. + * Leaves the stream open when done. + * @param in the byte array to copy from + * @param out the OutputStream to copy to + * @throws IOException in case of I/O errors + */ + public static void copy(byte[] in, OutputStream out) throws IOException { + Assert.notNull(in, "No input byte array specified"); + Assert.notNull(out, "No OutputStream specified"); + out.write(in); + } + + /** + * Copy the contents of the given String to the given output OutputStream. + * Leaves the stream open when done. + * @param in the String to copy from + * @param charset the Charset + * @param out the OutputStream to copy to + * @throws IOException in case of I/O errors + */ + public static void copy(String in, Charset charset, OutputStream out) throws IOException { + Assert.notNull(in, "No input String specified"); + Assert.notNull(charset, "No charset specified"); + Assert.notNull(out, "No OutputStream specified"); + Writer writer = new OutputStreamWriter(out, charset); + writer.write(in); + writer.flush(); + } + + /** + * Copy the contents of the given InputStream to the given OutputStream. + * Leaves both streams open when done. + * @param in the InputStream to copy from + * @param out the OutputStream to copy to + * @return the number of bytes copied + * @throws IOException in case of I/O errors + */ + public static int copy(InputStream in, OutputStream out) throws IOException { + Assert.notNull(in, "No InputStream specified"); + Assert.notNull(out, "No OutputStream specified"); + int byteCount = 0; + byte[] buffer = new byte[BUFFER_SIZE]; + int bytesRead = -1; + while ((bytesRead = in.read(buffer)) != -1) { + out.write(buffer, 0, bytesRead); + byteCount += bytesRead; + } + out.flush(); + return byteCount; + } + + /** + * Returns a variant of the given {@link InputStream} where calling + * {@link InputStream#close() close()} has no effect. + * @param in the InputStream to decorate + * @return a version of the InputStream that ignores calls to close + */ + public static InputStream nonClosing(InputStream in) { + Assert.notNull(in, "No InputStream specified"); + return new NonClosingInputStream(in); + } + + /** + * Returns a variant of the given {@link OutputStream} where calling + * {@link OutputStream#close() close()} has no effect. + * @param in the OutputStream to decorate + * @return a version of the OutputStream that ignores calls to close + */ + public static OutputStream nonClosing(OutputStream out) { + Assert.notNull(out, "No OutputStream specified"); + return new NonClosingOutputStream(out); + } + + + private static class NonClosingInputStream extends FilterInputStream { + + public NonClosingInputStream(InputStream in) { + super(in); + } + + @Override + public void close() throws IOException { + } + } + + + private static class NonClosingOutputStream extends FilterOutputStream { + + public NonClosingOutputStream(OutputStream out) { + super(out); + } + + @Override + public void write(byte[] b, int off, int let) throws IOException { + // It is critical that we override this method for performance + out.write(b, off, let); + } + + @Override + public void close() throws IOException { + } + } +} diff --git a/spring-core/src/test/java/org/springframework/util/StreamUtilsTests.java b/spring-core/src/test/java/org/springframework/util/StreamUtilsTests.java new file mode 100644 index 00000000000..fe10d235967 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/util/StreamUtilsTests.java @@ -0,0 +1,129 @@ +/* + * Copyright 2002-2013 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.util; + +import static org.hamcrest.Matchers.equalTo; +import static org.junit.Assert.assertThat; +import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.charset.Charset; +import java.util.Random; +import java.util.UUID; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.InOrder; + +/** + * Tests for {@link StreamUtils}. + * + * @author Phillip Webb + */ +public class StreamUtilsTests { + + private byte[] bytes = new byte[StreamUtils.BUFFER_SIZE + 10]; + + private String string = ""; + + @Before + public void setup() { + new Random().nextBytes(bytes); + while (string.length() < StreamUtils.BUFFER_SIZE + 10) { + string += UUID.randomUUID().toString(); + } + } + + @Test + public void copyToByteArray() throws Exception { + InputStream inputStream = spy(new ByteArrayInputStream(bytes)); + byte[] actual = StreamUtils.copyToByteArray(inputStream); + assertThat(actual, equalTo(bytes)); + verify(inputStream, never()).close(); + } + + @Test + public void copyToString() throws Exception { + Charset charset = Charset.defaultCharset(); + InputStream inputStream = spy(new ByteArrayInputStream(string.getBytes(charset))); + String actual = StreamUtils.copyToString(inputStream, charset); + assertThat(actual, equalTo(string)); + verify(inputStream, never()).close(); + } + + @Test + public void copyBytes() throws Exception { + ByteArrayOutputStream out = spy(new ByteArrayOutputStream()); + StreamUtils.copy(bytes, out); + assertThat(out.toByteArray(), equalTo(bytes)); + verify(out, never()).close(); + } + + @Test + public void copyString() throws Exception { + Charset charset = Charset.defaultCharset(); + ByteArrayOutputStream out = spy(new ByteArrayOutputStream()); + StreamUtils.copy(string, charset, out); + assertThat(out.toByteArray(), equalTo(string.getBytes(charset))); + verify(out, never()).close(); + } + + @Test + public void copyStream() throws Exception { + ByteArrayOutputStream out = spy(new ByteArrayOutputStream()); + StreamUtils.copy(new ByteArrayInputStream(bytes), out); + assertThat(out.toByteArray(), equalTo(bytes)); + verify(out, never()).close(); + } + + @Test + public void nonClosingInputStream() throws Exception { + InputStream source = mock(InputStream.class); + InputStream nonClosing = StreamUtils.nonClosing(source); + nonClosing.read(); + nonClosing.read(bytes); + nonClosing.read(bytes, 1, 2); + nonClosing.close(); + InOrder ordered = inOrder(source); + ordered.verify(source).read(); + ordered.verify(source).read(bytes, 0, bytes.length); + ordered.verify(source).read(bytes, 1, 2); + ordered.verify(source, never()).close(); + } + + @Test + public void nonClosingOutputStream() throws Exception { + OutputStream source = mock(OutputStream.class); + OutputStream nonClosing = StreamUtils.nonClosing(source); + nonClosing.write(1); + nonClosing.write(bytes); + nonClosing.write(bytes, 1, 2); + nonClosing.close(); + InOrder ordered = inOrder(source); + ordered.verify(source).write(1); + ordered.verify(source).write(bytes, 0, bytes.length); + ordered.verify(source).write(bytes, 1, 2); + ordered.verify(source, never()).close(); + } +} diff --git a/spring-web/src/main/java/org/springframework/http/client/BufferingClientHttpRequestWrapper.java b/spring-web/src/main/java/org/springframework/http/client/BufferingClientHttpRequestWrapper.java index 813f98cfad9..bb878444201 100644 --- a/spring-web/src/main/java/org/springframework/http/client/BufferingClientHttpRequestWrapper.java +++ b/spring-web/src/main/java/org/springframework/http/client/BufferingClientHttpRequestWrapper.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2011 the original author or authors. + * Copyright 2002-2013 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. @@ -23,7 +23,7 @@ import java.net.URI; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.util.Assert; -import org.springframework.util.FileCopyUtils; +import org.springframework.util.StreamUtils; /** * Simple implementation of {@link ClientHttpRequest} that wraps another request. @@ -53,8 +53,7 @@ final class BufferingClientHttpRequestWrapper extends AbstractBufferingClientHtt @Override protected ClientHttpResponse executeInternal(HttpHeaders headers, byte[] bufferedOutput) throws IOException { this.request.getHeaders().putAll(headers); - OutputStream body = this.request.getBody(); - FileCopyUtils.copy(bufferedOutput, body); + StreamUtils.copy(bufferedOutput, this.request.getBody()); ClientHttpResponse response = this.request.execute(); return new BufferingClientHttpResponseWrapper(response); } diff --git a/spring-web/src/main/java/org/springframework/http/client/BufferingClientHttpResponseWrapper.java b/spring-web/src/main/java/org/springframework/http/client/BufferingClientHttpResponseWrapper.java index f280790fec8..f075b202bd5 100644 --- a/spring-web/src/main/java/org/springframework/http/client/BufferingClientHttpResponseWrapper.java +++ b/spring-web/src/main/java/org/springframework/http/client/BufferingClientHttpResponseWrapper.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-2013 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. @@ -23,6 +23,7 @@ import java.io.InputStream; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.util.FileCopyUtils; +import org.springframework.util.StreamUtils; /** * Simple implementation of {@link ClientHttpResponse} that reads the request's body into memory, @@ -61,7 +62,7 @@ final class BufferingClientHttpResponseWrapper implements ClientHttpResponse { public InputStream getBody() throws IOException { if (this.body == null) { - this.body = FileCopyUtils.copyToByteArray(this.response.getBody()); + this.body = StreamUtils.copyToByteArray(this.response.getBody()); } return new ByteArrayInputStream(this.body); } diff --git a/spring-web/src/main/java/org/springframework/http/client/InterceptingClientHttpRequest.java b/spring-web/src/main/java/org/springframework/http/client/InterceptingClientHttpRequest.java index 9d397604799..a422614fe9f 100644 --- a/spring-web/src/main/java/org/springframework/http/client/InterceptingClientHttpRequest.java +++ b/spring-web/src/main/java/org/springframework/http/client/InterceptingClientHttpRequest.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-2013 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. @@ -24,7 +24,7 @@ import java.util.List; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.HttpRequest; -import org.springframework.util.FileCopyUtils; +import org.springframework.util.StreamUtils; /** * Wrapper for a {@link ClientHttpRequest} that has support for {@link ClientHttpRequestInterceptor}s. @@ -86,7 +86,7 @@ class InterceptingClientHttpRequest extends AbstractBufferingClientHttpRequest { delegate.getHeaders().putAll(request.getHeaders()); if (body.length > 0) { - FileCopyUtils.copy(body, delegate.getBody()); + StreamUtils.copy(body, delegate.getBody()); } return delegate.execute(); } diff --git a/spring-web/src/main/java/org/springframework/http/client/SimpleStreamingClientHttpRequest.java b/spring-web/src/main/java/org/springframework/http/client/SimpleStreamingClientHttpRequest.java index a5e831102c4..2382365570e 100644 --- a/spring-web/src/main/java/org/springframework/http/client/SimpleStreamingClientHttpRequest.java +++ b/spring-web/src/main/java/org/springframework/http/client/SimpleStreamingClientHttpRequest.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-2013 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.FilterOutputStream; import java.io.IOException; import java.io.OutputStream; import java.net.HttpURLConnection; @@ -27,6 +26,7 @@ import java.util.Map; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; +import org.springframework.util.StreamUtils; /** * {@link ClientHttpRequest} implementation that uses standard J2SE facilities to execute streaming requests. @@ -77,7 +77,7 @@ final class SimpleStreamingClientHttpRequest extends AbstractClientHttpRequest { this.connection.connect(); this.body = this.connection.getOutputStream(); } - return new NonClosingOutputStream(this.body); + return StreamUtils.nonClosing(this.body); } private void writeHeaders(HttpHeaders headers) { @@ -106,26 +106,4 @@ final class SimpleStreamingClientHttpRequest extends AbstractClientHttpRequest { return new SimpleClientHttpResponse(this.connection); } - - private static class NonClosingOutputStream extends FilterOutputStream { - - private NonClosingOutputStream(OutputStream out) { - super(out); - } - - @Override - public void write(byte[] b) throws IOException { - super.write(b); - } - - @Override - public void write(byte[] b, int off, int let) throws IOException { - out.write(b, off, let); - } - - @Override - public void close() throws IOException { - } - } - } diff --git a/spring-web/src/main/java/org/springframework/http/converter/ByteArrayHttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/ByteArrayHttpMessageConverter.java index f1ddc0080f7..d1b25f7c353 100644 --- a/spring-web/src/main/java/org/springframework/http/converter/ByteArrayHttpMessageConverter.java +++ b/spring-web/src/main/java/org/springframework/http/converter/ByteArrayHttpMessageConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-2013 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. @@ -22,7 +22,7 @@ import java.io.IOException; import org.springframework.http.HttpInputMessage; import org.springframework.http.HttpOutputMessage; import org.springframework.http.MediaType; -import org.springframework.util.FileCopyUtils; +import org.springframework.util.StreamUtils; /** * Implementation of {@link HttpMessageConverter} that can read and write byte arrays. @@ -49,14 +49,9 @@ public class ByteArrayHttpMessageConverter extends AbstractHttpMessageConverter< @Override public byte[] readInternal(Class clazz, HttpInputMessage inputMessage) throws IOException { long contentLength = inputMessage.getHeaders().getContentLength(); - if (contentLength >= 0) { - ByteArrayOutputStream bos = new ByteArrayOutputStream((int) contentLength); - FileCopyUtils.copy(inputMessage.getBody(), bos); - return bos.toByteArray(); - } - else { - return FileCopyUtils.copyToByteArray(inputMessage.getBody()); - } + ByteArrayOutputStream bos = new ByteArrayOutputStream(contentLength >= 0 ? (int) contentLength : StreamUtils.BUFFER_SIZE); + StreamUtils.copy(inputMessage.getBody(), bos); + return bos.toByteArray(); } @Override @@ -66,7 +61,7 @@ public class ByteArrayHttpMessageConverter extends AbstractHttpMessageConverter< @Override protected void writeInternal(byte[] bytes, HttpOutputMessage outputMessage) throws IOException { - FileCopyUtils.copy(bytes, outputMessage.getBody()); + StreamUtils.copy(bytes, outputMessage.getBody()); } } diff --git a/spring-web/src/main/java/org/springframework/http/converter/FormHttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/FormHttpMessageConverter.java index f66a38bf192..516eb92ab25 100644 --- a/spring-web/src/main/java/org/springframework/http/converter/FormHttpMessageConverter.java +++ b/spring-web/src/main/java/org/springframework/http/converter/FormHttpMessageConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2011 the original author or authors. + * Copyright 2002-2013 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. @@ -17,7 +17,6 @@ package org.springframework.http.converter; import java.io.IOException; -import java.io.InputStreamReader; import java.io.OutputStream; import java.io.UnsupportedEncodingException; import java.net.URLDecoder; @@ -37,9 +36,9 @@ import org.springframework.http.HttpInputMessage; import org.springframework.http.HttpOutputMessage; import org.springframework.http.MediaType; import org.springframework.util.Assert; -import org.springframework.util.FileCopyUtils; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; +import org.springframework.util.StreamUtils; import org.springframework.util.StringUtils; /** @@ -170,7 +169,7 @@ public class FormHttpMessageConverter implements HttpMessageConverter parts, HttpOutputMessage outputMessage) diff --git a/spring-web/src/main/java/org/springframework/http/converter/ResourceHttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/ResourceHttpMessageConverter.java index 9991fbcc804..69f22a92e10 100644 --- a/spring-web/src/main/java/org/springframework/http/converter/ResourceHttpMessageConverter.java +++ b/spring-web/src/main/java/org/springframework/http/converter/ResourceHttpMessageConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2011 the original author or authors. + * Copyright 2002-2013 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. @@ -18,6 +18,7 @@ package org.springframework.http.converter; import java.io.IOException; import java.io.InputStream; + import javax.activation.FileTypeMap; import javax.activation.MimetypesFileTypeMap; @@ -28,7 +29,7 @@ import org.springframework.http.HttpInputMessage; import org.springframework.http.HttpOutputMessage; import org.springframework.http.MediaType; import org.springframework.util.ClassUtils; -import org.springframework.util.FileCopyUtils; +import org.springframework.util.StreamUtils; import org.springframework.util.StringUtils; /** @@ -61,7 +62,7 @@ public class ResourceHttpMessageConverter extends AbstractHttpMessageConverter clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException { - byte[] body = FileCopyUtils.copyToByteArray(inputMessage.getBody()); + byte[] body = StreamUtils.copyToByteArray(inputMessage.getBody()); return new ByteArrayResource(body); } @@ -84,7 +85,7 @@ public class ResourceHttpMessageConverter extends AbstractHttpMessageConverter clazz, HttpInputMessage inputMessage) throws IOException { Charset charset = getContentTypeCharset(inputMessage.getHeaders().getContentType()); - return FileCopyUtils.copyToString(new InputStreamReader(inputMessage.getBody(), charset)); + return StreamUtils.copyToString(inputMessage.getBody(), charset); } @Override @@ -105,7 +103,7 @@ public class StringHttpMessageConverter extends AbstractHttpMessageConverter