From a6469baa4f84de1b9da0131db981e2f42b09969d Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Mon, 1 Feb 2016 18:08:03 +0100 Subject: [PATCH] Add HttpRequestBuilder default implementation This commit adds a `DefaultHttpRequestBuilder` and its companion static builders in `HttpRequestBuilders`. This allows to build client requests with a friendly builder API, inspired by Spring's MockMvc API. --- .../reactive/DefaultHttpRequestBuilder.java | 167 ++++++++++++++++++ .../{ => reactive}/HttpRequestBuilder.java | 2 +- .../client/reactive/HttpRequestBuilders.java | 110 ++++++++++++ 3 files changed, 278 insertions(+), 1 deletion(-) create mode 100644 spring-web-reactive/src/main/java/org/springframework/web/client/reactive/DefaultHttpRequestBuilder.java rename spring-web-reactive/src/main/java/org/springframework/web/client/{ => reactive}/HttpRequestBuilder.java (95%) create mode 100644 spring-web-reactive/src/main/java/org/springframework/web/client/reactive/HttpRequestBuilders.java diff --git a/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/DefaultHttpRequestBuilder.java b/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/DefaultHttpRequestBuilder.java new file mode 100644 index 00000000000..5ee4731a12b --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/DefaultHttpRequestBuilder.java @@ -0,0 +1,167 @@ +/* + * 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.web.client.reactive; + + +import java.net.URI; +import java.net.URISyntaxException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +import org.reactivestreams.Publisher; +import reactor.core.publisher.Flux; + +import org.springframework.core.ResolvableType; +import org.springframework.core.codec.Encoder; +import org.springframework.http.HttpCookie; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.http.client.reactive.ClientHttpRequest; +import org.springframework.http.client.reactive.ClientHttpRequestFactory; +import org.springframework.web.client.RestClientException; + +/** + * Builds a {@link ClientHttpRequest} + * + *

See static factory methods in {@link HttpRequestBuilders} + * + * @author Brian Clozel + * @see HttpRequestBuilders + */ +public class DefaultHttpRequestBuilder implements HttpRequestBuilder { + + protected HttpMethod httpMethod; + + protected HttpHeaders httpHeaders; + + protected URI url; + + protected Flux contentPublisher; + + protected List> messageEncoders; + + protected final List cookies = new ArrayList(); + + protected DefaultHttpRequestBuilder() { + } + + public DefaultHttpRequestBuilder(HttpMethod httpMethod, String urlTemplate, Object... urlVariables) throws RestClientException { + this.httpMethod = httpMethod; + this.httpHeaders = new HttpHeaders(); + this.url = parseURI(urlTemplate); + } + + public DefaultHttpRequestBuilder(HttpMethod httpMethod, URI url) { + this.httpMethod = httpMethod; + this.httpHeaders = new HttpHeaders(); + this.url = url; + } + + protected DefaultHttpRequestBuilder setMessageEncoders(List> messageEncoders) { + this.messageEncoders = messageEncoders; + return this; + } + + private URI parseURI(String uri) throws RestClientException { + try { + return new URI(uri); + } + catch (URISyntaxException e) { + throw new RestClientException("could not parse URL template", e); + } + } + + public DefaultHttpRequestBuilder param(String name, String... values) { + return this; + } + + public DefaultHttpRequestBuilder header(String name, String... values) { + Arrays.stream(values).forEach(value -> this.httpHeaders.add(name, value)); + return this; + } + + public DefaultHttpRequestBuilder headers(HttpHeaders httpHeaders) { + this.httpHeaders = httpHeaders; + return this; + } + + public DefaultHttpRequestBuilder contentType(MediaType contentType) { + this.httpHeaders.setContentType(contentType); + return this; + } + + public DefaultHttpRequestBuilder contentType(String contentType) { + this.httpHeaders.setContentType(MediaType.parseMediaType(contentType)); + return this; + } + + public DefaultHttpRequestBuilder accept(MediaType... mediaTypes) { + this.httpHeaders.setAccept(Arrays.asList(mediaTypes)); + return this; + } + + public DefaultHttpRequestBuilder accept(String... mediaTypes) { + this.httpHeaders.setAccept(Arrays.stream(mediaTypes) + .map(type -> MediaType.parseMediaType(type)) + .collect(Collectors.toList())); + return this; + } + + public DefaultHttpRequestBuilder content(Object content) { + this.contentPublisher = Flux.just(content); + return this; + } + + public DefaultHttpRequestBuilder contentStream(Publisher content) { + this.contentPublisher = Flux.from(content); + return this; + } + + public ClientHttpRequest build(ClientHttpRequestFactory factory) { + ClientHttpRequest request = factory.createRequest(this.httpMethod, this.url, this.httpHeaders); + request.getHeaders().putAll(this.httpHeaders); + + if (this.contentPublisher != null) { + ResolvableType requestBodyType = ResolvableType.forInstance(this.contentPublisher); + MediaType mediaType = request.getHeaders().getContentType(); + + Optional> messageEncoder = resolveEncoder(requestBodyType, mediaType); + + if (messageEncoder.isPresent()) { + request.setBody(messageEncoder.get().encode(this.contentPublisher, requestBodyType, mediaType)); + } + else { + // TODO: wrap with client exception? + request.setBody(Flux.error(new IllegalStateException("Can't write request body" + + "of type '" + requestBodyType.toString() + + "' for content-type '" + mediaType.toString() + "'"))); + } + } + + return request; + } + + protected Optional> resolveEncoder(ResolvableType type, MediaType mediaType) { + return this.messageEncoders.stream() + .filter(e -> e.canEncode(type, mediaType)).findFirst(); + } + +} \ No newline at end of file diff --git a/spring-web-reactive/src/main/java/org/springframework/web/client/HttpRequestBuilder.java b/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/HttpRequestBuilder.java similarity index 95% rename from spring-web-reactive/src/main/java/org/springframework/web/client/HttpRequestBuilder.java rename to spring-web-reactive/src/main/java/org/springframework/web/client/reactive/HttpRequestBuilder.java index a9717445b9b..55d5e182294 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/client/HttpRequestBuilder.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/HttpRequestBuilder.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.web.client; +package org.springframework.web.client.reactive; import org.springframework.http.client.reactive.ClientHttpRequest; import org.springframework.http.client.reactive.ClientHttpRequestFactory; diff --git a/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/HttpRequestBuilders.java b/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/HttpRequestBuilders.java new file mode 100644 index 00000000000..13009f3651c --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/HttpRequestBuilders.java @@ -0,0 +1,110 @@ +/* + * 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.web.client.reactive; + +import org.springframework.http.HttpMethod; + +/** + * Static factory methods for {@link DefaultHttpRequestBuilder RequestBuilders}. + * + * @author Brian Clozel + */ +public abstract class HttpRequestBuilders { + + /** + * Create a {@link DefaultHttpRequestBuilder} for a GET request. + * + * @param urlTemplate a URL template; the resulting URL will be encoded + * @param urlVariables zero or more URL variables + */ + public static DefaultHttpRequestBuilder get(String urlTemplate, Object... urlVariables) { + return new DefaultHttpRequestBuilder(HttpMethod.GET, urlTemplate, urlVariables); + } + + /** + * Create a {@link DefaultHttpRequestBuilder} for a POST request. + * + * @param urlTemplate a URL template; the resulting URL will be encoded + * @param urlVariables zero or more URL variables + */ + public static DefaultHttpRequestBuilder post(String urlTemplate, Object... urlVariables) { + return new DefaultHttpRequestBuilder(HttpMethod.POST, urlTemplate, urlVariables); + } + + + /** + * Create a {@link DefaultHttpRequestBuilder} for a PUT request. + * + * @param urlTemplate a URL template; the resulting URL will be encoded + * @param urlVariables zero or more URL variables + */ + public static DefaultHttpRequestBuilder put(String urlTemplate, Object... urlVariables) { + return new DefaultHttpRequestBuilder(HttpMethod.PUT, urlTemplate, urlVariables); + } + + /** + * Create a {@link DefaultHttpRequestBuilder} for a PATCH request. + * + * @param urlTemplate a URL template; the resulting URL will be encoded + * @param urlVariables zero or more URL variables + */ + public static DefaultHttpRequestBuilder patch(String urlTemplate, Object... urlVariables) { + return new DefaultHttpRequestBuilder(HttpMethod.PATCH, urlTemplate, urlVariables); + } + + /** + * Create a {@link DefaultHttpRequestBuilder} for a DELETE request. + * + * @param urlTemplate a URL template; the resulting URL will be encoded + * @param urlVariables zero or more URL variables + */ + public static DefaultHttpRequestBuilder delete(String urlTemplate, Object... urlVariables) { + return new DefaultHttpRequestBuilder(HttpMethod.DELETE, urlTemplate, urlVariables); + } + + /** + * Create a {@link DefaultHttpRequestBuilder} for an OPTIONS request. + * + * @param urlTemplate a URL template; the resulting URL will be encoded + * @param urlVariables zero or more URL variables + */ + public static DefaultHttpRequestBuilder options(String urlTemplate, Object... urlVariables) { + return new DefaultHttpRequestBuilder(HttpMethod.OPTIONS, urlTemplate, urlVariables); + } + + /** + * Create a {@link DefaultHttpRequestBuilder} for a HEAD request. + * + * @param urlTemplate a URL template; the resulting URL will be encoded + * @param urlVariables zero or more URL variables + */ + public static DefaultHttpRequestBuilder head(String urlTemplate, Object... urlVariables) { + return new DefaultHttpRequestBuilder(HttpMethod.HEAD, urlTemplate, urlVariables); + } + + /** + * Create a {@link DefaultHttpRequestBuilder} for a request with the given HTTP method. + * + * @param httpMethod the HTTP method + * @param urlTemplate a URL template; the resulting URL will be encoded + * @param urlVariables zero or more URL variables + */ + public static DefaultHttpRequestBuilder request(HttpMethod httpMethod, String urlTemplate, Object... urlVariables) { + return new DefaultHttpRequestBuilder(httpMethod, urlTemplate, urlVariables); + } + +} \ No newline at end of file