ResourceHttpMessageWriter refactoring

Fold ResourceRegionHttpMessageWriter into ResourceHttpMessageWriter.
The latter was a private helper (not meant to be exposed) and the two
have much in common now sharing a number of private helper methods.

The combined class does not extend AbstractServerHttpMessageConverter
from which it was not using anything.

Internally the combined class now delegates directly to ResourceEncoder
or ResourceRegionEncoder as needed. The former is no longer wrapped
with EncoderHttpMessageWriter which is not required since "resource"
MediaType determination is a bit different.

The consolidation makes it easy to see the entire algorithm in one
place especially for server side rendering (and HTTP ranges). It
also allows for consistent determination of the "resource" MediaType
via MediaTypeFactory for all use cases.
This commit is contained in:
Rossen Stoyanchev
2017-03-18 10:35:18 -04:00
parent d46c307f55
commit 76fe5f6fce
6 changed files with 231 additions and 445 deletions

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2016 the original author or authors.
* Copyright 2002-2017 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.
@@ -20,100 +20,134 @@ import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.Map;
import org.junit.Before;
import org.junit.Test;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import org.springframework.core.ResolvableType;
import org.springframework.core.io.ByteArrayResource;
import org.springframework.core.io.Resource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpRange;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.mock.http.server.reactive.test.MockServerHttpRequest;
import org.springframework.mock.http.server.reactive.test.MockServerHttpResponse;
import org.springframework.util.MimeTypeUtils;
import org.springframework.util.StringUtils;
import static org.hamcrest.Matchers.containsInAnyOrder;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.startsWith;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertThat;
import static org.springframework.http.MediaType.TEXT_PLAIN;
import static org.springframework.mock.http.server.reactive.test.MockServerHttpRequest.get;
/**
* Unit tests for {@link ResourceHttpMessageWriter}.
*
* @author Brian Clozel
* @author Rossen Stoyanchev
*/
public class ResourceHttpMessageWriterTests {
private ResourceHttpMessageWriter writer = new ResourceHttpMessageWriter();
private MockServerHttpResponse response = new MockServerHttpResponse();
private Resource resource;
private static final Map<String, Object> HINTS = Collections.emptyMap();
@Before
public void setUp() throws Exception {
String content = "Spring Framework test resource content.";
this.resource = new ByteArrayResource(content.getBytes(StandardCharsets.UTF_8));
}
private final ResourceHttpMessageWriter writer = new ResourceHttpMessageWriter();
private final MockServerHttpResponse response = new MockServerHttpResponse();
private final Mono<Resource> input = Mono.just(new ByteArrayResource(
"Spring Framework test resource content.".getBytes(StandardCharsets.UTF_8)));
@Test
public void writableMediaTypes() throws Exception {
public void getWritableMediaTypes() throws Exception {
assertThat(this.writer.getWritableMediaTypes(),
containsInAnyOrder(MimeTypeUtils.APPLICATION_OCTET_STREAM, MimeTypeUtils.ALL));
}
@Test
public void shouldWriteResource() throws Exception {
MockServerHttpRequest request = get("/").build();
testWrite(request);
public void writeResource() throws Exception {
assertThat(this.response.getHeaders().getContentType(), is(MediaType.TEXT_PLAIN));
testWrite(get("/").build());
assertThat(this.response.getHeaders().getContentType(), is(TEXT_PLAIN));
assertThat(this.response.getHeaders().getContentLength(), is(39L));
assertThat(this.response.getHeaders().getFirst(HttpHeaders.ACCEPT_RANGES), is("bytes"));
Mono<String> result = this.response.getBodyAsString();
StepVerifier.create(result)
.expectNext("Spring Framework test resource content.")
String content = "Spring Framework test resource content.";
StepVerifier.create(this.response.getBodyAsString()).expectNext(content).expectComplete().verify();
}
@Test
public void writeSingleRegion() throws Exception {
testWrite(get("/").range(of(0, 5)).build());
assertThat(this.response.getHeaders().getContentType(), is(TEXT_PLAIN));
assertThat(this.response.getHeaders().getFirst(HttpHeaders.CONTENT_RANGE), is("bytes 0-5/39"));
assertThat(this.response.getHeaders().getContentLength(), is(6L));
StepVerifier.create(this.response.getBodyAsString()).expectNext("Spring").expectComplete().verify();
}
@Test
public void writeMultipleRegions() throws Exception {
testWrite(get("/").range(of(0,5), of(7,15), of(17,20), of(22,38)).build());
HttpHeaders headers = this.response.getHeaders();
String contentType = headers.getContentType().toString();
String boundary = contentType.substring(30);
assertThat(contentType, startsWith("multipart/byteranges;boundary="));
StepVerifier.create(this.response.getBodyAsString())
.consumeNextWith(content -> {
String[] actualRanges = StringUtils.tokenizeToStringArray(content, "\r\n", false, true);
String[] expected = new String[] {
"--" + boundary,
"Content-Type: text/plain",
"Content-Range: bytes 0-5/39",
"Spring",
"--" + boundary,
"Content-Type: text/plain",
"Content-Range: bytes 7-15/39",
"Framework",
"--" + boundary,
"Content-Type: text/plain",
"Content-Range: bytes 17-20/39",
"test",
"--" + boundary,
"Content-Type: text/plain",
"Content-Range: bytes 22-38/39",
"resource content.",
"--" + boundary + "--"
};
assertArrayEquals(expected, actualRanges);
})
.expectComplete()
.verify();
}
@Test
public void shouldWriteResourceRange() throws Exception {
MockServerHttpRequest request = get("/").range(HttpRange.createByteRange(0, 5)).build();
testWrite(request);
public void invalidRange() throws Exception {
assertThat(this.response.getHeaders().getContentType(), is(MediaType.TEXT_PLAIN));
assertThat(this.response.getHeaders().getFirst(HttpHeaders.CONTENT_RANGE), is("bytes 0-5/39"));
assertThat(this.response.getHeaders().getFirst(HttpHeaders.ACCEPT_RANGES), is("bytes"));
assertThat(this.response.getHeaders().getContentLength(), is(6L));
Mono<String> result = this.response.getBodyAsString();
StepVerifier.create(result).expectNext("Spring").expectComplete().verify();
}
@Test
public void shouldSetRangeNotSatisfiableStatus() throws Exception {
MockServerHttpRequest request = get("/").header(HttpHeaders.RANGE, "invalid").build();
testWrite(request);
testWrite(get("/").header(HttpHeaders.RANGE, "invalid").build());
assertThat(this.response.getHeaders().getFirst(HttpHeaders.ACCEPT_RANGES), is("bytes"));
assertThat(this.response.getStatusCode(), is(HttpStatus.REQUESTED_RANGE_NOT_SATISFIABLE));
}
private void testWrite(MockServerHttpRequest request) {
Mono<Resource> input = Mono.just(this.resource);
ResolvableType type = ResolvableType.forClass(Resource.class);
MediaType contentType = MediaType.TEXT_PLAIN;
Map<String, Object> hints = Collections.emptyMap();
Mono<Void> mono = this.writer.write(input, null, type, contentType, request, this.response, hints);
StepVerifier.create(mono).expectNextCount(0).expectComplete().verify();
Mono<Void> mono = this.writer.write(this.input, null, null, TEXT_PLAIN, request, this.response, HINTS);
StepVerifier.create(mono).expectComplete().verify();
}
private static HttpRange of(int first, int last) {
return HttpRange.createByteRange(first, last);
}
}

View File

@@ -1,136 +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.codec;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import org.springframework.core.io.ByteArrayResource;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.ResourceRegion;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.mock.http.server.reactive.test.MockServerHttpResponse;
import org.springframework.util.MimeTypeUtils;
import org.springframework.util.StringUtils;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.startsWith;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertThat;
/**
* Unit tests for {@link ResourceRegionHttpMessageWriter}.
* @author Brian Clozel
*/
public class ResourceRegionHttpMessageWriterTests {
private ResourceRegionHttpMessageWriter writer = new ResourceRegionHttpMessageWriter();
private MockServerHttpResponse response = new MockServerHttpResponse();
private Resource resource;
@Rule
public ExpectedException expectedException = ExpectedException.none();
@Before
public void setUp() throws Exception {
String content = "Spring Framework test resource content.";
this.resource = new ByteArrayResource(content.getBytes(StandardCharsets.UTF_8));
}
@Test
public void shouldWriteResourceRegion() throws Exception {
ResourceRegion region = new ResourceRegion(this.resource, 0, 6);
Map<String, Object> hints = Collections.emptyMap();
Mono<Void> mono = this.writer.writeRegions(Mono.just(region), MediaType.TEXT_PLAIN, this.response, hints);
StepVerifier.create(mono).expectComplete().verify();
assertThat(this.response.getHeaders().getContentType(), is(MediaType.TEXT_PLAIN));
assertThat(this.response.getHeaders().getFirst(HttpHeaders.CONTENT_RANGE), is("bytes 0-5/39"));
assertThat(this.response.getHeaders().getContentLength(), is(6L));
Mono<String> result = response.getBodyAsString();
StepVerifier.create(result).expectNext("Spring").expectComplete().verify();
}
@Test
public void shouldWriteMultipleResourceRegions() throws Exception {
Flux<ResourceRegion> regions = Flux.just(
new ResourceRegion(this.resource, 0, 6),
new ResourceRegion(this.resource, 7, 9),
new ResourceRegion(this.resource, 17, 4),
new ResourceRegion(this.resource, 22, 17)
);
String boundary = MimeTypeUtils.generateMultipartBoundaryString();
Map<String, Object> hints = new HashMap<>(1);
hints.put(ResourceRegionHttpMessageWriter.BOUNDARY_STRING_HINT, boundary);
Mono<Void> mono = this.writer.writeRegions(regions, MediaType.TEXT_PLAIN, this.response, hints);
StepVerifier.create(mono).expectComplete().verify();
HttpHeaders headers = this.response.getHeaders();
assertThat(headers.getContentType().toString(), startsWith("multipart/byteranges;boundary=" + boundary));
Mono<String> result = response.getBodyAsString();
StepVerifier.create(result)
.consumeNextWith(content -> {
String[] ranges = StringUtils
.tokenizeToStringArray(content, "\r\n", false, true);
String[] expected = new String[] {
"--" + boundary,
"Content-Type: text/plain",
"Content-Range: bytes 0-5/39",
"Spring",
"--" + boundary,
"Content-Type: text/plain",
"Content-Range: bytes 7-15/39",
"Framework",
"--" + boundary,
"Content-Type: text/plain",
"Content-Range: bytes 17-20/39",
"test",
"--" + boundary,
"Content-Type: text/plain",
"Content-Range: bytes 22-38/39",
"resource content.",
"--" + boundary + "--"
};
assertArrayEquals(expected, ranges);
})
.expectComplete()
.verify();
}
}