Skip to content

Commit

Permalink
Reuse InputStream in ResourceRegionHttpMessageConverter
Browse files Browse the repository at this point in the history
The converter now tries to keep reading from the same InputStream which
should be possible with ordered and non-overlapping regions. When
necessary the InputStream is re-opened.

Closes gh-24214
  • Loading branch information
rstoyanchev committed Dec 18, 2019
1 parent 7474ee7 commit 0eacb44
Show file tree
Hide file tree
Showing 3 changed files with 70 additions and 14 deletions.
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 the original author or authors.
* Copyright 2002-2019 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.
Expand Down Expand Up @@ -170,7 +170,7 @@ public static long copyRange(InputStream in, OutputStream out, long start, long
}

long bytesToCopy = end - start + 1;
byte[] buffer = new byte[StreamUtils.BUFFER_SIZE];
byte[] buffer = new byte[(int) Math.min(StreamUtils.BUFFER_SIZE, bytesToCopy)];
while (bytesToCopy > 0) {
int bytesRead = in.read(buffer);
if (bytesRead == -1) {
Expand Down
Expand Up @@ -179,11 +179,23 @@ private void writeResourceRegionCollection(Collection<ResourceRegion> resourceRe
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();
try {
Resource resource = null;
InputStream in = null;
long inputStreamPosition = 0;

try {
for (ResourceRegion region : resourceRegions) {
long start = region.getPosition() - inputStreamPosition;
if (start < 0 || resource != region.getResource()) {
if (in != null) {
in.close();
}
resource = region.getResource();
in = resource.getInputStream();
inputStreamPosition = 0;
start = region.getPosition();
}
long end = start + region.getCount() - 1;
// Writing MIME header.
println(out);
print(out, "--" + boundaryString);
Expand All @@ -193,20 +205,25 @@ private void writeResourceRegionCollection(Collection<ResourceRegion> resourceRe
println(out);
}
Long resourceLength = region.getResource().contentLength();
end = Math.min(end, resourceLength - 1);
print(out, "Content-Range: bytes " + start + '-' + end + '/' + resourceLength);
end = Math.min(end, resourceLength - inputStreamPosition - 1);
print(out, "Content-Range: bytes " +
region.getPosition() + '-' + (region.getPosition() + region.getCount() - 1) +
'/' + resourceLength);
println(out);
println(out);
// Printing content
StreamUtils.copyRange(in, out, start, end);
inputStreamPosition += (end + 1);
}
finally {
try {
}
finally {
try {
if (in != null) {
in.close();
}
catch (IOException ex) {
// ignore
}
}
catch (IOException ex) {
// ignore
}
}

Expand Down
Expand Up @@ -144,6 +144,45 @@ public void partialContentMultipleByteRanges() throws Exception {
assertThat(ranges[15]).isEqualTo("resource content.");
}

@Test
public void partialContentMultipleByteRangesInRandomOrderAndOverlapping() throws Exception {
MockHttpOutputMessage outputMessage = new MockHttpOutputMessage();
Resource body = new ClassPathResource("byterangeresource.txt", getClass());
List<HttpRange> rangeList = HttpRange.parseRanges("bytes=7-15,0-5,17-20,20-29");
List<ResourceRegion> regions = new ArrayList<>();
for(HttpRange range : rangeList) {
regions.add(range.toResourceRegion(body));
}

converter.write(regions, MediaType.TEXT_PLAIN, outputMessage);

HttpHeaders headers = outputMessage.getHeaders();
assertThat(headers.getContentType().toString()).startsWith("multipart/byteranges;boundary=");
String boundary = "--" + headers.getContentType().toString().substring(30);
String content = outputMessage.getBodyAsString(StandardCharsets.UTF_8);
String[] ranges = StringUtils.tokenizeToStringArray(content, "\r\n", false, true);

assertThat(ranges[0]).isEqualTo(boundary);
assertThat(ranges[1]).isEqualTo("Content-Type: text/plain");
assertThat(ranges[2]).isEqualTo("Content-Range: bytes 7-15/39");
assertThat(ranges[3]).isEqualTo("Framework");

assertThat(ranges[4]).isEqualTo(boundary);
assertThat(ranges[5]).isEqualTo("Content-Type: text/plain");
assertThat(ranges[6]).isEqualTo("Content-Range: bytes 0-5/39");
assertThat(ranges[7]).isEqualTo("Spring");

assertThat(ranges[8]).isEqualTo(boundary);
assertThat(ranges[9]).isEqualTo("Content-Type: text/plain");
assertThat(ranges[10]).isEqualTo("Content-Range: bytes 17-20/39");
assertThat(ranges[11]).isEqualTo("test");

assertThat(ranges[12]).isEqualTo(boundary);
assertThat(ranges[13]).isEqualTo("Content-Type: text/plain");
assertThat(ranges[14]).isEqualTo("Content-Range: bytes 20-29/39");
assertThat(ranges[15]).isEqualTo("t resource");
}

@Test // SPR-15041
public void applicationOctetStreamDefaultContentType() throws Exception {
MockHttpOutputMessage outputMessage = new MockHttpOutputMessage();
Expand Down

0 comments on commit 0eacb44

Please sign in to comment.