/* * 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(); } } }