diff --git a/spring-web/src/main/java/org/springframework/http/HttpRangeResource.java b/spring-web/src/main/java/org/springframework/http/HttpRangeResource.java
new file mode 100644
index 0000000000..0e209a63ff
--- /dev/null
+++ b/spring-web/src/main/java/org/springframework/http/HttpRangeResource.java
@@ -0,0 +1,119 @@
+/*
+ * 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.0
+ */
+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 httpRanges;
+ }
+
+ @Override
+ public boolean exists() {
+ return resource.exists();
+ }
+
+ @Override
+ public boolean isReadable() {
+ return resource.isReadable();
+ }
+
+ @Override
+ public boolean isOpen() {
+ return resource.isOpen();
+ }
+
+ @Override
+ public URL getURL() throws IOException {
+ return resource.getURL();
+ }
+
+ @Override
+ public URI getURI() throws IOException {
+ return resource.getURI();
+ }
+
+ @Override
+ public File getFile() throws IOException {
+ return resource.getFile();
+ }
+
+ @Override
+ public long contentLength() throws IOException {
+ return resource.contentLength();
+ }
+
+ @Override
+ public long lastModified() throws IOException {
+ return resource.lastModified();
+ }
+
+ @Override
+ public Resource createRelative(String relativePath) throws IOException {
+ return resource.createRelative(relativePath);
+ }
+
+ @Override
+ public String getFilename() {
+ return resource.getFilename();
+ }
+
+ @Override
+ public String getDescription() {
+ return resource.getDescription();
+ }
+
+ @Override
+ public InputStream getInputStream() throws IOException {
+ return 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 3d7db2fba5..f7c9fe0279 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
@@ -16,8 +16,12 @@
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;
@@ -25,23 +29,33 @@ 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.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;
/**
- * Implementation of {@link HttpMessageConverter} that can read and write {@link Resource Resources}.
+ * Implementation of {@link HttpMessageConverter} that can read and write {@link Resource Resources}
+ * and supports byte range requests.
*
*
By default, this converter can read all media types. The Java Activation Framework (JAF) -
* 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 {
@@ -64,7 +78,7 @@ public class ResourceHttpMessageConverter extends AbstractHttpMessageConverter clazz, HttpInputMessage inputMessage)
throws IOException, HttpMessageNotReadableException {
- if (InputStreamResource.class == clazz){
+ if (InputStreamResource.class == clazz) {
return new InputStreamResource(inputMessage.getBody());
}
else if (clazz.isAssignableFrom(ByteArrayResource.class)) {
@@ -94,6 +108,9 @@ public class ResourceHttpMessageConverter extends AbstractHttpMessageConverter Integer.MAX_VALUE) {
+ throw new IOException("Resource content too long (beyond Integer.MAX_VALUE): " + resource);
+ }
return (contentLength < 0 ? null : contentLength);
}
@@ -101,18 +118,140 @@ 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((int) 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;
}
}
- outputMessage.getBody().flush();
}
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 10752f22ea..b94508af62 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
@@ -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 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();
@@ -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());
+ }
+
}
diff --git a/spring-web/src/test/resources/org/springframework/http/converter/byterangeresource.txt b/spring-web/src/test/resources/org/springframework/http/converter/byterangeresource.txt
new file mode 100644
index 0000000000..84bbb9ddaf
--- /dev/null
+++ b/spring-web/src/test/resources/org/springframework/http/converter/byterangeresource.txt
@@ -0,0 +1 @@
+Spring Framework test 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 a169dc795d..8aa17b53ec 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,9 +26,12 @@ 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;
@@ -194,6 +197,19 @@ public class HttpEntityMethodProcessor extends AbstractMessageConverterMethodPro
return;
}
}
+ 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 exc) {
+ outputMessage.setStatusCode(HttpStatus.REQUESTED_RANGE_NOT_SATISFIABLE);
+ outputMessage.flush();
+ return;
+ }
+ }
// Try even with null body. ResponseBodyAdvice could get involved.
writeWithMessageConverters(body, returnType, inputMessage, outputMessage);
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 b69911a832..5780b2d11c 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
@@ -24,10 +24,16 @@ import javax.servlet.http.HttpServletRequest;
import org.springframework.core.Conventions;
import org.springframework.core.MethodParameter;
import org.springframework.core.annotation.AnnotationUtils;
+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;
import org.springframework.http.server.ServletServerHttpRequest;
+import org.springframework.http.server.ServletServerHttpResponse;
import org.springframework.validation.BindingResult;
import org.springframework.web.HttpMediaTypeNotAcceptableException;
import org.springframework.web.HttpMediaTypeNotSupportedException;
@@ -164,9 +170,25 @@ public class RequestResponseBodyMethodProcessor extends AbstractMessageConverter
throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException {
mavContainer.setRequestHandled(true);
+ 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 exc) {
+ outputMessage.setStatusCode(HttpStatus.REQUESTED_RANGE_NOT_SATISFIABLE);
+ outputMessage.flush();
+ return;
+ }
+ }
// Try even with null return value. ResponseBodyAdvice could get involved.
- writeWithMessageConverters(returnValue, returnType, webRequest);
+ 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 b13d7fbc32..ae195b02d4 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
@@ -16,15 +16,12 @@
package org.springframework.web.servlet.resource;
-import java.io.FileNotFoundException;
import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
import java.net.URLDecoder;
import java.util.ArrayList;
import java.util.List;
+
import javax.servlet.ServletException;
-import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@@ -36,15 +33,15 @@ import org.springframework.core.io.Resource;
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.server.ServletServerHttpRequest;
+import org.springframework.http.server.ServletServerHttpResponse;
import org.springframework.util.Assert;
-import org.springframework.util.ClassUtils;
import org.springframework.util.CollectionUtils;
-import org.springframework.util.MimeTypeUtils;
import org.springframework.util.ObjectUtils;
import org.springframework.util.ResourceUtils;
-import org.springframework.util.StreamUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.HttpMediaTypeNotAcceptableException;
import org.springframework.web.HttpRequestHandler;
@@ -94,16 +91,14 @@ public class ResourceHttpRequestHandler extends WebContentGenerator
private static final Log logger = LogFactory.getLog(ResourceHttpRequestHandler.class);
- private static final boolean jafPresent = ClassUtils.isPresent(
- "javax.activation.FileTypeMap", ResourceHttpRequestHandler.class.getClassLoader());
-
-
private final List locations = new ArrayList(4);
private final List resourceResolvers = new ArrayList(4);
private final List resourceTransformers = new ArrayList(4);
+ private ResourceHttpMessageConverter resourceHttpMessageConverter;
+
private ContentNegotiationManager contentNegotiationManager;
private CorsConfiguration corsConfiguration;
@@ -165,6 +160,20 @@ public class ResourceHttpRequestHandler extends WebContentGenerator
return this.resourceTransformers;
}
+ /**
+ * Configure the {@link ResourceHttpMessageConverter} to use.
+ *
By default a {@link ResourceHttpMessageConverter} will be configured.
+ * @since 4.3.0
+ */
+ public void setResourceHttpMessageConverter(ResourceHttpMessageConverter resourceHttpMessageConverter) {
+ this.resourceHttpMessageConverter = resourceHttpMessageConverter;
+ }
+
+ public ResourceHttpMessageConverter getResourceHttpMessageConverter() {
+ return resourceHttpMessageConverter;
+ }
+
+
/**
* Configure a {@code ContentNegotiationManager} to determine the media types
* for resources being served. If the manager contains a path
@@ -177,7 +186,7 @@ public class ResourceHttpRequestHandler extends WebContentGenerator
* settings is used to create the manager. See the Javadoc of
* {@code ContentNegotiationManagerFactoryBean} for details
* @param contentNegotiationManager the manager to use
- * @since 4.3
+ * @since 4.3.0
*/
public void setContentNegotiationManager(ContentNegotiationManager contentNegotiationManager) {
this.contentNegotiationManager = contentNegotiationManager;
@@ -215,6 +224,9 @@ public class ResourceHttpRequestHandler extends WebContentGenerator
if (this.contentNegotiationManager == null) {
this.contentNegotiationManager = initContentNegotiationManager();
}
+ if( this.resourceHttpMessageConverter == null) {
+ this.resourceHttpMessageConverter = new ResourceHttpMessageConverter();
+ }
}
/**
@@ -226,7 +238,7 @@ public class ResourceHttpRequestHandler extends WebContentGenerator
if (CollectionUtils.isEmpty(this.locations)) {
return;
}
- for (int i = getResourceResolvers().size()-1; i >= 0; i--) {
+ for (int i = getResourceResolvers().size() - 1; i >= 0; i--) {
if (getResourceResolvers().get(i) instanceof PathResourceResolver) {
PathResourceResolver pathResolver = (PathResourceResolver) getResourceResolvers().get(i);
if (ObjectUtils.isEmpty(pathResolver.getAllowedLocations())) {
@@ -310,12 +322,26 @@ public class ResourceHttpRequestHandler extends WebContentGenerator
return;
}
+ ServletServerHttpResponse outputMessage = new ServletServerHttpResponse(response);
if (request.getHeader(HttpHeaders.RANGE) == null) {
setHeaders(response, resource, mediaType);
- writeContent(response, resource);
+ this.resourceHttpMessageConverter.write(resource, mediaType, outputMessage);
}
else {
- writePartialContent(request, response, resource, mediaType);
+ 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);
+ }
+ catch (IllegalArgumentException ex) {
+ Long contentLength = resource.contentLength();
+ if (contentLength != null) {
+ response.addHeader("Content-Range", "bytes */" + resource.contentLength());
+ }
+ response.sendError(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
+ }
}
}
@@ -507,140 +533,6 @@ public class ResourceHttpRequestHandler extends WebContentGenerator
response.setHeader(HttpHeaders.ACCEPT_RANGES, "bytes");
}
- /**
- * Write the actual content out to the given servlet response,
- * streaming the resource's content.
- * @param response current servlet response
- * @param resource the identified resource (never {@code null})
- * @throws IOException in case of errors while writing the content
- */
- protected void writeContent(HttpServletResponse response, Resource resource) throws IOException {
- try {
- InputStream in = resource.getInputStream();
- try {
- StreamUtils.copy(in, response.getOutputStream());
- }
- catch (NullPointerException ex) {
- // ignore, see SPR-13620
- }
- finally {
- try {
- in.close();
- }
- catch (Throwable ex) {
- // ignore, see SPR-12999
- }
- }
- }
- catch (FileNotFoundException ex) {
- // ignore, see SPR-12999
- }
- }
-
- /**
- * Write parts of the resource as indicated by the request {@code Range} header.
- * @param request current servlet request
- * @param response current servlet response
- * @param resource the identified resource (never {@code null})
- * @param contentType the content type
- * @throws IOException in case of errors while writing the content
- */
- protected void writePartialContent(HttpServletRequest request, HttpServletResponse response,
- Resource resource, MediaType contentType) throws IOException {
-
- long length = resource.contentLength();
-
- List ranges;
- try {
- HttpHeaders headers = new ServletServerHttpRequest(request).getHeaders();
- ranges = headers.getRange();
- }
- catch (IllegalArgumentException ex) {
- response.addHeader("Content-Range", "bytes */" + length);
- response.sendError(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
- return;
- }
-
- response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
-
- if (ranges.size() == 1) {
- HttpRange range = ranges.get(0);
-
- long start = range.getRangeStart(length);
- long end = range.getRangeEnd(length);
- long rangeLength = end - start + 1;
-
- setHeaders(response, resource, contentType);
- response.addHeader("Content-Range", "bytes " + start + "-" + end + "/" + length);
- response.setContentLength((int) rangeLength);
-
- InputStream in = resource.getInputStream();
- try {
- copyRange(in, response.getOutputStream(), start, end);
- }
- finally {
- try {
- in.close();
- }
- catch (IOException ex) {
- // ignore
- }
- }
- }
- else {
- String boundaryString = MimeTypeUtils.generateMultipartBoundaryString();
- response.setContentType("multipart/byteranges; boundary=" + boundaryString);
-
- ServletOutputStream out = response.getOutputStream();
-
- for (HttpRange range : ranges) {
- long start = range.getRangeStart(length);
- long end = range.getRangeEnd(length);
-
- InputStream in = resource.getInputStream();
-
- // Writing MIME header.
- out.println();
- out.println("--" + boundaryString);
- if (contentType != null) {
- out.println("Content-Type: " + contentType);
- }
- out.println("Content-Range: bytes " + start + "-" + end + "/" + length);
- out.println();
-
- // Printing content
- copyRange(in, out, start, end);
- }
- out.println();
- out.print("--" + boundaryString + "--");
- }
- }
-
- 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;
- }
- }
- }
-
-
@Override
public String toString() {
return "ResourceHttpRequestHandler [locations=" + getLocations() + ", resolvers=" + getResourceResolvers() + "]";
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 1dca78ed4a..8e50998299 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
@@ -21,7 +21,6 @@ import java.net.URI;
import java.nio.charset.Charset;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
-import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.List;
@@ -33,11 +32,14 @@ import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.springframework.core.MethodParameter;
+import org.springframework.core.io.ByteArrayResource;
+import org.springframework.core.io.Resource;
import org.springframework.http.HttpEntity;
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;
@@ -55,13 +57,7 @@ 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.any;
-import static org.mockito.BDDMockito.eq;
-import static org.mockito.BDDMockito.given;
-import static org.mockito.BDDMockito.isA;
-import static org.mockito.BDDMockito.mock;
-import static org.mockito.BDDMockito.reset;
-import static org.mockito.BDDMockito.verify;
+import static org.mockito.BDDMockito.*;
import static org.springframework.web.servlet.HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE;
/**
@@ -72,6 +68,7 @@ import static org.springframework.web.servlet.HandlerMapping.PRODUCIBLE_MEDIA_TY
*
* @author Arjen Poutsma
* @author Rossen Stoyanchev
+ * @author Brian Clozel
*/
public class HttpEntityMethodProcessorMockTests {
@@ -79,7 +76,9 @@ public class HttpEntityMethodProcessorMockTests {
private HttpEntityMethodProcessor processor;
- private HttpMessageConverter messageConverter;
+ private HttpMessageConverter stringHttpMessageConverter;
+
+ private HttpMessageConverter resourceMessageConverter;
private MethodParameter paramHttpEntity;
private MethodParameter paramRequestEntity;
@@ -87,6 +86,7 @@ public class HttpEntityMethodProcessorMockTests {
private MethodParameter paramInt;
private MethodParameter returnTypeResponseEntity;
private MethodParameter returnTypeResponseEntityProduces;
+ private MethodParameter returnTypeResponseEntityResource;
private MethodParameter returnTypeHttpEntity;
private MethodParameter returnTypeHttpEntitySubclass;
private MethodParameter returnTypeInt;
@@ -106,12 +106,16 @@ public class HttpEntityMethodProcessorMockTests {
dateFormat = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss z", Locale.US);
dateFormat.setTimeZone(TimeZone.getTimeZone("GMT"));
- messageConverter = mock(HttpMessageConverter.class);
- given(messageConverter.getSupportedMediaTypes()).willReturn(Collections.singletonList(MediaType.TEXT_PLAIN));
+ stringHttpMessageConverter = mock(HttpMessageConverter.class);
+ given(stringHttpMessageConverter.getSupportedMediaTypes()).willReturn(Collections.singletonList(MediaType.TEXT_PLAIN));
+ resourceMessageConverter = mock(HttpMessageConverter.class);
+ given(resourceMessageConverter.getSupportedMediaTypes()).willReturn(Collections.singletonList(MediaType.ALL));
List> converters = new ArrayList<>();
- converters.add(messageConverter);
+ converters.add(stringHttpMessageConverter);
+ converters.add(resourceMessageConverter);
processor = new HttpEntityMethodProcessor(converters);
- reset(messageConverter);
+ reset(stringHttpMessageConverter);
+ reset(resourceMessageConverter);
Method handle1 = getClass().getMethod("handle1", HttpEntity.class, ResponseEntity.class,
Integer.TYPE, RequestEntity.class);
@@ -125,6 +129,7 @@ public class HttpEntityMethodProcessorMockTests {
returnTypeHttpEntity = new MethodParameter(getClass().getMethod("handle2", HttpEntity.class), -1);
returnTypeHttpEntitySubclass = new MethodParameter(getClass().getMethod("handle2x", HttpEntity.class), -1);
returnTypeInt = new MethodParameter(getClass().getMethod("handle3"), -1);
+ returnTypeResponseEntityResource = new MethodParameter(getClass().getMethod("handle5"), -1);
mavContainer = new ModelAndViewContainer();
servletRequest = new MockHttpServletRequest("GET", "/foo");
@@ -159,8 +164,8 @@ public class HttpEntityMethodProcessorMockTests {
servletRequest.addHeader("Content-Type", contentType.toString());
servletRequest.setContent(body.getBytes(Charset.forName("UTF-8")));
- given(messageConverter.canRead(String.class, contentType)).willReturn(true);
- given(messageConverter.read(eq(String.class), isA(HttpInputMessage.class))).willReturn(body);
+ given(stringHttpMessageConverter.canRead(String.class, contentType)).willReturn(true);
+ given(stringHttpMessageConverter.read(eq(String.class), isA(HttpInputMessage.class))).willReturn(body);
Object result = processor.resolveArgument(paramHttpEntity, mavContainer, webRequest, null);
@@ -181,8 +186,8 @@ public class HttpEntityMethodProcessorMockTests {
servletRequest.setRequestURI("/path");
servletRequest.setContent(body.getBytes(Charset.forName("UTF-8")));
- given(messageConverter.canRead(String.class, contentType)).willReturn(true);
- given(messageConverter.read(eq(String.class), isA(HttpInputMessage.class))).willReturn(body);
+ given(stringHttpMessageConverter.canRead(String.class, contentType)).willReturn(true);
+ given(stringHttpMessageConverter.read(eq(String.class), isA(HttpInputMessage.class))).willReturn(body);
Object result = processor.resolveArgument(paramRequestEntity, mavContainer, webRequest, null);
@@ -201,8 +206,8 @@ public class HttpEntityMethodProcessorMockTests {
servletRequest.setMethod("POST");
servletRequest.addHeader("Content-Type", contentType.toString());
- given(messageConverter.getSupportedMediaTypes()).willReturn(Collections.singletonList(contentType));
- given(messageConverter.canRead(String.class, contentType)).willReturn(false);
+ given(stringHttpMessageConverter.getSupportedMediaTypes()).willReturn(Collections.singletonList(contentType));
+ given(stringHttpMessageConverter.canRead(String.class, contentType)).willReturn(false);
processor.resolveArgument(paramHttpEntity, mavContainer, webRequest, null);
@@ -230,7 +235,7 @@ public class HttpEntityMethodProcessorMockTests {
processor.handleReturnValue(returnValue, returnTypeResponseEntity, mavContainer, webRequest);
assertTrue(mavContainer.isRequestHandled());
- verify(messageConverter).write(eq(body), eq(accepted), isA(HttpOutputMessage.class));
+ verify(stringHttpMessageConverter).write(eq(body), eq(accepted), isA(HttpOutputMessage.class));
}
@Test
@@ -241,12 +246,12 @@ public class HttpEntityMethodProcessorMockTests {
servletRequest.addHeader("Accept", "text/*");
servletRequest.setAttribute(PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE, Collections.singleton(MediaType.TEXT_HTML));
- given(messageConverter.canWrite(String.class, MediaType.TEXT_HTML)).willReturn(true);
+ given(stringHttpMessageConverter.canWrite(String.class, MediaType.TEXT_HTML)).willReturn(true);
processor.handleReturnValue(returnValue, returnTypeResponseEntityProduces, mavContainer, webRequest);
assertTrue(mavContainer.isRequestHandled());
- verify(messageConverter).write(eq(body), eq(MediaType.TEXT_HTML), isA(HttpOutputMessage.class));
+ verify(stringHttpMessageConverter).write(eq(body), eq(MediaType.TEXT_HTML), isA(HttpOutputMessage.class));
}
@SuppressWarnings("unchecked")
@@ -262,15 +267,15 @@ public class HttpEntityMethodProcessorMockTests {
given(advice.beforeBodyWrite(any(), any(), any(), any(), any(), any())).willReturn("Foo");
HttpEntityMethodProcessor processor = new HttpEntityMethodProcessor(
- Collections.singletonList(messageConverter), null, Collections.singletonList(advice));
+ Collections.singletonList(stringHttpMessageConverter), null, Collections.singletonList(advice));
- reset(messageConverter);
- given(messageConverter.canWrite(String.class, MediaType.TEXT_HTML)).willReturn(true);
+ reset(stringHttpMessageConverter);
+ given(stringHttpMessageConverter.canWrite(String.class, MediaType.TEXT_HTML)).willReturn(true);
processor.handleReturnValue(returnValue, returnTypeResponseEntity, mavContainer, webRequest);
assertTrue(mavContainer.isRequestHandled());
- verify(messageConverter).write(eq("Foo"), eq(MediaType.TEXT_HTML), isA(HttpOutputMessage.class));
+ verify(stringHttpMessageConverter).write(eq("Foo"), eq(MediaType.TEXT_HTML), isA(HttpOutputMessage.class));
}
@Test(expected = HttpMediaTypeNotAcceptableException.class)
@@ -281,9 +286,9 @@ public class HttpEntityMethodProcessorMockTests {
MediaType accepted = MediaType.APPLICATION_ATOM_XML;
servletRequest.addHeader("Accept", accepted.toString());
- given(messageConverter.canWrite(String.class, null)).willReturn(true);
- given(messageConverter.getSupportedMediaTypes()).willReturn(Collections.singletonList(MediaType.TEXT_PLAIN));
- given(messageConverter.canWrite(String.class, accepted)).willReturn(false);
+ given(stringHttpMessageConverter.canWrite(String.class, null)).willReturn(true);
+ given(stringHttpMessageConverter.getSupportedMediaTypes()).willReturn(Collections.singletonList(MediaType.TEXT_PLAIN));
+ given(stringHttpMessageConverter.canWrite(String.class, accepted)).willReturn(false);
processor.handleReturnValue(returnValue, returnTypeResponseEntity, mavContainer, webRequest);
@@ -298,9 +303,9 @@ public class HttpEntityMethodProcessorMockTests {
MediaType accepted = MediaType.TEXT_PLAIN;
servletRequest.addHeader("Accept", accepted.toString());
- given(messageConverter.canWrite(String.class, null)).willReturn(true);
- given(messageConverter.getSupportedMediaTypes()).willReturn(Collections.singletonList(MediaType.TEXT_PLAIN));
- given(messageConverter.canWrite(String.class, accepted)).willReturn(false);
+ given(stringHttpMessageConverter.canWrite(String.class, null)).willReturn(true);
+ given(stringHttpMessageConverter.getSupportedMediaTypes()).willReturn(Collections.singletonList(MediaType.TEXT_PLAIN));
+ given(stringHttpMessageConverter.canWrite(String.class, accepted)).willReturn(false);
processor.handleReturnValue(returnValue, returnTypeResponseEntityProduces, mavContainer, webRequest);
@@ -340,7 +345,7 @@ public class HttpEntityMethodProcessorMockTests {
processor.handleReturnValue(returnValue, returnTypeResponseEntity, mavContainer, webRequest);
ArgumentCaptor outputMessage = ArgumentCaptor.forClass(HttpOutputMessage.class);
- verify(messageConverter).write(eq("body"), eq(MediaType.TEXT_PLAIN), outputMessage.capture());
+ verify(stringHttpMessageConverter).write(eq("body"), eq(MediaType.TEXT_PLAIN), outputMessage.capture());
assertTrue(mavContainer.isRequestHandled());
assertEquals("headerValue", outputMessage.getValue().getHeaders().get("header").get(0));
}
@@ -519,50 +524,58 @@ public class HttpEntityMethodProcessorMockTests {
}
@Test
- public void varyHeader() throws Exception {
- String[] entityValues = {"Accept-Language", "User-Agent"};
- String[] existingValues = {};
- String[] expected = {"Accept-Language, User-Agent"};
- testVaryHeader(entityValues, existingValues, expected);
+ public void handleReturnTypeResource() throws Exception {
+ ResponseEntity returnValue = ResponseEntity
+ .ok(new ByteArrayResource("Content".getBytes(Charset.forName("UTF-8"))));
+
+ given(resourceMessageConverter.canWrite(ByteArrayResource.class, null)).willReturn(true);
+ given(resourceMessageConverter.getSupportedMediaTypes()).willReturn(Collections.singletonList(MediaType.ALL));
+ given(resourceMessageConverter.canWrite(ByteArrayResource.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(200, servletResponse.getStatus());
}
@Test
- public void varyHeaderWithExistingWildcard() throws Exception {
- String[] entityValues = {"Accept-Language"};
- String[] existingValues = {"*"};
- String[] expected = {"*"};
- testVaryHeader(entityValues, existingValues, expected);
+ 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 varyHeaderWithExistingCommaValues() throws Exception {
- String[] entityValues = {"Accept-Language", "User-Agent"};
- String[] existingValues = {"Accept-Encoding", "Accept-Language"};
- String[] expected = {"Accept-Encoding", "Accept-Language", "User-Agent"};
- testVaryHeader(entityValues, existingValues, expected);
- }
+ public void handleReturnTypeResourceIllegalByteRange() throws Exception {
+ Resource resource = new ByteArrayResource("Content".getBytes(Charset.forName("UTF-8")));
+ ResponseEntity returnValue = ResponseEntity.ok(resource);
+ servletRequest.addHeader("Range", "illegal");
- @Test
- public void varyHeaderWithExistingCommaSeparatedValues() throws Exception {
- String[] entityValues = {"Accept-Language", "User-Agent"};
- String[] existingValues = {"Accept-Encoding, Accept-Language"};
- String[] expected = {"Accept-Encoding, Accept-Language", "User-Agent"};
- testVaryHeader(entityValues, existingValues, expected);
- }
+ given(resourceMessageConverter.canWrite(ByteArrayResource.class, null)).willReturn(true);
+ given(resourceMessageConverter.getSupportedMediaTypes()).willReturn(Collections.singletonList(MediaType.ALL));
- @Test
- public void handleReturnValueVaryHeader() throws Exception {
- String[] entityValues = {"Accept-Language", "User-Agent"};
- String[] existingValues = {"Accept-Encoding, Accept-Language"};
- String[] expected = {"Accept-Encoding, Accept-Language", "User-Agent"};
- testVaryHeader(entityValues, existingValues, expected);
- }
+ 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(messageConverter.canWrite(String.class, null)).willReturn(true);
- given(messageConverter.getSupportedMediaTypes()).willReturn(Collections.singletonList(MediaType.TEXT_PLAIN));
- given(messageConverter.canWrite(String.class, accepted)).willReturn(true);
+ given(stringHttpMessageConverter.canWrite(String.class, null)).willReturn(true);
+ given(stringHttpMessageConverter.getSupportedMediaTypes()).willReturn(Collections.singletonList(MediaType.TEXT_PLAIN));
+ given(stringHttpMessageConverter.canWrite(String.class, accepted)).willReturn(true);
}
private void assertResponseNotModified() {
@@ -575,23 +588,9 @@ public class HttpEntityMethodProcessorMockTests {
assertTrue(mavContainer.isRequestHandled());
assertEquals(HttpStatus.OK.value(), servletResponse.getStatus());
ArgumentCaptor outputMessage = ArgumentCaptor.forClass(HttpOutputMessage.class);
- verify(messageConverter).write(eq(body), eq(MediaType.TEXT_PLAIN), outputMessage.capture());
+ verify(stringHttpMessageConverter).write(eq(body), eq(MediaType.TEXT_PLAIN), outputMessage.capture());
}
- private void testVaryHeader(String[] entityValues, String[] existingValues, String[] expected) throws Exception {
- ResponseEntity returnValue = ResponseEntity.ok().varyBy(entityValues).body("Foo");
- for (String value : existingValues) {
- servletResponse.addHeader("Vary", value);
- }
- initStringMessageConversion(MediaType.TEXT_PLAIN);
- processor.handleReturnValue(returnValue, returnTypeResponseEntity, mavContainer, webRequest);
-
- assertTrue(mavContainer.isRequestHandled());
- assertEquals(Arrays.asList(expected), servletResponse.getHeaders("Vary"));
- verify(messageConverter).write(eq("Foo"), eq(MediaType.TEXT_PLAIN), isA(HttpOutputMessage.class));
- }
-
-
@SuppressWarnings("unused")
public ResponseEntity handle1(HttpEntity httpEntity, ResponseEntity entity,
int i, RequestEntity requestEntity) {
@@ -620,8 +619,11 @@ public class HttpEntityMethodProcessorMockTests {
return null;
}
+ @SuppressWarnings("unused")
+ public ResponseEntity handle5() {return null;}
+
@SuppressWarnings("unused")
public static class CustomHttpEntity extends HttpEntity