Support HTTP range requests in Controllers
Prior to this commit, HTTP Range requests were only supported by the
ResourceHttpRequestHandler when serving static resources.
This commit improves the ResourceHttpMessageConverter that
now supports partial writes of Resources.
For this, the `HttpEntityMethodProcessor` and
`RequestResponseBodyMethodProcessor` now wrap resources with HTTP
range information in a `HttpRangeResource`, if necessary. The
message converter handle those types and knows how to handle partial
writes.
Controller methods can now handle Range requests for
return types that extend Resource or HttpEntity:
@RequestMapping("/example/video.mp4")
public Resource handler() { }
@RequestMapping("/example/video.mp4")
public HttpEntity<Resource> handler() { }
Issue: 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.
|
||||
@@ -16,28 +16,41 @@
|
||||
|
||||
package org.springframework.http.converter;
|
||||
|
||||
import static org.hamcrest.core.Is.*;
|
||||
import static org.hamcrest.core.IsInstanceOf.*;
|
||||
import static org.junit.Assert.*;
|
||||
import static org.mockito.BDDMockito.*;
|
||||
import static org.mockito.Matchers.any;
|
||||
import static org.mockito.Mockito.doThrow;
|
||||
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 static org.hamcrest.core.Is.*;
|
||||
import static org.hamcrest.core.IsInstanceOf.*;
|
||||
import static org.junit.Assert.*;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* @author Arjen Poutsma
|
||||
* @author Kazuki Shimizu
|
||||
* @author Brian Clozel
|
||||
*/
|
||||
public class ResourceHttpMessageConverterTests {
|
||||
|
||||
@@ -45,18 +58,18 @@ public class ResourceHttpMessageConverterTests {
|
||||
|
||||
|
||||
@Test
|
||||
public void canRead() {
|
||||
public void canReadResource() {
|
||||
assertTrue(converter.canRead(Resource.class, new MediaType("application", "octet-stream")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void canWrite() {
|
||||
public void canWriteResource() {
|
||||
assertTrue(converter.canWrite(Resource.class, new MediaType("application", "octet-stream")));
|
||||
assertTrue(converter.canWrite(Resource.class, MediaType.ALL));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void read() throws IOException {
|
||||
public void shouldReadImageResource() throws IOException {
|
||||
byte[] body = FileCopyUtils.copyToByteArray(getClass().getResourceAsStream("logo.jpg"));
|
||||
MockHttpInputMessage inputMessage = new MockHttpInputMessage(body);
|
||||
inputMessage.getHeaders().setContentType(MediaType.IMAGE_JPEG);
|
||||
@@ -65,7 +78,7 @@ public class ResourceHttpMessageConverterTests {
|
||||
}
|
||||
|
||||
@Test // SPR-13443
|
||||
public void readWithInputStreamResource() throws IOException {
|
||||
public void shouldReadInputStreamResource() throws IOException {
|
||||
try (InputStream body = getClass().getResourceAsStream("logo.jpg") ) {
|
||||
MockHttpInputMessage inputMessage = new MockHttpInputMessage(body);
|
||||
inputMessage.getHeaders().setContentType(MediaType.IMAGE_JPEG);
|
||||
@@ -76,7 +89,7 @@ public class ResourceHttpMessageConverterTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void write() throws IOException {
|
||||
public void shouldWriteImageResource() throws IOException {
|
||||
MockHttpOutputMessage outputMessage = new MockHttpOutputMessage();
|
||||
Resource body = new ClassPathResource("logo.jpg", getClass());
|
||||
converter.write(body, null, outputMessage);
|
||||
@@ -85,6 +98,116 @@ 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();
|
||||
@@ -94,4 +217,45 @@ public class ResourceHttpMessageConverterTests {
|
||||
assertTrue(Arrays.equals(byteArray, outputMessage.getBodyAsBytes()));
|
||||
}
|
||||
|
||||
// SPR-12999
|
||||
@Test @SuppressWarnings("unchecked")
|
||||
public void writeContentNotGettingInputStream() throws Exception {
|
||||
MockHttpOutputMessage outputMessage = new MockHttpOutputMessage();
|
||||
Resource resource = mock(Resource.class);
|
||||
given(resource.getInputStream()).willThrow(FileNotFoundException.class);
|
||||
|
||||
converter.write(resource, MediaType.APPLICATION_OCTET_STREAM, outputMessage);
|
||||
|
||||
assertEquals(0, outputMessage.getHeaders().getContentLength());
|
||||
}
|
||||
|
||||
// SPR-12999
|
||||
@Test
|
||||
public void writeContentNotClosingInputStream() throws Exception {
|
||||
MockHttpOutputMessage outputMessage = new MockHttpOutputMessage();
|
||||
Resource resource = mock(Resource.class);
|
||||
InputStream inputStream = mock(InputStream.class);
|
||||
given(resource.getInputStream()).willReturn(inputStream);
|
||||
given(inputStream.read(any())).willReturn(-1);
|
||||
doThrow(new NullPointerException()).when(inputStream).close();
|
||||
|
||||
converter.write(resource, MediaType.APPLICATION_OCTET_STREAM, outputMessage);
|
||||
|
||||
assertEquals(0, outputMessage.getHeaders().getContentLength());
|
||||
}
|
||||
|
||||
// SPR-13620
|
||||
@Test @SuppressWarnings("unchecked")
|
||||
public void writeContentInputStreamThrowingNullPointerException() throws Exception {
|
||||
MockHttpOutputMessage outputMessage = new MockHttpOutputMessage();
|
||||
Resource resource = mock(Resource.class);
|
||||
InputStream in = mock(InputStream.class);
|
||||
given(resource.getInputStream()).willReturn(in);
|
||||
given(in.read(any())).willThrow(NullPointerException.class);
|
||||
|
||||
converter.write(resource, MediaType.APPLICATION_OCTET_STREAM, outputMessage);
|
||||
|
||||
assertEquals(0, outputMessage.getHeaders().getContentLength());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user