/*
* 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.mock.http.server.reactive;
import java.net.InetSocketAddress;
import java.net.URI;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Flux;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DefaultDataBufferFactory;
import org.springframework.http.HttpCookie;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpRange;
import org.springframework.http.MediaType;
import org.springframework.http.server.reactive.AbstractServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.util.UriComponentsBuilder;
/**
* Mock implementation of {@link ServerHttpRequest}.
*
* <p><strong>Note:</strong> this class extends the same
* {@link AbstractServerHttpRequest} base class as actual server-specific
* implementation and is therefore read-only once created. Use static builder
* methods in this class to build up request instances.
*
* @author Rossen Stoyanchev
* @since 5.0
*/
public class MockServerHttpRequest extends AbstractServerHttpRequest {
private final HttpMethod httpMethod;
private final String contextPath;
private final MultiValueMap<String, HttpCookie> cookies;
private final InetSocketAddress remoteAddress;
private final Flux<DataBuffer> body;
private MockServerHttpRequest(HttpMethod httpMethod, URI uri, String contextPath,
HttpHeaders headers, MultiValueMap<String, HttpCookie> cookies,
InetSocketAddress remoteAddress,
Publisher<? extends DataBuffer> body) {
super(uri, headers);
this.httpMethod = httpMethod;
this.contextPath = (contextPath != null ? contextPath : "");
this.cookies = cookies;
this.remoteAddress = remoteAddress;
this.body = Flux.from(body);
}
@Override
public HttpMethod getMethod() {
return this.httpMethod;
}
@Override
public String getContextPath() {
return this.contextPath;
}
@Override
public Optional<InetSocketAddress> getRemoteAddress() {
return Optional.ofNullable(this.remoteAddress);
}
@Override
public Flux<DataBuffer> getBody() {
return this.body;
}
@Override
protected MultiValueMap<String, HttpCookie> initCookies() {
return this.cookies;
}
/**
* Shortcut to wrap the request with a {@code MockServerWebExchange}.
*/
public MockServerWebExchange toExchange() {
return new MockServerWebExchange(this);
}
// Static builder methods
/**
* Create a builder with the given HTTP method and a {@link URI}.
* @param method the HTTP method (GET, POST, etc)
* @param url the URL
* @return the created builder
*/
public static BodyBuilder method(HttpMethod method, URI url) {
return new DefaultBodyBuilder(method, url);
}
/**
* Alternative to {@link #method(HttpMethod, URI)} that accepts a URI template.
* @param method the HTTP method (GET, POST, etc)
* @param urlTemplate the URL template
* @param vars variables to expand into the template
* @return the created builder
*/
public static BodyBuilder method(HttpMethod method, String urlTemplate, Object... vars) {
URI url = UriComponentsBuilder.fromUriString(urlTemplate).buildAndExpand(vars).encode().toUri();
return new DefaultBodyBuilder(method, url);
}
/**
* Create an HTTP GET builder with the given url.
* @param urlTemplate a URL template; the resulting URL will be encoded
* @param uriVars zero or more URI variables
* @return the created builder
*/
public static BaseBuilder<?> get(String urlTemplate, Object... uriVars) {
return method(HttpMethod.GET, urlTemplate, uriVars);
}
/**
* Create an HTTP HEAD builder with the given url.
* @param urlTemplate a URL template; the resulting URL will be encoded
* @param uriVars zero or more URI variables
* @return the created builder
*/
public static BaseBuilder<?> head(String urlTemplate, Object... uriVars) {
return method(HttpMethod.HEAD, urlTemplate, uriVars);
}
/**
* Create an HTTP POST builder with the given url.
* @param urlTemplate a URL template; the resulting URL will be encoded
* @param uriVars zero or more URI variables
* @return the created builder
*/
public static BodyBuilder post(String urlTemplate, Object... uriVars) {
return method(HttpMethod.POST, urlTemplate, uriVars);
}
/**
* Create an HTTP PUT builder with the given url.
* @param urlTemplate a URL template; the resulting URL will be encoded
* @param uriVars zero or more URI variables
* @return the created builder
*/
public static BodyBuilder put(String urlTemplate, Object... uriVars) {
return method(HttpMethod.PUT, urlTemplate, uriVars);
}
/**
* Create an HTTP PATCH builder with the given url.
* @param urlTemplate a URL template; the resulting URL will be encoded
* @param uriVars zero or more URI variables
* @return the created builder
*/
public static BodyBuilder patch(String urlTemplate, Object... uriVars) {
return method(HttpMethod.PATCH, urlTemplate, uriVars);
}
/**
* Create an HTTP DELETE builder with the given url.
* @param urlTemplate a URL template; the resulting URL will be encoded
* @param uriVars zero or more URI variables
* @return the created builder
*/
public static BaseBuilder<?> delete(String urlTemplate, Object... uriVars) {
return method(HttpMethod.DELETE, urlTemplate, uriVars);
}
/**
* Creates an HTTP OPTIONS builder with the given url.
* @param urlTemplate a URL template; the resulting URL will be encoded
* @param uriVars zero or more URI variables
* @return the created builder
*/
public static BaseBuilder<?> options(String urlTemplate, Object... uriVars) {
return method(HttpMethod.OPTIONS, urlTemplate, uriVars);
}
/**
* Request builder exposing properties not related to the body.
* @param <B> the builder sub-class
*/
public interface BaseBuilder<B extends BaseBuilder<B>> {
/**
* Set the contextPath to return.
*/
B contextPath(String contextPath);
/**
* Set the remote address to return.
*/
B remoteAddress(InetSocketAddress remoteAddress);
/**
* Add one or more cookies.
*/
B cookie(String path, HttpCookie... cookie);
/**
* Add the given cookies.
* @param cookies the cookies.
*/
B cookies(MultiValueMap<String, HttpCookie> cookies);
/**
* Add the given, single header value under the given name.
* @param headerName the header name
* @param headerValues the header value(s)
* @see HttpHeaders#add(String, String)
*/
B header(String headerName, String... headerValues);
/**
* Add the given header values.
* @param headers the header values
*/
B headers(MultiValueMap<String, String> headers);
/**
* Set the list of acceptable {@linkplain MediaType media types}, as
* specified by the {@code Accept} header.
* @param acceptableMediaTypes the acceptable media types
*/
B accept(MediaType... acceptableMediaTypes);
/**
* Set the list of acceptable {@linkplain Charset charsets}, as specified
* by the {@code Accept-Charset} header.
* @param acceptableCharsets the acceptable charsets
*/
B acceptCharset(Charset... acceptableCharsets);
/**
* Set the value of the {@code If-Modified-Since} header.
* <p>The date should be specified as the number of milliseconds since
* January 1, 1970 GMT.
* @param ifModifiedSince the new value of the header
*/
B ifModifiedSince(long ifModifiedSince);
/**
* Set the (new) value of the {@code If-Unmodified-Since} header.
* <p>The date should be specified as the number of milliseconds since
* January 1, 1970 GMT.
* @param ifUnmodifiedSince the new value of the header
* @see HttpHeaders#setIfUnmodifiedSince(long)
*/
B ifUnmodifiedSince(long ifUnmodifiedSince);
/**
* Set the values of the {@code If-None-Match} header.
* @param ifNoneMatches the new value of the header
*/
B ifNoneMatch(String... ifNoneMatches);
/**
* Set the (new) value of the Range header.
* @param ranges the HTTP ranges
* @see HttpHeaders#setRange(List)
*/
B range(HttpRange... ranges);
/**
* Builds the request with no body.
* @return the request
* @see BodyBuilder#body(Publisher)
* @see BodyBuilder#body(String)
*/
MockServerHttpRequest build();
/**
* Shortcut for:<br>
* {@code build().toExchange()}
*/
MockServerWebExchange toExchange();
}
/**
* A builder that adds a body to the request.
*/
public interface BodyBuilder extends BaseBuilder<BodyBuilder> {
/**
* Set the length of the body in bytes, as specified by the
* {@code Content-Length} header.
* @param contentLength the content length
* @return this builder
* @see HttpHeaders#setContentLength(long)
*/
BodyBuilder contentLength(long contentLength);
/**
* Set the {@linkplain MediaType media type} of the body, as specified
* by the {@code Content-Type} header.
* @param contentType the content type
* @return this builder
* @see HttpHeaders#setContentType(MediaType)
*/
BodyBuilder contentType(MediaType contentType);
/**
* Set the body of the request and build it.
* @param body the body
* @return the built request entity
*/
MockServerHttpRequest body(Publisher<? extends DataBuffer> body);
/**
* Set the body of the request and build it.
* <p>The String is assumed to be UTF-8 encoded unless the request has a
* "content-type" header with a charset attribute.
* @param body the body as text
* @return the built request entity
*/
MockServerHttpRequest body(String body);
}
private static class DefaultBodyBuilder implements BodyBuilder {
private final HttpMethod method;
private final URI url;
private String contextPath;
private final HttpHeaders headers = new HttpHeaders();
private final MultiValueMap<String, HttpCookie> cookies = new LinkedMultiValueMap<>();
private InetSocketAddress remoteAddress;
public DefaultBodyBuilder(HttpMethod method, URI url) {
this.method = method;
this.url = url;
}
@Override
public BodyBuilder contextPath(String contextPath) {
this.contextPath = contextPath;
return this;
}
@Override
public BodyBuilder remoteAddress(InetSocketAddress remoteAddress) {
this.remoteAddress = remoteAddress;
return this;
}
@Override
public BodyBuilder cookie(String path, HttpCookie... cookies) {
this.cookies.put(path, Arrays.asList(cookies));
return this;
}
@Override
public BodyBuilder cookies(MultiValueMap<String, HttpCookie> cookies) {
this.cookies.putAll(cookies);
return this;
}
@Override
public BodyBuilder header(String headerName, String... headerValues) {
for (String headerValue : headerValues) {
this.headers.add(headerName, headerValue);
}
return this;
}
@Override
public BodyBuilder headers(MultiValueMap<String, String> headers) {
this.headers.putAll(headers);
return this;
}
@Override
public BodyBuilder accept(MediaType... acceptableMediaTypes) {
this.headers.setAccept(Arrays.asList(acceptableMediaTypes));
return this;
}
@Override
public BodyBuilder acceptCharset(Charset... acceptableCharsets) {
this.headers.setAcceptCharset(Arrays.asList(acceptableCharsets));
return this;
}
@Override
public BodyBuilder contentLength(long contentLength) {
this.headers.setContentLength(contentLength);
return this;
}
@Override
public BodyBuilder contentType(MediaType contentType) {
this.headers.setContentType(contentType);
return this;
}
@Override
public BodyBuilder ifModifiedSince(long ifModifiedSince) {
this.headers.setIfModifiedSince(ifModifiedSince);
return this;
}
@Override
public BodyBuilder ifUnmodifiedSince(long ifUnmodifiedSince) {
this.headers.setIfUnmodifiedSince(ifUnmodifiedSince);
return this;
}
@Override
public BodyBuilder ifNoneMatch(String... ifNoneMatches) {
this.headers.setIfNoneMatch(Arrays.asList(ifNoneMatches));
return this;
}
@Override
public BodyBuilder range(HttpRange... ranges) {
this.headers.setRange(Arrays.asList(ranges));
return this;
}
@Override
public MockServerHttpRequest body(Publisher<? extends DataBuffer> body) {
return new MockServerHttpRequest(this.method, this.url, this.contextPath,
this.headers, this.cookies, this.remoteAddress, body);
}
@Override
public MockServerHttpRequest body(String body) {
Charset charset = getCharset();
byte[] bytes = body.getBytes(charset);
ByteBuffer byteBuffer = ByteBuffer.wrap(bytes);
DataBuffer buffer = new DefaultDataBufferFactory().wrap(byteBuffer);
return body(Flux.just(buffer));
}
private Charset getCharset() {
MediaType contentType = this.headers.getContentType();
Charset charset = (contentType != null ? contentType.getCharset() : null);
charset = charset != null ? charset : StandardCharsets.UTF_8;
return charset;
}
@Override
public MockServerHttpRequest build() {
return body(Flux.empty());
}
@Override
public MockServerWebExchange toExchange() {
return build().toExchange();
}
}
}