Avoid package dependency cycles

This commit is contained in:
Juergen Hoeller 2016-09-20 22:41:11 +02:00
parent 65e01eabf0
commit d94ce0a1b2
6 changed files with 76 additions and 51 deletions

View File

@ -28,6 +28,8 @@ import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
import org.springframework.core.ResolvableType; import org.springframework.core.ResolvableType;
import org.springframework.core.io.InputStreamResource;
import org.springframework.core.io.Resource;
import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferFactory; import org.springframework.core.io.buffer.DataBufferFactory;
import org.springframework.core.io.buffer.DataBufferUtils; import org.springframework.core.io.buffer.DataBufferUtils;
@ -134,7 +136,7 @@ public class ResourceRegionEncoder extends AbstractEncoder<ResourceRegion> {
private byte[] getContentRangeHeader(ResourceRegion region) { private byte[] getContentRangeHeader(ResourceRegion region) {
long start = region.getPosition(); long start = region.getPosition();
long end = start + region.getCount() - 1; long end = start + region.getCount() - 1;
OptionalLong contentLength = ResourceUtils.contentLength(region.getResource()); OptionalLong contentLength = contentLength(region.getResource());
if (contentLength.isPresent()) { if (contentLength.isPresent()) {
return getAsciiBytes("Content-Range: bytes " + start + "-" + end + "/" + contentLength.getAsLong() + "\r\n\r\n"); return getAsciiBytes("Content-Range: bytes " + start + "-" + end + "/" + contentLength.getAsLong() + "\r\n\r\n");
} }
@ -143,4 +145,22 @@ public class ResourceRegionEncoder extends AbstractEncoder<ResourceRegion> {
} }
} }
/**
* Determine, if possible, the contentLength of the given resource without reading it.
* @param resource the resource instance
* @return the contentLength of the resource
*/
private OptionalLong contentLength(Resource resource) {
// Don't try to determine contentLength on InputStreamResource - cannot be read afterwards...
// Note: custom InputStreamResource subclasses could provide a pre-calculated content length!
if (InputStreamResource.class != resource.getClass()) {
try {
return OptionalLong.of(resource.contentLength());
}
catch (IOException ignored) {
}
}
return OptionalLong.empty();
}
} }

View File

@ -18,16 +18,11 @@ package org.springframework.util;
import java.io.File; import java.io.File;
import java.io.FileNotFoundException; import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.MalformedURLException; import java.net.MalformedURLException;
import java.net.URI; import java.net.URI;
import java.net.URISyntaxException; import java.net.URISyntaxException;
import java.net.URL; import java.net.URL;
import java.net.URLConnection; import java.net.URLConnection;
import java.util.OptionalLong;
import org.springframework.core.io.InputStreamResource;
import org.springframework.core.io.Resource;
/** /**
* Utility methods for resolving resource locations to files in the * Utility methods for resolving resource locations to files in the
@ -389,23 +384,4 @@ public abstract class ResourceUtils {
con.setUseCaches(con.getClass().getSimpleName().startsWith("JNLP")); con.setUseCaches(con.getClass().getSimpleName().startsWith("JNLP"));
} }
/**
* Determine, if possible, the contentLength of the given resource
* without reading it.
* @param resource the resource instance
* @return the contentLength of the resource
*/
public static OptionalLong contentLength(Resource resource) {
// Don't try to determine contentLength on InputStreamResource - cannot be read afterwards...
// Note: custom InputStreamResource subclasses could provide a pre-calculated content length!
if (InputStreamResource.class != resource.getClass()) {
try {
return OptionalLong.of(resource.contentLength());
}
catch (IOException ignored) {
}
}
return OptionalLong.empty();
}
} }

View File

@ -527,9 +527,7 @@ public class ResourceWebHandlerTests {
TestSubscriber.subscribe(this.handler.handle(this.exchange)) TestSubscriber.subscribe(this.handler.handle(this.exchange))
.assertErrorWith(throwable -> { .assertErrorWith(throwable -> {
assertThat(throwable, instanceOf(ResponseStatusException.class)); assertThat(throwable, instanceOf(IllegalArgumentException.class));
ResponseStatusException exc = (ResponseStatusException) throwable;
assertThat(exc.getStatus(), is(HttpStatus.REQUESTED_RANGE_NOT_SATISFIABLE));
}); });
} }

View File

@ -23,6 +23,7 @@ import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.OptionalLong;
import org.reactivestreams.Publisher; import org.reactivestreams.Publisher;
import reactor.core.publisher.Flux; import reactor.core.publisher.Flux;
@ -31,6 +32,7 @@ import reactor.core.publisher.Mono;
import org.springframework.core.ResolvableType; import org.springframework.core.ResolvableType;
import org.springframework.core.codec.ResourceDecoder; import org.springframework.core.codec.ResourceDecoder;
import org.springframework.core.codec.ResourceEncoder; import org.springframework.core.codec.ResourceEncoder;
import org.springframework.core.io.InputStreamResource;
import org.springframework.core.io.Resource; import org.springframework.core.io.Resource;
import org.springframework.core.io.support.ResourceRegion; import org.springframework.core.io.support.ResourceRegion;
import org.springframework.http.HttpHeaders; import org.springframework.http.HttpHeaders;
@ -43,8 +45,6 @@ import org.springframework.http.ZeroCopyHttpOutputMessage;
import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse; import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.util.MimeTypeUtils; import org.springframework.util.MimeTypeUtils;
import org.springframework.util.ResourceUtils;
import org.springframework.web.server.ResponseStatusException;
/** /**
* Implementation of {@link HttpMessageWriter} that can write * Implementation of {@link HttpMessageWriter} that can write
@ -74,19 +74,15 @@ public class ResourceHttpMessageWriter extends AbstractServerHttpMessageWriter<R
this.resourceRegionHttpMessageWriter = new ResourceRegionHttpMessageWriter(bufferSize); this.resourceRegionHttpMessageWriter = new ResourceRegionHttpMessageWriter(bufferSize);
} }
@Override @Override
protected Map<String, Object> resolveWriteHints(ResolvableType streamType, ResolvableType elementType, protected Map<String, Object> resolveWriteHints(ResolvableType streamType, ResolvableType elementType,
MediaType mediaType, ServerHttpRequest request) { MediaType mediaType, ServerHttpRequest request) {
try {
List<HttpRange> httpRanges = request.getHeaders().getRange(); List<HttpRange> httpRanges = request.getHeaders().getRange();
if (!httpRanges.isEmpty()) { if (!httpRanges.isEmpty()) {
return Collections.singletonMap(ResourceHttpMessageWriter.HTTP_RANGE_REQUEST_HINT, httpRanges); return Collections.singletonMap(ResourceHttpMessageWriter.HTTP_RANGE_REQUEST_HINT, httpRanges);
} }
}
catch (IllegalArgumentException ex) {
throw new ResponseStatusException(HttpStatus.REQUESTED_RANGE_NOT_SATISFIABLE,
"Could not parse Range request header", ex);
}
return Collections.emptyMap(); return Collections.emptyMap();
} }
@ -139,12 +135,31 @@ public class ResourceHttpMessageWriter extends AbstractServerHttpMessageWriter<R
headers.setContentType(mediaType); headers.setContentType(mediaType);
} }
if (headers.getContentLength() < 0) { if (headers.getContentLength() < 0) {
ResourceUtils.contentLength(resource).ifPresent(headers::setContentLength); contentLength(resource).ifPresent(headers::setContentLength);
} }
} }
/**
* Determine, if possible, the contentLength of the given resource without reading it.
* @param resource the resource instance
* @return the contentLength of the resource
*/
private OptionalLong contentLength(Resource resource) {
// Don't try to determine contentLength on InputStreamResource - cannot be read afterwards...
// Note: custom InputStreamResource subclasses could provide a pre-calculated content length!
if (InputStreamResource.class != resource.getClass()) {
try {
return OptionalLong.of(resource.contentLength());
}
catch (IOException ignored) {
}
}
return OptionalLong.empty();
}
private Mono<Void> writeContent(Resource resource, ResolvableType type, private Mono<Void> writeContent(Resource resource, ResolvableType type,
ReactiveHttpOutputMessage outputMessage, Map<String, Object> hints) { ReactiveHttpOutputMessage outputMessage, Map<String, Object> hints) {
if (outputMessage instanceof ZeroCopyHttpOutputMessage) { if (outputMessage instanceof ZeroCopyHttpOutputMessage) {
Optional<File> file = getFile(resource); Optional<File> file = getFile(resource);
if (file.isPresent()) { if (file.isPresent()) {

View File

@ -28,6 +28,7 @@ import reactor.core.publisher.Mono;
import org.springframework.core.ResolvableType; import org.springframework.core.ResolvableType;
import org.springframework.core.codec.ResourceRegionEncoder; import org.springframework.core.codec.ResourceRegionEncoder;
import org.springframework.core.io.InputStreamResource;
import org.springframework.core.io.Resource; import org.springframework.core.io.Resource;
import org.springframework.core.io.support.ResourceRegion; import org.springframework.core.io.support.ResourceRegion;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
@ -80,7 +81,7 @@ class ResourceRegionHttpMessageWriter extends EncoderHttpMessageWriter<ResourceR
private void writeSingleResourceRegionHeaders(ResourceRegion region, MediaType contentType, private void writeSingleResourceRegionHeaders(ResourceRegion region, MediaType contentType,
ReactiveHttpOutputMessage outputMessage) { ReactiveHttpOutputMessage outputMessage) {
OptionalLong resourceLength = ResourceUtils.contentLength(region.getResource()); OptionalLong resourceLength = contentLength(region.getResource());
resourceLength.ifPresent(length -> { resourceLength.ifPresent(length -> {
long start = region.getPosition(); long start = region.getPosition();
long end = start + region.getCount() - 1; long end = start + region.getCount() - 1;
@ -91,6 +92,24 @@ class ResourceRegionHttpMessageWriter extends EncoderHttpMessageWriter<ResourceR
outputMessage.getHeaders().setContentType(contentType); outputMessage.getHeaders().setContentType(contentType);
} }
/**
* Determine, if possible, the contentLength of the given resource without reading it.
* @param resource the resource instance
* @return the contentLength of the resource
*/
private OptionalLong contentLength(Resource resource) {
// Don't try to determine contentLength on InputStreamResource - cannot be read afterwards...
// Note: custom InputStreamResource subclasses could provide a pre-calculated content length!
if (InputStreamResource.class != resource.getClass()) {
try {
return OptionalLong.of(resource.contentLength());
}
catch (IOException ignored) {
}
}
return OptionalLong.empty();
}
private Mono<Void> writeResourceRegion(ResourceRegion region, private Mono<Void> writeResourceRegion(ResourceRegion region,
ResolvableType type, ReactiveHttpOutputMessage outputMessage) { ResolvableType type, ReactiveHttpOutputMessage outputMessage) {
if (outputMessage instanceof ZeroCopyHttpOutputMessage) { if (outputMessage instanceof ZeroCopyHttpOutputMessage) {

View File

@ -16,13 +16,9 @@
package org.springframework.http.codec; package org.springframework.http.codec;
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.*;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.Collections; import java.util.Collections;
import org.hamcrest.Matchers;
import org.junit.Before; import org.junit.Before;
import org.junit.Rule; import org.junit.Rule;
import org.junit.Test; import org.junit.Test;
@ -40,16 +36,18 @@ import org.springframework.core.io.buffer.DataBufferUtils;
import org.springframework.core.io.buffer.support.DataBufferTestUtils; import org.springframework.core.io.buffer.support.DataBufferTestUtils;
import org.springframework.http.HttpHeaders; import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpRange; import org.springframework.http.HttpRange;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.mock.http.server.reactive.test.MockServerHttpRequest; import org.springframework.mock.http.server.reactive.test.MockServerHttpRequest;
import org.springframework.mock.http.server.reactive.test.MockServerHttpResponse; import org.springframework.mock.http.server.reactive.test.MockServerHttpResponse;
import org.springframework.tests.TestSubscriber; import org.springframework.tests.TestSubscriber;
import org.springframework.util.MimeTypeUtils; import org.springframework.util.MimeTypeUtils;
import org.springframework.web.server.ResponseStatusException;
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.*;
/** /**
* Unit tests for {@link ResourceHttpMessageWriter}. * Unit tests for {@link ResourceHttpMessageWriter}.
*
* @author Brian Clozel * @author Brian Clozel
*/ */
public class ResourceHttpMessageWriterTests { public class ResourceHttpMessageWriterTests {
@ -72,6 +70,7 @@ public class ResourceHttpMessageWriterTests {
this.resource = new ByteArrayResource(content.getBytes(StandardCharsets.UTF_8)); this.resource = new ByteArrayResource(content.getBytes(StandardCharsets.UTF_8));
} }
@Test @Test
public void writableMediaTypes() throws Exception { public void writableMediaTypes() throws Exception {
assertThat(this.writer.getWritableMediaTypes(), assertThat(this.writer.getWritableMediaTypes(),
@ -80,7 +79,6 @@ public class ResourceHttpMessageWriterTests {
@Test @Test
public void shouldWriteResource() throws Exception { public void shouldWriteResource() throws Exception {
TestSubscriber.subscribe(this.writer.write(Mono.just(resource), null, ResolvableType.forClass(Resource.class), TestSubscriber.subscribe(this.writer.write(Mono.just(resource), null, ResolvableType.forClass(Resource.class),
MediaType.TEXT_PLAIN, this.request, this.response, Collections.emptyMap())).assertComplete(); MediaType.TEXT_PLAIN, this.request, this.response, Collections.emptyMap())).assertComplete();
@ -93,7 +91,6 @@ public class ResourceHttpMessageWriterTests {
@Test @Test
public void shouldWriteResourceRange() throws Exception { public void shouldWriteResourceRange() throws Exception {
this.request.getHeaders().setRange(Collections.singletonList(HttpRange.createByteRange(0, 5))); this.request.getHeaders().setRange(Collections.singletonList(HttpRange.createByteRange(0, 5)));
TestSubscriber.subscribe(this.writer.write(Mono.just(resource), null, ResolvableType.forClass(Resource.class), TestSubscriber.subscribe(this.writer.write(Mono.just(resource), null, ResolvableType.forClass(Resource.class),
@ -111,10 +108,9 @@ public class ResourceHttpMessageWriterTests {
public void shouldThrowErrorInvalidRange() throws Exception { public void shouldThrowErrorInvalidRange() throws Exception {
this.request.getHeaders().set(HttpHeaders.RANGE, "invalid"); this.request.getHeaders().set(HttpHeaders.RANGE, "invalid");
this.expectedException.expect(ResponseStatusException.class); this.expectedException.expect(IllegalArgumentException.class);
TestSubscriber.subscribe(this.writer.write(Mono.just(resource), null, ResolvableType.forClass(Resource.class), TestSubscriber.subscribe(this.writer.write(Mono.just(resource), null, ResolvableType.forClass(Resource.class),
MediaType.TEXT_PLAIN, this.request, this.response, Collections.emptyMap())); MediaType.TEXT_PLAIN, this.request, this.response, Collections.emptyMap()));
this.expectedException.expect(Matchers.hasProperty("status", is(HttpStatus.REQUESTED_RANGE_NOT_SATISFIABLE)));
} }
private Mono<String> reduceToString(Publisher<DataBuffer> buffers, DataBufferFactory bufferFactory) { private Mono<String> reduceToString(Publisher<DataBuffer> buffers, DataBufferFactory bufferFactory) {
@ -127,4 +123,5 @@ public class ResourceHttpMessageWriterTests {
}) })
.map(buffer -> DataBufferTestUtils.dumpString(buffer, StandardCharsets.UTF_8)); .map(buffer -> DataBufferTestUtils.dumpString(buffer, StandardCharsets.UTF_8));
} }
} }