Refactor HTTP Range support with ResourceRegion
Prior to this commit, the `ResourceHttpMessageConverter` would support all HTTP Range requests and `MethodProcessors` would "wrap" controller handler return values with a `HttpRangeResource` to support that use case in Controllers. This commit refactors that support in several ways: * a new ResourceRegion class has been introduced * a new, separate, ResourceRegionHttpMessageConverter handles the HTTP range use cases when serving static resources with the ResourceHttpRequestHandler * the support of HTTP range requests on Controller handlers has been removed until a better solution is found Issue: SPR-14221, SPR-13834
This commit is contained in:
parent
7737c3c7e5
commit
5ac31fb39d
|
|
@ -0,0 +1,72 @@
|
|||
/*
|
||||
* 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.core.io;
|
||||
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Region of a {@link Resource} implementation, materialized by a {@code position}
|
||||
* within the {@link Resource} and a byte {@code count} for the length of that region.
|
||||
* @author Arjen Poutsma
|
||||
* @since 4.3.0
|
||||
*/
|
||||
public class ResourceRegion {
|
||||
|
||||
private final Resource resource;
|
||||
|
||||
private final long position;
|
||||
|
||||
private final long count;
|
||||
|
||||
/**
|
||||
* Create a new {@code ResourceRegion} from a given {@link Resource}.
|
||||
* This region of a resource is reprensented by a start {@code position}
|
||||
* and a byte {@code count} within the given {@code Resource}.
|
||||
* @param resource a Resource
|
||||
* @param position the start position of the region in that resource
|
||||
* @param count the byte count of the region in that resource
|
||||
*/
|
||||
public ResourceRegion(Resource resource, long position, long count) {
|
||||
Assert.notNull(resource, "'resource' must not be null");
|
||||
Assert.isTrue(position >= 0, "'position' must be larger than or equal to 0");
|
||||
Assert.isTrue(count >= 0, "'count' must be larger than or equal to 0");
|
||||
this.resource = resource;
|
||||
this.position = position;
|
||||
this.count = count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the underlying {@link Resource} for this {@code ResourceRegion}
|
||||
*/
|
||||
public Resource getResource() {
|
||||
return this.resource;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the start position of this region in the underlying {@link Resource}
|
||||
*/
|
||||
public long getPosition() {
|
||||
return this.position;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the byte count of this region in the underlying {@link Resource}
|
||||
*/
|
||||
public long getCount() {
|
||||
return this.count;
|
||||
}
|
||||
}
|
||||
|
|
@ -37,6 +37,7 @@ import java.nio.charset.Charset;
|
|||
*
|
||||
* @author Juergen Hoeller
|
||||
* @author Phillip Webb
|
||||
* @author Brian Clozel
|
||||
* @since 3.2.2
|
||||
* @see FileCopyUtils
|
||||
*/
|
||||
|
|
@ -131,6 +132,43 @@ public abstract class StreamUtils {
|
|||
return byteCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy a range of content of the given InputStream to the given OutputStream.
|
||||
* <p>If the specified range exceeds the length of the InputStream, this copies
|
||||
* up to the end of the stream and returns the actual number of copied bytes.
|
||||
* <p>Leaves both streams open when done.
|
||||
* @param in the InputStream to copy from
|
||||
* @param out the OutputStream to copy to
|
||||
* @param start the position to start copying from
|
||||
* @param end the position to end copying
|
||||
* @return the number of bytes copied
|
||||
* @throws IOException in case of I/O errors
|
||||
* @since 4.3.0
|
||||
*/
|
||||
public static long 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 == -1) {
|
||||
break;
|
||||
}
|
||||
else if (bytesRead <= bytesToCopy) {
|
||||
out.write(buffer, 0, bytesRead);
|
||||
bytesToCopy -= bytesRead;
|
||||
}
|
||||
else {
|
||||
out.write(buffer, 0, (int) bytesToCopy);
|
||||
bytesToCopy = 0;
|
||||
}
|
||||
}
|
||||
return end - start + 1 - bytesToCopy;
|
||||
}
|
||||
|
||||
/**
|
||||
* Drain the remaining content of the given InputStream.
|
||||
* Leaves the InputStream open when done.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,44 @@
|
|||
/*
|
||||
* 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.core.io;
|
||||
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
/**
|
||||
* Unit tests for the {@link ResourceRegion} class.
|
||||
*
|
||||
* @author Brian Clozel
|
||||
*/
|
||||
public class ResourceRegionTests {
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void shouldThrowExceptionWithNullResource() {
|
||||
new ResourceRegion(null, 0, 1);
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void shouldThrowExceptionForNegativePosition() {
|
||||
new ResourceRegion(mock(Resource.class), -1, 1);
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void shouldThrowExceptionForNegativeCount() {
|
||||
new ResourceRegion(mock(Resource.class), 0, -1);
|
||||
}
|
||||
}
|
||||
|
|
@ -21,6 +21,7 @@ import java.io.ByteArrayOutputStream;
|
|||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.nio.charset.Charset;
|
||||
import java.util.Arrays;
|
||||
import java.util.Random;
|
||||
import java.util.UUID;
|
||||
|
||||
|
|
@ -93,6 +94,15 @@ public class StreamUtilsTests {
|
|||
verify(out, never()).close();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void copyRange() throws Exception {
|
||||
ByteArrayOutputStream out = spy(new ByteArrayOutputStream());
|
||||
StreamUtils.copyRange(new ByteArrayInputStream(bytes), out, 0, 100);
|
||||
byte[] range = Arrays.copyOfRange(bytes, 0, 101);
|
||||
assertThat(out.toByteArray(), equalTo(range));
|
||||
verify(out, never()).close();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void nonClosingInputStream() throws Exception {
|
||||
InputStream source = mock(InputStream.class);
|
||||
|
|
|
|||
|
|
@ -16,12 +16,16 @@
|
|||
|
||||
package org.springframework.http;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.core.io.InputStreamResource;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.core.io.ResourceRegion;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
|
@ -55,6 +59,30 @@ public abstract class HttpRange {
|
|||
*/
|
||||
public abstract long getRangeEnd(long length);
|
||||
|
||||
/**
|
||||
* Turn a {@code Resource} into a {@link ResourceRegion} using the range
|
||||
* information contained in the current {@code HttpRange}.
|
||||
* @param resource the {@code Resource} to select the region from
|
||||
* @return the selected region of the given {@code Resource}
|
||||
* @since 4.3.0
|
||||
*/
|
||||
public ResourceRegion toResourceRegion(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!
|
||||
Assert.isTrue(InputStreamResource.class != resource.getClass(),
|
||||
"Can't convert an InputStreamResource to a ResourceRegion");
|
||||
try {
|
||||
long contentLength = resource.contentLength();
|
||||
Assert.isTrue(contentLength > 0, "Resource content length should be > 0");
|
||||
long start = getRangeStart(contentLength);
|
||||
long end = getRangeEnd(contentLength);
|
||||
return new ResourceRegion(resource, start, end - start + 1);
|
||||
}
|
||||
catch (IOException exc) {
|
||||
throw new IllegalArgumentException("Can't convert this Resource to a ResourceRegion", exc);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Create an {@code HttpRange} from the given position to the end.
|
||||
|
|
@ -133,6 +161,26 @@ public abstract class HttpRange {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert each {@code HttpRange} into a {@code ResourceRegion},
|
||||
* selecting the appropriate segment of the given {@code Resource}
|
||||
* using the HTTP Range information.
|
||||
*
|
||||
* @param ranges the list of ranges
|
||||
* @param resource the resource to select the regions from
|
||||
* @return the list of regions for the given resource
|
||||
*/
|
||||
public static List<ResourceRegion> toResourceRegions(List<HttpRange> ranges, Resource resource) {
|
||||
if(ranges == null || ranges.size() == 0) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
List<ResourceRegion> regions = new ArrayList<ResourceRegion>(ranges.size());
|
||||
for(HttpRange range : ranges) {
|
||||
regions.add(range.toResourceRegion(resource));
|
||||
}
|
||||
return regions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a string representation of the given list of {@code HttpRange} objects.
|
||||
* <p>This method can be used to for an {@code Range} header.
|
||||
|
|
|
|||
|
|
@ -1,123 +0,0 @@
|
|||
/*
|
||||
* 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.
|
||||
*
|
||||
* <p>Used as an argument for partial conversion operations in
|
||||
* {@link org.springframework.http.converter.ResourceHttpMessageConverter}.
|
||||
*
|
||||
* @author Brian Clozel
|
||||
* @since 4.3
|
||||
* @see HttpRange
|
||||
*/
|
||||
public class HttpRangeResource implements Resource {
|
||||
|
||||
private final List<HttpRange> httpRanges;
|
||||
|
||||
private final Resource resource;
|
||||
|
||||
|
||||
public HttpRangeResource(List<HttpRange> 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<HttpRange> getHttpRanges() {
|
||||
return this.httpRanges;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public boolean exists() {
|
||||
return this.resource.exists();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isReadable() {
|
||||
return this.resource.isReadable();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isOpen() {
|
||||
return this.resource.isOpen();
|
||||
}
|
||||
|
||||
@Override
|
||||
public URL getURL() throws IOException {
|
||||
return this.resource.getURL();
|
||||
}
|
||||
|
||||
@Override
|
||||
public URI getURI() throws IOException {
|
||||
return this.resource.getURI();
|
||||
}
|
||||
|
||||
@Override
|
||||
public File getFile() throws IOException {
|
||||
return this.resource.getFile();
|
||||
}
|
||||
|
||||
@Override
|
||||
public long contentLength() throws IOException {
|
||||
return this.resource.contentLength();
|
||||
}
|
||||
|
||||
@Override
|
||||
public long lastModified() throws IOException {
|
||||
return this.resource.lastModified();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Resource createRelative(String relativePath) throws IOException {
|
||||
return this.resource.createRelative(relativePath);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getFilename() {
|
||||
return this.resource.getFilename();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDescription() {
|
||||
return this.resource.getDescription();
|
||||
}
|
||||
|
||||
@Override
|
||||
public InputStream getInputStream() throws IOException {
|
||||
return this.resource.getInputStream();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -19,8 +19,7 @@ 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;
|
||||
|
||||
|
|
@ -31,12 +30,8 @@ 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;
|
||||
|
||||
|
|
@ -48,13 +43,10 @@ import org.springframework.util.StringUtils;
|
|||
* if available - is used to determine the {@code Content-Type} of written resources.
|
||||
* If JAF is not available, {@code application/octet-stream} is used.
|
||||
*
|
||||
* <p>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<Resource> {
|
||||
|
|
@ -114,13 +106,7 @@ public class ResourceHttpMessageConverter extends AbstractHttpMessageConverter<R
|
|||
protected void writeInternal(Resource resource, HttpOutputMessage outputMessage)
|
||||
throws IOException, HttpMessageNotWritableException {
|
||||
|
||||
outputMessage.getHeaders().add(HttpHeaders.ACCEPT_RANGES, "bytes");
|
||||
if (resource instanceof HttpRangeResource) {
|
||||
writePartialContent((HttpRangeResource) resource, outputMessage);
|
||||
}
|
||||
else {
|
||||
writeContent(resource, outputMessage);
|
||||
}
|
||||
writeContent(resource, outputMessage);
|
||||
}
|
||||
|
||||
protected void writeContent(Resource resource, HttpOutputMessage outputMessage)
|
||||
|
|
@ -147,99 +133,6 @@ public class ResourceHttpMessageConverter extends AbstractHttpMessageConverter<R
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Write parts of the resource as indicated by the request {@code Range} header.
|
||||
* @param resource the identified resource (never {@code null})
|
||||
* @param outputMessage current servlet response
|
||||
* @throws IOException in case of errors while writing the content
|
||||
*/
|
||||
protected void writePartialContent(HttpRangeResource resource, HttpOutputMessage outputMessage) throws IOException {
|
||||
Assert.notNull(resource, "Resource should not be null");
|
||||
List<HttpRange> 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(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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Inner class to avoid a hard-coded JAF dependency.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,188 @@
|
|||
/*
|
||||
* 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.converter;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.lang.reflect.ParameterizedType;
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.Collection;
|
||||
|
||||
import org.springframework.core.io.ResourceRegion;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpInputMessage;
|
||||
import org.springframework.http.HttpOutputMessage;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.MimeTypeUtils;
|
||||
import org.springframework.util.StreamUtils;
|
||||
|
||||
/**
|
||||
* Implementation of {@link HttpMessageConverter} that can write a single {@link ResourceRegion ResourceRegion},
|
||||
* or Collections of {@link ResourceRegion ResourceRegions}.
|
||||
*
|
||||
* @author Brian Clozel
|
||||
* @since 4.3.0
|
||||
*/
|
||||
public class ResourceRegionHttpMessageConverter extends AbstractGenericHttpMessageConverter<Object> {
|
||||
|
||||
public ResourceRegionHttpMessageConverter() {
|
||||
super(MediaType.ALL);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canWrite(Class<?> clazz, MediaType mediaType) {
|
||||
return canWrite(clazz, null, mediaType);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canWrite(Type type, Class<?> clazz, MediaType mediaType) {
|
||||
if (!(type instanceof ParameterizedType)) {
|
||||
return ResourceRegion.class.isAssignableFrom((Class) type);
|
||||
}
|
||||
ParameterizedType parameterizedType = (ParameterizedType) type;
|
||||
if (!(parameterizedType.getRawType() instanceof Class)) {
|
||||
return false;
|
||||
}
|
||||
Class<?> rawType = (Class<?>) parameterizedType.getRawType();
|
||||
if (!(Collection.class.isAssignableFrom(rawType))) {
|
||||
return false;
|
||||
}
|
||||
if (parameterizedType.getActualTypeArguments().length != 1) {
|
||||
return false;
|
||||
}
|
||||
Type typeArgument = parameterizedType.getActualTypeArguments()[0];
|
||||
if (!(typeArgument instanceof Class)) {
|
||||
return false;
|
||||
}
|
||||
Class<?> typeArgumentClass = (Class<?>) typeArgument;
|
||||
return typeArgumentClass.isAssignableFrom(ResourceRegion.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
protected void writeInternal(Object object, Type type, HttpOutputMessage outputMessage)
|
||||
throws IOException, HttpMessageNotWritableException {
|
||||
|
||||
if (object instanceof ResourceRegion) {
|
||||
writeResourceRegion((ResourceRegion) object, outputMessage);
|
||||
}
|
||||
else {
|
||||
Collection<ResourceRegion> regions = (Collection<ResourceRegion>) object;
|
||||
if(regions.size() == 1) {
|
||||
writeResourceRegion(regions.iterator().next(), outputMessage);
|
||||
}
|
||||
else {
|
||||
writeResourceRegionCollection((Collection<ResourceRegion>) object, outputMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected void writeResourceRegion(ResourceRegion region, HttpOutputMessage outputMessage)
|
||||
throws IOException {
|
||||
|
||||
Assert.notNull(region, "ResourceRegion should not be null");
|
||||
HttpHeaders responseHeaders = outputMessage.getHeaders();
|
||||
long start = region.getPosition();
|
||||
long end = start + region.getCount() - 1;
|
||||
Long resourceLength = region.getResource().contentLength();
|
||||
end = Math.min(end, resourceLength - 1);
|
||||
long rangeLength = end - start + 1;
|
||||
responseHeaders.add("Content-Range", "bytes " + start + "-" + end + "/" + resourceLength);
|
||||
responseHeaders.setContentLength(rangeLength);
|
||||
InputStream in = region.getResource().getInputStream();
|
||||
try {
|
||||
StreamUtils.copyRange(in, outputMessage.getBody(), start, end);
|
||||
}
|
||||
finally {
|
||||
try {
|
||||
in.close();
|
||||
}
|
||||
catch (IOException ex) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void writeResourceRegionCollection(Collection<ResourceRegion> resourceRegions,
|
||||
HttpOutputMessage outputMessage) throws IOException {
|
||||
|
||||
Assert.notNull(resourceRegions, "Collection of ResourceRegion should not be null");
|
||||
HttpHeaders responseHeaders = outputMessage.getHeaders();
|
||||
MediaType contentType = responseHeaders.getContentType();
|
||||
String boundaryString = MimeTypeUtils.generateMultipartBoundaryString();
|
||||
responseHeaders.set(HttpHeaders.CONTENT_TYPE, "multipart/byteranges; boundary=" + boundaryString);
|
||||
OutputStream out = outputMessage.getBody();
|
||||
for (ResourceRegion region : resourceRegions) {
|
||||
long start = region.getPosition();
|
||||
long end = start + region.getCount() - 1;
|
||||
InputStream in = region.getResource().getInputStream();
|
||||
// Writing MIME header.
|
||||
println(out);
|
||||
print(out, "--" + boundaryString);
|
||||
println(out);
|
||||
if (contentType != null) {
|
||||
print(out, "Content-Type: " + contentType.toString());
|
||||
println(out);
|
||||
}
|
||||
Long resourceLength = region.getResource().contentLength();
|
||||
end = Math.min(end, resourceLength - 1);
|
||||
print(out, "Content-Range: bytes " + start + "-" + end + "/" + resourceLength);
|
||||
println(out);
|
||||
println(out);
|
||||
// Printing content
|
||||
StreamUtils.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"));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canRead(Type type, Class<?> contextClass, MediaType mediaType) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean supports(Class<?> clazz) {
|
||||
// should not be called as we override canRead/canWrite
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public Object read(Type type, Class<?> contextClass, HttpInputMessage inputMessage)
|
||||
throws IOException, HttpMessageNotReadableException {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ResourceRegion readInternal(Class<? extends Object> clazz, HttpInputMessage inputMessage)
|
||||
throws IOException, HttpMessageNotReadableException {
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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.
|
||||
|
|
@ -15,17 +15,26 @@
|
|||
*/
|
||||
package org.springframework.http;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.Charset;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
import org.springframework.core.io.ByteArrayResource;
|
||||
import org.springframework.core.io.InputStreamResource;
|
||||
import org.springframework.core.io.ResourceRegion;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link HttpRange}.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @author Brian Clozel
|
||||
*/
|
||||
public class HttpRangeTests {
|
||||
|
||||
|
|
@ -100,4 +109,38 @@ public class HttpRangeTests {
|
|||
assertEquals("Invalid Range header", "bytes=0-499, 9500-, -500", HttpRange.toString(ranges));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void toResourceRegion() {
|
||||
byte[] bytes = "Spring Framework".getBytes(Charset.forName("UTF-8"));
|
||||
ByteArrayResource resource = new ByteArrayResource(bytes);
|
||||
HttpRange range = HttpRange.createByteRange(0, 5);
|
||||
ResourceRegion region = range.toResourceRegion(resource);
|
||||
assertEquals(resource, region.getResource());
|
||||
assertEquals(0L, region.getPosition());
|
||||
assertEquals(6L, region.getCount());
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void toResourceRegionInputStreamResource() {
|
||||
InputStreamResource resource = mock(InputStreamResource.class);
|
||||
HttpRange range = HttpRange.createByteRange(0, 9);
|
||||
range.toResourceRegion(resource);
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void toResourceRegionIllegalLength() {
|
||||
ByteArrayResource resource = mock(ByteArrayResource.class);
|
||||
given(resource.contentLength()).willReturn(-1L);
|
||||
HttpRange range = HttpRange.createByteRange(0, 9);
|
||||
range.toResourceRegion(resource);
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void toResourceRegionExceptionLength() {
|
||||
ByteArrayResource resource = mock(ByteArrayResource.class);
|
||||
given(resource.contentLength()).willThrow(IOException.class);
|
||||
HttpRange range = HttpRange.createByteRange(0, 9);
|
||||
range.toResourceRegion(resource);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,25 +27,18 @@ 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 org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* @author Arjen Poutsma
|
||||
|
|
@ -98,116 +91,6 @@ 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();
|
||||
|
|
|
|||
|
|
@ -0,0 +1,142 @@
|
|||
/*
|
||||
* 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.converter;
|
||||
|
||||
import static org.hamcrest.Matchers.is;
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
import java.lang.reflect.Type;
|
||||
import java.nio.charset.Charset;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.hamcrest.Matchers;
|
||||
import org.junit.Test;
|
||||
|
||||
import org.springframework.core.ParameterizedTypeReference;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.core.io.ResourceRegion;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpRange;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.MockHttpOutputMessage;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* Test cases for {@link ResourceRegionHttpMessageConverter} class.
|
||||
*
|
||||
* @author Brian Clozel
|
||||
*/
|
||||
public class ResourceRegionHttpMessageConverterTests {
|
||||
|
||||
private final ResourceRegionHttpMessageConverter converter = new ResourceRegionHttpMessageConverter();
|
||||
|
||||
@Test
|
||||
public void canReadResource() {
|
||||
assertFalse(converter.canRead(Resource.class, MediaType.APPLICATION_OCTET_STREAM));
|
||||
assertFalse(converter.canRead(Resource.class, MediaType.ALL));
|
||||
assertFalse(converter.canRead(List.class, MediaType.APPLICATION_OCTET_STREAM));
|
||||
assertFalse(converter.canRead(List.class, MediaType.ALL));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void canWriteResource() {
|
||||
assertTrue(converter.canWrite(ResourceRegion.class, null, MediaType.APPLICATION_OCTET_STREAM));
|
||||
assertTrue(converter.canWrite(ResourceRegion.class, null, MediaType.ALL));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void canWriteResourceCollection() {
|
||||
Type resourceRegionList = new ParameterizedTypeReference<List<ResourceRegion>>() {}.getType();
|
||||
assertTrue(converter.canWrite(resourceRegionList, null, MediaType.APPLICATION_OCTET_STREAM));
|
||||
assertTrue(converter.canWrite(resourceRegionList, null, MediaType.ALL));
|
||||
|
||||
assertFalse(converter.canWrite(List.class, MediaType.APPLICATION_OCTET_STREAM));
|
||||
assertFalse(converter.canWrite(List.class, MediaType.ALL));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldWritePartialContentByteRange() throws Exception {
|
||||
MockHttpOutputMessage outputMessage = new MockHttpOutputMessage();
|
||||
Resource body = new ClassPathResource("byterangeresource.txt", getClass());
|
||||
ResourceRegion region = HttpRange.createByteRange(0, 5).toResourceRegion(body);
|
||||
converter.write(region, 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());
|
||||
ResourceRegion region = HttpRange.createByteRange(7).toResourceRegion(body);
|
||||
converter.write(region, 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 partialContentMultipleByteRanges() throws Exception {
|
||||
MockHttpOutputMessage outputMessage = new MockHttpOutputMessage();
|
||||
Resource body = new ClassPathResource("byterangeresource.txt", getClass());
|
||||
List<HttpRange> rangeList = HttpRange.parseRanges("bytes=0-5,7-15,17-20,22-38");
|
||||
List<ResourceRegion> regions = new ArrayList<ResourceRegion>();
|
||||
for(HttpRange range : rangeList) {
|
||||
regions.add(range.toResourceRegion(body));
|
||||
}
|
||||
|
||||
converter.write(regions, 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"));
|
||||
|
||||
assertThat(ranges[12], is(boundary));
|
||||
assertThat(ranges[13], is("Content-Type: text/plain"));
|
||||
assertThat(ranges[14], is("Content-Range: bytes 22-38/39"));
|
||||
assertThat(ranges[15], is("resource content."));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -26,12 +26,9 @@ 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;
|
||||
|
|
@ -197,24 +194,8 @@ public class HttpEntityMethodProcessor extends AbstractMessageConverterMethodPro
|
|||
}
|
||||
}
|
||||
|
||||
Object body = responseEntity.getBody();
|
||||
if (inputMessage.getHeaders().containsKey(HttpHeaders.RANGE) &&
|
||||
Resource.class.isAssignableFrom(body.getClass())) {
|
||||
try {
|
||||
List<HttpRange> httpRanges = inputMessage.getHeaders().getRange();
|
||||
Resource bodyResource = (Resource) body;
|
||||
body = new HttpRangeResource(httpRanges, bodyResource);
|
||||
outputMessage.setStatusCode(HttpStatus.PARTIAL_CONTENT);
|
||||
}
|
||||
catch (IllegalArgumentException ex) {
|
||||
outputMessage.setStatusCode(HttpStatus.REQUESTED_RANGE_NOT_SATISFIABLE);
|
||||
outputMessage.flush();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Try even with null body. ResponseBodyAdvice could get involved.
|
||||
writeWithMessageConverters(body, returnType, inputMessage, outputMessage);
|
||||
writeWithMessageConverters(responseEntity.getBody(), returnType, inputMessage, outputMessage);
|
||||
|
||||
// Ensure headers are flushed even if no body was written.
|
||||
outputMessage.flush();
|
||||
|
|
|
|||
|
|
@ -19,16 +19,12 @@ package org.springframework.web.servlet.mvc.method.annotation;
|
|||
import java.io.IOException;
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.List;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
import org.springframework.core.Conventions;
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.core.annotation.AnnotatedElementUtils;
|
||||
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;
|
||||
|
|
@ -173,21 +169,6 @@ public class RequestResponseBodyMethodProcessor extends AbstractMessageConverter
|
|||
ServletServerHttpRequest inputMessage = createInputMessage(webRequest);
|
||||
ServletServerHttpResponse outputMessage = createOutputMessage(webRequest);
|
||||
|
||||
if (inputMessage.getHeaders().containsKey(HttpHeaders.RANGE) &&
|
||||
Resource.class.isAssignableFrom(returnValue.getClass())) {
|
||||
try {
|
||||
List<HttpRange> httpRanges = inputMessage.getHeaders().getRange();
|
||||
Resource bodyResource = (Resource) returnValue;
|
||||
returnValue = new HttpRangeResource(httpRanges, bodyResource);
|
||||
outputMessage.setStatusCode(HttpStatus.PARTIAL_CONTENT);
|
||||
}
|
||||
catch (IllegalArgumentException ex) {
|
||||
outputMessage.setStatusCode(HttpStatus.REQUESTED_RANGE_NOT_SATISFIABLE);
|
||||
outputMessage.flush();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Try even with null return value. ResponseBodyAdvice could get involved.
|
||||
writeWithMessageConverters(returnValue, returnType, inputMessage, outputMessage);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import java.io.IOException;
|
|||
import java.net.URLDecoder;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.ServletResponse;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
|
@ -30,12 +31,13 @@ import org.apache.commons.logging.LogFactory;
|
|||
|
||||
import org.springframework.beans.factory.InitializingBean;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.core.io.ResourceRegion;
|
||||
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.converter.ResourceRegionHttpMessageConverter;
|
||||
import org.springframework.http.server.ServletServerHttpRequest;
|
||||
import org.springframework.http.server.ServletServerHttpResponse;
|
||||
import org.springframework.util.Assert;
|
||||
|
|
@ -105,6 +107,8 @@ public class ResourceHttpRequestHandler extends WebContentGenerator
|
|||
|
||||
private ResourceHttpMessageConverter resourceHttpMessageConverter;
|
||||
|
||||
private ResourceRegionHttpMessageConverter resourceRegionHttpMessageConverter;
|
||||
|
||||
private ContentNegotiationManager contentNegotiationManager;
|
||||
|
||||
private CorsConfiguration corsConfiguration;
|
||||
|
|
@ -169,7 +173,7 @@ public class ResourceHttpRequestHandler extends WebContentGenerator
|
|||
/**
|
||||
* Configure the {@link ResourceHttpMessageConverter} to use.
|
||||
* <p>By default a {@link ResourceHttpMessageConverter} will be configured.
|
||||
* @since 4.3
|
||||
* @since 4.3.0
|
||||
*/
|
||||
public void setResourceHttpMessageConverter(ResourceHttpMessageConverter resourceHttpMessageConverter) {
|
||||
this.resourceHttpMessageConverter = resourceHttpMessageConverter;
|
||||
|
|
@ -179,6 +183,20 @@ public class ResourceHttpRequestHandler extends WebContentGenerator
|
|||
return this.resourceHttpMessageConverter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure the {@link ResourceRegionHttpMessageConverter} to use.
|
||||
* <p>By default a {@link ResourceRegionHttpMessageConverter} will be configured.
|
||||
* @since 4.3.0
|
||||
*/
|
||||
public ResourceRegionHttpMessageConverter getResourceRegionHttpMessageConverter() {
|
||||
return resourceRegionHttpMessageConverter;
|
||||
}
|
||||
|
||||
public void setResourceRegionHttpMessageConverter(
|
||||
ResourceRegionHttpMessageConverter resourceRegionHttpMessageConverter) {
|
||||
this.resourceRegionHttpMessageConverter = resourceRegionHttpMessageConverter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure a {@code ContentNegotiationManager} to determine the media types
|
||||
* for resources being served. If the manager contains a path
|
||||
|
|
@ -225,13 +243,15 @@ public class ResourceHttpRequestHandler extends WebContentGenerator
|
|||
this.resourceResolvers.add(new PathResourceResolver());
|
||||
}
|
||||
initAllowedLocations();
|
||||
|
||||
if (this.contentNegotiationManager == null) {
|
||||
this.contentNegotiationManager = initContentNegotiationManager();
|
||||
}
|
||||
if (this.resourceHttpMessageConverter == null) {
|
||||
this.resourceHttpMessageConverter = new ResourceHttpMessageConverter();
|
||||
}
|
||||
if(this.resourceRegionHttpMessageConverter == null) {
|
||||
this.resourceRegionHttpMessageConverter = new ResourceRegionHttpMessageConverter();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -328,6 +348,8 @@ public class ResourceHttpRequestHandler extends WebContentGenerator
|
|||
}
|
||||
|
||||
ServletServerHttpResponse outputMessage = new ServletServerHttpResponse(response);
|
||||
outputMessage.getHeaders().add(HttpHeaders.ACCEPT_RANGES, "bytes");
|
||||
|
||||
if (request.getHeader(HttpHeaders.RANGE) == null) {
|
||||
setHeaders(response, resource, mediaType);
|
||||
this.resourceHttpMessageConverter.write(resource, mediaType, outputMessage);
|
||||
|
|
@ -336,9 +358,15 @@ public class ResourceHttpRequestHandler extends WebContentGenerator
|
|||
ServletServerHttpRequest inputMessage = new ServletServerHttpRequest(request);
|
||||
try {
|
||||
List<HttpRange> httpRanges = inputMessage.getHeaders().getRange();
|
||||
HttpRangeResource rangeResource = new HttpRangeResource(httpRanges, resource);
|
||||
response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
|
||||
this.resourceHttpMessageConverter.write(rangeResource, mediaType, outputMessage);
|
||||
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.addHeader("Content-Range", "bytes */" + resource.contentLength());
|
||||
|
|
|
|||
|
|
@ -16,6 +16,10 @@
|
|||
|
||||
package org.springframework.web.servlet.mvc.method.annotation;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
import static org.mockito.BDDMockito.*;
|
||||
import static org.springframework.web.servlet.HandlerMapping.*;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.net.URI;
|
||||
import java.nio.charset.Charset;
|
||||
|
|
@ -39,7 +43,6 @@ 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;
|
||||
|
|
@ -53,13 +56,6 @@ import org.springframework.web.bind.annotation.RequestMapping;
|
|||
import org.springframework.web.context.request.ServletWebRequest;
|
||||
import org.springframework.web.method.support.ModelAndViewContainer;
|
||||
|
||||
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.*;
|
||||
import static org.springframework.web.servlet.HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE;
|
||||
|
||||
/**
|
||||
* Test fixture for {@link HttpEntityMethodProcessor} delegating to a mock
|
||||
* {@link HttpMessageConverter}.
|
||||
|
|
@ -539,39 +535,6 @@ public class HttpEntityMethodProcessorMockTests {
|
|||
assertEquals(200, servletResponse.getStatus());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void handleReturnTypeResourceByteRange() throws Exception {
|
||||
Resource resource = new ByteArrayResource("Content".getBytes(Charset.forName("UTF-8")));
|
||||
ResponseEntity<Resource> 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 handleReturnTypeResourceIllegalByteRange() throws Exception {
|
||||
Resource resource = new ByteArrayResource("Content".getBytes(Charset.forName("UTF-8")));
|
||||
ResponseEntity<Resource> returnValue = ResponseEntity.ok(resource);
|
||||
servletRequest.addHeader("Range", "illegal");
|
||||
|
||||
given(resourceMessageConverter.canWrite(ByteArrayResource.class, null)).willReturn(true);
|
||||
given(resourceMessageConverter.getSupportedMediaTypes()).willReturn(Collections.singletonList(MediaType.ALL));
|
||||
|
||||
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(stringHttpMessageConverter.canWrite(String.class, null)).willReturn(true);
|
||||
given(stringHttpMessageConverter.getSupportedMediaTypes()).willReturn(Collections.singletonList(MediaType.TEXT_PLAIN));
|
||||
|
|
|
|||
|
|
@ -36,7 +36,6 @@ import org.springframework.core.io.ByteArrayResource;
|
|||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.http.HttpInputMessage;
|
||||
import org.springframework.http.HttpOutputMessage;
|
||||
import org.springframework.http.HttpRangeResource;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.converter.HttpMessageConverter;
|
||||
import org.springframework.http.converter.HttpMessageNotReadableException;
|
||||
|
|
@ -313,37 +312,6 @@ public class RequestResponseBodyMethodProcessorMockTests {
|
|||
assertEquals(200, servletResponse.getStatus());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void handleReturnTypeResourceByteRange() throws Exception {
|
||||
Resource returnValue = new ByteArrayResource("Content".getBytes(Charset.forName("UTF-8")));
|
||||
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, returnTypeResource, 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 handleReturnTypeResourceIllegalByteRange() throws Exception {
|
||||
Resource returnValue = new ByteArrayResource("Content".getBytes(Charset.forName("UTF-8")));
|
||||
servletRequest.addHeader("Range", "illegal");
|
||||
|
||||
given(resourceMessageConverter.canWrite(ByteArrayResource.class, null)).willReturn(true);
|
||||
given(resourceMessageConverter.getSupportedMediaTypes()).willReturn(Collections.singletonList(MediaType.ALL));
|
||||
|
||||
processor.handleReturnValue(returnValue, returnTypeResource, mavContainer, webRequest);
|
||||
|
||||
then(resourceMessageConverter).should(never()).write(any(ByteArrayResource.class),
|
||||
eq(MediaType.APPLICATION_OCTET_STREAM), any(HttpOutputMessage.class));
|
||||
assertEquals(416, servletResponse.getStatus());
|
||||
}
|
||||
|
||||
// SPR-9841
|
||||
|
||||
@Test
|
||||
|
|
|
|||
Loading…
Reference in New Issue