diff --git a/spring-core/src/main/java/org/springframework/core/io/ResourceRegion.java b/spring-core/src/main/java/org/springframework/core/io/ResourceRegion.java
new file mode 100644
index 00000000000..2774349ff16
--- /dev/null
+++ b/spring-core/src/main/java/org/springframework/core/io/ResourceRegion.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright 2002-2016 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.core.io;
+
+import org.springframework.util.Assert;
+
+/**
+ * Region of a {@link Resource} implementation, materialized by a {@code position}
+ * within the {@link Resource} and a byte {@code count} for the length of that region.
+ * @author Arjen Poutsma
+ * @since 4.3.0
+ */
+public class ResourceRegion {
+
+ private final Resource resource;
+
+ private final long position;
+
+ private final long count;
+
+ /**
+ * Create a new {@code ResourceRegion} from a given {@link Resource}.
+ * This region of a resource is reprensented by a start {@code position}
+ * and a byte {@code count} within the given {@code Resource}.
+ * @param resource a Resource
+ * @param position the start position of the region in that resource
+ * @param count the byte count of the region in that resource
+ */
+ public ResourceRegion(Resource resource, long position, long count) {
+ Assert.notNull(resource, "'resource' must not be null");
+ Assert.isTrue(position >= 0, "'position' must be larger than or equal to 0");
+ Assert.isTrue(count >= 0, "'count' must be larger than or equal to 0");
+ this.resource = resource;
+ this.position = position;
+ this.count = count;
+ }
+
+ /**
+ * Return the underlying {@link Resource} for this {@code ResourceRegion}
+ */
+ public Resource getResource() {
+ return this.resource;
+ }
+
+ /**
+ * Return the start position of this region in the underlying {@link Resource}
+ */
+ public long getPosition() {
+ return this.position;
+ }
+
+ /**
+ * Return the byte count of this region in the underlying {@link Resource}
+ */
+ public long getCount() {
+ return this.count;
+ }
+}
\ No newline at end of file
diff --git a/spring-core/src/main/java/org/springframework/util/StreamUtils.java b/spring-core/src/main/java/org/springframework/util/StreamUtils.java
index cba22848fb8..c15993439f4 100644
--- a/spring-core/src/main/java/org/springframework/util/StreamUtils.java
+++ b/spring-core/src/main/java/org/springframework/util/StreamUtils.java
@@ -37,6 +37,7 @@ import java.nio.charset.Charset;
*
* @author Juergen Hoeller
* @author Phillip Webb
+ * @author Brian Clozel
* @since 3.2.2
* @see FileCopyUtils
*/
@@ -131,6 +132,43 @@ public abstract class StreamUtils {
return byteCount;
}
+ /**
+ * Copy a range of content of the given InputStream to the given OutputStream.
+ *
If the specified range exceeds the length of the InputStream, this copies
+ * up to the end of the stream and returns the actual number of copied bytes.
+ *
Leaves both streams open when done.
+ * @param in the InputStream to copy from
+ * @param out the OutputStream to copy to
+ * @param start the position to start copying from
+ * @param end the position to end copying
+ * @return the number of bytes copied
+ * @throws IOException in case of I/O errors
+ * @since 4.3.0
+ */
+ public static long copyRange(InputStream in, OutputStream out, long start, long end) throws IOException {
+ long skipped = in.skip(start);
+ if (skipped < start) {
+ throw new IOException("Skipped only " + skipped + " bytes out of " + start + " required.");
+ }
+ long bytesToCopy = end - start + 1;
+ byte buffer[] = new byte[StreamUtils.BUFFER_SIZE];
+ while (bytesToCopy > 0) {
+ int bytesRead = in.read(buffer);
+ if (bytesRead == -1) {
+ break;
+ }
+ else if (bytesRead <= bytesToCopy) {
+ out.write(buffer, 0, bytesRead);
+ bytesToCopy -= bytesRead;
+ }
+ else {
+ out.write(buffer, 0, (int) bytesToCopy);
+ bytesToCopy = 0;
+ }
+ }
+ return end - start + 1 - bytesToCopy;
+ }
+
/**
* Drain the remaining content of the given InputStream.
* Leaves the InputStream open when done.
diff --git a/spring-core/src/test/java/org/springframework/core/io/ResourceRegionTests.java b/spring-core/src/test/java/org/springframework/core/io/ResourceRegionTests.java
new file mode 100644
index 00000000000..9fd035f2a3b
--- /dev/null
+++ b/spring-core/src/test/java/org/springframework/core/io/ResourceRegionTests.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2002-2016 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.core.io;
+
+import static org.mockito.Mockito.mock;
+
+import org.junit.Test;
+
+/**
+ * Unit tests for the {@link ResourceRegion} class.
+ *
+ * @author Brian Clozel
+ */
+public class ResourceRegionTests {
+
+ @Test(expected = IllegalArgumentException.class)
+ public void shouldThrowExceptionWithNullResource() {
+ new ResourceRegion(null, 0, 1);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void shouldThrowExceptionForNegativePosition() {
+ new ResourceRegion(mock(Resource.class), -1, 1);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void shouldThrowExceptionForNegativeCount() {
+ new ResourceRegion(mock(Resource.class), 0, -1);
+ }
+}
diff --git a/spring-core/src/test/java/org/springframework/util/StreamUtilsTests.java b/spring-core/src/test/java/org/springframework/util/StreamUtilsTests.java
index 93295cadd71..48f173f2254 100644
--- a/spring-core/src/test/java/org/springframework/util/StreamUtilsTests.java
+++ b/spring-core/src/test/java/org/springframework/util/StreamUtilsTests.java
@@ -21,6 +21,7 @@ import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.charset.Charset;
+import java.util.Arrays;
import java.util.Random;
import java.util.UUID;
@@ -93,6 +94,15 @@ public class StreamUtilsTests {
verify(out, never()).close();
}
+ @Test
+ public void copyRange() throws Exception {
+ ByteArrayOutputStream out = spy(new ByteArrayOutputStream());
+ StreamUtils.copyRange(new ByteArrayInputStream(bytes), out, 0, 100);
+ byte[] range = Arrays.copyOfRange(bytes, 0, 101);
+ assertThat(out.toByteArray(), equalTo(range));
+ verify(out, never()).close();
+ }
+
@Test
public void nonClosingInputStream() throws Exception {
InputStream source = mock(InputStream.class);
diff --git a/spring-web/src/main/java/org/springframework/http/HttpRange.java b/spring-web/src/main/java/org/springframework/http/HttpRange.java
index 29f2e675021..0647b557484 100644
--- a/spring-web/src/main/java/org/springframework/http/HttpRange.java
+++ b/spring-web/src/main/java/org/springframework/http/HttpRange.java
@@ -16,12 +16,16 @@
package org.springframework.http;
+import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
+import org.springframework.core.io.InputStreamResource;
+import org.springframework.core.io.Resource;
+import org.springframework.core.io.ResourceRegion;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
@@ -55,6 +59,30 @@ public abstract class HttpRange {
*/
public abstract long getRangeEnd(long length);
+ /**
+ * Turn a {@code Resource} into a {@link ResourceRegion} using the range
+ * information contained in the current {@code HttpRange}.
+ * @param resource the {@code Resource} to select the region from
+ * @return the selected region of the given {@code Resource}
+ * @since 4.3.0
+ */
+ public ResourceRegion toResourceRegion(Resource resource) {
+ // Don't try to determine contentLength on InputStreamResource - cannot be read afterwards...
+ // Note: custom InputStreamResource subclasses could provide a pre-calculated content length!
+ Assert.isTrue(InputStreamResource.class != resource.getClass(),
+ "Can't convert an InputStreamResource to a ResourceRegion");
+ try {
+ long contentLength = resource.contentLength();
+ Assert.isTrue(contentLength > 0, "Resource content length should be > 0");
+ long start = getRangeStart(contentLength);
+ long end = getRangeEnd(contentLength);
+ return new ResourceRegion(resource, start, end - start + 1);
+ }
+ catch (IOException exc) {
+ throw new IllegalArgumentException("Can't convert this Resource to a ResourceRegion", exc);
+ }
+ }
+
/**
* Create an {@code HttpRange} from the given position to the end.
@@ -133,6 +161,26 @@ public abstract class HttpRange {
}
}
+ /**
+ * Convert each {@code HttpRange} into a {@code ResourceRegion},
+ * selecting the appropriate segment of the given {@code Resource}
+ * using the HTTP Range information.
+ *
+ * @param ranges the list of ranges
+ * @param resource the resource to select the regions from
+ * @return the list of regions for the given resource
+ */
+ public static List toResourceRegions(List ranges, Resource resource) {
+ if(ranges == null || ranges.size() == 0) {
+ return Collections.emptyList();
+ }
+ List regions = new ArrayList(ranges.size());
+ for(HttpRange range : ranges) {
+ regions.add(range.toResourceRegion(resource));
+ }
+ return regions;
+ }
+
/**
* Return a string representation of the given list of {@code HttpRange} objects.
*
This method can be used to for an {@code Range} header.
diff --git a/spring-web/src/main/java/org/springframework/http/HttpRangeResource.java b/spring-web/src/main/java/org/springframework/http/HttpRangeResource.java
deleted file mode 100644
index 66d7d32f9df..00000000000
--- a/spring-web/src/main/java/org/springframework/http/HttpRangeResource.java
+++ /dev/null
@@ -1,123 +0,0 @@
-/*
- * Copyright 2002-2016 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;
-
-import java.io.File;
-import java.io.IOException;
-import java.io.InputStream;
-import java.net.URI;
-import java.net.URL;
-import java.util.List;
-
-import org.springframework.core.io.Resource;
-import org.springframework.util.Assert;
-
-/**
- * Holder that combines a {@link Resource} descriptor with {@link HttpRange}
- * information to be used for reading selected parts of the resource.
- *
- *
Used as an argument for partial conversion operations in
- * {@link org.springframework.http.converter.ResourceHttpMessageConverter}.
- *
- * @author Brian Clozel
- * @since 4.3
- * @see HttpRange
- */
-public class HttpRangeResource implements Resource {
-
- private final List httpRanges;
-
- private final Resource resource;
-
-
- public HttpRangeResource(List httpRanges, Resource resource) {
- Assert.notEmpty(httpRanges, "List of HTTP Ranges should not be empty");
- this.httpRanges = httpRanges;
- this.resource = resource;
- }
-
-
- /**
- * Return the list of HTTP (byte) ranges describing the requested
- * parts of the Resource, as provided by the HTTP Range request.
- */
- public final List getHttpRanges() {
- return this.httpRanges;
- }
-
-
- @Override
- public boolean exists() {
- return this.resource.exists();
- }
-
- @Override
- public boolean isReadable() {
- return this.resource.isReadable();
- }
-
- @Override
- public boolean isOpen() {
- return this.resource.isOpen();
- }
-
- @Override
- public URL getURL() throws IOException {
- return this.resource.getURL();
- }
-
- @Override
- public URI getURI() throws IOException {
- return this.resource.getURI();
- }
-
- @Override
- public File getFile() throws IOException {
- return this.resource.getFile();
- }
-
- @Override
- public long contentLength() throws IOException {
- return this.resource.contentLength();
- }
-
- @Override
- public long lastModified() throws IOException {
- return this.resource.lastModified();
- }
-
- @Override
- public Resource createRelative(String relativePath) throws IOException {
- return this.resource.createRelative(relativePath);
- }
-
- @Override
- public String getFilename() {
- return this.resource.getFilename();
- }
-
- @Override
- public String getDescription() {
- return this.resource.getDescription();
- }
-
- @Override
- public InputStream getInputStream() throws IOException {
- return this.resource.getInputStream();
- }
-
-}
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 ce95b3a67da..9e3dc061286 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
@@ -19,8 +19,7 @@ package org.springframework.http.converter;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
-import java.io.OutputStream;
-import java.util.List;
+
import javax.activation.FileTypeMap;
import javax.activation.MimetypesFileTypeMap;
@@ -31,12 +30,8 @@ import org.springframework.core.io.Resource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.HttpOutputMessage;
-import org.springframework.http.HttpRange;
-import org.springframework.http.HttpRangeResource;
import org.springframework.http.MediaType;
-import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
-import org.springframework.util.MimeTypeUtils;
import org.springframework.util.StreamUtils;
import org.springframework.util.StringUtils;
@@ -48,13 +43,10 @@ import org.springframework.util.StringUtils;
* if available - is used to determine the {@code Content-Type} of written resources.
* If JAF is not available, {@code application/octet-stream} is used.
*
- *
This converter supports HTTP byte range requests and can write partial content, when provided
- * with an {@link HttpRangeResource} instance containing the required Range information.
*
* @author Arjen Poutsma
* @author Juergen Hoeller
* @author Kazuki Shimizu
- * @author Brian Clozel
* @since 3.0.2
*/
public class ResourceHttpMessageConverter extends AbstractHttpMessageConverter {
@@ -114,13 +106,7 @@ public class ResourceHttpMessageConverter extends AbstractHttpMessageConverter ranges = resource.getHttpRanges();
- HttpHeaders responseHeaders = outputMessage.getHeaders();
- MediaType contentType = responseHeaders.getContentType();
- Long length = getContentLength(resource, contentType);
-
- if (ranges.size() == 1) {
- HttpRange range = ranges.get(0);
- long start = range.getRangeStart(length);
- long end = range.getRangeEnd(length);
- long rangeLength = end - start + 1;
- responseHeaders.add("Content-Range", "bytes " + start + "-" + end + "/" + length);
- responseHeaders.setContentLength(rangeLength);
- InputStream in = resource.getInputStream();
- try {
- copyRange(in, outputMessage.getBody(), start, end);
- }
- finally {
- try {
- in.close();
- }
- catch (IOException ex) {
- // ignore
- }
- }
- }
- else {
- String boundaryString = MimeTypeUtils.generateMultipartBoundaryString();
- responseHeaders.set(HttpHeaders.CONTENT_TYPE, "multipart/byteranges; boundary=" + boundaryString);
- OutputStream out = outputMessage.getBody();
- for (HttpRange range : ranges) {
- long start = range.getRangeStart(length);
- long end = range.getRangeEnd(length);
- InputStream in = resource.getInputStream();
- // Writing MIME header.
- println(out);
- print(out, "--" + boundaryString);
- println(out);
- if (contentType != null) {
- print(out, "Content-Type: " + contentType.toString());
- println(out);
- }
- print(out, "Content-Range: bytes " + start + "-" + end + "/" + length);
- println(out);
- println(out);
- // Printing content
- copyRange(in, out, start, end);
- }
- println(out);
- print(out, "--" + boundaryString + "--");
- }
- }
-
- private static void println(OutputStream os) throws IOException {
- os.write('\r');
- os.write('\n');
- }
-
- private static void print(OutputStream os, String buf) throws IOException {
- os.write(buf.getBytes("US-ASCII"));
- }
-
- private void copyRange(InputStream in, OutputStream out, long start, long end) throws IOException {
- long skipped = in.skip(start);
- if (skipped < start) {
- throw new IOException("Skipped only " + skipped + " bytes out of " + start + " required.");
- }
-
- long bytesToCopy = end - start + 1;
- byte buffer[] = new byte[StreamUtils.BUFFER_SIZE];
- while (bytesToCopy > 0) {
- int bytesRead = in.read(buffer);
- if (bytesRead <= bytesToCopy) {
- out.write(buffer, 0, bytesRead);
- bytesToCopy -= bytesRead;
- }
- else {
- out.write(buffer, 0, (int) bytesToCopy);
- bytesToCopy = 0;
- }
- if (bytesRead == -1) {
- break;
- }
- }
- }
-
/**
* Inner class to avoid a hard-coded JAF dependency.
diff --git a/spring-web/src/main/java/org/springframework/http/converter/ResourceRegionHttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/ResourceRegionHttpMessageConverter.java
new file mode 100644
index 00000000000..763ed6fca85
--- /dev/null
+++ b/spring-web/src/main/java/org/springframework/http/converter/ResourceRegionHttpMessageConverter.java
@@ -0,0 +1,188 @@
+/*
+ * Copyright 2002-2016 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.converter;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Type;
+import java.util.Collection;
+
+import org.springframework.core.io.ResourceRegion;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpInputMessage;
+import org.springframework.http.HttpOutputMessage;
+import org.springframework.http.MediaType;
+import org.springframework.util.Assert;
+import org.springframework.util.MimeTypeUtils;
+import org.springframework.util.StreamUtils;
+
+/**
+ * Implementation of {@link HttpMessageConverter} that can write a single {@link ResourceRegion ResourceRegion},
+ * or Collections of {@link ResourceRegion ResourceRegions}.
+ *
+ * @author Brian Clozel
+ * @since 4.3.0
+ */
+public class ResourceRegionHttpMessageConverter extends AbstractGenericHttpMessageConverter