Support HTTP range requests in MVC Controllers
Prior to this commit, HTTP Range requests were only supported by the
ResourceHttpRequestHandler when serving static resources.
This commit improves the `HttpEntityMethodProcessor` and
the `RequestResponseBodyMethodProcessor`. They now extract
`ResourceRegion`s from the `Resource` instance returned by the
Controller and let the Resource-related message converters
handle the writing of the resource (including 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-15789, SPR-13834
This commit is contained in:
@@ -44,6 +44,7 @@ import org.springframework.http.MediaType;
|
||||
import org.springframework.http.converter.ByteArrayHttpMessageConverter;
|
||||
import org.springframework.http.converter.HttpMessageConverter;
|
||||
import org.springframework.http.converter.ResourceHttpMessageConverter;
|
||||
import org.springframework.http.converter.ResourceRegionHttpMessageConverter;
|
||||
import org.springframework.http.converter.StringHttpMessageConverter;
|
||||
import org.springframework.http.converter.cbor.MappingJackson2CborHttpMessageConverter;
|
||||
import org.springframework.http.converter.feed.AtomFeedHttpMessageConverter;
|
||||
@@ -579,6 +580,7 @@ class AnnotationDrivenBeanDefinitionParser implements BeanDefinitionParser {
|
||||
messageConverters.add(stringConverterDef);
|
||||
|
||||
messageConverters.add(createConverterDefinition(ResourceHttpMessageConverter.class, source));
|
||||
messageConverters.add(createConverterDefinition(ResourceRegionHttpMessageConverter.class, source));
|
||||
messageConverters.add(createConverterDefinition(SourceHttpMessageConverter.class, source));
|
||||
messageConverters.add(createConverterDefinition(AllEncompassingFormHttpMessageConverter.class, source));
|
||||
|
||||
|
||||
@@ -41,6 +41,7 @@ import org.springframework.http.MediaType;
|
||||
import org.springframework.http.converter.ByteArrayHttpMessageConverter;
|
||||
import org.springframework.http.converter.HttpMessageConverter;
|
||||
import org.springframework.http.converter.ResourceHttpMessageConverter;
|
||||
import org.springframework.http.converter.ResourceRegionHttpMessageConverter;
|
||||
import org.springframework.http.converter.StringHttpMessageConverter;
|
||||
import org.springframework.http.converter.cbor.MappingJackson2CborHttpMessageConverter;
|
||||
import org.springframework.http.converter.feed.AtomFeedHttpMessageConverter;
|
||||
@@ -790,6 +791,7 @@ public class WebMvcConfigurationSupport implements ApplicationContextAware, Serv
|
||||
messageConverters.add(new ByteArrayHttpMessageConverter());
|
||||
messageConverters.add(stringConverter);
|
||||
messageConverters.add(new ResourceHttpMessageConverter());
|
||||
messageConverters.add(new ResourceRegionHttpMessageConverter());
|
||||
messageConverters.add(new SourceHttpMessageConverter<>());
|
||||
messageConverters.add(new AllEncompassingFormHttpMessageConverter());
|
||||
|
||||
|
||||
@@ -30,10 +30,15 @@ import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.core.ParameterizedTypeReference;
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.core.io.support.ResourceRegion;
|
||||
import org.springframework.http.HttpEntity;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpOutputMessage;
|
||||
import org.springframework.http.HttpRange;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.converter.GenericHttpMessageConverter;
|
||||
import org.springframework.http.converter.HttpMessageConverter;
|
||||
@@ -75,6 +80,9 @@ public abstract class AbstractMessageConverterMethodProcessor extends AbstractMe
|
||||
|
||||
private static final MediaType MEDIA_TYPE_APPLICATION = new MediaType("application");
|
||||
|
||||
private static final Type RESOURCE_REGION_LIST_TYPE =
|
||||
new ParameterizedTypeReference<List<ResourceRegion>>() { }.getType();
|
||||
|
||||
|
||||
private static final UrlPathHelper decodingUrlPathHelper = new UrlPathHelper();
|
||||
|
||||
@@ -183,6 +191,24 @@ public abstract class AbstractMessageConverterMethodProcessor extends AbstractMe
|
||||
valueType = getReturnValueType(outputValue, returnType);
|
||||
declaredType = getGenericType(returnType);
|
||||
}
|
||||
|
||||
if (isResourceType(value, returnType)) {
|
||||
outputMessage.getHeaders().set(HttpHeaders.ACCEPT_RANGES, "bytes");
|
||||
if (value != null && inputMessage.getHeaders().getFirst(HttpHeaders.RANGE) != null) {
|
||||
Resource resource = (Resource) value;
|
||||
try {
|
||||
List<HttpRange> httpRanges = inputMessage.getHeaders().getRange();
|
||||
outputMessage.getServletResponse().setStatus(HttpStatus.PARTIAL_CONTENT.value());
|
||||
outputValue = HttpRange.toResourceRegions(httpRanges, resource);
|
||||
valueType = outputValue.getClass();
|
||||
declaredType = RESOURCE_REGION_LIST_TYPE;
|
||||
}
|
||||
catch (IllegalArgumentException ex) {
|
||||
outputMessage.getHeaders().set(HttpHeaders.CONTENT_RANGE, "bytes */" + resource.contentLength());
|
||||
outputMessage.getServletResponse().setStatus(HttpStatus.REQUESTED_RANGE_NOT_SATISFIABLE.value());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
HttpServletRequest request = inputMessage.getServletRequest();
|
||||
List<MediaType> requestedMediaTypes = getAcceptableMediaTypes(request);
|
||||
@@ -266,6 +292,13 @@ public abstract class AbstractMessageConverterMethodProcessor extends AbstractMe
|
||||
return (value != null ? value.getClass() : returnType.getParameterType());
|
||||
}
|
||||
|
||||
/**
|
||||
* Return whether the returned value or the declared return type extend {@link Resource}
|
||||
*/
|
||||
protected boolean isResourceType(@Nullable Object value, MethodParameter returnType) {
|
||||
return Resource.class.isAssignableFrom(value != null ? value.getClass() : returnType.getParameterType());
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the generic type of the {@code returnType} (or of the nested type
|
||||
* if it is an {@link HttpEntity}).
|
||||
|
||||
@@ -387,14 +387,8 @@ public class ResourceHttpRequestHandler extends WebContentGenerator
|
||||
try {
|
||||
List<HttpRange> httpRanges = inputMessage.getHeaders().getRange();
|
||||
response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
|
||||
if (httpRanges.size() == 1) {
|
||||
ResourceRegion resourceRegion = httpRanges.get(0).toResourceRegion(resource);
|
||||
this.resourceRegionHttpMessageConverter.write(resourceRegion, mediaType, outputMessage);
|
||||
}
|
||||
else {
|
||||
this.resourceRegionHttpMessageConverter.write(
|
||||
HttpRange.toResourceRegions(httpRanges, resource), mediaType, outputMessage);
|
||||
}
|
||||
}
|
||||
catch (IllegalArgumentException ex) {
|
||||
response.setHeader("Content-Range", "bytes */" + resource.contentLength());
|
||||
|
||||
Reference in New Issue
Block a user