From d81ec55a60a40dbfdda4846034e3170e23773394 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Fri, 13 Jul 2018 17:48:39 -0400 Subject: [PATCH] Support for encode() in UriComponentsBuilder The ability to request to encode before `build()`, and more importantly before expanding, allows stricter encoding to be applied to URI vars and consequently to neutralize the effect of characters with reserved meaning in a URI. Issue: SPR-17039 --- .../web/util/HierarchicalUriComponents.java | 243 ++++++++++++++---- .../web/util/UriComponents.java | 43 +++- .../web/util/UriComponentsBuilder.java | 73 ++++-- .../web/util/UriComponentsTests.java | 60 ++++- 4 files changed, 317 insertions(+), 102 deletions(-) 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 da945bacb1a..3dac6113bce 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 @@ -26,6 +26,8 @@ import java.util.Arrays; import java.util.Collections; import java.util.Iterator; import java.util.List; +import java.util.function.BiFunction; +import java.util.function.UnaryOperator; import org.springframework.lang.NonNull; import org.springframework.lang.Nullable; @@ -70,7 +72,7 @@ final class HierarchicalUriComponents extends UriComponents { } @Override - public PathComponent encode(Charset charset) { + public PathComponent encode(BiFunction encoder) { return this; } @@ -79,7 +81,7 @@ final class HierarchicalUriComponents extends UriComponents { } @Override - public PathComponent expand(UriTemplateVariables uriVariables) { + public PathComponent expand(UriTemplateVariables uriVariables, @Nullable UnaryOperator encoder) { return this; } @@ -112,7 +114,10 @@ final class HierarchicalUriComponents extends UriComponents { private final MultiValueMap queryParams; - private final boolean encoded; + private final EncodeState encodeState; + + @Nullable + private UnaryOperator variableEncoder; /** @@ -125,11 +130,10 @@ final class HierarchicalUriComponents extends UriComponents { * @param queryParams the query parameters * @param fragment the fragment * @param encoded whether the components are already encoded - * @param verify whether the components need to be checked for illegal characters */ HierarchicalUriComponents(@Nullable String scheme, @Nullable String fragment, @Nullable String userInfo, @Nullable String host, @Nullable String port, @Nullable PathComponent path, - @Nullable MultiValueMap queryParams, boolean encoded, boolean verify) { + @Nullable MultiValueMap queryParams, boolean encoded) { super(scheme, fragment); @@ -139,13 +143,31 @@ final class HierarchicalUriComponents extends UriComponents { this.path = (path != null ? path : NULL_PATH_COMPONENT); this.queryParams = CollectionUtils.unmodifiableMultiValueMap( queryParams != null ? queryParams : new LinkedMultiValueMap<>(0)); - this.encoded = encoded; + this.encodeState = encoded ? EncodeState.FULLY_ENCODED : EncodeState.RAW; - if (verify) { + // Check for illegal characters.. + if (encoded) { verify(); } } + private HierarchicalUriComponents(@Nullable String scheme, @Nullable String fragment, @Nullable String userInfo, + @Nullable String host, @Nullable String port, @Nullable PathComponent path, + @Nullable MultiValueMap queryParams, EncodeState encodeState, + @Nullable UnaryOperator variableEncoder) { + + super(scheme, fragment); + + this.userInfo = userInfo; + this.host = host; + this.port = port; + this.path = (path != null ? path : NULL_PATH_COMPONENT); + this.queryParams = CollectionUtils.unmodifiableMultiValueMap( + queryParams != null ? queryParams : new LinkedMultiValueMap<>(0)); + this.encodeState = encodeState; + this.variableEncoder = variableEncoder; + } + // Component getters @@ -232,15 +254,30 @@ final class HierarchicalUriComponents extends UriComponents { // Encoding - /** - * Encode all URI components using their specific encoding rules and return - * the result as a new {@code UriComponents} instance. - * @param charset the encoding of the values - * @return the encoded URI components - */ + HierarchicalUriComponents encodeTemplate(Charset charset) { + + if (this.encodeState.isEncoded()) { + return this; + } + + // Remember the charset to encode URI variables later.. + this.variableEncoder = value -> encodeUriComponent(value, charset, Type.URI); + + UriTemplateEncoder encoder = new UriTemplateEncoder(charset); + String schemeTo = (getScheme() != null ? encoder.apply(getScheme(), Type.SCHEME) : null); + String fragmentTo = (getFragment() != null ? encoder.apply(getFragment(), Type.FRAGMENT) : null); + String userInfoTo = (getUserInfo() != null ? encoder.apply(getUserInfo(), Type.USER_INFO) : null); + String hostTo = (getHost() != null ? encoder.apply(getHost(), getHostType()) : null); + PathComponent pathTo = this.path.encode(encoder); + MultiValueMap paramsTo = encodeQueryParams(encoder); + + return new HierarchicalUriComponents(schemeTo, fragmentTo, userInfoTo, + hostTo, this.port, pathTo, paramsTo, EncodeState.TEMPLATE_ENCODED, this.variableEncoder); + } + @Override public HierarchicalUriComponents encode(Charset charset) { - if (this.encoded) { + if (this.encodeState.isEncoded()) { return this; } String scheme = getScheme(); @@ -249,20 +286,22 @@ final class HierarchicalUriComponents extends UriComponents { String fragmentTo = (fragment != null ? encodeUriComponent(fragment, charset, Type.FRAGMENT) : null); String userInfoTo = (this.userInfo != null ? encodeUriComponent(this.userInfo, charset, Type.USER_INFO) : null); String hostTo = (this.host != null ? encodeUriComponent(this.host, charset, getHostType()) : null); - PathComponent pathTo = this.path.encode(charset); - MultiValueMap paramsTo = encodeQueryParams(charset); - return new HierarchicalUriComponents( - schemeTo, fragmentTo, userInfoTo, hostTo, this.port, pathTo, paramsTo, true, false); + BiFunction encoder = (s, type) -> encodeUriComponent(s, charset, type); + PathComponent pathTo = this.path.encode(encoder); + MultiValueMap paramsTo = encodeQueryParams(encoder); + + return new HierarchicalUriComponents(schemeTo, fragmentTo, userInfoTo, + hostTo, this.port, pathTo, paramsTo, EncodeState.FULLY_ENCODED, null); } - private MultiValueMap encodeQueryParams(Charset charset) { + private MultiValueMap encodeQueryParams(BiFunction encoder) { int size = this.queryParams.size(); MultiValueMap result = new LinkedMultiValueMap<>(size); this.queryParams.forEach((key, values) -> { - String name = encodeUriComponent(key, charset, Type.QUERY_PARAM); + String name = encoder.apply(key, Type.QUERY_PARAM); List encodedValues = new ArrayList<>(values.size()); for (String value : values) { - encodedValues.add(encodeUriComponent(value, charset, Type.QUERY_PARAM)); + encodedValues.add(encoder.apply(value, Type.QUERY_PARAM)); } result.put(name, encodedValues); }); @@ -324,18 +363,13 @@ final class HierarchicalUriComponents extends UriComponents { return (this.host != null && this.host.startsWith("[") ? Type.HOST_IPV6 : Type.HOST_IPV4); } - // Verifying /** - * Verifies all URI components to determine whether they contain any illegal - * characters, throwing an {@code IllegalArgumentException} if so. + * Check if any of the URI components contain any illegal characters. * @throws IllegalArgumentException if any component has illegal characters */ private void verify() { - if (!this.encoded) { - return; - } verifyUriComponent(getScheme(), Type.SCHEME); verifyUriComponent(this.userInfo, Type.USER_INFO); verifyUriComponent(this.host, getHostType()); @@ -385,18 +419,20 @@ final class HierarchicalUriComponents extends UriComponents { @Override protected HierarchicalUriComponents expandInternal(UriTemplateVariables uriVariables) { - Assert.state(!this.encoded, "Cannot expand an already encoded UriComponents object"); - - String schemeTo = expandUriComponent(getScheme(), uriVariables); - String fragmentTo = expandUriComponent(getFragment(), uriVariables); - String userInfoTo = expandUriComponent(this.userInfo, uriVariables); - String hostTo = expandUriComponent(this.host, uriVariables); - String portTo = expandUriComponent(this.port, uriVariables); - PathComponent pathTo = this.path.expand(uriVariables); + + Assert.state(!this.encodeState.equals(EncodeState.FULLY_ENCODED), + "URI components already encoded, and could not possibly contain '{' or '}'."); + + String schemeTo = expandUriComponent(getScheme(), uriVariables, this.variableEncoder); + String fragmentTo = expandUriComponent(getFragment(), uriVariables, this.variableEncoder); + String userInfoTo = expandUriComponent(this.userInfo, uriVariables, this.variableEncoder); + String hostTo = expandUriComponent(this.host, uriVariables, this.variableEncoder); + String portTo = expandUriComponent(this.port, uriVariables, this.variableEncoder); + PathComponent pathTo = this.path.expand(uriVariables, this.variableEncoder); MultiValueMap paramsTo = expandQueryParams(uriVariables); - return new HierarchicalUriComponents( - schemeTo, fragmentTo, userInfoTo, hostTo, portTo, pathTo, paramsTo, false, false); + return new HierarchicalUriComponents(schemeTo, fragmentTo, userInfoTo, + hostTo, portTo, pathTo, paramsTo, this.encodeState, this.variableEncoder); } private MultiValueMap expandQueryParams(UriTemplateVariables variables) { @@ -404,10 +440,10 @@ final class HierarchicalUriComponents extends UriComponents { MultiValueMap result = new LinkedMultiValueMap<>(size); UriTemplateVariables queryVariables = new QueryUriTemplateVariables(variables); this.queryParams.forEach((key, values) -> { - String name = expandUriComponent(key, queryVariables); + String name = expandUriComponent(key, queryVariables, this.variableEncoder); List expandedValues = new ArrayList<>(values.size()); for (String value : values) { - expandedValues.add(expandUriComponent(value, queryVariables)); + expandedValues.add(expandUriComponent(value, queryVariables, this.variableEncoder)); } result.put(name, expandedValues); }); @@ -417,8 +453,9 @@ final class HierarchicalUriComponents extends UriComponents { @Override public UriComponents normalize() { String normalizedPath = StringUtils.cleanPath(getPath()); + FullPathComponent path = new FullPathComponent(normalizedPath); return new HierarchicalUriComponents(getScheme(), getFragment(), this.userInfo, this.host, this.port, - new FullPathComponent(normalizedPath), this.queryParams, this.encoded, false); + path, this.queryParams, this.encodeState, this.variableEncoder); } @@ -462,7 +499,7 @@ final class HierarchicalUriComponents extends UriComponents { @Override public URI toUri() { try { - if (this.encoded) { + if (this.encodeState.isEncoded()) { return new URI(toUriString()); } else { @@ -689,6 +726,102 @@ final class HierarchicalUriComponents extends UriComponents { } + private enum EncodeState { + + /** + * Not encoded. + */ + RAW, + + /** + * URI vars expanded first and then each URI component encoded by + * quoting only illegal characters within that URI component. + */ + FULLY_ENCODED, + + /** + * URI template encoded first by quoting illegal characters only, and + * then URI vars encoded more strictly when expanded, by quoting both + * illegal chars and chars with reserved meaning. + */ + TEMPLATE_ENCODED; + + + public boolean isEncoded() { + return this.equals(FULLY_ENCODED) || this.equals(TEMPLATE_ENCODED); + } + } + + + private static class UriTemplateEncoder implements BiFunction { + + private final Charset charset; + + private final StringBuilder currentLiteral = new StringBuilder(); + + private final StringBuilder output = new StringBuilder(); + + + public UriTemplateEncoder(Charset charset) { + this.charset = charset; + } + + + @Override + public String apply(String source, Type type) { + + // Only URI variable, nothing to encode.. + if (source.length() > 1 && source.charAt(0) == '{' && source.charAt(source.length() -1) == '}') { + return source; + } + + // Only literal, encode all.. + if (source.indexOf('{') == -1) { + return encodeUriComponent(source, this.charset, type); + } + + // Mixed, encode all except for URI variables.. + + int level = 0; + clear(this.currentLiteral); + clear(this.output); + + for (char c : source.toCharArray()) { + if (c == '{') { + level++; + if (level == 1) { + encodeAndAppendCurrentLiteral(type); + } + } + if (c == '}') { + level--; + Assert.isTrue(level >=0, "Mismatched open and close braces"); + } + if (level > 0 || (level == 0 && c == '}')) { + this.output.append(c); + } + else { + this.currentLiteral.append(c); + } + } + + Assert.isTrue(level == 0, "Mismatched open and close braces"); + encodeAndAppendCurrentLiteral(type); + + return this.output.toString(); + } + + private void encodeAndAppendCurrentLiteral(Type type) { + this.output.append(encodeUriComponent(this.currentLiteral.toString(), this.charset, type)); + clear(this.currentLiteral); + } + + private void clear(StringBuilder sb) { + sb.delete(0, sb.length()); + } + } + + /** * Defines the contract for path (segments). */ @@ -698,11 +831,11 @@ final class HierarchicalUriComponents extends UriComponents { List getPathSegments(); - PathComponent encode(Charset charset); + PathComponent encode(BiFunction encoder); void verify(); - PathComponent expand(UriTemplateVariables uriVariables); + PathComponent expand(UriTemplateVariables uriVariables, @Nullable UnaryOperator encoder); void copyToUriComponentsBuilder(UriComponentsBuilder builder); } @@ -731,8 +864,8 @@ final class HierarchicalUriComponents extends UriComponents { } @Override - public PathComponent encode(Charset charset) { - String encodedPath = encodeUriComponent(getPath(), charset, Type.PATH); + public PathComponent encode(BiFunction encoder) { + String encodedPath = encoder.apply(getPath(), Type.PATH); return new FullPathComponent(encodedPath); } @@ -742,8 +875,8 @@ final class HierarchicalUriComponents extends UriComponents { } @Override - public PathComponent expand(UriTemplateVariables uriVariables) { - String expandedPath = expandUriComponent(getPath(), uriVariables); + public PathComponent expand(UriTemplateVariables uriVariables, @Nullable UnaryOperator encoder) { + String expandedPath = expandUriComponent(getPath(), uriVariables, encoder); return new FullPathComponent(expandedPath); } @@ -797,11 +930,11 @@ final class HierarchicalUriComponents extends UriComponents { } @Override - public PathComponent encode(Charset charset) { + public PathComponent encode(BiFunction encoder) { List pathSegments = getPathSegments(); List encodedPathSegments = new ArrayList<>(pathSegments.size()); for (String pathSegment : pathSegments) { - String encodedPathSegment = encodeUriComponent(pathSegment, charset, Type.PATH_SEGMENT); + String encodedPathSegment = encoder.apply(pathSegment, Type.PATH_SEGMENT); encodedPathSegments.add(encodedPathSegment); } return new PathSegmentComponent(encodedPathSegments); @@ -815,11 +948,11 @@ final class HierarchicalUriComponents extends UriComponents { } @Override - public PathComponent expand(UriTemplateVariables uriVariables) { + public PathComponent expand(UriTemplateVariables uriVariables, @Nullable UnaryOperator encoder) { List pathSegments = getPathSegments(); List expandedPathSegments = new ArrayList<>(pathSegments.size()); for (String pathSegment : pathSegments) { - String expandedPathSegment = expandUriComponent(pathSegment, uriVariables); + String expandedPathSegment = expandUriComponent(pathSegment, uriVariables, encoder); expandedPathSegments.add(expandedPathSegment); } return new PathSegmentComponent(expandedPathSegments); @@ -874,10 +1007,10 @@ final class HierarchicalUriComponents extends UriComponents { } @Override - public PathComponent encode(Charset charset) { + public PathComponent encode(BiFunction encoder) { List encodedComponents = new ArrayList<>(this.pathComponents.size()); for (PathComponent pathComponent : this.pathComponents) { - encodedComponents.add(pathComponent.encode(charset)); + encodedComponents.add(pathComponent.encode(encoder)); } return new PathComponentComposite(encodedComponents); } @@ -890,10 +1023,10 @@ final class HierarchicalUriComponents extends UriComponents { } @Override - public PathComponent expand(UriTemplateVariables uriVariables) { + public PathComponent expand(UriTemplateVariables uriVariables, @Nullable UnaryOperator encoder) { List expandedComponents = new ArrayList<>(this.pathComponents.size()); for (PathComponent pathComponent : this.pathComponents) { - expandedComponents.add(pathComponent.expand(uriVariables)); + expandedComponents.add(pathComponent.expand(uriVariables, encoder)); } return new PathComponentComposite(expandedComponents); } diff --git a/spring-web/src/main/java/org/springframework/web/util/UriComponents.java b/spring-web/src/main/java/org/springframework/web/util/UriComponents.java index 8bb328b11f7..6e3898df77b 100644 --- a/spring-web/src/main/java/org/springframework/web/util/UriComponents.java +++ b/spring-web/src/main/java/org/springframework/web/util/UriComponents.java @@ -24,6 +24,7 @@ import java.util.Arrays; import java.util.Iterator; import java.util.List; import java.util.Map; +import java.util.function.UnaryOperator; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -128,21 +129,22 @@ public abstract class UriComponents implements Serializable { /** - * A variant of {@link #encode(Charset)} that uses "UTF-8" as the charset. - * @return a new {@code UriComponents} instance with encoded values + * Invoke this after expanding URI variables to encode the + * resulting URI component values. + *

In comparison to {@link UriComponentsBuilder#encode()}, this method + * quotes only illegal characters within a given URI component type, + * but not all characters with reserved meaning. For most cases, prefer + * using {@link UriComponentsBuilder#encode()} over this method. + * @see UriComponentsBuilder#encode() */ public final UriComponents encode() { return encode(StandardCharsets.UTF_8); } /** - * Encode each URI component by percent encoding illegal characters, which - * includes non-US-ASCII characters, and also characters that are otherwise - * illegal within a given URI component type, as defined in RFC 3986. The - * effect of this method, with regards to encoding, is comparable to using - * the multi-argument constructor of {@link URI}. - * @param charset the encoding of the values contained in this map - * @return a new {@code UriComponents} instance with encoded values + * A variant of {@link #encode()} with a charset other than "UTF-8". + * @param charset the charset to use for encoding + * @see UriComponentsBuilder#encode(Charset) */ public abstract UriComponents encode(Charset charset); @@ -235,6 +237,13 @@ public abstract class UriComponents implements Serializable { @Nullable static String expandUriComponent(@Nullable String source, UriTemplateVariables uriVariables) { + return expandUriComponent(source, uriVariables, null); + } + + @Nullable + static String expandUriComponent(@Nullable String source, UriTemplateVariables uriVariables, + @Nullable UnaryOperator variableEncoder) { + if (source == null) { return null; } @@ -249,13 +258,14 @@ public abstract class UriComponents implements Serializable { while (matcher.find()) { String match = matcher.group(1); String variableName = getVariableName(match); - Object variableValue = uriVariables.getValue(variableName); - if (UriTemplateVariables.SKIP_VALUE.equals(variableValue)) { + Object variablesValue = uriVariables.getValue(variableName); + if (UriTemplateVariables.SKIP_VALUE.equals(variablesValue)) { continue; } - String variableValueString = getVariableValueAsString(variableValue); - String replacement = Matcher.quoteReplacement(variableValueString); - matcher.appendReplacement(sb, replacement); + String formattedValue = getVariableValueAsString(variablesValue); + formattedValue = Matcher.quoteReplacement(formattedValue); + formattedValue = variableEncoder != null ? variableEncoder.apply(formattedValue) : formattedValue; + matcher.appendReplacement(sb, formattedValue); } matcher.appendTail(sb); return sb.toString(); @@ -298,6 +308,11 @@ public abstract class UriComponents implements Serializable { */ public interface UriTemplateVariables { + /** + * Constant for a value that indicates a URI variable name should be + * ignored and left as is. This is useful for partial expanding of some + * but not all URI variables. + */ Object SKIP_VALUE = UriTemplateVariables.class; /** 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 9b29f13762e..128384fbf37 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 @@ -17,6 +17,8 @@ package org.springframework.web.util; import java.net.URI; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.LinkedList; import java.util.List; @@ -119,6 +121,10 @@ public class UriComponentsBuilder implements UriBuilder, Cloneable { @Nullable private String fragment; + private boolean encodeTemplate; + + private Charset charset = StandardCharsets.UTF_8; + /** * Default constructor. Protected to prevent direct instantiation. @@ -319,6 +325,43 @@ public class UriComponentsBuilder implements UriBuilder, Cloneable { } + // encode methods + + /** + * Request to have the URI template encoded first at build time, and + * URI variables encoded later when expanded. + * + *

In comparison to {@link UriComponents#encode()}, this method has the + * same effect on the URI template, i.e. each URI component is encoded by + * quoting only illegal characters within that URI component type. + * However URI variables are encoded more strictly, by quoting both illegal + * characters and characters with reserved meaning. + * + *

For most cases, prefer this method over {@link UriComponents#encode()} + * which is useful only if intentionally expanding variables with reserved + * characters. For example ';' is legal in a path, but also has reserved + * meaning as a separator. When expanding a variable that contains ';' it + * probably should be encoded, unless the intent is to insert path params + * through the expanded variable. + * + * @since 5.0.8 + */ + public final UriComponentsBuilder encode() { + return encode(StandardCharsets.UTF_8); + } + + /** + * A variant of {@link #encode()} with a charset other than "UTF-8". + * @param charset the charset to use for encoding + * @since 5.0.8 + */ + public UriComponentsBuilder encode(Charset charset) { + this.encodeTemplate = true; + this.charset = charset; + return this; + } + + // build methods /** @@ -341,8 +384,11 @@ public class UriComponentsBuilder implements UriBuilder, Cloneable { return new OpaqueUriComponents(this.scheme, this.ssp, this.fragment); } else { - return new HierarchicalUriComponents(this.scheme, this.fragment, this.userInfo, - this.host, this.port, this.pathBuilder.build(), this.queryParams, encoded, true); + HierarchicalUriComponents uriComponents = + new HierarchicalUriComponents(this.scheme, this.fragment, this.userInfo, + this.host, this.port, this.pathBuilder.build(), this.queryParams, encoded); + + return this.encodeTemplate ? uriComponents.encodeTemplate(this.charset) : uriComponents; } } @@ -354,7 +400,7 @@ public class UriComponentsBuilder implements UriBuilder, Cloneable { * @return the URI components with expanded values */ public UriComponents buildAndExpand(Map uriVariables) { - return build(false).expand(uriVariables); + return build().expand(uriVariables); } /** @@ -365,30 +411,17 @@ public class UriComponentsBuilder implements UriBuilder, Cloneable { * @return the URI components with expanded values */ public UriComponents buildAndExpand(Object... uriVariableValues) { - return build(false).expand(uriVariableValues); + return build().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(); + return encode().buildAndExpand(uriVariables).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(); + return encode().buildAndExpand(uriVariables).toUri(); } @@ -400,7 +433,7 @@ public class UriComponentsBuilder implements UriBuilder, Cloneable { * @see UriComponents#toUriString() */ public String toUriString() { - return build(false).encode().toUriString(); + return build().encode().toUriString(); } diff --git a/spring-web/src/test/java/org/springframework/web/util/UriComponentsTests.java b/spring-web/src/test/java/org/springframework/web/util/UriComponentsTests.java index 980dac38bbc..9ee01d00866 100644 --- a/spring-web/src/test/java/org/springframework/web/util/UriComponentsTests.java +++ b/spring-web/src/test/java/org/springframework/web/util/UriComponentsTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2018 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. @@ -24,9 +24,13 @@ import java.net.URI; import java.net.URISyntaxException; import java.util.Arrays; import java.util.Collections; +import java.util.HashMap; +import java.util.Map; import org.junit.Test; +import org.springframework.web.util.UriComponents.UriTemplateVariables; + import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.instanceOf; import static org.hamcrest.Matchers.not; @@ -35,16 +39,50 @@ import static org.junit.Assert.assertThat; import static org.springframework.web.util.UriComponentsBuilder.fromUriString; /** + * Unit tests for {@link UriComponents}. + * * @author Arjen Poutsma * @author Phillip Webb + * @author Rossen Stoyanchev */ public class UriComponentsTests { @Test - public void encode() { - UriComponents uriComponents = UriComponentsBuilder.fromPath("/hotel list").build(); - UriComponents encoded = uriComponents.encode(); - assertEquals("/hotel%20list", encoded.getPath()); + public void expandAndEncode() { + + UriComponents uri = UriComponentsBuilder + .fromPath("/hotel list/{city} specials").queryParam("q", "{value}").build() + .expand("Z\u00fcrich", "a+b").encode(); + + assertEquals("/hotel%20list/Z%C3%BCrich%20specials?q=a+b", uri.toString()); + } + + @Test + public void encodeAndExpand() { + + UriComponents uri = UriComponentsBuilder + .fromPath("/hotel list/{city} specials").queryParam("q", "{value}").encode().build() + .expand("Z\u00fcrich", "a+b"); + + assertEquals("/hotel%20list/Z%C3%BCrich%20specials?q=a%2Bb", uri.toString()); + } + + @Test + public void encodeAndExpandPartially() { + + Map uriVars = new HashMap<>(); + uriVars.put("city", "Z\u00fcrich"); + + UriComponents uri = UriComponentsBuilder + .fromPath("/hotel list/{city} specials").queryParam("q", "{value}").encode().build() + .expand(name -> uriVars.getOrDefault(name, UriTemplateVariables.SKIP_VALUE)); + + assertEquals("/hotel%20list/Z%C3%BCrich%20specials?q={value}", uri.toString()); + + uriVars.put("value", "a+b"); + uri = uri.expand(uriVars); + + assertEquals("/hotel%20list/Z%C3%BCrich%20specials?q=a%2Bb", uri.toString()); } @Test @@ -86,9 +124,7 @@ public class UriComponentsTests { assertEquals("http://example.com/1 2 3 4", uriComponents.toUriString()); } - // SPR-13311 - - @Test + @Test // SPR-13311 public void expandWithRegexVar() { String template = "/myurl/{name:[a-z]{1,5}}/show"; UriComponents uriComponents = UriComponentsBuilder.fromUriString(template).build(); @@ -96,9 +132,7 @@ public class UriComponentsTests { assertEquals("/myurl/test/show", uriComponents.getPath()); } - // SPR-12123 - - @Test + @Test // SPR-12123 public void port() { UriComponents uri1 = fromUriString("http://example.com:8080/bar").build(); UriComponents uri2 = fromUriString("http://example.com/bar").port(8080).build(); @@ -158,7 +192,7 @@ public class UriComponentsTests { } @Test - public void equalsHierarchicalUriComponents() throws Exception { + public void equalsHierarchicalUriComponents() { String url = "http://example.com"; UriComponents uric1 = UriComponentsBuilder.fromUriString(url).path("/{foo}").query("bar={baz}").build(); UriComponents uric2 = UriComponentsBuilder.fromUriString(url).path("/{foo}").query("bar={baz}").build(); @@ -170,7 +204,7 @@ public class UriComponentsTests { } @Test - public void equalsOpaqueUriComponents() throws Exception { + public void equalsOpaqueUriComponents() { String baseUrl = "http:example.com"; UriComponents uric1 = UriComponentsBuilder.fromUriString(baseUrl + "/foo/bar").build(); UriComponents uric2 = UriComponentsBuilder.fromUriString(baseUrl + "/foo/bar").build();