From 7b67b5bc2a3443026e92af28275d176648353110 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Fri, 27 Jan 2017 14:07:55 -0500 Subject: [PATCH 1/4] Introduce UriBuilderFactory and UriBuilder Issue: SPR-15124 --- .../web/util/DefaultUriBuilderFactory.java | 299 ++++++++++++++++++ .../web/util/HierarchicalUriComponents.java | 29 +- .../web/util/OpaqueUriComponents.java | 12 +- .../springframework/web/util/UriBuilder.java | 171 ++++++++++ .../web/util/UriBuilderFactory.java | 43 +++ .../web/util/UriComponentsBuilder.java | 51 ++- .../util/DefaultUriBuilderFactoryTests.java | 114 +++++++ 7 files changed, 695 insertions(+), 24 deletions(-) create mode 100644 spring-web/src/main/java/org/springframework/web/util/DefaultUriBuilderFactory.java create mode 100644 spring-web/src/main/java/org/springframework/web/util/UriBuilder.java create mode 100644 spring-web/src/main/java/org/springframework/web/util/UriBuilderFactory.java create mode 100644 spring-web/src/test/java/org/springframework/web/util/DefaultUriBuilderFactoryTests.java diff --git a/spring-web/src/main/java/org/springframework/web/util/DefaultUriBuilderFactory.java b/spring-web/src/main/java/org/springframework/web/util/DefaultUriBuilderFactory.java new file mode 100644 index 00000000000..8daaf768a01 --- /dev/null +++ b/spring-web/src/main/java/org/springframework/web/util/DefaultUriBuilderFactory.java @@ -0,0 +1,299 @@ +/* + * 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. + * 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.util; + +import java.net.URI; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.springframework.util.Assert; +import org.springframework.util.MultiValueMap; +import org.springframework.util.ObjectUtils; + +/** + * Default implementation of {@link UriBuilderFactory} using + * {@link UriComponentsBuilder} for building, encoding, and expanding URI + * templates. + * + *

Exposes configuration properties that customize the creation of all URIs + * built through this factory instance including a base URI, default URI + * variables, and an encoding mode. + * + * @author Rossen Stoyanchev + * @since 5.0 + */ +public class DefaultUriBuilderFactory implements UriBuilderFactory { + + public enum EncodingMode {URI_COMPONENT, VALUES_ONLY, NONE }; + + + private final UriComponentsBuilder baseUri; + + private final Map defaultUriVariables = new HashMap<>(); + + private EncodingMode encodingMode = EncodingMode.URI_COMPONENT; + + + /** + * Default constructor without a base URI. + */ + public DefaultUriBuilderFactory() { + this(UriComponentsBuilder.fromPath(null)); + } + + /** + * Constructor with a String "base URI". + *

The String given here is used to create a single "base" + * {@code UriComponentsBuilder}. Each time a new URI is prepared via + * {@link #uriString(String)} a new {@code UriComponentsBuilder} is created and + * merged with a clone of the "base" {@code UriComponentsBuilder}. + *

Note that the base URI may contain any or all components of a URI and + * those will apply to every URI. + */ + public DefaultUriBuilderFactory(String baseUri) { + this(UriComponentsBuilder.fromUriString(baseUri)); + } + + /** + * Alternate constructor with a {@code UriComponentsBuilder} as the base URI. + */ + public DefaultUriBuilderFactory(UriComponentsBuilder baseUri) { + Assert.notNull(baseUri, "'baseUri' is required."); + this.baseUri = baseUri; + } + + + /** + * Configure default URI variable values to use when expanding a URI with a + * Map of values. The map supplied when expanding a given URI can override + * default values. + * @param defaultUriVariables the default URI variables + */ + public void setDefaultUriVariables(Map defaultUriVariables) { + this.defaultUriVariables.clear(); + if (defaultUriVariables != null) { + this.defaultUriVariables.putAll(defaultUriVariables); + } + } + + /** + * Return the configured default URI variable values. + */ + public Map getDefaultUriVariables() { + return Collections.unmodifiableMap(this.defaultUriVariables); + } + + /** + * Specify the encoding mode to use when building URIs: + *

+ *

By default this is set to {@code "URI_COMPONENT"}. + * @param encodingMode the encoding mode to use + */ + public void setEncodingMode(EncodingMode encodingMode) { + this.encodingMode = encodingMode; + } + + /** + * Return the configured encoding mode. + */ + public EncodingMode getEncodingMode() { + return this.encodingMode; + } + + + // UriTemplateHandler + + public URI expand(String uriTemplate, Map uriVars) { + return uriString(uriTemplate).build(uriVars); + } + + public URI expand(String uriTemplate, Object... uriVars) { + return uriString(uriTemplate).build(uriVars); + } + + // UriBuilderFactory + + public UriBuilder uriString(String uriTemplate) { + return new DefaultUriBuilder(uriTemplate); + } + + + /** + * {@link DefaultUriBuilderFactory} specific implementation of UriBuilder. + */ + private class DefaultUriBuilder implements UriBuilder { + + private final UriComponentsBuilder uriComponentsBuilder; + + + public DefaultUriBuilder(String uriTemplate) { + this.uriComponentsBuilder = initUriComponentsBuilder(uriTemplate); + } + + private UriComponentsBuilder initUriComponentsBuilder(String uriTemplate) { + + // Merge base URI with child URI template + UriComponentsBuilder result = baseUri.cloneBuilder(); + UriComponents child = UriComponentsBuilder.fromUriString(uriTemplate).build(); + result.uriComponents(child); + + // Split path into path segments + List pathList = result.build().getPathSegments(); + String[] pathArray = pathList.toArray(new String[pathList.size()]); + result.replacePath(null); + result.pathSegment(pathArray); + + return result; + } + + @Override + public DefaultUriBuilder scheme(String scheme) { + this.uriComponentsBuilder.scheme(scheme); + return this; + } + + @Override + public DefaultUriBuilder userInfo(String userInfo) { + this.uriComponentsBuilder.userInfo(userInfo); + return this; + } + + @Override + public DefaultUriBuilder host(String host) { + this.uriComponentsBuilder.host(host); + return this; + } + + @Override + public DefaultUriBuilder port(int port) { + this.uriComponentsBuilder.port(port); + return this; + } + + @Override + public DefaultUriBuilder port(String port) { + this.uriComponentsBuilder.port(port); + return this; + } + + @Override + public DefaultUriBuilder path(String path) { + this.uriComponentsBuilder.path(path); + return this; + } + + @Override + public DefaultUriBuilder replacePath(String path) { + this.uriComponentsBuilder.replacePath(path); + return this; + } + + @Override + public DefaultUriBuilder pathSegment(String... pathSegments) { + this.uriComponentsBuilder.pathSegment(pathSegments); + return this; + } + + @Override + public DefaultUriBuilder query(String query) { + this.uriComponentsBuilder.query(query); + return this; + } + + @Override + public DefaultUriBuilder replaceQuery(String query) { + this.uriComponentsBuilder.replaceQuery(query); + return this; + } + + @Override + public DefaultUriBuilder queryParam(String name, Object... values) { + this.uriComponentsBuilder.queryParam(name, values); + return this; + } + + @Override + public DefaultUriBuilder replaceQueryParam(String name, Object... values) { + this.uriComponentsBuilder.replaceQueryParam(name, values); + return this; + } + + @Override + public DefaultUriBuilder queryParams(MultiValueMap params) { + this.uriComponentsBuilder.queryParams(params); + return this; + } + + @Override + public DefaultUriBuilder replaceQueryParams(MultiValueMap params) { + this.uriComponentsBuilder.replaceQueryParams(params); + return this; + } + + @Override + public DefaultUriBuilder fragment(String fragment) { + this.uriComponentsBuilder.fragment(fragment); + return this; + } + + @Override + public URI build(Map uriVars) { + if (!defaultUriVariables.isEmpty()) { + Map map = new HashMap<>(); + map.putAll(defaultUriVariables); + map.putAll(uriVars); + uriVars = map; + } + if (encodingMode.equals(EncodingMode.VALUES_ONLY)) { + uriVars = UriUtils.encodeUriVariables(uriVars); + } + UriComponents uriComponents = this.uriComponentsBuilder.build().expand(uriVars); + if (encodingMode.equals(EncodingMode.URI_COMPONENT)) { + uriComponents = uriComponents.encode(); + } + return URI.create(uriComponents.toString()); + } + + @Override + public URI build(Object... uriVars) { + if (ObjectUtils.isEmpty(uriVars) && !defaultUriVariables.isEmpty()) { + return build(Collections.emptyMap()); + } + if (encodingMode.equals(EncodingMode.VALUES_ONLY)) { + uriVars = UriUtils.encodeUriVariables(uriVars); + } + UriComponents uriComponents = this.uriComponentsBuilder.build().expand(uriVars); + if (encodingMode.equals(EncodingMode.URI_COMPONENT)) { + uriComponents = uriComponents.encode(); + } + return URI.create(uriComponents.toString()); + } + } + +} diff --git a/spring-web/src/main/java/org/springframework/web/util/HierarchicalUriComponents.java b/spring-web/src/main/java/org/springframework/web/util/HierarchicalUriComponents.java index 0912430d079..905f29eef3e 100644 --- a/spring-web/src/main/java/org/springframework/web/util/HierarchicalUriComponents.java +++ b/spring-web/src/main/java/org/springframework/web/util/HierarchicalUriComponents.java @@ -446,14 +446,27 @@ final class HierarchicalUriComponents extends UriComponents { @Override protected void copyToUriComponentsBuilder(UriComponentsBuilder builder) { - builder.scheme(getScheme()); - builder.userInfo(getUserInfo()); - builder.host(getHost()); - builder.port(getPort()); - builder.replacePath(""); - this.path.copyToUriComponentsBuilder(builder); - builder.replaceQueryParams(getQueryParams()); - builder.fragment(getFragment()); + if (getScheme() != null) { + builder.scheme(getScheme()); + } + if (getUserInfo() != null) { + builder.userInfo(getUserInfo()); + } + if (getHost() != null) { + builder.host(getHost()); + } + if (getPort() != -1) { + builder.port(getPort()); + } + if (getPath() != null) { + this.path.copyToUriComponentsBuilder(builder); + } + if (!getQueryParams().isEmpty()) { + builder.queryParams(getQueryParams()); + } + if (getFragment() != null) { + builder.fragment(getFragment()); + } } diff --git a/spring-web/src/main/java/org/springframework/web/util/OpaqueUriComponents.java b/spring-web/src/main/java/org/springframework/web/util/OpaqueUriComponents.java index bf3c7e34954..f0e1c295754 100644 --- a/spring-web/src/main/java/org/springframework/web/util/OpaqueUriComponents.java +++ b/spring-web/src/main/java/org/springframework/web/util/OpaqueUriComponents.java @@ -137,9 +137,15 @@ final class OpaqueUriComponents extends UriComponents { @Override protected void copyToUriComponentsBuilder(UriComponentsBuilder builder) { - builder.scheme(getScheme()); - builder.schemeSpecificPart(getSchemeSpecificPart()); - builder.fragment(getFragment()); + if (getScheme() != null) { + builder.scheme(getScheme()); + } + if (getSchemeSpecificPart() != null) { + builder.schemeSpecificPart(getSchemeSpecificPart()); + } + if (getFragment() != null) { + builder.fragment(getFragment()); + } } diff --git a/spring-web/src/main/java/org/springframework/web/util/UriBuilder.java b/spring-web/src/main/java/org/springframework/web/util/UriBuilder.java new file mode 100644 index 00000000000..2388d325812 --- /dev/null +++ b/spring-web/src/main/java/org/springframework/web/util/UriBuilder.java @@ -0,0 +1,171 @@ +/* + * 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. + * 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, + * WITHOUUriBuilder 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.util; + +import java.net.URI; +import java.util.Map; + +import org.springframework.util.MultiValueMap; + +/** + * Builder-style methods to prepare and expand a URI template with variables. + * + *

Effectively a generalization of {@link UriComponentsBuilder} but with + * shortcuts to expand directly into {@link URI} rather than + * {@link UriComponents} and also leaving common concerns such as encoding + * preferences, a base URI, and others as implementation concerns. + * + *

Typically obtained via {@link UriBuilderFactory} which serves as a central + * component configured once and used to create many URLs. + * + * @author Rossen Stoyanchev + * @since 5.0 + * @see UriBuilderFactory + * @see UriComponentsBuilder + */ +public interface UriBuilder { + + /** + * Set the URI scheme which may contain URI template variables, + * and may also be {@code null} to clear the scheme of this builder. + * @param scheme the URI scheme + */ + UriBuilder scheme(String scheme); + + /** + * Set the URI user info which may contain URI template variables, and + * may also be {@code null} to clear the user info of this builder. + * @param userInfo the URI user info + */ + UriBuilder userInfo(String userInfo); + + /** + * Set the URI host which may contain URI template variables, and may also + * be {@code null} to clear the host of this builder. + * @param host the URI host + */ + UriBuilder host(String host); + + /** + * Set the URI port. Passing {@code -1} will clear the port of this builder. + * @param port the URI port + */ + UriBuilder port(int port); + + /** + * Set the URI port . Use this method only when the port needs to be + * parameterized with a URI variable. Otherwise use {@link #port(int)}. + * Passing {@code null} will clear the port of this builder. + * @param port the URI port + */ + UriBuilder port(String port); + + /** + * Append the given path to the existing path of this builder. + * The given path may contain URI template variables. + * @param path the URI path + */ + UriBuilder path(String path); + + /** + * Set the path of this builder overriding the existing path values. + * @param path the URI path or {@code null} for an empty path. + */ + UriBuilder replacePath(String path); + + /** + * Append path segments to the existing path. Each path segment may contain + * URI template variables and should not contain any slashes. + * Use {@code path("/")} subsequently to ensure a trailing slash. + * @param pathSegments the URI path segments + */ + UriBuilder pathSegment(String... pathSegments) throws IllegalArgumentException; + + /** + * Append the given query to the existing query of this builder. + * The given query may contain URI template variables. + *

Note: The presence of reserved characters can prevent + * correct parsing of the URI string. For example if a query parameter + * contains {@code '='} or {@code '&'} characters, the query string cannot + * be parsed unambiguously. Such values should be substituted for URI + * variables to enable correct parsing: + *

+	 * builder.query("filter={value}").uriString("hot&cold");
+	 * 
+ * @param query the query string + */ + UriBuilder query(String query); + + /** + * Set the query of this builder overriding all existing query parameters. + * @param query the query string or {@code null} to remove all query params + */ + UriBuilder replaceQuery(String query); + + /** + * Append the given query parameter to the existing query parameters. The + * given name or any of the values may contain URI template variables. If no + * values are given, the resulting URI will contain the query parameter name + * only (i.e. {@code ?foo} instead of {@code ?foo=bar}. + * @param name the query parameter name + * @param values the query parameter values + */ + UriBuilder queryParam(String name, Object... values); + + /** + * Add the given query parameters. + * @param params the params + */ + UriBuilder queryParams(MultiValueMap params); + + /** + * Set the query parameter values overriding all existing query values for + * the same parameter. If no values are given, the query parameter is removed. + * @param name the query parameter name + * @param values the query parameter values + */ + UriBuilder replaceQueryParam(String name, Object... values); + + /** + * Set the query parameter values overriding all existing query values. + * @param params the query parameter name + */ + UriBuilder replaceQueryParams(MultiValueMap params); + + /** + * Set the URI fragment. The given fragment may contain URI template variables, + * and may also be {@code null} to clear the fragment of this builder. + * @param fragment the URI fragment + */ + UriBuilder fragment(String fragment); + + /** + * Build a {@link URI} instance and replaces URI template variables + * with the values from an array. + * @param uriVariables the map of URI variables + * @return the URI + */ + URI build(Object... uriVariables); + + /** + * Build a {@link URI} instance and replaces URI template variables + * with the values from a map. + * @param uriVariables the map of URI variables + * @return the URI + */ + URI build(Map uriVariables); + +} diff --git a/spring-web/src/main/java/org/springframework/web/util/UriBuilderFactory.java b/spring-web/src/main/java/org/springframework/web/util/UriBuilderFactory.java new file mode 100644 index 00000000000..504a7d50920 --- /dev/null +++ b/spring-web/src/main/java/org/springframework/web/util/UriBuilderFactory.java @@ -0,0 +1,43 @@ +/* + * 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. + * 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.util; + +/** + * Factory for instances of {@link UriBuilder}. + * + *

A single {@link UriBuilderFactory} may be created once, configured with + * common properties such as a base URI, and then used to create many URIs. + * + *

Extends {@link UriTemplateHandler} which has a similar purpose but only + * provides shortcuts for expanding URI templates, not builder style methods. + * + * @author Rossen Stoyanchev + * @since 5.0 + */ +public interface UriBuilderFactory extends UriTemplateHandler { + + /** + * Return a builder that is initialized with the given URI string which may + * be a URI template and represent full URI or just a path. + *

Depending on the factory implementation and configuration, the builder + * may merge the given URI string with a base URI and apply other operations. + * Refer to the specific factory implementation for details. + * @param uriTemplate the URI template to create the builder with + * @return the UriBuilder + */ + UriBuilder uriString(String uriTemplate); + +} diff --git a/spring-web/src/main/java/org/springframework/web/util/UriComponentsBuilder.java b/spring-web/src/main/java/org/springframework/web/util/UriComponentsBuilder.java index 09495d39777..9f7694c3265 100644 --- a/spring-web/src/main/java/org/springframework/web/util/UriComponentsBuilder.java +++ b/spring-web/src/main/java/org/springframework/web/util/UriComponentsBuilder.java @@ -57,7 +57,7 @@ import org.springframework.web.util.HierarchicalUriComponents.PathComponent; * @see #fromPath(String) * @see #fromUri(URI) */ -public class UriComponentsBuilder implements Cloneable { +public class UriComponentsBuilder implements UriBuilder, Cloneable { private static final Pattern QUERY_PARAM_PATTERN = Pattern.compile("([^&=]+)(=?)([^&]+)?"); @@ -360,6 +360,30 @@ public class UriComponentsBuilder implements Cloneable { return build(false).expand(uriVariableValues); } + + /** + * Build a {@link URI} instance and replaces URI template variables + * with the values from an array. + * @param uriVariables the map of URI variables + * @return the URI + */ + @Override + public URI build(Object... uriVariables) { + return buildAndExpand(uriVariables).encode().toUri(); + } + + /** + * Build a {@link URI} instance and replaces URI template variables + * with the values from a map. + * @param uriVariables the map of URI variables + * @return the URI + */ + @Override + public URI build(Map uriVariables) { + return buildAndExpand(uriVariables).encode().toUri(); + } + + /** * Build a URI String. This is a shortcut method which combines calls * to {@link #build()}, then {@link UriComponents#encode()} and finally @@ -372,10 +396,10 @@ public class UriComponentsBuilder implements Cloneable { } - // URI components methods + // Instance methods /** - * Initialize all components of this URI builder with the components of the given URI. + * Initialize components of this builder from components of the given URI. * @param uri the URI * @return this UriComponentsBuilder */ @@ -412,24 +436,25 @@ public class UriComponentsBuilder implements Cloneable { } /** - * Set the URI scheme. The given scheme may contain URI template variables, - * and may also be {@code null} to clear the scheme of this builder. - * @param scheme the URI scheme + * Initialize components of this {@link UriComponentsBuilder} from the + * components of the given {@link UriComponents}. + * @param uriComponents the UriComponents instance * @return this UriComponentsBuilder */ - public UriComponentsBuilder scheme(String scheme) { - this.scheme = scheme; + public UriComponentsBuilder uriComponents(UriComponents uriComponents) { + Assert.notNull(uriComponents, "UriComponents must not be null"); + uriComponents.copyToUriComponentsBuilder(this); return this; } /** - * Set all components of this URI builder from the given {@link UriComponents}. - * @param uriComponents the UriComponents instance + * Set the URI scheme. The given scheme may contain URI template variables, + * and may also be {@code null} to clear the scheme of this builder. + * @param scheme the URI scheme * @return this UriComponentsBuilder */ - public UriComponentsBuilder uriComponents(UriComponents uriComponents) { - Assert.notNull(uriComponents, "UriComponents must not be null"); - uriComponents.copyToUriComponentsBuilder(this); + public UriComponentsBuilder scheme(String scheme) { + this.scheme = scheme; return this; } diff --git a/spring-web/src/test/java/org/springframework/web/util/DefaultUriBuilderFactoryTests.java b/spring-web/src/test/java/org/springframework/web/util/DefaultUriBuilderFactoryTests.java new file mode 100644 index 00000000000..f7931ffcc0d --- /dev/null +++ b/spring-web/src/test/java/org/springframework/web/util/DefaultUriBuilderFactoryTests.java @@ -0,0 +1,114 @@ +/* + * 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. + * 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.util; + +import java.net.URI; +import java.util.Collections; + +import org.junit.Test; + +import org.springframework.web.util.DefaultUriBuilderFactory.EncodingMode; + +import static junit.framework.TestCase.assertEquals; + +/** + * Unit tests for {@link DefaultUriBuilderFactory}. + * @author Rossen Stoyanchev + */ +public class DefaultUriBuilderFactoryTests { + + private static final String baseUrl = "http://foo.com/bar"; + + + @Test + public void defaultSettings() throws Exception { + DefaultUriBuilderFactory factory = new DefaultUriBuilderFactory(); + URI uri = factory.uriString("/foo").pathSegment("{id}").build("a/b"); + assertEquals("/foo/a%2Fb", uri.toString()); + } + + @Test + public void baseUri() throws Exception { + DefaultUriBuilderFactory factory = new DefaultUriBuilderFactory("http://foo.com/bar?id=123"); + URI uri = factory.uriString("/baz").port(8080).build(); + assertEquals("http://foo.com:8080/bar/baz?id=123", uri.toString()); + } + + @Test + public void baseUriWithPathOverride() throws Exception { + DefaultUriBuilderFactory factory = new DefaultUriBuilderFactory("http://foo.com/bar"); + URI uri = factory.uriString("").replacePath("/baz").build(); + assertEquals("http://foo.com/baz", uri.toString()); + } + + @Test + public void defaultUriVars() throws Exception { + DefaultUriBuilderFactory factory = new DefaultUriBuilderFactory("http://{host}/bar"); + factory.setDefaultUriVariables(Collections.singletonMap("host", "foo.com")); + URI uri = factory.uriString("/{id}").build(Collections.singletonMap("id", "123")); + assertEquals("http://foo.com/bar/123", uri.toString()); + } + + @Test + public void defaultUriVarsWithOverride() throws Exception { + DefaultUriBuilderFactory factory = new DefaultUriBuilderFactory("http://{host}/bar"); + factory.setDefaultUriVariables(Collections.singletonMap("host", "spring.io")); + URI uri = factory.uriString("/baz").build(Collections.singletonMap("host", "docs.spring.io")); + assertEquals("http://docs.spring.io/bar/baz", uri.toString()); + } + + @Test + public void defaultUriVarsWithEmptyVarArg() throws Exception { + DefaultUriBuilderFactory factory = new DefaultUriBuilderFactory("http://{host}/bar"); + factory.setDefaultUriVariables(Collections.singletonMap("host", "foo.com")); + URI uri = factory.uriString("/baz").build(); + assertEquals("Expected delegation to build(Map) method", "http://foo.com/bar/baz", uri.toString()); + } + + @Test + public void encodingValuesOnly() throws Exception { + DefaultUriBuilderFactory factory = new DefaultUriBuilderFactory(); + factory.setEncodingMode(EncodingMode.VALUES_ONLY); + UriBuilder uriBuilder = factory.uriString("/foo/a%2Fb/{id}"); + + String id = "c/d"; + String expected = "/foo/a%2Fb/c%2Fd"; + + assertEquals(expected, uriBuilder.build(id).toString()); + assertEquals(expected, uriBuilder.build(Collections.singletonMap("id", id)).toString()); + } + + @Test + public void encodingNone() throws Exception { + DefaultUriBuilderFactory factory = new DefaultUriBuilderFactory(); + factory.setEncodingMode(EncodingMode.NONE); + UriBuilder uriBuilder = factory.uriString("/foo/a%2Fb/{id}"); + + String id = "c%2Fd"; + String expected = "/foo/a%2Fb/c%2Fd"; + + assertEquals(expected, uriBuilder.build(id).toString()); + assertEquals(expected, uriBuilder.build(Collections.singletonMap("id", id)).toString()); + } + + @Test + public void initialPathSplitIntoPathSegments() throws Exception { + DefaultUriBuilderFactory factory = new DefaultUriBuilderFactory("/foo/{bar}"); + URI uri = factory.uriString("/baz/{id}").build("a/b", "c/d"); + assertEquals("/foo/a%2Fb/baz/c%2Fd", uri.toString()); + } + +} From f6ec35394f09ddb4ba7cc48bba2189ea74e6b8a5 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Thu, 26 Jan 2017 15:20:56 -0500 Subject: [PATCH 2/4] Introduce WebClientOperations Issue: SPR-15124 --- .../client/DefaultWebClientOperations.java | 227 ++++++++++++++ .../DefaultWebClientOperationsBuilder.java | 51 +++ .../function/client/WebClientOperations.java | 295 ++++++++++++++++++ .../client/WebClientIntegrationTests.java | 149 ++++----- 4 files changed, 641 insertions(+), 81 deletions(-) create mode 100644 spring-web-reactive/src/main/java/org/springframework/web/reactive/function/client/DefaultWebClientOperations.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/web/reactive/function/client/DefaultWebClientOperationsBuilder.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/web/reactive/function/client/WebClientOperations.java diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/function/client/DefaultWebClientOperations.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/function/client/DefaultWebClientOperations.java new file mode 100644 index 00000000000..bc2ea689f81 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/function/client/DefaultWebClientOperations.java @@ -0,0 +1,227 @@ +/* + * 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. + * 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.reactive.function.client; + +import java.net.URI; +import java.nio.charset.Charset; +import java.time.ZonedDateTime; +import java.util.function.Function; + +import org.jetbrains.annotations.NotNull; +import org.reactivestreams.Publisher; +import reactor.core.publisher.Mono; + +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.util.MultiValueMap; +import org.springframework.web.reactive.function.BodyInserter; +import org.springframework.web.util.DefaultUriBuilderFactory; +import org.springframework.web.util.UriBuilderFactory; + + +/** + * Default implementation of {@link WebClientOperations}. + * + * @author Rossen Stoyanchev + * @since 5.0 + */ +class DefaultWebClientOperations implements WebClientOperations { + + private final WebClient webClient; + + private final UriBuilderFactory uriBuilderFactory; + + + DefaultWebClientOperations(WebClient webClient, UriBuilderFactory factory) { + this.webClient = webClient; + this.uriBuilderFactory = (factory != null ? factory : new DefaultUriBuilderFactory()); + } + + + private WebClient getWebClient() { + return this.webClient; + } + + private UriBuilderFactory getUriBuilderFactory() { + return this.uriBuilderFactory; + } + + + @Override + public UriSpec get() { + return method(HttpMethod.GET); + } + + @Override + public UriSpec head() { + return method(HttpMethod.HEAD); + } + + @Override + public UriSpec post() { + return method(HttpMethod.POST); + } + + @Override + public UriSpec put() { + return method(HttpMethod.PUT); + } + + @Override + public UriSpec patch() { + return method(HttpMethod.PATCH); + } + + @Override + public UriSpec delete() { + return method(HttpMethod.DELETE); + } + + @Override + public UriSpec options() { + return method(HttpMethod.OPTIONS); + } + + @NotNull + private UriSpec method(HttpMethod httpMethod) { + return new DefaultUriSpec(httpMethod); + } + + + @Override + public WebClientOperations filter(ExchangeFilterFunction filterFunction) { + WebClient filteredWebClient = this.webClient.filter(filterFunction); + return new DefaultWebClientOperations(filteredWebClient, this.uriBuilderFactory); + } + + + private class DefaultUriSpec implements UriSpec { + + private final HttpMethod httpMethod; + + + DefaultUriSpec(HttpMethod httpMethod) { + this.httpMethod = httpMethod; + } + + @Override + public HeaderSpec uri(URI uri) { + return new DefaultHeaderSpec(ClientRequest.method(this.httpMethod, uri)); + } + + @Override + public HeaderSpec uri(String uriTemplate, Object... uriVariables) { + return uri(getUriBuilderFactory().expand(uriTemplate)); + } + + @Override + public HeaderSpec uri(Function uriFunction) { + return uri(uriFunction.apply(getUriBuilderFactory())); + } + } + + private class DefaultHeaderSpec implements HeaderSpec { + + private ClientRequest.BodyBuilder requestBuilder; + + + DefaultHeaderSpec(ClientRequest.BodyBuilder requestBuilder) { + this.requestBuilder = requestBuilder; + } + + + @Override + public DefaultHeaderSpec header(String headerName, String... headerValues) { + this.requestBuilder.header(headerName, headerValues); + return this; + } + + @Override + public DefaultHeaderSpec headers(HttpHeaders headers) { + this.requestBuilder.headers(headers); + return this; + } + + @Override + public DefaultHeaderSpec accept(MediaType... acceptableMediaTypes) { + this.requestBuilder.accept(acceptableMediaTypes); + return this; + } + + @Override + public DefaultHeaderSpec acceptCharset(Charset... acceptableCharsets) { + this.requestBuilder.acceptCharset(acceptableCharsets); + return this; + } + + @Override + public DefaultHeaderSpec contentType(MediaType contentType) { + this.requestBuilder.contentType(contentType); + return this; + } + + @Override + public DefaultHeaderSpec contentLength(long contentLength) { + this.requestBuilder.contentLength(contentLength); + return this; + } + + @Override + public DefaultHeaderSpec cookie(String name, String value) { + this.requestBuilder.cookie(name, value); + return this; + } + + @Override + public DefaultHeaderSpec cookies(MultiValueMap cookies) { + this.requestBuilder.cookies(cookies); + return this; + } + + @Override + public DefaultHeaderSpec ifModifiedSince(ZonedDateTime ifModifiedSince) { + this.requestBuilder.ifModifiedSince(ifModifiedSince); + return this; + } + + @Override + public DefaultHeaderSpec ifNoneMatch(String... ifNoneMatches) { + this.requestBuilder.ifNoneMatch(ifNoneMatches); + return this; + } + + @Override + public Mono exchange() { + ClientRequest request = this.requestBuilder.build(); + return getWebClient().exchange(request); + } + + @Override + public Mono exchange(BodyInserter inserter) { + ClientRequest request = this.requestBuilder.body(inserter); + return getWebClient().exchange(request); + } + + @Override + public > Mono exchange(S publisher, Class elementClass) { + ClientRequest request = this.requestBuilder.body(publisher, elementClass); + return getWebClient().exchange(request); + } + } + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/function/client/DefaultWebClientOperationsBuilder.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/function/client/DefaultWebClientOperationsBuilder.java new file mode 100644 index 00000000000..f8c7749837d --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/function/client/DefaultWebClientOperationsBuilder.java @@ -0,0 +1,51 @@ +/* + * 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. + * 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.reactive.function.client; + +import org.springframework.util.Assert; +import org.springframework.web.util.UriBuilderFactory; + +/** + * Default implementation of {@link WebClientOperations.Builder}. + * + * @author Rossen Stoyanchev + * @since 5.0 + */ +class DefaultWebClientOperationsBuilder implements WebClientOperations.Builder { + + private final WebClient webClient; + + private UriBuilderFactory uriBuilderFactory; + + + public DefaultWebClientOperationsBuilder(WebClient webClient) { + Assert.notNull(webClient, "WebClient is required"); + this.webClient = webClient; + } + + + @Override + public WebClientOperations.Builder uriBuilderFactory(UriBuilderFactory uriBuilderFactory) { + this.uriBuilderFactory = uriBuilderFactory; + return this; + } + + @Override + public WebClientOperations build() { + return new DefaultWebClientOperations(this.webClient, this.uriBuilderFactory); + } + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/function/client/WebClientOperations.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/function/client/WebClientOperations.java new file mode 100644 index 00000000000..7200f8fb8c3 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/function/client/WebClientOperations.java @@ -0,0 +1,295 @@ +/* + * 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. + * 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.reactive.function.client; + +import java.net.URI; +import java.nio.charset.Charset; +import java.time.ZonedDateTime; +import java.util.function.Function; + +import org.reactivestreams.Publisher; +import reactor.core.publisher.Mono; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.http.client.reactive.ClientHttpRequest; +import org.springframework.util.MultiValueMap; +import org.springframework.web.reactive.function.BodyInserter; +import org.springframework.web.util.UriBuilderFactory; + +/** + * The main class for performing requests through a WebClient. + * + *

+ *
+ * // Create WebClient (application-wide)
+ *
+ * ClientHttpConnector connector = new ReactorClientHttpConnector();
+ * WebClient webClient = WebClient.create(connector);
+ *
+ * // Create WebClientOperations (per base URI)
+ *
+ * String baseUri = "http://abc.com";
+ * UriBuilderFactory factory = new DefaultUriBuilderFactory(baseUri);
+ * WebClientOperations operations = WebClientOperations.create(webClient, factory);
+ *
+ * // Perform requests...
+ *
+ * Mono result = operations.get()
+ *     .uri("/foo")
+ *     .exchange()
+ *     .then(response -> response.bodyToMono(String.class));
+ * 
+ * + * @author Rossen Stoyanchev + * @since 5.0 + */ +public interface WebClientOperations { + + /** + * Prepare an HTTP GET request. + * @return a spec for specifying the target URL + */ + UriSpec get(); + + /** + * Prepare an HTTP HEAD request. + * @return a spec for specifying the target URL + */ + UriSpec head(); + + /** + * Prepare an HTTP POST request. + * @return a spec for specifying the target URL + */ + UriSpec post(); + + /** + * Prepare an HTTP PUT request. + * @return a spec for specifying the target URL + */ + UriSpec put(); + + /** + * Prepare an HTTP PATCH request. + * @return a spec for specifying the target URL + */ + UriSpec patch(); + + /** + * Prepare an HTTP DELETE request. + * @return a spec for specifying the target URL + */ + UriSpec delete(); + + /** + * Prepare an HTTP OPTIONS request. + * @return a spec for specifying the target URL + */ + UriSpec options(); + + + /** + * Filter the client with the given {@code ExchangeFilterFunction}. + * @param filterFunction the filter to apply to this client + * @return the filtered client + * @see ExchangeFilterFunction#apply(ExchangeFunction) + */ + WebClientOperations filter(ExchangeFilterFunction filterFunction); + + + // Static, factory methods + + /** + * Create {@link WebClientOperations} that wraps the given {@link WebClient}. + * @param webClient the underlying client to use + */ + static WebClientOperations create(WebClient webClient) { + return builder(webClient).build(); + } + + /** + * Create {@link WebClientOperations} with a builder for additional + * configuration options. + * @param webClient the underlying client to use + */ + static WebClientOperations.Builder builder(WebClient webClient) { + return new DefaultWebClientOperationsBuilder(webClient); + } + + + /** + * A mutable builder for a {@link WebClientOperations}. + */ + interface Builder { + + /** + * Configure a {@code UriBuilderFactory} for use with this client for + * example to define a common "base" URI. + * @param uriBuilderFactory the URI builder factory + */ + Builder uriBuilderFactory(UriBuilderFactory uriBuilderFactory); + + /** + * Builder the {@link WebClient} instance. + */ + WebClientOperations build(); + + } + + + /** + * Contract for specifying the URI for a request. + */ + interface UriSpec { + + /** + * Specify the URI using an absolute, fully constructed {@link URI}. + */ + HeaderSpec uri(URI uri); + + /** + * Specify the URI for the request using a URI template and URI variables. + * If a {@link UriBuilderFactory} was configured for the client (e.g. + * with a base URI) it will be used to expand the URI template. + * @see Builder#uriBuilderFactory(UriBuilderFactory) + */ + HeaderSpec uri(String uri, Object... uriVariables); + + /** + * Build the URI for the request using the {@link UriBuilderFactory} + * configured for this client. + * @see Builder#uriBuilderFactory(UriBuilderFactory) + */ + HeaderSpec uri(Function uriFunction); + + } + + /** + * Contract for specifying request headers leading up to the exchange. + */ + interface HeaderSpec { + + /** + * Set the list of acceptable {@linkplain MediaType media types}, as + * specified by the {@code Accept} header. + * @param acceptableMediaTypes the acceptable media types + * @return this builder + */ + HeaderSpec accept(MediaType... acceptableMediaTypes); + + /** + * Set the list of acceptable {@linkplain Charset charsets}, as specified + * by the {@code Accept-Charset} header. + * @param acceptableCharsets the acceptable charsets + * @return this builder + */ + HeaderSpec acceptCharset(Charset... acceptableCharsets); + + /** + * 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) + */ + HeaderSpec 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) + */ + HeaderSpec contentType(MediaType contentType); + + /** + * Add a cookie with the given name and value. + * @param name the cookie name + * @param value the cookie value + * @return this builder + */ + HeaderSpec cookie(String name, String value); + + /** + * Copy the given cookies into the entity's cookies map. + * + * @param cookies the existing cookies to copy from + * @return this builder + */ + HeaderSpec cookies(MultiValueMap cookies); + + /** + * Set the value of the {@code If-Modified-Since} header. + *

The date should be specified as the number of milliseconds since + * January 1, 1970 GMT. + * @param ifModifiedSince the new value of the header + * @return this builder + */ + HeaderSpec ifModifiedSince(ZonedDateTime ifModifiedSince); + + /** + * Set the values of the {@code If-None-Match} header. + * @param ifNoneMatches the new value of the header + * @return this builder + */ + HeaderSpec ifNoneMatch(String... ifNoneMatches); + + /** + * Add the given, single header value under the given name. + * @param headerName the header name + * @param headerValues the header value(s) + * @return this builder + */ + HeaderSpec header(String headerName, String... headerValues); + + /** + * Copy the given headers into the entity's headers map. + * @param headers the existing headers to copy from + * @return this builder + */ + HeaderSpec headers(HttpHeaders headers); + + /** + * Perform the request without a request body. + * @return a {@code Mono} with the response + */ + Mono exchange(); + + /** + * Set the body of the request to the given {@code BodyInserter} and + * perform the request. + * @param inserter the {@code BodyInserter} that writes to the request + * @param the type contained in the body + * @return a {@code Mono} with the response + */ + Mono exchange(BodyInserter inserter); + + /** + * Set the body of the request to the given {@code Publisher} and + * perform the request. + * @param publisher the {@code Publisher} to write to the request + * @param elementClass the class of elements contained in the publisher + * @param the type of the elements contained in the publisher + * @param the type of the {@code Publisher} + * @return a {@code Mono} with the response + */ + > Mono exchange(S publisher, Class elementClass); + + } + +} diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/function/client/WebClientIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/function/client/WebClientIntegrationTests.java index 8cfc60dab99..9d8a2c0b924 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/function/client/WebClientIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/function/client/WebClientIntegrationTests.java @@ -38,37 +38,53 @@ import org.springframework.http.client.reactive.ReactorClientHttpConnector; import org.springframework.http.codec.Pojo; import org.springframework.web.reactive.function.BodyExtractors; import org.springframework.web.reactive.function.BodyInserters; +import org.springframework.web.util.DefaultUriBuilderFactory; +import org.springframework.web.util.UriBuilderFactory; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertThat; import static org.springframework.web.reactive.function.BodyExtractors.toFlux; import static org.springframework.web.reactive.function.BodyExtractors.toMono; +import static org.springframework.web.reactive.function.BodyInserters.fromObject; /** - * {@link WebClient} integration tests with the {@code Flux} and {@code Mono} API. + * Integration tests using a {@link WebClient} through {@link WebClientOperations}. * * @author Brian Clozel + * @author Rossen Stoyanchev */ public class WebClientIntegrationTests { private MockWebServer server; - private WebClient webClient; + private WebClientOperations operations; + @Before public void setup() { this.server = new MockWebServer(); - this.webClient = WebClient.create(new ReactorClientHttpConnector()); + + WebClient webClient = WebClient.create(new ReactorClientHttpConnector()); + UriBuilderFactory uriBuilderFactory = new DefaultUriBuilderFactory(this.server.url("/").toString()); + + this.operations = WebClientOperations.builder(webClient) + .uriBuilderFactory(uriBuilderFactory) + .build(); + } + + @After + public void tearDown() throws Exception { + this.server.shutdown(); } + @Test public void headers() throws Exception { - HttpUrl baseUrl = server.url("/greeting?name=Spring"); this.server.enqueue(new MockResponse().setHeader("Content-Type", "text/plain").setBody("Hello Spring!")); - ClientRequest request = ClientRequest.GET(baseUrl.toString()).build(); - Mono result = this.webClient - .exchange(request) + Mono result = this.operations.get() + .uri("/greeting?name=Spring") + .exchange() .map(response -> response.headers().asHttpHeaders()); StepVerifier.create(result) @@ -88,16 +104,13 @@ public class WebClientIntegrationTests { @Test public void plainText() throws Exception { - HttpUrl baseUrl = server.url("/greeting?name=Spring"); this.server.enqueue(new MockResponse().setBody("Hello Spring!")); - ClientRequest request = ClientRequest.GET(baseUrl.toString()) + Mono result = this.operations.get() + .uri("/greeting?name=Spring") .header("X-Test-Header", "testvalue") - .build(); - - Mono result = this.webClient - .exchange(request) - .then(response -> response.body(toMono(String.class))); + .exchange() + .then(response -> response.bodyToMono(String.class)); StepVerifier.create(result) .expectNext("Hello Spring!") @@ -113,18 +126,15 @@ public class WebClientIntegrationTests { @Test public void jsonString() throws Exception { - HttpUrl baseUrl = server.url("/json"); String content = "{\"bar\":\"barbar\",\"foo\":\"foofoo\"}"; this.server.enqueue(new MockResponse().setHeader("Content-Type", "application/json") .setBody(content)); - ClientRequest request = ClientRequest.GET(baseUrl.toString()) + Mono result = this.operations.get() + .uri("/json") .accept(MediaType.APPLICATION_JSON) - .build(); - - Mono result = this.webClient - .exchange(request) - .then(response -> response.body(toMono(String.class))); + .exchange() + .then(response -> response.bodyToMono(String.class)); StepVerifier.create(result) .expectNext(content) @@ -139,17 +149,14 @@ public class WebClientIntegrationTests { @Test public void jsonPojoMono() throws Exception { - HttpUrl baseUrl = server.url("/pojo"); this.server.enqueue(new MockResponse().setHeader("Content-Type", "application/json") .setBody("{\"bar\":\"barbar\",\"foo\":\"foofoo\"}")); - ClientRequest request = ClientRequest.GET(baseUrl.toString()) + Mono result = this.operations.get() + .uri("/pojo") .accept(MediaType.APPLICATION_JSON) - .build(); - - Mono result = this.webClient - .exchange(request) - .then(response -> response.body(toMono(Pojo.class))); + .exchange() + .then(response -> response.bodyToMono(Pojo.class)); StepVerifier.create(result) .consumeNextWith(p -> assertEquals("barbar", p.getBar())) @@ -172,9 +179,11 @@ public class WebClientIntegrationTests { .accept(MediaType.APPLICATION_JSON) .build(); - Flux result = this.webClient - .exchange(request) - .flatMap(response -> response.body(toFlux(Pojo.class))); + Flux result = this.operations.get() + .uri("/pojos") + .accept(MediaType.APPLICATION_JSON) + .exchange() + .flatMap(response -> response.bodyToFlux(Pojo.class)); StepVerifier.create(result) .consumeNextWith(p -> assertThat(p.getBar(), Matchers.is("bar1"))) @@ -195,15 +204,12 @@ public class WebClientIntegrationTests { .setHeader("Content-Type", "application/json") .setBody("{\"bar\":\"BARBAR\",\"foo\":\"FOOFOO\"}")); - Pojo spring = new Pojo("foofoo", "barbar"); - ClientRequest request = ClientRequest.POST(baseUrl.toString()) + Mono result = this.operations.post() + .uri("/pojo/capitalize") .accept(MediaType.APPLICATION_JSON) .contentType(MediaType.APPLICATION_JSON) - .body(BodyInserters.fromObject(spring)); - - Mono result = this.webClient - .exchange(request) - .then(response -> response.body(BodyExtractors.toMono(Pojo.class))); + .exchange(fromObject(new Pojo("foofoo", "barbar"))) + .then(response -> response.bodyToMono(Pojo.class)); StepVerifier.create(result) .consumeNextWith(p -> assertEquals("BARBAR", p.getBar())) @@ -221,17 +227,14 @@ public class WebClientIntegrationTests { @Test public void cookies() throws Exception { - HttpUrl baseUrl = server.url("/test"); this.server.enqueue(new MockResponse() .setHeader("Content-Type", "text/plain").setBody("test")); - ClientRequest request = ClientRequest.GET(baseUrl.toString()) + Mono result = this.operations.get() + .uri("/test") .cookie("testkey", "testvalue") - .build(); - - Mono result = this.webClient - .exchange(request) - .then(response -> response.body(toMono(String.class))); + .exchange() + .then(response -> response.bodyToMono(String.class)); StepVerifier.create(result) .expectNext("test") @@ -246,19 +249,13 @@ public class WebClientIntegrationTests { @Test public void notFound() throws Exception { - HttpUrl baseUrl = server.url("/greeting?name=Spring"); this.server.enqueue(new MockResponse().setResponseCode(404) .setHeader("Content-Type", "text/plain").setBody("Not Found")); - ClientRequest request = ClientRequest.GET(baseUrl.toString()).build(); - - Mono result = this.webClient - .exchange(request); + Mono result = this.operations.get().uri("/greeting?name=Spring").exchange(); StepVerifier.create(result) - .consumeNextWith(response -> { - assertEquals(HttpStatus.NOT_FOUND, response.statusCode()); - }) + .consumeNextWith(response -> assertEquals(HttpStatus.NOT_FOUND, response.statusCode())) .expectComplete() .verify(Duration.ofSeconds(3)); @@ -270,21 +267,18 @@ public class WebClientIntegrationTests { @Test public void buildFilter() throws Exception { - HttpUrl baseUrl = server.url("/greeting?name=Spring"); this.server.enqueue(new MockResponse().setHeader("Content-Type", "text/plain").setBody("Hello Spring!")); - ExchangeFilterFunction filter = (request, next) -> { - ClientRequest filteredRequest = ClientRequest.from(request) - .header("foo", "bar").build(); - return next.exchange(filteredRequest); - }; - WebClient filteredClient = WebClient.builder(new ReactorClientHttpConnector()) - .filter(filter).build(); - - ClientRequest request = ClientRequest.GET(baseUrl.toString()).build(); + WebClientOperations filteredClient = this.operations.filter( + (request, next) -> { + ClientRequest filteredRequest = ClientRequest.from(request).header("foo", "bar").build(); + return next.exchange(filteredRequest); + }); - Mono result = filteredClient.exchange(request) - .then(response -> response.body(toMono(String.class))); + Mono result = filteredClient.get() + .uri("/greeting?name=Spring") + .exchange() + .then(response -> response.bodyToMono(String.class)); StepVerifier.create(result) .expectNext("Hello Spring!") @@ -299,21 +293,18 @@ public class WebClientIntegrationTests { @Test public void filter() throws Exception { - HttpUrl baseUrl = server.url("/greeting?name=Spring"); this.server.enqueue(new MockResponse().setHeader("Content-Type", "text/plain").setBody("Hello Spring!")); - ExchangeFilterFunction filter = (request, next) -> { - ClientRequest filteredRequest = ClientRequest.from(request) - .header("foo", "bar").build(); - return next.exchange(filteredRequest); - }; - WebClient client = WebClient.create(new ReactorClientHttpConnector()); - WebClient filteredClient = client.filter(filter); + WebClientOperations filteredClient = this.operations.filter( + (request, next) -> { + ClientRequest filteredRequest = ClientRequest.from(request).header("foo", "bar").build(); + return next.exchange(filteredRequest); + }); - ClientRequest request = ClientRequest.GET(baseUrl.toString()).build(); - - Mono result = filteredClient.exchange(request) - .then(response -> response.body(toMono(String.class))); + Mono result = filteredClient.get() + .uri("/greeting?name=Spring") + .exchange() + .then(response -> response.bodyToMono(String.class)); StepVerifier.create(result) .expectNext("Hello Spring!") @@ -326,8 +317,4 @@ public class WebClientIntegrationTests { } - @After - public void tearDown() throws Exception { - this.server.shutdown(); - } } \ No newline at end of file From 8edc68095751246503e8b7cd282a916ee6c29d0f Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Fri, 27 Jan 2017 15:59:03 -0500 Subject: [PATCH 3/4] Scale back static factory methods in ClientRequest Following on the introduction of WebClientOperations in the last commit this commit removes the HTTP method specific factory methods and also scales back the builder options in ClientRequest. ClientRequest is now expected to be used mainly from an ExchangeFilterFunction which may modify the ClientRequest using the from(ClientRequest) entry point rather creating from scratch. Issue: SPR-15124 --- .../function/client/ClientRequest.java | 177 ++---------------- .../client/DefaultClientRequestBuilder.java | 56 +----- .../client/DefaultWebClientOperations.java | 37 ++-- .../reactive/FlushingIntegrationTests.java | 29 +-- .../DefaultClientRequestBuilderTests.java | 113 ++--------- .../client/ExchangeFilterFunctionsTests.java | 10 +- .../client/WebClientIntegrationTests.java | 11 -- .../SseHandlerFunctionIntegrationTests.java | 62 +++--- .../annotation/SseIntegrationTests.java | 75 ++++---- 9 files changed, 142 insertions(+), 428 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/function/client/ClientRequest.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/function/client/ClientRequest.java index b0dd1186170..608d17084a7 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/function/client/ClientRequest.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/function/client/ClientRequest.java @@ -17,27 +17,25 @@ package org.springframework.web.reactive.function.client; import java.net.URI; -import java.nio.charset.Charset; -import java.time.ZonedDateTime; import org.reactivestreams.Publisher; import reactor.core.publisher.Mono; 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.util.Assert; import org.springframework.util.MultiValueMap; import org.springframework.web.reactive.function.BodyInserter; -import org.springframework.web.util.DefaultUriTemplateHandler; -import org.springframework.web.util.UriTemplateHandler; /** - * Represents a typed, immutable, client-side HTTP request, as executed by the {@link WebClient}. - * Instances of this interface are created via static builder methods: - * {@link #method(HttpMethod, String, Object...)}, {@link #GET(String, Object...)}, etc. + * Represents a typed, immutable, client-side HTTP request, as executed by the + * {@link WebClient}. Instances of this interface can be created via static + * builder methods in this class. * + *

Note that applications are more likely to perform requests through + * {@link WebClientOperations} rather than using this directly. + * : * @param the type of the body that this request contains * @author Brian Clozel * @author Arjen Poutsma @@ -45,8 +43,6 @@ import org.springframework.web.util.UriTemplateHandler; */ public interface ClientRequest { - // Instance methods - /** * Return the HTTP method. */ @@ -81,6 +77,7 @@ public interface ClientRequest { */ Mono writeTo(ClientHttpRequest request, WebClientStrategies strategies); + // Static builder methods /** @@ -89,7 +86,7 @@ public interface ClientRequest { * @param other the request to copy the method, URI, headers, and cookies from * @return the created builder */ - static BodyBuilder from(ClientRequest other) { + static Builder from(ClientRequest other) { Assert.notNull(other, "'other' must not be null"); return new DefaultClientRequestBuilder(other.method(), other.url()) .headers(other.headers()) @@ -102,100 +99,15 @@ public interface ClientRequest { * @param url the URL * @return the created builder */ - static BodyBuilder method(HttpMethod method, URI url) { - return new DefaultClientRequestBuilder(method, url); - } - - /** - * Create a builder with the given method and url template. - * @param method the HTTP method (GET, POST, etc) - * @param urlTemplate the URL template - * @param uriVariables optional variables to expand the template - * @return the created builder - */ - static BodyBuilder method(HttpMethod method, String urlTemplate, Object... uriVariables) { - UriTemplateHandler templateHandler = new DefaultUriTemplateHandler(); - URI url = templateHandler.expand(urlTemplate, uriVariables); + static Builder method(HttpMethod method, URI url) { return new DefaultClientRequestBuilder(method, url); } - /** - * Create an HTTP GET builder with the given url template. - * @param urlTemplate the URL template - * @param uriVariables optional variables to expand the template - * @return the created builder - */ - static HeadersBuilder GET(String urlTemplate, Object... uriVariables) { - return method(HttpMethod.GET, urlTemplate, uriVariables); - } - - /** - * Create an HTTP HEAD builder with the given url template. - * @param urlTemplate the URL template - * @param uriVariables optional variables to expand the template - * @return the created builder - */ - static HeadersBuilder HEAD(String urlTemplate, Object... uriVariables) { - return method(HttpMethod.HEAD, urlTemplate, uriVariables); - } - - /** - * Create an HTTP POST builder with the given url template. - * @param urlTemplate the URL template - * @param uriVariables optional variables to expand the template - * @return the created builder - */ - static BodyBuilder POST(String urlTemplate, Object... uriVariables) { - return method(HttpMethod.POST, urlTemplate, uriVariables); - } - - /** - * Create an HTTP PUT builder with the given url template. - * @param urlTemplate the URL template - * @param uriVariables optional variables to expand the template - * @return the created builder - */ - static BodyBuilder PUT(String urlTemplate, Object... uriVariables) { - return method(HttpMethod.PUT, urlTemplate, uriVariables); - } - - /** - * Create an HTTP PATCH builder with the given url template. - * @param urlTemplate the URL template - * @param uriVariables optional variables to expand the template - * @return the created builder - */ - static BodyBuilder PATCH(String urlTemplate, Object... uriVariables) { - return method(HttpMethod.PATCH, urlTemplate, uriVariables); - } - - /** - * Create an HTTP DELETE builder with the given url template. - * @param urlTemplate the URL template - * @param uriVariables optional variables to expand the template - * @return the created builder - */ - static HeadersBuilder DELETE(String urlTemplate, Object... uriVariables) { - return method(HttpMethod.DELETE, urlTemplate, uriVariables); - } - - /** - * Creates an HTTP OPTIONS builder with the given url template. - * @param urlTemplate the URL template - * @param uriVariables optional variables to expand the template - * @return the created builder - */ - static HeadersBuilder OPTIONS(String urlTemplate, Object... uriVariables) { - return method(HttpMethod.OPTIONS, urlTemplate, uriVariables); - } - /** - * Defines a builder that adds headers to the request. - * - * @param the builder subclass + * Defines a builder for a request. */ - interface HeadersBuilder> { + interface Builder { /** * Add the given, single header value under the given name. @@ -204,7 +116,7 @@ public interface ClientRequest { * @return this builder * @see HttpHeaders#add(String, String) */ - B header(String headerName, String... headerValues); + Builder header(String headerName, String... headerValues); /** * Copy the given headers into the entity's headers map. @@ -212,39 +124,7 @@ public interface ClientRequest { * @param headers the existing HttpHeaders to copy from * @return this builder */ - B headers(HttpHeaders headers); - - /** - * Set the list of acceptable {@linkplain MediaType media types}, as - * specified by the {@code Accept} header. - * @param acceptableMediaTypes the acceptable media types - * @return this builder - */ - 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 - * @return this builder - */ - B acceptCharset(Charset... acceptableCharsets); - - /** - * Set the value of the {@code If-Modified-Since} header. - *

The date should be specified as the number of milliseconds since - * January 1, 1970 GMT. - * @param ifModifiedSince the new value of the header - * @return this builder - */ - B ifModifiedSince(ZonedDateTime ifModifiedSince); - - /** - * Set the values of the {@code If-None-Match} header. - * @param ifNoneMatches the new value of the header - * @return this builder - */ - B ifNoneMatch(String... ifNoneMatches); + Builder headers(HttpHeaders headers); /** * Add a cookie with the given name and value. @@ -252,7 +132,7 @@ public interface ClientRequest { * @param value the cookie value * @return this builder */ - B cookie(String name, String value); + Builder cookie(String name, String value); /** * Copy the given cookies into the entity's cookies map. @@ -260,39 +140,13 @@ public interface ClientRequest { * @param cookies the existing cookies to copy from * @return this builder */ - B cookies(MultiValueMap cookies); + Builder cookies(MultiValueMap cookies); /** * Builds the request entity with no body. * @return the request entity */ ClientRequest build(); - } - - - /** - * Defines a builder that adds a body to the request entity. - */ - interface BodyBuilder extends HeadersBuilder { - - - /** - * 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 to the given {@code BodyInserter} and return it. @@ -314,5 +168,4 @@ public interface ClientRequest { } - } diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/function/client/DefaultClientRequestBuilder.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/function/client/DefaultClientRequestBuilder.java index 0af5c1b2c20..e0f65d6db8d 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/function/client/DefaultClientRequestBuilder.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/function/client/DefaultClientRequestBuilder.java @@ -17,11 +17,6 @@ package org.springframework.web.reactive.function.client; import java.net.URI; -import java.nio.charset.Charset; -import java.time.ZoneId; -import java.time.ZonedDateTime; -import java.time.format.DateTimeFormatter; -import java.util.Arrays; import java.util.Collections; import java.util.Map; import java.util.function.Supplier; @@ -33,7 +28,6 @@ import reactor.core.publisher.Mono; 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.codec.HttpMessageWriter; import org.springframework.util.Assert; @@ -44,12 +38,12 @@ import org.springframework.web.reactive.function.BodyInserter; import org.springframework.web.reactive.function.BodyInserters; /** - * Default implementation of {@link ClientRequest.BodyBuilder}. + * Default implementation of {@link ClientRequest.Builder}. * * @author Arjen Poutsma * @since 5.0 */ -class DefaultClientRequestBuilder implements ClientRequest.BodyBuilder { +class DefaultClientRequestBuilder implements ClientRequest.Builder { private final HttpMethod method; @@ -66,7 +60,7 @@ class DefaultClientRequestBuilder implements ClientRequest.BodyBuilder { } @Override - public ClientRequest.BodyBuilder header(String headerName, String... headerValues) { + public ClientRequest.Builder header(String headerName, String... headerValues) { for (String headerValue : headerValues) { this.headers.add(headerName, headerValue); } @@ -74,7 +68,7 @@ class DefaultClientRequestBuilder implements ClientRequest.BodyBuilder { } @Override - public ClientRequest.BodyBuilder headers(HttpHeaders headers) { + public ClientRequest.Builder headers(HttpHeaders headers) { if (headers != null) { this.headers.putAll(headers); } @@ -82,39 +76,13 @@ class DefaultClientRequestBuilder implements ClientRequest.BodyBuilder { } @Override - public ClientRequest.BodyBuilder accept(MediaType... acceptableMediaTypes) { - this.headers.setAccept(Arrays.asList(acceptableMediaTypes)); - return this; - } - - @Override - public ClientRequest.BodyBuilder acceptCharset(Charset... acceptableCharsets) { - this.headers.setAcceptCharset(Arrays.asList(acceptableCharsets)); - return this; - } - - @Override - public ClientRequest.BodyBuilder ifModifiedSince(ZonedDateTime ifModifiedSince) { - ZonedDateTime gmt = ifModifiedSince.withZoneSameInstant(ZoneId.of("GMT")); - String headerValue = DateTimeFormatter.RFC_1123_DATE_TIME.format(gmt); - this.headers.set(HttpHeaders.IF_MODIFIED_SINCE, headerValue); - return this; - } - - @Override - public ClientRequest.BodyBuilder ifNoneMatch(String... ifNoneMatches) { - this.headers.setIfNoneMatch(Arrays.asList(ifNoneMatches)); - return this; - } - - @Override - public ClientRequest.BodyBuilder cookie(String name, String value) { + public ClientRequest.Builder cookie(String name, String value) { this.cookies.add(name, value); return this; } @Override - public ClientRequest.BodyBuilder cookies(MultiValueMap cookies) { + public ClientRequest.Builder cookies(MultiValueMap cookies) { if (cookies != null) { this.cookies.putAll(cookies); } @@ -126,18 +94,6 @@ class DefaultClientRequestBuilder implements ClientRequest.BodyBuilder { return body(BodyInserters.empty()); } - @Override - public ClientRequest.BodyBuilder contentLength(long contentLength) { - this.headers.setContentLength(contentLength); - return this; - } - - @Override - public ClientRequest.BodyBuilder contentType(MediaType contentType) { - this.headers.setContentType(contentType); - return this; - } - @Override public ClientRequest body(BodyInserter inserter) { Assert.notNull(inserter, "'inserter' must not be null"); diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/function/client/DefaultWebClientOperations.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/function/client/DefaultWebClientOperations.java index bc2ea689f81..0c06efe9dc6 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/function/client/DefaultWebClientOperations.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/function/client/DefaultWebClientOperations.java @@ -17,7 +17,10 @@ package org.springframework.web.reactive.function.client; import java.net.URI; import java.nio.charset.Charset; +import java.time.ZoneId; import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.util.Arrays; import java.util.function.Function; import org.jetbrains.annotations.NotNull; @@ -137,47 +140,53 @@ class DefaultWebClientOperations implements WebClientOperations { private class DefaultHeaderSpec implements HeaderSpec { - private ClientRequest.BodyBuilder requestBuilder; + private final ClientRequest.Builder requestBuilder; + private final HttpHeaders headers = new HttpHeaders(); - DefaultHeaderSpec(ClientRequest.BodyBuilder requestBuilder) { + + DefaultHeaderSpec(ClientRequest.Builder requestBuilder) { this.requestBuilder = requestBuilder; } @Override public DefaultHeaderSpec header(String headerName, String... headerValues) { - this.requestBuilder.header(headerName, headerValues); + for (String headerValue : headerValues) { + this.headers.add(headerName, headerValue); + } return this; } @Override public DefaultHeaderSpec headers(HttpHeaders headers) { - this.requestBuilder.headers(headers); + if (headers != null) { + this.headers.putAll(headers); + } return this; } @Override public DefaultHeaderSpec accept(MediaType... acceptableMediaTypes) { - this.requestBuilder.accept(acceptableMediaTypes); + this.headers.setAccept(Arrays.asList(acceptableMediaTypes)); return this; } @Override public DefaultHeaderSpec acceptCharset(Charset... acceptableCharsets) { - this.requestBuilder.acceptCharset(acceptableCharsets); + this.headers.setAcceptCharset(Arrays.asList(acceptableCharsets)); return this; } @Override public DefaultHeaderSpec contentType(MediaType contentType) { - this.requestBuilder.contentType(contentType); + this.headers.setContentType(contentType); return this; } @Override public DefaultHeaderSpec contentLength(long contentLength) { - this.requestBuilder.contentLength(contentLength); + this.headers.setContentLength(contentLength); return this; } @@ -195,31 +204,33 @@ class DefaultWebClientOperations implements WebClientOperations { @Override public DefaultHeaderSpec ifModifiedSince(ZonedDateTime ifModifiedSince) { - this.requestBuilder.ifModifiedSince(ifModifiedSince); + ZonedDateTime gmt = ifModifiedSince.withZoneSameInstant(ZoneId.of("GMT")); + String headerValue = DateTimeFormatter.RFC_1123_DATE_TIME.format(gmt); + this.headers.set(HttpHeaders.IF_MODIFIED_SINCE, headerValue); return this; } @Override public DefaultHeaderSpec ifNoneMatch(String... ifNoneMatches) { - this.requestBuilder.ifNoneMatch(ifNoneMatches); + this.headers.setIfNoneMatch(Arrays.asList(ifNoneMatches)); return this; } @Override public Mono exchange() { - ClientRequest request = this.requestBuilder.build(); + ClientRequest request = this.requestBuilder.headers(this.headers).build(); return getWebClient().exchange(request); } @Override public Mono exchange(BodyInserter inserter) { - ClientRequest request = this.requestBuilder.body(inserter); + ClientRequest request = this.requestBuilder.headers(this.headers).body(inserter); return getWebClient().exchange(request); } @Override public > Mono exchange(S publisher, Class elementClass) { - ClientRequest request = this.requestBuilder.body(publisher, elementClass); + ClientRequest request = this.requestBuilder.headers(this.headers).body(publisher, elementClass); return getWebClient().exchange(request); } } diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/FlushingIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/FlushingIntegrationTests.java index 7cd0d14e323..5e72bcea6d1 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/FlushingIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/FlushingIntegrationTests.java @@ -37,15 +37,17 @@ import org.springframework.http.server.reactive.ServerHttpResponse; import org.springframework.http.server.reactive.bootstrap.RxNettyHttpServer; import org.springframework.util.Assert; import org.springframework.web.reactive.function.BodyExtractors; -import org.springframework.web.reactive.function.client.ClientRequest; import org.springframework.web.reactive.function.client.WebClient; +import org.springframework.web.reactive.function.client.WebClientOperations; +import org.springframework.web.util.DefaultUriBuilderFactory; +import org.springframework.web.util.UriBuilderFactory; /** * @author Sebastien Deleuze */ public class FlushingIntegrationTests extends AbstractHttpHandlerIntegrationTests { - private WebClient webClient; + private WebClientOperations operations; @Before @@ -55,15 +57,18 @@ public class FlushingIntegrationTests extends AbstractHttpHandlerIntegrationTest Assume.assumeFalse(this.server instanceof RxNettyHttpServer); super.setup(); - this.webClient = WebClient.create(new ReactorClientHttpConnector()); + + WebClient client = WebClient.create(new ReactorClientHttpConnector()); + UriBuilderFactory factory = new DefaultUriBuilderFactory("http://localhost:" + this.port); + this.operations = WebClientOperations.builder(client).uriBuilderFactory(factory).build(); } @Test public void writeAndFlushWith() throws Exception { - ClientRequest request = ClientRequest.GET("http://localhost:" + port + "/write-and-flush").build(); - Mono result = this.webClient - .exchange(request) + Mono result = this.operations.get() + .uri("/write-and-flush") + .exchange() .flatMap(response -> response.body(BodyExtractors.toFlux(String.class))) .takeUntil(s -> s.endsWith("data1")) .reduce((s1, s2) -> s1 + s2); @@ -76,9 +81,9 @@ public class FlushingIntegrationTests extends AbstractHttpHandlerIntegrationTest @Test // SPR-14991 public void writeAndAutoFlushOnComplete() { - ClientRequest request = ClientRequest.GET("http://localhost:" + port + "/write-and-complete").build(); - Mono result = this.webClient - .exchange(request) + Mono result = this.operations.get() + .uri("/write-and-complete") + .exchange() .flatMap(response -> response.bodyToFlux(String.class)) .reduce((s1, s2) -> s1 + s2); @@ -90,9 +95,9 @@ public class FlushingIntegrationTests extends AbstractHttpHandlerIntegrationTest @Test // SPR-14992 public void writeAndAutoFlushBeforeComplete() { - ClientRequest request = ClientRequest.GET("http://localhost:" + port + "/write-and-never-complete").build(); - Flux result = this.webClient - .exchange(request) + Flux result = this.operations.get() + .uri("/write-and-never-complete") + .exchange() .flatMap(response -> response.bodyToFlux(String.class)); StepVerifier.create(result) diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/function/client/DefaultClientRequestBuilderTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/function/client/DefaultClientRequestBuilderTests.java index a32dbd53918..b16ad3c0c7e 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/function/client/DefaultClientRequestBuilderTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/function/client/DefaultClientRequestBuilderTests.java @@ -18,11 +18,7 @@ package org.springframework.web.reactive.function.client; import java.net.URI; import java.nio.ByteBuffer; -import java.nio.charset.Charset; -import java.time.ZonedDateTime; import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; import java.util.List; import org.junit.Test; @@ -31,8 +27,6 @@ import reactor.core.publisher.Mono; import org.springframework.core.codec.CharSequenceEncoder; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DefaultDataBufferFactory; -import org.springframework.http.HttpMethod; -import org.springframework.http.MediaType; import org.springframework.http.client.reactive.ClientHttpRequest; import org.springframework.http.codec.EncoderHttpMessageWriter; import org.springframework.http.codec.HttpMessageWriter; @@ -45,6 +39,9 @@ import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; +import static org.springframework.http.HttpMethod.DELETE; +import static org.springframework.http.HttpMethod.GET; +import static org.springframework.http.HttpMethod.POST; /** * @author Arjen Poutsma @@ -53,12 +50,12 @@ public class DefaultClientRequestBuilderTests { @Test public void from() throws Exception { - ClientRequest other = ClientRequest.GET("http://example.com") + ClientRequest other = ClientRequest.method(GET, URI.create("http://example.com")) .header("foo", "bar") .cookie("baz", "qux").build(); ClientRequest result = ClientRequest.from(other).build(); assertEquals(new URI("http://example.com"), result.url()); - assertEquals(HttpMethod.GET, result.method()); + assertEquals(GET, result.method()); assertEquals("bar", result.headers().getFirst("foo")); assertEquals("qux", result.cookies().getFirst("baz")); } @@ -66,112 +63,26 @@ public class DefaultClientRequestBuilderTests { @Test public void method() throws Exception { URI url = new URI("http://example.com"); - ClientRequest result = ClientRequest.method(HttpMethod.DELETE, url).build(); + ClientRequest result = ClientRequest.method(DELETE, url).build(); assertEquals(url, result.url()); - assertEquals(HttpMethod.DELETE, result.method()); - } - - @Test - public void GET() throws Exception { - URI url = new URI("http://example.com"); - ClientRequest result = ClientRequest.GET(url.toString()).build(); - assertEquals(url, result.url()); - assertEquals(HttpMethod.GET, result.method()); - } - - @Test - public void HEAD() throws Exception { - URI url = new URI("http://example.com"); - ClientRequest result = ClientRequest.HEAD(url.toString()).build(); - assertEquals(url, result.url()); - assertEquals(HttpMethod.HEAD, result.method()); - } - - @Test - public void POST() throws Exception { - URI url = new URI("http://example.com"); - ClientRequest result = ClientRequest.POST(url.toString()).build(); - assertEquals(url, result.url()); - assertEquals(HttpMethod.POST, result.method()); - } - - @Test - public void PUT() throws Exception { - URI url = new URI("http://example.com"); - ClientRequest result = ClientRequest.PUT(url.toString()).build(); - assertEquals(url, result.url()); - assertEquals(HttpMethod.PUT, result.method()); - } - - @Test - public void PATCH() throws Exception { - URI url = new URI("http://example.com"); - ClientRequest result = ClientRequest.PATCH(url.toString()).build(); - assertEquals(url, result.url()); - assertEquals(HttpMethod.PATCH, result.method()); - } - - @Test - public void DELETE() throws Exception { - URI url = new URI("http://example.com"); - ClientRequest result = ClientRequest.DELETE(url.toString()).build(); - assertEquals(url, result.url()); - assertEquals(HttpMethod.DELETE, result.method()); - } - - @Test - public void OPTIONS() throws Exception { - URI url = new URI("http://example.com"); - ClientRequest result = ClientRequest.OPTIONS(url.toString()).build(); - assertEquals(url, result.url()); - assertEquals(HttpMethod.OPTIONS, result.method()); - } - - @Test - public void accept() throws Exception { - MediaType json = MediaType.APPLICATION_JSON; - ClientRequest result = ClientRequest.GET("http://example.com").accept(json).build(); - assertEquals(Collections.singletonList(json), result.headers().getAccept()); - } - - @Test - public void acceptCharset() throws Exception { - Charset charset = Charset.defaultCharset(); - ClientRequest result = ClientRequest.GET("http://example.com") - .acceptCharset(charset).build(); - assertEquals(Collections.singletonList(charset), result.headers().getAcceptCharset()); - } - - @Test - public void ifModifiedSince() throws Exception { - ZonedDateTime now = ZonedDateTime.now(); - ClientRequest result = ClientRequest.GET("http://example.com") - .ifModifiedSince(now).build(); - assertEquals(now.toInstant().toEpochMilli()/1000, result.headers().getIfModifiedSince()/1000); - } - - @Test - public void ifNoneMatch() throws Exception { - ClientRequest result = ClientRequest.GET("http://example.com") - .ifNoneMatch("\"v2.7\"", "\"v2.8\"").build(); - assertEquals(Arrays.asList("\"v2.7\"", "\"v2.8\""), result.headers().getIfNoneMatch()); + assertEquals(DELETE, result.method()); } @Test public void cookie() throws Exception { - ClientRequest result = ClientRequest.GET("http://example.com") + ClientRequest result = ClientRequest.method(GET, URI.create("http://example.com")) .cookie("foo", "bar").build(); assertEquals("bar", result.cookies().getFirst("foo")); } @Test public void build() throws Exception { - ClientRequest result = ClientRequest.GET("http://example.com") + ClientRequest result = ClientRequest.method(GET, URI.create("http://example.com")) .header("MyKey", "MyValue") .cookie("foo", "bar") .build(); - MockClientHttpRequest request = new MockClientHttpRequest(HttpMethod.GET, "/"); + MockClientHttpRequest request = new MockClientHttpRequest(GET, "/"); WebClientStrategies strategies = mock(WebClientStrategies.class); result.writeTo(request, strategies).block(); @@ -193,7 +104,7 @@ public class DefaultClientRequestBuilderTests { return response.writeWith(Mono.just(buffer)); }; - ClientRequest result = ClientRequest.POST("http://example.com") + ClientRequest result = ClientRequest.method(POST, URI.create("http://example.com")) .body(inserter); List> messageWriters = new ArrayList<>(); @@ -202,7 +113,7 @@ public class DefaultClientRequestBuilderTests { WebClientStrategies strategies = mock(WebClientStrategies.class); when(strategies.messageWriters()).thenReturn(messageWriters::stream); - MockClientHttpRequest request = new MockClientHttpRequest(HttpMethod.GET, "/"); + MockClientHttpRequest request = new MockClientHttpRequest(GET, "/"); result.writeTo(request, strategies).block(); assertNotNull(request.getBody()); } diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/function/client/ExchangeFilterFunctionsTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/function/client/ExchangeFilterFunctionsTests.java index 299ce98b22b..37bcc3e4c8f 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/function/client/ExchangeFilterFunctionsTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/function/client/ExchangeFilterFunctionsTests.java @@ -16,15 +16,19 @@ package org.springframework.web.reactive.function.client; +import java.net.URI; + import org.junit.Test; import reactor.core.publisher.Mono; import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.mock; +import static org.springframework.http.HttpMethod.GET; /** * @author Arjen Poutsma @@ -33,7 +37,7 @@ public class ExchangeFilterFunctionsTests { @Test public void andThen() throws Exception { - ClientRequest request = ClientRequest.GET("http://example.com").build(); + ClientRequest request = ClientRequest.method(GET, URI.create("http://example.com")).build(); ClientResponse response = mock(ClientResponse.class); ExchangeFunction exchange = r -> Mono.just(response); @@ -63,7 +67,7 @@ public class ExchangeFilterFunctionsTests { @Test public void apply() throws Exception { - ClientRequest request = ClientRequest.GET("http://example.com").build(); + ClientRequest request = ClientRequest.method(GET, URI.create("http://example.com")).build(); ClientResponse response = mock(ClientResponse.class); ExchangeFunction exchange = r -> Mono.just(response); @@ -82,7 +86,7 @@ public class ExchangeFilterFunctionsTests { @Test public void basicAuthentication() throws Exception { - ClientRequest request = ClientRequest.GET("http://example.com").build(); + ClientRequest request = ClientRequest.method(GET, URI.create("http://example.com")).build(); ClientResponse response = mock(ClientResponse.class); ExchangeFunction exchange = r -> { diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/function/client/WebClientIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/function/client/WebClientIntegrationTests.java index 9d8a2c0b924..5dabfc061cd 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/function/client/WebClientIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/function/client/WebClientIntegrationTests.java @@ -18,7 +18,6 @@ package org.springframework.web.reactive.function.client; import java.time.Duration; -import okhttp3.HttpUrl; import okhttp3.mockwebserver.MockResponse; import okhttp3.mockwebserver.MockWebServer; import okhttp3.mockwebserver.RecordedRequest; @@ -36,15 +35,11 @@ import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.client.reactive.ReactorClientHttpConnector; import org.springframework.http.codec.Pojo; -import org.springframework.web.reactive.function.BodyExtractors; -import org.springframework.web.reactive.function.BodyInserters; import org.springframework.web.util.DefaultUriBuilderFactory; import org.springframework.web.util.UriBuilderFactory; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertThat; -import static org.springframework.web.reactive.function.BodyExtractors.toFlux; -import static org.springframework.web.reactive.function.BodyExtractors.toMono; import static org.springframework.web.reactive.function.BodyInserters.fromObject; /** @@ -171,14 +166,9 @@ public class WebClientIntegrationTests { @Test public void jsonPojoFlux() throws Exception { - HttpUrl baseUrl = server.url("/pojos"); this.server.enqueue(new MockResponse().setHeader("Content-Type", "application/json") .setBody("[{\"bar\":\"bar1\",\"foo\":\"foo1\"},{\"bar\":\"bar2\",\"foo\":\"foo2\"}]")); - ClientRequest request = ClientRequest.GET(baseUrl.toString()) - .accept(MediaType.APPLICATION_JSON) - .build(); - Flux result = this.operations.get() .uri("/pojos") .accept(MediaType.APPLICATION_JSON) @@ -199,7 +189,6 @@ public class WebClientIntegrationTests { @Test public void postJsonPojo() throws Exception { - HttpUrl baseUrl = server.url("/pojo/capitalize"); this.server.enqueue(new MockResponse() .setHeader("Content-Type", "application/json") .setBody("{\"bar\":\"BARBAR\",\"foo\":\"FOOFOO\"}")); diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/function/server/SseHandlerFunctionIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/function/server/SseHandlerFunctionIntegrationTests.java index 23618990b5c..e58fc194310 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/function/server/SseHandlerFunctionIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/function/server/SseHandlerFunctionIntegrationTests.java @@ -18,22 +18,24 @@ package org.springframework.web.reactive.function.server; import java.time.Duration; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; import org.junit.Before; import org.junit.Test; -import static org.springframework.http.MediaType.TEXT_EVENT_STREAM; -import static org.springframework.web.reactive.function.BodyExtractors.toFlux; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; -import org.springframework.core.ResolvableType; import org.springframework.http.client.reactive.ReactorClientHttpConnector; import org.springframework.http.codec.ServerSentEvent; -import org.springframework.web.reactive.function.client.ClientRequest; import org.springframework.web.reactive.function.client.WebClient; +import org.springframework.web.reactive.function.client.WebClientOperations; +import org.springframework.web.util.DefaultUriBuilderFactory; +import org.springframework.web.util.UriBuilderFactory; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.springframework.core.ResolvableType.forClassWithGenerics; +import static org.springframework.http.MediaType.TEXT_EVENT_STREAM; +import static org.springframework.web.reactive.function.BodyExtractors.toFlux; import static org.springframework.web.reactive.function.BodyInserters.fromServerSentEvents; import static org.springframework.web.reactive.function.server.RouterFunctions.route; @@ -42,12 +44,15 @@ import static org.springframework.web.reactive.function.server.RouterFunctions.r */ public class SseHandlerFunctionIntegrationTests extends AbstractRouterFunctionIntegrationTests { + private WebClientOperations operations; - private WebClient webClient; @Before - public void createWebClient() { - this.webClient = WebClient.create(new ReactorClientHttpConnector()); + public void setup() throws Exception { + super.setup(); + WebClient client = WebClient.create(new ReactorClientHttpConnector()); + UriBuilderFactory factory = new DefaultUriBuilderFactory("http://localhost:" + this.port); + this.operations = WebClientOperations.builder(client).uriBuilderFactory(factory).build(); } @Override @@ -60,13 +65,10 @@ public class SseHandlerFunctionIntegrationTests extends AbstractRouterFunctionIn @Test public void sseAsString() throws Exception { - ClientRequest request = ClientRequest - .GET("http://localhost:{port}/string", this.port) - .accept(TEXT_EVENT_STREAM) - .build(); - - Flux result = this.webClient - .exchange(request) + Flux result = this.operations.get() + .uri("/string") + .accept(TEXT_EVENT_STREAM) + .exchange() .flatMap(response -> response.body(toFlux(String.class))); StepVerifier.create(result) @@ -77,14 +79,10 @@ public class SseHandlerFunctionIntegrationTests extends AbstractRouterFunctionIn } @Test public void sseAsPerson() throws Exception { - ClientRequest request = - ClientRequest - .GET("http://localhost:{port}/person", this.port) - .accept(TEXT_EVENT_STREAM) - .build(); - - Flux result = this.webClient - .exchange(request) + Flux result = this.operations.get() + .uri("/person") + .accept(TEXT_EVENT_STREAM) + .exchange() .flatMap(response -> response.body(toFlux(Person.class))); StepVerifier.create(result) @@ -96,16 +94,12 @@ public class SseHandlerFunctionIntegrationTests extends AbstractRouterFunctionIn @Test public void sseAsEvent() throws Exception { - ClientRequest request = - ClientRequest - .GET("http://localhost:{port}/event", this.port) - .accept(TEXT_EVENT_STREAM) - .build(); - - ResolvableType type = ResolvableType.forClassWithGenerics(ServerSentEvent.class, String.class); - Flux> result = this.webClient - .exchange(request) - .flatMap(response -> response.body(toFlux(type))); + Flux> result = this.operations.get() + .uri("/event") + .accept(TEXT_EVENT_STREAM) + .exchange() + .flatMap(response -> response.body(toFlux( + forClassWithGenerics(ServerSentEvent.class, String.class)))); StepVerifier.create(result) .consumeNextWith( event -> { diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/SseIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/SseIntegrationTests.java index 676f30c5648..83387cc0a25 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/SseIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/SseIntegrationTests.java @@ -18,14 +18,9 @@ package org.springframework.web.reactive.result.method.annotation; import java.time.Duration; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; import org.junit.Before; import org.junit.Test; -import static org.springframework.http.MediaType.TEXT_EVENT_STREAM; -import static org.springframework.web.reactive.function.BodyExtractors.toFlux; import reactor.core.publisher.Flux; - import reactor.test.StepVerifier; import org.springframework.context.annotation.AnnotationConfigApplicationContext; @@ -40,9 +35,17 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.reactive.DispatcherHandler; import org.springframework.web.reactive.config.EnableWebReactive; -import org.springframework.web.reactive.function.client.ClientRequest; import org.springframework.web.reactive.function.client.WebClient; +import org.springframework.web.reactive.function.client.WebClientOperations; import org.springframework.web.server.adapter.WebHttpHandlerBuilder; +import org.springframework.web.util.DefaultUriBuilderFactory; +import org.springframework.web.util.UriBuilderFactory; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.springframework.core.ResolvableType.forClassWithGenerics; +import static org.springframework.http.MediaType.TEXT_EVENT_STREAM; +import static org.springframework.web.reactive.function.BodyExtractors.toFlux; /** @@ -52,14 +55,16 @@ public class SseIntegrationTests extends AbstractHttpHandlerIntegrationTests { private AnnotationConfigApplicationContext wac; - private WebClient webClient; + private WebClientOperations operations; @Override @Before public void setup() throws Exception { super.setup(); - this.webClient = WebClient.create(new ReactorClientHttpConnector()); + WebClient client = WebClient.create(new ReactorClientHttpConnector()); + UriBuilderFactory factory = new DefaultUriBuilderFactory("http://localhost:" + this.port + "/sse"); + this.operations = WebClientOperations.builder(client).uriBuilderFactory(factory).build(); } @@ -74,14 +79,11 @@ public class SseIntegrationTests extends AbstractHttpHandlerIntegrationTests { @Test public void sseAsString() throws Exception { - ClientRequest request = ClientRequest - .GET("http://localhost:{port}/sse/string", this.port) - .accept(TEXT_EVENT_STREAM) - .build(); - - Flux result = this.webClient - .exchange(request) - .flatMap(response -> response.body(toFlux(String.class))); + Flux result = this.operations.get() + .uri("/string") + .accept(TEXT_EVENT_STREAM) + .exchange() + .flatMap(response -> response.bodyToFlux(String.class)); StepVerifier.create(result) .expectNext("foo 0") @@ -91,15 +93,11 @@ public class SseIntegrationTests extends AbstractHttpHandlerIntegrationTests { } @Test public void sseAsPerson() throws Exception { - ClientRequest request = - ClientRequest - .GET("http://localhost:{port}/sse/person", this.port) - .accept(TEXT_EVENT_STREAM) - .build(); - - Flux result = this.webClient - .exchange(request) - .flatMap(response -> response.body(toFlux(Person.class))); + Flux result = this.operations.get() + .uri("/person") + .accept(TEXT_EVENT_STREAM) + .exchange() + .flatMap(response -> response.bodyToFlux(Person.class)); StepVerifier.create(result) .expectNext(new Person("foo 0")) @@ -110,15 +108,11 @@ public class SseIntegrationTests extends AbstractHttpHandlerIntegrationTests { @Test public void sseAsEvent() throws Exception { - ClientRequest request = - ClientRequest - .GET("http://localhost:{port}/sse/event", this.port) - .accept(TEXT_EVENT_STREAM) - .build(); - - ResolvableType type = ResolvableType.forClassWithGenerics(ServerSentEvent.class, String.class); - Flux> result = this.webClient - .exchange(request) + ResolvableType type = forClassWithGenerics(ServerSentEvent.class, String.class); + Flux> result = this.operations.get() + .uri("/event") + .accept(TEXT_EVENT_STREAM) + .exchange() .flatMap(response -> response.body(toFlux(type))); StepVerifier.create(result) @@ -142,15 +136,12 @@ public class SseIntegrationTests extends AbstractHttpHandlerIntegrationTests { @Test public void sseAsEventWithoutAcceptHeader() throws Exception { - ClientRequest request = - ClientRequest - .GET("http://localhost:{port}/sse/event", this.port) + Flux> result = this.operations.get() + .uri("/event") .accept(TEXT_EVENT_STREAM) - .build(); - - Flux> result = this.webClient - .exchange(request) - .flatMap(response -> response.body(toFlux(ResolvableType.forClassWithGenerics(ServerSentEvent.class, String.class)))); + .exchange() + .flatMap(response -> response.body(toFlux( + forClassWithGenerics(ServerSentEvent.class, String.class)))); StepVerifier.create(result) .consumeNextWith( event -> { From d057c3d981730c398c3a68cf0d71c9dfff69ba3b Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Fri, 27 Jan 2017 16:38:16 -0500 Subject: [PATCH 4/4] Deprecate DefaultUriTemplate handler Following on the introduction of the UriBuilderFactory and its DefaultUriBuilderFactory implementation, this commit deprecates DefaultUriTemplate (and AbstractUriTemplateHandler). The new DefaultUriBuilderFactory has comparable functionality and is more flexible but cannot be merged into the existing hierarchy and be backwards compatible with regards to protected methods. Issue: SPR-15124 --- .../web/client/AsyncRestTemplate.java | 14 ++++-- .../web/client/RestTemplate.java | 25 +++++++---- .../web/util/AbstractUriTemplateHandler.java | 2 + .../web/util/DefaultUriTemplateHandler.java | 2 + .../web/util/HierarchicalUriComponents.java | 5 ++- .../web/util/UriTemplateHandler.java | 12 ++--- .../web/client/RestTemplateTests.java | 7 ++- .../util/DefaultUriBuilderFactoryTests.java | 44 ++++++++++++++----- 8 files changed, 74 insertions(+), 37 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/web/client/AsyncRestTemplate.java b/spring-web/src/main/java/org/springframework/web/client/AsyncRestTemplate.java index 351c46e7ad5..00c9a87677a 100644 --- a/spring-web/src/main/java/org/springframework/web/client/AsyncRestTemplate.java +++ b/spring-web/src/main/java/org/springframework/web/client/AsyncRestTemplate.java @@ -45,6 +45,7 @@ import org.springframework.util.Assert; import org.springframework.util.concurrent.ListenableFuture; import org.springframework.util.concurrent.ListenableFutureAdapter; import org.springframework.web.util.AbstractUriTemplateHandler; +import org.springframework.web.util.DefaultUriBuilderFactory; import org.springframework.web.util.UriTemplateHandler; /** @@ -163,9 +164,16 @@ public class AsyncRestTemplate extends InterceptingAsyncHttpAccessor implements */ public void setDefaultUriVariables(Map defaultUriVariables) { UriTemplateHandler handler = this.syncTemplate.getUriTemplateHandler(); - Assert.isInstanceOf(AbstractUriTemplateHandler.class, handler, - "Can only use this property in conjunction with a DefaultUriTemplateHandler"); - ((AbstractUriTemplateHandler) handler).setDefaultUriVariables(defaultUriVariables); + if (handler instanceof DefaultUriBuilderFactory) { + ((DefaultUriBuilderFactory) handler).setDefaultUriVariables(defaultUriVariables); + } + else if (handler instanceof AbstractUriTemplateHandler) { + ((AbstractUriTemplateHandler) handler).setDefaultUriVariables(defaultUriVariables); + } + else { + throw new IllegalArgumentException( + "This property is not supported with the configured UriTemplateHandler."); + } } /** diff --git a/spring-web/src/main/java/org/springframework/web/client/RestTemplate.java b/spring-web/src/main/java/org/springframework/web/client/RestTemplate.java index 316a5d2c809..22721dc2128 100644 --- a/spring-web/src/main/java/org/springframework/web/client/RestTemplate.java +++ b/spring-web/src/main/java/org/springframework/web/client/RestTemplate.java @@ -53,7 +53,7 @@ import org.springframework.http.converter.xml.SourceHttpMessageConverter; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.web.util.AbstractUriTemplateHandler; -import org.springframework.web.util.DefaultUriTemplateHandler; +import org.springframework.web.util.DefaultUriBuilderFactory; import org.springframework.web.util.UriTemplateHandler; /** @@ -149,7 +149,7 @@ public class RestTemplate extends InterceptingHttpAccessor implements RestOperat private ResponseErrorHandler errorHandler = new DefaultResponseErrorHandler(); - private UriTemplateHandler uriTemplateHandler = new DefaultUriTemplateHandler(); + private UriTemplateHandler uriTemplateHandler = new DefaultUriBuilderFactory(); private final ResponseExtractor headersExtractor = new HeadersExtractor(); @@ -254,24 +254,31 @@ public class RestTemplate extends InterceptingHttpAccessor implements RestOperat /** * Configure default URI variable values. This is a shortcut for: *

-	 * DefaultUriTemplateHandler handler = new DefaultUriTemplateHandler();
+	 * DefaultUriBuilderFactory factory = new DefaultUriBuilderFactory();
 	 * handler.setDefaultUriVariables(...);
 	 *
 	 * RestTemplate restTemplate = new RestTemplate();
 	 * restTemplate.setUriTemplateHandler(handler);
 	 * 
- * @param defaultUriVariables the default URI variable values + * @param uriVars the default URI variable values * @since 4.3 */ - public void setDefaultUriVariables(Map defaultUriVariables) { - Assert.isInstanceOf(AbstractUriTemplateHandler.class, this.uriTemplateHandler, - "Can only use this property in conjunction with an AbstractUriTemplateHandler"); - ((AbstractUriTemplateHandler) this.uriTemplateHandler).setDefaultUriVariables(defaultUriVariables); + public void setDefaultUriVariables(Map uriVars) { + if (this.uriTemplateHandler instanceof DefaultUriBuilderFactory) { + ((DefaultUriBuilderFactory) this.uriTemplateHandler).setDefaultUriVariables(uriVars); + } + else if (this.uriTemplateHandler instanceof AbstractUriTemplateHandler) { + ((AbstractUriTemplateHandler) this.uriTemplateHandler).setDefaultUriVariables(uriVars); + } + else { + throw new IllegalArgumentException( + "This property is not supported with the configured UriTemplateHandler."); + } } /** * Configure the {@link UriTemplateHandler} to use to expand URI templates. - * By default the {@link DefaultUriTemplateHandler} is used which relies on + * By default the {@link DefaultUriBuilderFactory} is used which relies on * Spring's URI template support and exposes several useful properties that * customize its behavior for encoding and for prepending a common base URL. * An alternative implementation may be used to plug an external URI diff --git a/spring-web/src/main/java/org/springframework/web/util/AbstractUriTemplateHandler.java b/spring-web/src/main/java/org/springframework/web/util/AbstractUriTemplateHandler.java index b4a8cbb3186..d8a88c46375 100644 --- a/spring-web/src/main/java/org/springframework/web/util/AbstractUriTemplateHandler.java +++ b/spring-web/src/main/java/org/springframework/web/util/AbstractUriTemplateHandler.java @@ -33,7 +33,9 @@ import org.springframework.util.Assert; * * @author Rossen Stoyanchev * @since 4.3 + * @deprecated as of 5.0 in favor of {@link DefaultUriBuilderFactory} */ +@Deprecated public abstract class AbstractUriTemplateHandler implements UriTemplateHandler { private String baseUrl; diff --git a/spring-web/src/main/java/org/springframework/web/util/DefaultUriTemplateHandler.java b/spring-web/src/main/java/org/springframework/web/util/DefaultUriTemplateHandler.java index 8c0ad71504f..9ad9357a30c 100644 --- a/spring-web/src/main/java/org/springframework/web/util/DefaultUriTemplateHandler.java +++ b/spring-web/src/main/java/org/springframework/web/util/DefaultUriTemplateHandler.java @@ -33,7 +33,9 @@ import java.util.Map; * * @author Rossen Stoyanchev * @since 4.2 + * @deprecated as of 5.0 in favor of {@link DefaultUriBuilderFactory} */ +@Deprecated public class DefaultUriTemplateHandler extends AbstractUriTemplateHandler { private boolean parsePath; diff --git a/spring-web/src/main/java/org/springframework/web/util/HierarchicalUriComponents.java b/spring-web/src/main/java/org/springframework/web/util/HierarchicalUriComponents.java index 905f29eef3e..98c9a7cdd15 100644 --- a/spring-web/src/main/java/org/springframework/web/util/HierarchicalUriComponents.java +++ b/spring-web/src/main/java/org/springframework/web/util/HierarchicalUriComponents.java @@ -455,8 +455,9 @@ final class HierarchicalUriComponents extends UriComponents { if (getHost() != null) { builder.host(getHost()); } - if (getPort() != -1) { - builder.port(getPort()); + // Avoid parsing the port, may have URI variable.. + if (this.port != null) { + builder.port(this.port); } if (getPath() != null) { this.path.copyToUriComponentsBuilder(builder); diff --git a/spring-web/src/main/java/org/springframework/web/util/UriTemplateHandler.java b/spring-web/src/main/java/org/springframework/web/util/UriTemplateHandler.java index c03eb191ab4..d394d34f31a 100644 --- a/spring-web/src/main/java/org/springframework/web/util/UriTemplateHandler.java +++ b/spring-web/src/main/java/org/springframework/web/util/UriTemplateHandler.java @@ -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,18 +20,14 @@ import java.net.URI; import java.util.Map; /** - * Strategy for expanding a URI template with full control over the URI template - * syntax and the encoding of variables. Also a convenient central point for - * pre-processing all URI templates for example to insert a common base path. + * Strategy for expanding a URI template. * *

Supported as a property on the {@code RestTemplate} as well as the - * {@code AsyncRestTemplate}. The {@link DefaultUriTemplateHandler} is built - * on Spring's URI template support via {@link UriComponentsBuilder}. An - * alternative implementation may be used to plug external URI template libraries. + * {@code AsyncRestTemplate}. * * @author Rossen Stoyanchev * @since 4.2 - * @see org.springframework.web.client.RestTemplate#setUriTemplateHandler + * @see DefaultUriBuilderFactory */ public interface UriTemplateHandler { diff --git a/spring-web/src/test/java/org/springframework/web/client/RestTemplateTests.java b/spring-web/src/test/java/org/springframework/web/client/RestTemplateTests.java index 1b3f4fe24c1..1ada39213a4 100644 --- a/spring-web/src/test/java/org/springframework/web/client/RestTemplateTests.java +++ b/spring-web/src/test/java/org/springframework/web/client/RestTemplateTests.java @@ -42,7 +42,7 @@ import org.springframework.http.client.ClientHttpRequestFactory; import org.springframework.http.client.ClientHttpResponse; import org.springframework.http.converter.GenericHttpMessageConverter; import org.springframework.http.converter.HttpMessageConverter; -import org.springframework.web.util.DefaultUriTemplateHandler; +import org.springframework.web.util.DefaultUriBuilderFactory; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; @@ -83,7 +83,7 @@ public class RestTemplateTests { response = mock(ClientHttpResponse.class); errorHandler = mock(ResponseErrorHandler.class); converter = mock(HttpMessageConverter.class); - template = new RestTemplate(Collections.>singletonList(converter)); + template = new RestTemplate(Collections.singletonList(converter)); template.setRequestFactory(requestFactory); template.setErrorHandler(errorHandler); } @@ -273,8 +273,7 @@ public class RestTemplateTests { @Test public void getForObjectWithCustomUriTemplateHandler() throws Exception { - DefaultUriTemplateHandler uriTemplateHandler = new DefaultUriTemplateHandler(); - uriTemplateHandler.setParsePath(true); + DefaultUriBuilderFactory uriTemplateHandler = new DefaultUriBuilderFactory(); template.setUriTemplateHandler(uriTemplateHandler); URI expectedUri = new URI("http://example.com/hotels/1/pic/pics%2Flogo.png/size/150x150"); diff --git a/spring-web/src/test/java/org/springframework/web/util/DefaultUriBuilderFactoryTests.java b/spring-web/src/test/java/org/springframework/web/util/DefaultUriBuilderFactoryTests.java index f7931ffcc0d..c7481d7de51 100644 --- a/spring-web/src/test/java/org/springframework/web/util/DefaultUriBuilderFactoryTests.java +++ b/spring-web/src/test/java/org/springframework/web/util/DefaultUriBuilderFactoryTests.java @@ -16,12 +16,14 @@ package org.springframework.web.util; import java.net.URI; -import java.util.Collections; +import java.util.HashMap; +import java.util.Map; import org.junit.Test; import org.springframework.web.util.DefaultUriBuilderFactory.EncodingMode; +import static java.util.Collections.singletonMap; import static junit.framework.TestCase.assertEquals; /** @@ -30,9 +32,6 @@ import static junit.framework.TestCase.assertEquals; */ public class DefaultUriBuilderFactoryTests { - private static final String baseUrl = "http://foo.com/bar"; - - @Test public void defaultSettings() throws Exception { DefaultUriBuilderFactory factory = new DefaultUriBuilderFactory(); @@ -57,27 +56,39 @@ public class DefaultUriBuilderFactoryTests { @Test public void defaultUriVars() throws Exception { DefaultUriBuilderFactory factory = new DefaultUriBuilderFactory("http://{host}/bar"); - factory.setDefaultUriVariables(Collections.singletonMap("host", "foo.com")); - URI uri = factory.uriString("/{id}").build(Collections.singletonMap("id", "123")); + factory.setDefaultUriVariables(singletonMap("host", "foo.com")); + URI uri = factory.uriString("/{id}").build(singletonMap("id", "123")); assertEquals("http://foo.com/bar/123", uri.toString()); } @Test public void defaultUriVarsWithOverride() throws Exception { DefaultUriBuilderFactory factory = new DefaultUriBuilderFactory("http://{host}/bar"); - factory.setDefaultUriVariables(Collections.singletonMap("host", "spring.io")); - URI uri = factory.uriString("/baz").build(Collections.singletonMap("host", "docs.spring.io")); + factory.setDefaultUriVariables(singletonMap("host", "spring.io")); + URI uri = factory.uriString("/baz").build(singletonMap("host", "docs.spring.io")); assertEquals("http://docs.spring.io/bar/baz", uri.toString()); } @Test public void defaultUriVarsWithEmptyVarArg() throws Exception { DefaultUriBuilderFactory factory = new DefaultUriBuilderFactory("http://{host}/bar"); - factory.setDefaultUriVariables(Collections.singletonMap("host", "foo.com")); + factory.setDefaultUriVariables(singletonMap("host", "foo.com")); URI uri = factory.uriString("/baz").build(); assertEquals("Expected delegation to build(Map) method", "http://foo.com/bar/baz", uri.toString()); } + @Test + public void defaultUriVarsSpr14147() throws Exception { + Map defaultUriVars = new HashMap<>(2); + defaultUriVars.put("host", "api.example.com"); + defaultUriVars.put("port", "443"); + DefaultUriBuilderFactory factory = new DefaultUriBuilderFactory(); + factory.setDefaultUriVariables(defaultUriVars); + + URI uri = factory.expand("https://{host}:{port}/v42/customers/{id}", singletonMap("id", 123L)); + assertEquals("https://api.example.com:443/v42/customers/123", uri.toString()); + } + @Test public void encodingValuesOnly() throws Exception { DefaultUriBuilderFactory factory = new DefaultUriBuilderFactory(); @@ -88,7 +99,18 @@ public class DefaultUriBuilderFactoryTests { String expected = "/foo/a%2Fb/c%2Fd"; assertEquals(expected, uriBuilder.build(id).toString()); - assertEquals(expected, uriBuilder.build(Collections.singletonMap("id", id)).toString()); + assertEquals(expected, uriBuilder.build(singletonMap("id", id)).toString()); + } + + @Test + public void encodingValuesOnlySpr14147() throws Exception { + DefaultUriBuilderFactory factory = new DefaultUriBuilderFactory(); + factory.setEncodingMode(EncodingMode.VALUES_ONLY); + factory.setDefaultUriVariables(singletonMap("host", "www.example.com")); + UriBuilder uriBuilder = factory.uriString("http://{host}/user/{userId}/dashboard"); + + assertEquals("http://www.example.com/user/john%3Bdoe/dashboard", + uriBuilder.build(singletonMap("userId", "john;doe")).toString()); } @Test @@ -101,7 +123,7 @@ public class DefaultUriBuilderFactoryTests { String expected = "/foo/a%2Fb/c%2Fd"; assertEquals(expected, uriBuilder.build(id).toString()); - assertEquals(expected, uriBuilder.build(Collections.singletonMap("id", id)).toString()); + assertEquals(expected, uriBuilder.build(singletonMap("id", id)).toString()); } @Test