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:
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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."));
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user