Browse Source

Add range requests support in ResourceWebHandler

This commit handles range requests in `ResourceWebHandler`, using the
`ResourceHttpMessageWriter` configured within the handler.

This `WebHandler` will add a `HTTP_RANGE_REQUEST_HINT` writer hint
containing all the HTTP Range information of the request.

Issue: SPR-14664
pull/1175/head
Brian Clozel 10 years ago
parent
commit
3596e72050
  1. 15
      spring-web-reactive/src/main/java/org/springframework/web/reactive/resource/ResourceWebHandler.java
  2. 48
      spring-web-reactive/src/test/java/org/springframework/web/reactive/resource/ResourceWebHandlerTests.java
  3. 1
      spring-web-reactive/src/test/resources/org/springframework/web/reactive/resource/test/foo.txt

15
spring-web-reactive/src/main/java/org/springframework/web/reactive/resource/ResourceWebHandler.java

@ -54,6 +54,7 @@ import org.springframework.web.reactive.HandlerMapping;
import org.springframework.web.reactive.accept.CompositeContentTypeResolver; import org.springframework.web.reactive.accept.CompositeContentTypeResolver;
import org.springframework.web.reactive.accept.PathExtensionContentTypeResolver; import org.springframework.web.reactive.accept.PathExtensionContentTypeResolver;
import org.springframework.web.server.MethodNotAllowedException; import org.springframework.web.server.MethodNotAllowedException;
import org.springframework.web.server.ResponseStatusException;
import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebHandler; import org.springframework.web.server.WebHandler;
@ -82,6 +83,7 @@ import org.springframework.web.server.WebHandler;
* client. * client.
* *
* @author Rossen Stoyanchev * @author Rossen Stoyanchev
* @author Brian Clozel
* @since 5.0 * @since 5.0
*/ */
public class ResourceWebHandler public class ResourceWebHandler
@ -188,7 +190,7 @@ public class ResourceWebHandler
} }
/** /**
* Return the list of configured resource converters. * Return the configured resource message writer.
*/ */
public ResourceHttpMessageWriter getResourceHttpMessageWriter() { public ResourceHttpMessageWriter getResourceHttpMessageWriter() {
return this.resourceHttpMessageWriter; return this.resourceHttpMessageWriter;
@ -333,15 +335,12 @@ public class ResourceWebHandler
return Mono.empty(); return Mono.empty();
} }
// TODO: range requests
setHeaders(exchange, resource, mediaType); setHeaders(exchange, resource, mediaType);
return this.resourceHttpMessageWriter.write(Mono.just(resource),
return this.resourceHttpMessageWriter.write( null, ResolvableType.forClass(Resource.class), mediaType,
Mono.just(resource), ResolvableType.forClass(Resource.class), exchange.getRequest(), exchange.getResponse(), Collections.emptyMap());
mediaType, exchange.getResponse(), Collections.emptyMap());
} }
catch (IOException ex) { catch (IOException|ResponseStatusException ex) {
return Mono.error(ex); return Mono.error(ex);
} }
}); });

48
spring-web-reactive/src/test/java/org/springframework/web/reactive/resource/ResourceWebHandlerTests.java

@ -16,6 +16,10 @@
package org.springframework.web.reactive.resource; package org.springframework.web.reactive.resource;
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.*;
import static org.springframework.web.reactive.HandlerMapping.*;
import java.io.IOException; import java.io.IOException;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.ArrayList; import java.util.ArrayList;
@ -25,12 +29,17 @@ import java.util.List;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import org.junit.Before; import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test; import org.junit.Test;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource; import org.springframework.core.io.Resource;
import org.springframework.core.io.UrlResource; import org.springframework.core.io.UrlResource;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferFactory;
import org.springframework.core.io.buffer.DataBufferUtils;
import org.springframework.core.io.buffer.DefaultDataBufferFactory;
import org.springframework.core.io.buffer.support.DataBufferTestUtils; import org.springframework.core.io.buffer.support.DataBufferTestUtils;
import org.springframework.http.CacheControl; import org.springframework.http.CacheControl;
import org.springframework.http.HttpHeaders; import org.springframework.http.HttpHeaders;
@ -44,14 +53,12 @@ import org.springframework.util.StringUtils;
import org.springframework.web.reactive.accept.CompositeContentTypeResolver; import org.springframework.web.reactive.accept.CompositeContentTypeResolver;
import org.springframework.web.reactive.accept.RequestedContentTypeResolverBuilder; import org.springframework.web.reactive.accept.RequestedContentTypeResolverBuilder;
import org.springframework.web.server.MethodNotAllowedException; import org.springframework.web.server.MethodNotAllowedException;
import org.springframework.web.server.ResponseStatusException;
import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.adapter.DefaultServerWebExchange; import org.springframework.web.server.adapter.DefaultServerWebExchange;
import org.springframework.web.server.session.DefaultWebSessionManager; import org.springframework.web.server.session.DefaultWebSessionManager;
import org.springframework.web.server.session.WebSessionManager; import org.springframework.web.server.session.WebSessionManager;
import static org.junit.Assert.*;
import static org.springframework.web.reactive.HandlerMapping.*;
/** /**
* Unit tests for {@link ResourceWebHandler}. * Unit tests for {@link ResourceWebHandler}.
* *
@ -69,6 +76,8 @@ public class ResourceWebHandlerTests {
private WebSessionManager sessionManager = new DefaultWebSessionManager(); private WebSessionManager sessionManager = new DefaultWebSessionManager();
private DataBufferFactory bufferFactory = new DefaultDataBufferFactory();
@Before @Before
public void setUp() throws Exception { public void setUp() throws Exception {
@ -437,7 +446,6 @@ public class ResourceWebHandlerTests {
} }
@Test @Test
@Ignore
public void partialContentByteRange() throws Exception { public void partialContentByteRange() throws Exception {
this.request.addHeader("Range", "bytes=0-1"); this.request.addHeader("Range", "bytes=0-1");
this.exchange.getAttributes().put(PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE, "foo.txt"); this.exchange.getAttributes().put(PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE, "foo.txt");
@ -453,7 +461,6 @@ public class ResourceWebHandlerTests {
} }
@Test @Test
@Ignore
public void partialContentByteRangeNoEnd() throws Exception { public void partialContentByteRangeNoEnd() throws Exception {
this.request.addHeader("Range", "bytes=9-"); this.request.addHeader("Range", "bytes=9-");
this.exchange.getAttributes().put(PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE, "foo.txt"); this.exchange.getAttributes().put(PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE, "foo.txt");
@ -469,7 +476,6 @@ public class ResourceWebHandlerTests {
} }
@Test @Test
@Ignore
public void partialContentByteRangeLargeEnd() throws Exception { public void partialContentByteRangeLargeEnd() throws Exception {
this.request.addHeader("Range", "bytes=9-10000"); this.request.addHeader("Range", "bytes=9-10000");
this.exchange.getAttributes().put(PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE, "foo.txt"); this.exchange.getAttributes().put(PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE, "foo.txt");
@ -485,7 +491,6 @@ public class ResourceWebHandlerTests {
} }
@Test @Test
@Ignore
public void partialContentSuffixRange() throws Exception { public void partialContentSuffixRange() throws Exception {
this.request.addHeader("Range", "bytes=-1"); this.request.addHeader("Range", "bytes=-1");
this.exchange.getAttributes().put(PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE, "foo.txt"); this.exchange.getAttributes().put(PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE, "foo.txt");
@ -501,7 +506,6 @@ public class ResourceWebHandlerTests {
} }
@Test @Test
@Ignore
public void partialContentSuffixRangeLargeSuffix() throws Exception { public void partialContentSuffixRangeLargeSuffix() throws Exception {
this.request.addHeader("Range", "bytes=-11"); this.request.addHeader("Range", "bytes=-11");
this.exchange.getAttributes().put(PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE, "foo.txt"); this.exchange.getAttributes().put(PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE, "foo.txt");
@ -517,20 +521,19 @@ public class ResourceWebHandlerTests {
} }
@Test @Test
@Ignore
public void partialContentInvalidRangeHeader() throws Exception { public void partialContentInvalidRangeHeader() throws Exception {
this.request.addHeader("Range", "bytes= foo bar"); this.request.addHeader("Range", "bytes= foo bar");
this.exchange.getAttributes().put(PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE, "foo.txt"); this.exchange.getAttributes().put(PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE, "foo.txt");
this.handler.handle(this.exchange).blockMillis(5000);
assertEquals(HttpStatus.REQUESTED_RANGE_NOT_SATISFIABLE, this.response.getStatusCode()); TestSubscriber.subscribe(this.handler.handle(this.exchange))
assertEquals("bytes */10", this.response.getHeaders().getFirst("Content-Range")); .assertErrorWith(throwable -> {
assertEquals("bytes", this.response.getHeaders().getFirst("Accept-Ranges")); assertThat(throwable, instanceOf(ResponseStatusException.class));
assertEquals(1, this.response.getHeaders().get("Accept-Ranges").size()); ResponseStatusException exc = (ResponseStatusException) throwable;
assertThat(exc.getStatus(), is(HttpStatus.REQUESTED_RANGE_NOT_SATISFIABLE));
});
} }
@Test @Test
@Ignore
public void partialContentMultipleByteRanges() throws Exception { public void partialContentMultipleByteRanges() throws Exception {
this.request.addHeader("Range", "bytes=0-1, 4-5, 8-9"); this.request.addHeader("Range", "bytes=0-1, 4-5, 8-9");
this.exchange.getAttributes().put(PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE, "foo.txt"); this.exchange.getAttributes().put(PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE, "foo.txt");
@ -538,11 +541,18 @@ public class ResourceWebHandlerTests {
assertEquals(HttpStatus.PARTIAL_CONTENT, this.response.getStatusCode()); assertEquals(HttpStatus.PARTIAL_CONTENT, this.response.getStatusCode());
assertTrue(this.response.getHeaders().getContentType().toString() assertTrue(this.response.getHeaders().getContentType().toString()
.startsWith("multipart/byteranges; boundary=")); .startsWith("multipart/byteranges;boundary="));
String boundary = "--" + this.response.getHeaders().getContentType().toString().substring(31); String boundary = "--" + this.response.getHeaders().getContentType().toString().substring(30);
TestSubscriber.subscribe(this.response.getBody()) Mono<DataBuffer> reduced = Flux.from(this.response.getBody())
.reduce(this.bufferFactory.allocateBuffer(), (previous, current) -> {
previous.write(current);
DataBufferUtils.release(current);
return previous;
});
TestSubscriber.subscribe(reduced)
.assertValuesWith(buf -> { .assertValuesWith(buf -> {
String content = DataBufferTestUtils.dumpString(buf, StandardCharsets.UTF_8); String content = DataBufferTestUtils.dumpString(buf, StandardCharsets.UTF_8);
String[] ranges = StringUtils.tokenizeToStringArray(content, "\r\n", false, true); String[] ranges = StringUtils.tokenizeToStringArray(content, "\r\n", false, true);

1
spring-web-reactive/src/test/resources/org/springframework/web/reactive/resource/test/foo.txt

@ -0,0 +1 @@
Some text.
Loading…
Cancel
Save