expandedValues = result.computeIfAbsent(name, k -> new ArrayList<>(values.size()));
- for (String value : values) {
- expandedValues.add(expandUriComponent(value, queryVariables, this.variableEncoder));
- }
- });
- return CollectionUtils.unmodifiableMultiValueMap(result);
+ return this.queryParams.stream()
+ .map(qp -> qp.expandUriComponent(queryVariables, this.variableEncoder))
+ .toList();
}
@Override
@@ -568,6 +538,7 @@ public int hashCode() {
/**
* Enumeration used to identify the allowed characters per URI component.
* Contains methods to indicate whether a given character is valid in a specific URI component.
+ *
* @see RFC 3986
*/
enum Type {
@@ -749,7 +720,7 @@ private enum EncodeState {
* 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;
@@ -833,7 +804,7 @@ else if (level > 0) {
* for example, {@code "/{year:\d{1,4}}"}.
*/
private boolean isUriVariable(CharSequence source) {
- if (source.length() < 2 || source.charAt(0) != '{' || source.charAt(source.length() -1) != '}') {
+ if (source.length() < 2 || source.charAt(0) != '{' || source.charAt(source.length() - 1) != '}') {
return false;
}
boolean hasText = false;
@@ -1097,4 +1068,29 @@ else if (value instanceof Collection> collection) {
}
}
+ record QueryParam(String name, @Nullable String value) implements Serializable{
+ public String toUriString() {
+ return this.name + (this.value == null ? "" : "=" + this.value);
+ }
+
+ public QueryParam encodeQueryParam(BiFunction encoder) {
+ return new QueryParam(encoder.apply(this.name, Type.QUERY_PARAM),
+ this.value != null ? encoder.apply(this.value, Type.QUERY_PARAM) : null);
+ }
+
+ public void verifyUriComponent() {
+ HierarchicalUriComponents.verifyUriComponent(this.name, Type.QUERY_PARAM);
+ HierarchicalUriComponents.verifyUriComponent(this.value, Type.QUERY_PARAM);
+ }
+
+ public QueryParam expandUriComponent(UriTemplateVariables queryVariables, @Nullable UnaryOperator variableEncoder) {
+ return new QueryParam(Objects.requireNonNull(UriComponents.expandUriComponent(this.name, queryVariables, variableEncoder)),
+ UriComponents.expandUriComponent(this.value, queryVariables, variableEncoder));
+ }
+
+ public boolean hasName(String name) {
+ return name.equals(this.name);
+ }
+ }
+
}
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 e8783e4cf2e0..3e1cf162d0cf 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
@@ -34,7 +34,6 @@
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
-import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
@@ -89,7 +88,7 @@ public class UriComponentsBuilder implements UriBuilder, Cloneable {
private CompositePathComponentBuilder pathBuilder;
- private final MultiValueMap queryParams = new LinkedMultiValueMap<>();
+ private final List queryParams = new ArrayList<>();
private @Nullable String fragment;
@@ -286,7 +285,7 @@ private UriComponents buildInternal(EncodingHint hint) {
result = new OpaqueUriComponents(this.scheme, this.ssp, this.fragment);
}
else {
- MultiValueMap queryParams = new LinkedMultiValueMap<>(this.queryParams);
+ List queryParams = new ArrayList<>(this.queryParams);
HierarchicalUriComponents uric = new HierarchicalUriComponents(this.scheme, this.fragment,
this.userInfo, this.host, this.port, this.pathBuilder.build(), queryParams,
hint == EncodingHint.FULLY_ENCODED);
@@ -575,11 +574,11 @@ public UriComponentsBuilder queryParam(String name, @Nullable Object... values)
if (!ObjectUtils.isEmpty(values)) {
for (Object value : values) {
String valueAsString = getQueryParamValue(value);
- this.queryParams.add(name, valueAsString);
+ this.queryParams.add(new HierarchicalUriComponents.QueryParam(name, valueAsString));
}
}
else {
- this.queryParams.add(name, null);
+ this.queryParams.add(new HierarchicalUriComponents.QueryParam(name, null));
}
resetSchemeSpecificPart();
return this;
@@ -619,16 +618,27 @@ public UriComponentsBuilder queryParamIfPresent(String name, Optional> value)
@Override
public UriComponentsBuilder queryParams(@Nullable MultiValueMap params) {
if (params != null) {
- this.queryParams.addAll(params);
+ addAllToQueryParams(params);
resetSchemeSpecificPart();
}
return this;
}
+ private void addAllToQueryParams(MultiValueMap params) {
+ params.forEach((key, values) -> {
+ if (values.isEmpty()) {
+ this.queryParams.add(new HierarchicalUriComponents.QueryParam(key, null));
+ }
+ else{
+ values.forEach(v -> this.queryParams.add(new HierarchicalUriComponents.QueryParam(key, v)));
+ }
+ });
+ }
+
@Override
public UriComponentsBuilder replaceQueryParam(String name, Object... values) {
Assert.notNull(name, "Name must not be null");
- this.queryParams.remove(name);
+ this.queryParams.removeIf(qp->qp.hasName(name));
if (!ObjectUtils.isEmpty(values)) {
queryParam(name, values);
}
@@ -649,7 +659,7 @@ public UriComponentsBuilder replaceQueryParam(String name, @Nullable Collection<
public UriComponentsBuilder replaceQueryParams(@Nullable MultiValueMap params) {
this.queryParams.clear();
if (params != null) {
- this.queryParams.putAll(params);
+ addAllToQueryParams(params);
}
return this;
}
diff --git a/spring-web/src/test/java/org/springframework/web/filter/RequestLoggingFilterTests.java b/spring-web/src/test/java/org/springframework/web/filter/RequestLoggingFilterTests.java
index 349834760492..6e6de9197283 100644
--- a/spring-web/src/test/java/org/springframework/web/filter/RequestLoggingFilterTests.java
+++ b/spring-web/src/test/java/org/springframework/web/filter/RequestLoggingFilterTests.java
@@ -114,8 +114,9 @@ void queryStringIncluded() throws Exception {
applyFilter();
- assertThat(filter.beforeRequestMessage).contains("/hotels?booking=42&code=masked&category=hotel&category=resort&ignore=masked");
- assertThat(filter.afterRequestMessage).contains("/hotels?booking=42&code=masked&category=hotel&category=resort&ignore=masked");
+ String expectedRequest = "/hotels?booking=42&code=masked&ignore=masked&category=hotel&category=resort";
+ assertThat(filter.beforeRequestMessage).contains(expectedRequest);
+ assertThat(filter.afterRequestMessage).contains(expectedRequest);
}
@Test
diff --git a/spring-web/src/test/java/org/springframework/web/util/UriComponentsBuilderTests.java b/spring-web/src/test/java/org/springframework/web/util/UriComponentsBuilderTests.java
index 991b91801d7f..94e6a46ac568 100644
--- a/spring-web/src/test/java/org/springframework/web/util/UriComponentsBuilderTests.java
+++ b/spring-web/src/test/java/org/springframework/web/util/UriComponentsBuilderTests.java
@@ -17,6 +17,7 @@
package org.springframework.web.util;
import java.net.URI;
+import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
@@ -25,9 +26,11 @@
import java.util.Map;
import java.util.Optional;
import java.util.function.BiConsumer;
+import java.util.stream.Stream;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.CsvSource;
import org.junit.jupiter.params.provider.EnumSource;
import org.springframework.util.LinkedMultiValueMap;
@@ -930,4 +933,50 @@ void expandPortAndPathWithoutSeparator(ParserType parserType) {
assertThat(uri.toString()).isEqualTo("ws://localhost:7777/test");
}
+ @ParameterizedTest(name = "{0} for {3}, {4}, {5} with parser {1}") //gh 34788
+ @CsvSource(textBlock = """
+ #testedPattern parserType urlExpected p1 p2 p3
+ 'http://myhost?a={p1}&b={p2}&a={p3}',RFC, 'http://myhost?a=a1&b=b1&a=a2','a1','b1','a2'
+ 'http://myhost?a={p1}&b={p2}&a={p3}',WHAT_WG, 'http://myhost?a=a1&b=b1&a=a2','a1','b1','a2'
+ 'http://myhost?a={p1}&b={p2}&a={p1}',RFC, 'http://myhost?a=a1&b=b1&a=a1','a1','b1','a1'
+ 'http://myhost?a={p1}&a={p1}&b={p3}',RFC, 'http://myhost?a=a1&a=a1&b=b1','a1','a1','b1',
+ 'http://myhost?a={p1}&a={p2}&b={p3}',RFC, 'http://myhost?a=a1&a=a2&b=b1','a1','a2','b1'
+ 'http://myhost?a={p1}&b={p2}&a={p1}',RFC, 'http://myhost?a=a1&b=&a=a1','a1','','a1'
+ 'http://myhost?a={p1}&b=&a={p2}', RFC, 'http://myhost?a=a1&b=&a=a2','a1','a2',
+ 'http://myhost?a={p1}&b=t&a={p2}', RFC, 'http://myhost?a=a1&b=t&a=a2','a1','a2',
+ 'http://myhost?a=t&b={p1}&a={p2}', RFC, 'http://myhost?a=t&b=b1&a=a1','b1','a1',
+ 'http://myhost?a={p1}&b={p2}&a={p1}',WHAT_WG, 'http://myhost?a=a1&b=b1&a=a1','a1','b1','a1'
+ 'http://myhost?a={p1}&a={p1}&b={p3}',WHAT_WG, 'http://myhost?a=a1&a=a1&b=b1','a1','a1','b1',
+ 'http://myhost?a={p1}&a={p2}&b={p3}',WHAT_WG, 'http://myhost?a=a1&a=a2&b=b1','a1','a2','b1'
+ 'http://myhost?a={p1}&b={p2}&a={p1}',WHAT_WG, 'http://myhost?a=a1&b=&a=a1','a1','','a1'
+ 'http://myhost?a={p1}&b=&a={p2}', WHAT_WG, 'http://myhost?a=a1&b=&a=a2','a1','a2',
+ 'http://myhost?a={p1}&b=t&a={p2}', WHAT_WG, 'http://myhost?a=a1&b=t&a=a2','a1','a2',
+ 'http://myhost?a=t&b={p1}&a={p2}', WHAT_WG, 'http://myhost?a=t&b=b1&a=a1','b1','a1',
+ """)
+ void queryParamOrderShouldBeKept(String testedPattern, ParserType parserType, String urlexpected, String p1, String p2, String p3){
+ ArrayList params = new ArrayList<>();
+ Stream.of(p1, p2, p3).forEach(p->addIfNotNull(p, params));
+ Map paramsMap = new HashMap<>();
+ putIfNotNull("p1", p1, paramsMap);
+ putIfNotNull("p2", p2, paramsMap);
+ putIfNotNull("p3", p3, paramsMap);
+ UriComponentsBuilder uriComponentsBuilder = UriComponentsBuilder.fromUriString(testedPattern, parserType);
+ assertThat(uriComponentsBuilder).satisfies(
+ ucb -> assertThat(ucb.buildAndExpand(params.toArray())).as("with params as varags").hasToString(urlexpected),
+ ucb -> assertThat(ucb.buildAndExpand(paramsMap)).as("with params as Map").hasToString(urlexpected)
+ );
+ }
+
+ private static void putIfNotNull(String key, String param, Map paramsMap) {
+ if (param != null) {
+ paramsMap.put(key, param);
+ }
+ }
+
+ private static void addIfNotNull(String param, ArrayList params) {
+ if (param !=null){
+ params.add(param);
+ }
+ }
+
}