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 { + + public ResourceRegionHttpMessageConverter() { + super(MediaType.ALL); + } + + @Override + public boolean canWrite(Class clazz, MediaType mediaType) { + return canWrite(clazz, null, mediaType); + } + + @Override + public boolean canWrite(Type type, Class clazz, MediaType mediaType) { + if (!(type instanceof ParameterizedType)) { + return ResourceRegion.class.isAssignableFrom((Class) type); + } + ParameterizedType parameterizedType = (ParameterizedType) type; + if (!(parameterizedType.getRawType() instanceof Class)) { + return false; + } + Class rawType = (Class) parameterizedType.getRawType(); + if (!(Collection.class.isAssignableFrom(rawType))) { + return false; + } + if (parameterizedType.getActualTypeArguments().length != 1) { + return false; + } + Type typeArgument = parameterizedType.getActualTypeArguments()[0]; + if (!(typeArgument instanceof Class)) { + return false; + } + Class typeArgumentClass = (Class) typeArgument; + return typeArgumentClass.isAssignableFrom(ResourceRegion.class); + } + + @Override + @SuppressWarnings("unchecked") + protected void writeInternal(Object object, Type type, HttpOutputMessage outputMessage) + throws IOException, HttpMessageNotWritableException { + + if (object instanceof ResourceRegion) { + writeResourceRegion((ResourceRegion) object, outputMessage); + } + else { + Collection regions = (Collection) object; + if(regions.size() == 1) { + writeResourceRegion(regions.iterator().next(), outputMessage); + } + else { + writeResourceRegionCollection((Collection) object, outputMessage); + } + } + } + + protected void writeResourceRegion(ResourceRegion region, HttpOutputMessage outputMessage) + throws IOException { + + Assert.notNull(region, "ResourceRegion should not be null"); + HttpHeaders responseHeaders = outputMessage.getHeaders(); + long start = region.getPosition(); + long end = start + region.getCount() - 1; + Long resourceLength = region.getResource().contentLength(); + end = Math.min(end, resourceLength - 1); + long rangeLength = end - start + 1; + responseHeaders.add("Content-Range", "bytes " + start + "-" + end + "/" + resourceLength); + responseHeaders.setContentLength(rangeLength); + InputStream in = region.getResource().getInputStream(); + try { + StreamUtils.copyRange(in, outputMessage.getBody(), start, end); + } + finally { + try { + in.close(); + } + catch (IOException ex) { + // ignore + } + } + } + + private void writeResourceRegionCollection(Collection resourceRegions, + HttpOutputMessage outputMessage) throws IOException { + + Assert.notNull(resourceRegions, "Collection of ResourceRegion should not be null"); + HttpHeaders responseHeaders = outputMessage.getHeaders(); + MediaType contentType = responseHeaders.getContentType(); + String boundaryString = MimeTypeUtils.generateMultipartBoundaryString(); + responseHeaders.set(HttpHeaders.CONTENT_TYPE, "multipart/byteranges; boundary=" + boundaryString); + OutputStream out = outputMessage.getBody(); + for (ResourceRegion region : resourceRegions) { + long start = region.getPosition(); + long end = start + region.getCount() - 1; + InputStream in = region.getResource().getInputStream(); + // Writing MIME header. + println(out); + print(out, "--" + boundaryString); + println(out); + if (contentType != null) { + print(out, "Content-Type: " + contentType.toString()); + println(out); + } + Long resourceLength = region.getResource().contentLength(); + end = Math.min(end, resourceLength - 1); + print(out, "Content-Range: bytes " + start + "-" + end + "/" + resourceLength); + println(out); + println(out); + // Printing content + StreamUtils.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")); + } + + @Override + public boolean canRead(Type type, Class contextClass, MediaType mediaType) { + return false; + } + + @Override + protected boolean supports(Class clazz) { + // should not be called as we override canRead/canWrite + return false; + } + + + @Override + public Object read(Type type, Class contextClass, HttpInputMessage inputMessage) + throws IOException, HttpMessageNotReadableException { + return null; + } + + @Override + protected ResourceRegion readInternal(Class clazz, HttpInputMessage inputMessage) + throws IOException, HttpMessageNotReadableException { + return null; + } + +} diff --git a/spring-web/src/test/java/org/springframework/http/HttpRangeTests.java b/spring-web/src/test/java/org/springframework/http/HttpRangeTests.java index 85870b3c9ba..49f83cbd767 100644 --- a/spring-web/src/test/java/org/springframework/http/HttpRangeTests.java +++ b/spring-web/src/test/java/org/springframework/http/HttpRangeTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * 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. @@ -15,17 +15,26 @@ */ package org.springframework.http; +import java.io.IOException; +import java.nio.charset.Charset; import java.util.ArrayList; import java.util.List; import org.junit.Test; import static org.junit.Assert.*; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +import org.springframework.core.io.ByteArrayResource; +import org.springframework.core.io.InputStreamResource; +import org.springframework.core.io.ResourceRegion; /** * Unit tests for {@link HttpRange}. * * @author Rossen Stoyanchev + * @author Brian Clozel */ public class HttpRangeTests { @@ -100,4 +109,38 @@ public class HttpRangeTests { assertEquals("Invalid Range header", "bytes=0-499, 9500-, -500", HttpRange.toString(ranges)); } + @Test + public void toResourceRegion() { + byte[] bytes = "Spring Framework".getBytes(Charset.forName("UTF-8")); + ByteArrayResource resource = new ByteArrayResource(bytes); + HttpRange range = HttpRange.createByteRange(0, 5); + ResourceRegion region = range.toResourceRegion(resource); + assertEquals(resource, region.getResource()); + assertEquals(0L, region.getPosition()); + assertEquals(6L, region.getCount()); + } + + @Test(expected = IllegalArgumentException.class) + public void toResourceRegionInputStreamResource() { + InputStreamResource resource = mock(InputStreamResource.class); + HttpRange range = HttpRange.createByteRange(0, 9); + range.toResourceRegion(resource); + } + + @Test(expected = IllegalArgumentException.class) + public void toResourceRegionIllegalLength() { + ByteArrayResource resource = mock(ByteArrayResource.class); + given(resource.contentLength()).willReturn(-1L); + HttpRange range = HttpRange.createByteRange(0, 9); + range.toResourceRegion(resource); + } + + @Test(expected = IllegalArgumentException.class) + public void toResourceRegionExceptionLength() { + ByteArrayResource resource = mock(ByteArrayResource.class); + given(resource.contentLength()).willThrow(IOException.class); + HttpRange range = HttpRange.createByteRange(0, 9); + range.toResourceRegion(resource); + } + } diff --git a/spring-web/src/test/java/org/springframework/http/converter/ResourceHttpMessageConverterTests.java b/spring-web/src/test/java/org/springframework/http/converter/ResourceHttpMessageConverterTests.java index b94508af621..d3a3d96fa6b 100644 --- a/spring-web/src/test/java/org/springframework/http/converter/ResourceHttpMessageConverterTests.java +++ b/spring-web/src/test/java/org/springframework/http/converter/ResourceHttpMessageConverterTests.java @@ -27,25 +27,18 @@ import static org.mockito.Mockito.mock; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; -import java.nio.charset.Charset; import java.util.Arrays; -import java.util.List; -import org.hamcrest.Matchers; import org.junit.Test; import org.springframework.core.io.ByteArrayResource; import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.InputStreamResource; import org.springframework.core.io.Resource; -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpRange; -import org.springframework.http.HttpRangeResource; import org.springframework.http.MediaType; import org.springframework.http.MockHttpInputMessage; import org.springframework.http.MockHttpOutputMessage; import org.springframework.util.FileCopyUtils; -import org.springframework.util.StringUtils; /** * @author Arjen Poutsma @@ -98,116 +91,6 @@ public class ResourceHttpMessageConverterTests { assertEquals("Invalid content-length", body.getFile().length(), outputMessage.getHeaders().getContentLength()); } - @Test - public void shouldWritePartialContentByteRange() throws Exception { - MockHttpOutputMessage outputMessage = new MockHttpOutputMessage(); - Resource body = new ClassPathResource("byterangeresource.txt", getClass()); - List httpRangeList = HttpRange.parseRanges("bytes=0-5"); - - converter.write(new HttpRangeResource(httpRangeList, body), MediaType.TEXT_PLAIN, outputMessage); - - HttpHeaders headers = outputMessage.getHeaders(); - assertThat(headers.getContentType(), is(MediaType.TEXT_PLAIN)); - assertThat(headers.getContentLength(), is(6L)); - assertThat(headers.get(HttpHeaders.CONTENT_RANGE).size(), is(1)); - assertThat(headers.get(HttpHeaders.CONTENT_RANGE).get(0), is("bytes 0-5/39")); - assertThat(outputMessage.getBodyAsString(Charset.forName("UTF-8")), is("Spring")); - } - - @Test - public void shouldWritePartialContentByteRangeNoEnd() throws Exception { - MockHttpOutputMessage outputMessage = new MockHttpOutputMessage(); - Resource body = new ClassPathResource("byterangeresource.txt", getClass()); - List httpRangeList = HttpRange.parseRanges("bytes=7-"); - - converter.write(new HttpRangeResource(httpRangeList, body), MediaType.TEXT_PLAIN, outputMessage); - - HttpHeaders headers = outputMessage.getHeaders(); - assertThat(headers.getContentType(), is(MediaType.TEXT_PLAIN)); - assertThat(headers.getContentLength(), is(32L)); - assertThat(headers.get(HttpHeaders.CONTENT_RANGE).size(), is(1)); - assertThat(headers.get(HttpHeaders.CONTENT_RANGE).get(0), is("bytes 7-38/39")); - assertThat(outputMessage.getBodyAsString(Charset.forName("UTF-8")), is("Framework test resource content.")); - } - - @Test - public void shouldWritePartialContentByteRangeLargeEnd() throws Exception { - MockHttpOutputMessage outputMessage = new MockHttpOutputMessage(); - Resource body = new ClassPathResource("byterangeresource.txt", getClass()); - List httpRangeList = HttpRange.parseRanges("bytes=7-10000"); - - converter.write(new HttpRangeResource(httpRangeList, body), MediaType.TEXT_PLAIN, outputMessage); - - HttpHeaders headers = outputMessage.getHeaders(); - assertThat(headers.getContentType(), is(MediaType.TEXT_PLAIN)); - assertThat(headers.getContentLength(), is(32L)); - assertThat(headers.get(HttpHeaders.CONTENT_RANGE).size(), is(1)); - assertThat(headers.get(HttpHeaders.CONTENT_RANGE).get(0), is("bytes 7-38/39")); - assertThat(outputMessage.getBodyAsString(Charset.forName("UTF-8")), is("Framework test resource content.")); - } - - @Test - public void shouldWritePartialContentSuffixRange() throws Exception { - MockHttpOutputMessage outputMessage = new MockHttpOutputMessage(); - Resource body = new ClassPathResource("byterangeresource.txt", getClass()); - List httpRangeList = HttpRange.parseRanges("bytes=-8"); - - converter.write(new HttpRangeResource(httpRangeList, body), MediaType.TEXT_PLAIN, outputMessage); - - HttpHeaders headers = outputMessage.getHeaders(); - assertThat(headers.getContentType(), is(MediaType.TEXT_PLAIN)); - assertThat(headers.getContentLength(), is(8L)); - assertThat(headers.get(HttpHeaders.CONTENT_RANGE).size(), is(1)); - assertThat(headers.get(HttpHeaders.CONTENT_RANGE).get(0), is("bytes 31-38/39")); - assertThat(outputMessage.getBodyAsString(Charset.forName("UTF-8")), is("content.")); - } - - @Test - public void shouldWritePartialContentSuffixRangeLargeSuffix() throws Exception { - MockHttpOutputMessage outputMessage = new MockHttpOutputMessage(); - Resource body = new ClassPathResource("byterangeresource.txt", getClass()); - List httpRangeList = HttpRange.parseRanges("bytes=-50"); - - converter.write(new HttpRangeResource(httpRangeList, body), MediaType.TEXT_PLAIN, outputMessage); - - HttpHeaders headers = outputMessage.getHeaders(); - assertThat(headers.getContentType(), is(MediaType.TEXT_PLAIN)); - assertThat(headers.getContentLength(), is(39L)); - assertThat(headers.get(HttpHeaders.CONTENT_RANGE).size(), is(1)); - assertThat(headers.get(HttpHeaders.CONTENT_RANGE).get(0), is("bytes 0-38/39")); - assertThat(outputMessage.getBodyAsString(Charset.forName("UTF-8")), is("Spring Framework test resource content.")); - } - - @Test - public void partialContentMultipleByteRanges() throws Exception { - MockHttpOutputMessage outputMessage = new MockHttpOutputMessage(); - Resource body = new ClassPathResource("byterangeresource.txt", getClass()); - List httpRangeList = HttpRange.parseRanges("bytes=0-5, 7-15, 17-20"); - - converter.write(new HttpRangeResource(httpRangeList, body), MediaType.TEXT_PLAIN, outputMessage); - - HttpHeaders headers = outputMessage.getHeaders(); - assertThat(headers.getContentType().toString(), Matchers.startsWith("multipart/byteranges;boundary=")); - String boundary = "--" + headers.getContentType().toString().substring(30); - String content = outputMessage.getBodyAsString(Charset.forName("UTF-8")); - String[] ranges = StringUtils.tokenizeToStringArray(content, "\r\n", false, true); - - assertThat(ranges[0], is(boundary)); - assertThat(ranges[1], is("Content-Type: text/plain")); - assertThat(ranges[2], is("Content-Range: bytes 0-5/39")); - assertThat(ranges[3], is("Spring")); - - assertThat(ranges[4], is(boundary)); - assertThat(ranges[5], is("Content-Type: text/plain")); - assertThat(ranges[6], is("Content-Range: bytes 7-15/39")); - assertThat(ranges[7], is("Framework")); - - assertThat(ranges[8], is(boundary)); - assertThat(ranges[9], is("Content-Type: text/plain")); - assertThat(ranges[10], is("Content-Range: bytes 17-20/39")); - assertThat(ranges[11], is("test")); - } - @Test // SPR-10848 public void writeByteArrayNullMediaType() throws IOException { MockHttpOutputMessage outputMessage = new MockHttpOutputMessage(); diff --git a/spring-web/src/test/java/org/springframework/http/converter/ResourceRegionHttpMessageConverterTests.java b/spring-web/src/test/java/org/springframework/http/converter/ResourceRegionHttpMessageConverterTests.java new file mode 100644 index 00000000000..a027c0d56bc --- /dev/null +++ b/spring-web/src/test/java/org/springframework/http/converter/ResourceRegionHttpMessageConverterTests.java @@ -0,0 +1,142 @@ +/* + * 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 static org.hamcrest.Matchers.is; +import static org.junit.Assert.*; + +import java.lang.reflect.Type; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.List; + +import org.hamcrest.Matchers; +import org.junit.Test; + +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.Resource; +import org.springframework.core.io.ResourceRegion; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpRange; +import org.springframework.http.MediaType; +import org.springframework.http.MockHttpOutputMessage; +import org.springframework.util.StringUtils; + +/** + * Test cases for {@link ResourceRegionHttpMessageConverter} class. + * + * @author Brian Clozel + */ +public class ResourceRegionHttpMessageConverterTests { + + private final ResourceRegionHttpMessageConverter converter = new ResourceRegionHttpMessageConverter(); + + @Test + public void canReadResource() { + assertFalse(converter.canRead(Resource.class, MediaType.APPLICATION_OCTET_STREAM)); + assertFalse(converter.canRead(Resource.class, MediaType.ALL)); + assertFalse(converter.canRead(List.class, MediaType.APPLICATION_OCTET_STREAM)); + assertFalse(converter.canRead(List.class, MediaType.ALL)); + } + + @Test + public void canWriteResource() { + assertTrue(converter.canWrite(ResourceRegion.class, null, MediaType.APPLICATION_OCTET_STREAM)); + assertTrue(converter.canWrite(ResourceRegion.class, null, MediaType.ALL)); + } + + @Test + public void canWriteResourceCollection() { + Type resourceRegionList = new ParameterizedTypeReference>() {}.getType(); + assertTrue(converter.canWrite(resourceRegionList, null, MediaType.APPLICATION_OCTET_STREAM)); + assertTrue(converter.canWrite(resourceRegionList, null, MediaType.ALL)); + + assertFalse(converter.canWrite(List.class, MediaType.APPLICATION_OCTET_STREAM)); + assertFalse(converter.canWrite(List.class, MediaType.ALL)); + } + + @Test + public void shouldWritePartialContentByteRange() throws Exception { + MockHttpOutputMessage outputMessage = new MockHttpOutputMessage(); + Resource body = new ClassPathResource("byterangeresource.txt", getClass()); + ResourceRegion region = HttpRange.createByteRange(0, 5).toResourceRegion(body); + converter.write(region, MediaType.TEXT_PLAIN, outputMessage); + + HttpHeaders headers = outputMessage.getHeaders(); + assertThat(headers.getContentType(), is(MediaType.TEXT_PLAIN)); + assertThat(headers.getContentLength(), is(6L)); + assertThat(headers.get(HttpHeaders.CONTENT_RANGE).size(), is(1)); + assertThat(headers.get(HttpHeaders.CONTENT_RANGE).get(0), is("bytes 0-5/39")); + assertThat(outputMessage.getBodyAsString(Charset.forName("UTF-8")), is("Spring")); + } + + @Test + public void shouldWritePartialContentByteRangeNoEnd() throws Exception { + MockHttpOutputMessage outputMessage = new MockHttpOutputMessage(); + Resource body = new ClassPathResource("byterangeresource.txt", getClass()); + ResourceRegion region = HttpRange.createByteRange(7).toResourceRegion(body); + converter.write(region, MediaType.TEXT_PLAIN, outputMessage); + + HttpHeaders headers = outputMessage.getHeaders(); + assertThat(headers.getContentType(), is(MediaType.TEXT_PLAIN)); + assertThat(headers.getContentLength(), is(32L)); + assertThat(headers.get(HttpHeaders.CONTENT_RANGE).size(), is(1)); + assertThat(headers.get(HttpHeaders.CONTENT_RANGE).get(0), is("bytes 7-38/39")); + assertThat(outputMessage.getBodyAsString(Charset.forName("UTF-8")), is("Framework test resource content.")); + } + + @Test + public void partialContentMultipleByteRanges() throws Exception { + MockHttpOutputMessage outputMessage = new MockHttpOutputMessage(); + Resource body = new ClassPathResource("byterangeresource.txt", getClass()); + List rangeList = HttpRange.parseRanges("bytes=0-5,7-15,17-20,22-38"); + List regions = new ArrayList(); + for(HttpRange range : rangeList) { + regions.add(range.toResourceRegion(body)); + } + + converter.write(regions, MediaType.TEXT_PLAIN, outputMessage); + + HttpHeaders headers = outputMessage.getHeaders(); + assertThat(headers.getContentType().toString(), Matchers.startsWith("multipart/byteranges;boundary=")); + String boundary = "--" + headers.getContentType().toString().substring(30); + String content = outputMessage.getBodyAsString(Charset.forName("UTF-8")); + String[] ranges = StringUtils.tokenizeToStringArray(content, "\r\n", false, true); + + assertThat(ranges[0], is(boundary)); + assertThat(ranges[1], is("Content-Type: text/plain")); + assertThat(ranges[2], is("Content-Range: bytes 0-5/39")); + assertThat(ranges[3], is("Spring")); + + assertThat(ranges[4], is(boundary)); + assertThat(ranges[5], is("Content-Type: text/plain")); + assertThat(ranges[6], is("Content-Range: bytes 7-15/39")); + assertThat(ranges[7], is("Framework")); + + assertThat(ranges[8], is(boundary)); + assertThat(ranges[9], is("Content-Type: text/plain")); + assertThat(ranges[10], is("Content-Range: bytes 17-20/39")); + assertThat(ranges[11], is("test")); + + assertThat(ranges[12], is(boundary)); + assertThat(ranges[13], is("Content-Type: text/plain")); + assertThat(ranges[14], is("Content-Range: bytes 22-38/39")); + assertThat(ranges[15], is("resource content.")); + } + +} \ No newline at end of file diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/HttpEntityMethodProcessor.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/HttpEntityMethodProcessor.java index b3913d79588..6545d4a8404 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/HttpEntityMethodProcessor.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/HttpEntityMethodProcessor.java @@ -26,12 +26,9 @@ import java.util.Map; import org.springframework.core.MethodParameter; import org.springframework.core.ResolvableType; -import org.springframework.core.io.Resource; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; -import org.springframework.http.HttpRange; -import org.springframework.http.HttpRangeResource; import org.springframework.http.HttpStatus; import org.springframework.http.RequestEntity; import org.springframework.http.ResponseEntity; @@ -197,24 +194,8 @@ public class HttpEntityMethodProcessor extends AbstractMessageConverterMethodPro } } - Object body = responseEntity.getBody(); - if (inputMessage.getHeaders().containsKey(HttpHeaders.RANGE) && - Resource.class.isAssignableFrom(body.getClass())) { - try { - List httpRanges = inputMessage.getHeaders().getRange(); - Resource bodyResource = (Resource) body; - body = new HttpRangeResource(httpRanges, bodyResource); - outputMessage.setStatusCode(HttpStatus.PARTIAL_CONTENT); - } - catch (IllegalArgumentException ex) { - outputMessage.setStatusCode(HttpStatus.REQUESTED_RANGE_NOT_SATISFIABLE); - outputMessage.flush(); - return; - } - } - // Try even with null body. ResponseBodyAdvice could get involved. - writeWithMessageConverters(body, returnType, inputMessage, outputMessage); + writeWithMessageConverters(responseEntity.getBody(), returnType, inputMessage, outputMessage); // Ensure headers are flushed even if no body was written. outputMessage.flush(); diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestResponseBodyMethodProcessor.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestResponseBodyMethodProcessor.java index 9e82398f1cd..e8de9e85925 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestResponseBodyMethodProcessor.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestResponseBodyMethodProcessor.java @@ -19,16 +19,12 @@ package org.springframework.web.servlet.mvc.method.annotation; import java.io.IOException; import java.lang.reflect.Type; import java.util.List; + import javax.servlet.http.HttpServletRequest; import org.springframework.core.Conventions; import org.springframework.core.MethodParameter; import org.springframework.core.annotation.AnnotatedElementUtils; -import org.springframework.core.io.Resource; -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpRange; -import org.springframework.http.HttpRangeResource; -import org.springframework.http.HttpStatus; import org.springframework.http.converter.HttpMessageConverter; import org.springframework.http.converter.HttpMessageNotReadableException; import org.springframework.http.converter.HttpMessageNotWritableException; @@ -173,21 +169,6 @@ public class RequestResponseBodyMethodProcessor extends AbstractMessageConverter ServletServerHttpRequest inputMessage = createInputMessage(webRequest); ServletServerHttpResponse outputMessage = createOutputMessage(webRequest); - if (inputMessage.getHeaders().containsKey(HttpHeaders.RANGE) && - Resource.class.isAssignableFrom(returnValue.getClass())) { - try { - List httpRanges = inputMessage.getHeaders().getRange(); - Resource bodyResource = (Resource) returnValue; - returnValue = new HttpRangeResource(httpRanges, bodyResource); - outputMessage.setStatusCode(HttpStatus.PARTIAL_CONTENT); - } - catch (IllegalArgumentException ex) { - outputMessage.setStatusCode(HttpStatus.REQUESTED_RANGE_NOT_SATISFIABLE); - outputMessage.flush(); - return; - } - } - // Try even with null return value. ResponseBodyAdvice could get involved. writeWithMessageConverters(returnValue, returnType, inputMessage, outputMessage); } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceHttpRequestHandler.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceHttpRequestHandler.java index 7cf6d69ebc6..f8101dcdefb 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceHttpRequestHandler.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceHttpRequestHandler.java @@ -20,6 +20,7 @@ import java.io.IOException; import java.net.URLDecoder; import java.util.ArrayList; import java.util.List; + import javax.servlet.ServletException; import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; @@ -30,12 +31,13 @@ import org.apache.commons.logging.LogFactory; import org.springframework.beans.factory.InitializingBean; import org.springframework.core.io.Resource; +import org.springframework.core.io.ResourceRegion; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.HttpRange; -import org.springframework.http.HttpRangeResource; import org.springframework.http.MediaType; import org.springframework.http.converter.ResourceHttpMessageConverter; +import org.springframework.http.converter.ResourceRegionHttpMessageConverter; import org.springframework.http.server.ServletServerHttpRequest; import org.springframework.http.server.ServletServerHttpResponse; import org.springframework.util.Assert; @@ -105,6 +107,8 @@ public class ResourceHttpRequestHandler extends WebContentGenerator private ResourceHttpMessageConverter resourceHttpMessageConverter; + private ResourceRegionHttpMessageConverter resourceRegionHttpMessageConverter; + private ContentNegotiationManager contentNegotiationManager; private CorsConfiguration corsConfiguration; @@ -169,7 +173,7 @@ public class ResourceHttpRequestHandler extends WebContentGenerator /** * Configure the {@link ResourceHttpMessageConverter} to use. *

By default a {@link ResourceHttpMessageConverter} will be configured. - * @since 4.3 + * @since 4.3.0 */ public void setResourceHttpMessageConverter(ResourceHttpMessageConverter resourceHttpMessageConverter) { this.resourceHttpMessageConverter = resourceHttpMessageConverter; @@ -179,6 +183,20 @@ public class ResourceHttpRequestHandler extends WebContentGenerator return this.resourceHttpMessageConverter; } + /** + * Configure the {@link ResourceRegionHttpMessageConverter} to use. + *

By default a {@link ResourceRegionHttpMessageConverter} will be configured. + * @since 4.3.0 + */ + public ResourceRegionHttpMessageConverter getResourceRegionHttpMessageConverter() { + return resourceRegionHttpMessageConverter; + } + + public void setResourceRegionHttpMessageConverter( + ResourceRegionHttpMessageConverter resourceRegionHttpMessageConverter) { + this.resourceRegionHttpMessageConverter = resourceRegionHttpMessageConverter; + } + /** * Configure a {@code ContentNegotiationManager} to determine the media types * for resources being served. If the manager contains a path @@ -225,13 +243,15 @@ public class ResourceHttpRequestHandler extends WebContentGenerator this.resourceResolvers.add(new PathResourceResolver()); } initAllowedLocations(); - if (this.contentNegotiationManager == null) { this.contentNegotiationManager = initContentNegotiationManager(); } if (this.resourceHttpMessageConverter == null) { this.resourceHttpMessageConverter = new ResourceHttpMessageConverter(); } + if(this.resourceRegionHttpMessageConverter == null) { + this.resourceRegionHttpMessageConverter = new ResourceRegionHttpMessageConverter(); + } } /** @@ -328,6 +348,8 @@ public class ResourceHttpRequestHandler extends WebContentGenerator } ServletServerHttpResponse outputMessage = new ServletServerHttpResponse(response); + outputMessage.getHeaders().add(HttpHeaders.ACCEPT_RANGES, "bytes"); + if (request.getHeader(HttpHeaders.RANGE) == null) { setHeaders(response, resource, mediaType); this.resourceHttpMessageConverter.write(resource, mediaType, outputMessage); @@ -336,9 +358,15 @@ public class ResourceHttpRequestHandler extends WebContentGenerator ServletServerHttpRequest inputMessage = new ServletServerHttpRequest(request); try { List httpRanges = inputMessage.getHeaders().getRange(); - HttpRangeResource rangeResource = new HttpRangeResource(httpRanges, resource); response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT); - this.resourceHttpMessageConverter.write(rangeResource, mediaType, outputMessage); + if(httpRanges.size() == 1) { + ResourceRegion resourceRegion = httpRanges.get(0).toResourceRegion(resource); + this.resourceRegionHttpMessageConverter.write(resourceRegion, mediaType, outputMessage); + } + else { + this.resourceRegionHttpMessageConverter + .write(HttpRange.toResourceRegions(httpRanges, resource), mediaType, outputMessage); + } } catch (IllegalArgumentException ex) { response.addHeader("Content-Range", "bytes */" + resource.contentLength()); diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/HttpEntityMethodProcessorMockTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/HttpEntityMethodProcessorMockTests.java index 8e509982997..6acf1028752 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/HttpEntityMethodProcessorMockTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/HttpEntityMethodProcessorMockTests.java @@ -16,6 +16,10 @@ package org.springframework.web.servlet.mvc.method.annotation; +import static org.junit.Assert.*; +import static org.mockito.BDDMockito.*; +import static org.springframework.web.servlet.HandlerMapping.*; + import java.lang.reflect.Method; import java.net.URI; import java.nio.charset.Charset; @@ -39,7 +43,6 @@ import org.springframework.http.HttpHeaders; import org.springframework.http.HttpInputMessage; import org.springframework.http.HttpMethod; import org.springframework.http.HttpOutputMessage; -import org.springframework.http.HttpRangeResource; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.RequestEntity; @@ -53,13 +56,6 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.context.request.ServletWebRequest; import org.springframework.web.method.support.ModelAndViewContainer; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; -import static org.mockito.BDDMockito.*; -import static org.springframework.web.servlet.HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE; - /** * Test fixture for {@link HttpEntityMethodProcessor} delegating to a mock * {@link HttpMessageConverter}. @@ -539,39 +535,6 @@ public class HttpEntityMethodProcessorMockTests { assertEquals(200, servletResponse.getStatus()); } - @Test - public void handleReturnTypeResourceByteRange() throws Exception { - Resource resource = new ByteArrayResource("Content".getBytes(Charset.forName("UTF-8"))); - ResponseEntity returnValue = ResponseEntity.ok(resource); - servletRequest.addHeader("Range", "bytes=0-5"); - - given(resourceMessageConverter.canWrite(HttpRangeResource.class, null)).willReturn(true); - given(resourceMessageConverter.getSupportedMediaTypes()).willReturn(Collections.singletonList(MediaType.ALL)); - given(resourceMessageConverter.canWrite(HttpRangeResource.class, MediaType.APPLICATION_OCTET_STREAM)).willReturn(true); - - processor.handleReturnValue(returnValue, returnTypeResponseEntityResource, mavContainer, webRequest); - - then(resourceMessageConverter).should(times(1)).write(any(ByteArrayResource.class), - eq(MediaType.APPLICATION_OCTET_STREAM), any(HttpOutputMessage.class)); - assertEquals(206, servletResponse.getStatus()); - } - - @Test - public void handleReturnTypeResourceIllegalByteRange() throws Exception { - Resource resource = new ByteArrayResource("Content".getBytes(Charset.forName("UTF-8"))); - ResponseEntity returnValue = ResponseEntity.ok(resource); - servletRequest.addHeader("Range", "illegal"); - - given(resourceMessageConverter.canWrite(ByteArrayResource.class, null)).willReturn(true); - given(resourceMessageConverter.getSupportedMediaTypes()).willReturn(Collections.singletonList(MediaType.ALL)); - - processor.handleReturnValue(returnValue, returnTypeResponseEntityResource, mavContainer, webRequest); - - then(resourceMessageConverter).should(never()).write(any(ByteArrayResource.class), - eq(MediaType.APPLICATION_OCTET_STREAM), any(HttpOutputMessage.class)); - assertEquals(416, servletResponse.getStatus()); - } - private void initStringMessageConversion(MediaType accepted) { given(stringHttpMessageConverter.canWrite(String.class, null)).willReturn(true); given(stringHttpMessageConverter.getSupportedMediaTypes()).willReturn(Collections.singletonList(MediaType.TEXT_PLAIN)); diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestResponseBodyMethodProcessorMockTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestResponseBodyMethodProcessorMockTests.java index c8cddcc1498..645e66ddf77 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestResponseBodyMethodProcessorMockTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestResponseBodyMethodProcessorMockTests.java @@ -36,7 +36,6 @@ import org.springframework.core.io.ByteArrayResource; import org.springframework.core.io.Resource; import org.springframework.http.HttpInputMessage; import org.springframework.http.HttpOutputMessage; -import org.springframework.http.HttpRangeResource; import org.springframework.http.MediaType; import org.springframework.http.converter.HttpMessageConverter; import org.springframework.http.converter.HttpMessageNotReadableException; @@ -313,37 +312,6 @@ public class RequestResponseBodyMethodProcessorMockTests { assertEquals(200, servletResponse.getStatus()); } - @Test - public void handleReturnTypeResourceByteRange() throws Exception { - Resource returnValue = new ByteArrayResource("Content".getBytes(Charset.forName("UTF-8"))); - servletRequest.addHeader("Range", "bytes=0-5"); - - given(resourceMessageConverter.canWrite(HttpRangeResource.class, null)).willReturn(true); - given(resourceMessageConverter.getSupportedMediaTypes()).willReturn(Collections.singletonList(MediaType.ALL)); - given(resourceMessageConverter.canWrite(HttpRangeResource.class, MediaType.APPLICATION_OCTET_STREAM)).willReturn(true); - - processor.handleReturnValue(returnValue, returnTypeResource, mavContainer, webRequest); - - then(resourceMessageConverter).should(times(1)).write(any(ByteArrayResource.class), - eq(MediaType.APPLICATION_OCTET_STREAM), any(HttpOutputMessage.class)); - assertEquals(206, servletResponse.getStatus()); - } - - @Test - public void handleReturnTypeResourceIllegalByteRange() throws Exception { - Resource returnValue = new ByteArrayResource("Content".getBytes(Charset.forName("UTF-8"))); - servletRequest.addHeader("Range", "illegal"); - - given(resourceMessageConverter.canWrite(ByteArrayResource.class, null)).willReturn(true); - given(resourceMessageConverter.getSupportedMediaTypes()).willReturn(Collections.singletonList(MediaType.ALL)); - - processor.handleReturnValue(returnValue, returnTypeResource, mavContainer, webRequest); - - then(resourceMessageConverter).should(never()).write(any(ByteArrayResource.class), - eq(MediaType.APPLICATION_OCTET_STREAM), any(HttpOutputMessage.class)); - assertEquals(416, servletResponse.getStatus()); - } - // SPR-9841 @Test