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 0000000000..8daaf768a0 --- /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 0912430d07..905f29eef3 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 bf3c7e3495..f0e1c29575 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 0000000000..2388d32581 --- /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 0000000000..504a7d5092 --- /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 09495d3977..9f7694c326 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 */ @@ -411,6 +435,18 @@ public class UriComponentsBuilder implements Cloneable { return this; } + /** + * Initialize components of this {@link UriComponentsBuilder} from the + * components of the given {@link UriComponents}. + * @param uriComponents the UriComponents instance + * @return this UriComponentsBuilder + */ + public UriComponentsBuilder uriComponents(UriComponents uriComponents) { + Assert.notNull(uriComponents, "UriComponents must not be null"); + uriComponents.copyToUriComponentsBuilder(this); + return this; + } + /** * 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. @@ -422,17 +458,6 @@ public class UriComponentsBuilder implements Cloneable { return this; } - /** - * Set all components of this URI builder from the given {@link UriComponents}. - * @param uriComponents the UriComponents instance - * @return this UriComponentsBuilder - */ - public UriComponentsBuilder uriComponents(UriComponents uriComponents) { - Assert.notNull(uriComponents, "UriComponents must not be null"); - uriComponents.copyToUriComponentsBuilder(this); - return this; - } - /** * Set the URI scheme-specific-part. When invoked, this method overwrites * {@linkplain #userInfo(String) user-info}, {@linkplain #host(String) host}, 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 0000000000..f7931ffcc0 --- /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()); + } + +}