Refactor HTTP Range support with ResourceRegion

Prior to this commit, the `ResourceHttpMessageConverter` would support
all HTTP Range requests and `MethodProcessors` would "wrap" controller
handler return values with a `HttpRangeResource` to support that use
case in Controllers.

This commit refactors that support in several ways:
* a new ResourceRegion class has been introduced
* a new, separate, ResourceRegionHttpMessageConverter handles the HTTP
range use cases when serving static resources with the
ResourceHttpRequestHandler
* the support of HTTP range requests on Controller handlers has been
removed until a better solution is found

Issue: SPR-14221, SPR-13834
This commit is contained in:
Brian Clozel
2016-05-02 15:39:07 +02:00
parent 7737c3c7e5
commit 5ac31fb39d
16 changed files with 627 additions and 468 deletions

View File

@@ -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<ResourceRegion> toResourceRegions(List<HttpRange> ranges, Resource resource) {
if(ranges == null || ranges.size() == 0) {
return Collections.emptyList();
}
List<ResourceRegion> regions = new ArrayList<ResourceRegion>(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.
* <p>This method can be used to for an {@code Range} header.

View File

@@ -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.
*
* <p>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<HttpRange> httpRanges;
private final Resource resource;
public HttpRangeResource(List<HttpRange> 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<HttpRange> 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();
}
}

View File

@@ -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.
*
* <p>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<Resource> {
@@ -114,13 +106,7 @@ public class ResourceHttpMessageConverter extends AbstractHttpMessageConverter<R
protected void writeInternal(Resource resource, HttpOutputMessage outputMessage)
throws IOException, HttpMessageNotWritableException {
outputMessage.getHeaders().add(HttpHeaders.ACCEPT_RANGES, "bytes");
if (resource instanceof HttpRangeResource) {
writePartialContent((HttpRangeResource) resource, outputMessage);
}
else {
writeContent(resource, outputMessage);
}
writeContent(resource, outputMessage);
}
protected void writeContent(Resource resource, HttpOutputMessage outputMessage)
@@ -147,99 +133,6 @@ public class ResourceHttpMessageConverter extends AbstractHttpMessageConverter<R
}
}
/**
* Write parts of the resource as indicated by the request {@code Range} header.
* @param resource the identified resource (never {@code null})
* @param outputMessage current servlet response
* @throws IOException in case of errors while writing the content
*/
protected void writePartialContent(HttpRangeResource resource, HttpOutputMessage outputMessage) throws IOException {
Assert.notNull(resource, "Resource should not be null");
List<HttpRange> 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.

View File

@@ -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<Object> {
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<ResourceRegion> regions = (Collection<ResourceRegion>) object;
if(regions.size() == 1) {
writeResourceRegion(regions.iterator().next(), outputMessage);
}
else {
writeResourceRegionCollection((Collection<ResourceRegion>) 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<ResourceRegion> 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<? extends Object> clazz, HttpInputMessage inputMessage)
throws IOException, HttpMessageNotReadableException {
return null;
}
}

View File

@@ -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);
}
}

View File

@@ -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<HttpRange> 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<HttpRange> 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<HttpRange> 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<HttpRange> 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<HttpRange> 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<HttpRange> 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();

View File

@@ -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<List<ResourceRegion>>() {}.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<HttpRange> rangeList = HttpRange.parseRanges("bytes=0-5,7-15,17-20,22-38");
List<ResourceRegion> regions = new ArrayList<ResourceRegion>();
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."));
}
}