diff --git a/spring-core/src/main/java/org/springframework/util/CollectionUtils.java b/spring-core/src/main/java/org/springframework/util/CollectionUtils.java index 530e5222ba0..a574fff2674 100644 --- a/spring-core/src/main/java/org/springframework/util/CollectionUtils.java +++ b/spring-core/src/main/java/org/springframework/util/CollectionUtils.java @@ -470,13 +470,10 @@ public abstract class CollectionUtils { MultiValueMap targetMap) { Assert.notNull(targetMap, "'targetMap' must not be null"); - Map> result = newLinkedHashMap(targetMap.size()); - targetMap.forEach((key, value) -> { - List values = Collections.unmodifiableList(value); - result.put(key, (List) values); - }); - Map> unmodifiableMap = Collections.unmodifiableMap(result); - return toMultiValueMap(unmodifiableMap); + if (targetMap instanceof UnmodifiableMultiValueMap) { + return (MultiValueMap) targetMap; + } + return new UnmodifiableMultiValueMap<>(targetMap); } diff --git a/spring-core/src/main/java/org/springframework/util/UnmodifiableMultiValueMap.java b/spring-core/src/main/java/org/springframework/util/UnmodifiableMultiValueMap.java new file mode 100644 index 00000000000..7623ff8770f --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/UnmodifiableMultiValueMap.java @@ -0,0 +1,732 @@ +/* + * Copyright 2002-2021 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 + * + * https://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.util; + +import java.io.Serializable; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.Spliterator; +import java.util.function.BiConsumer; +import java.util.function.BiFunction; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; + +import org.springframework.lang.Nullable; + +/** + * Unmodifiable wrapper for {@link MultiValueMap}. + * + * @author Arjen Poutsma + * @since 6.0 + * @param the key type + * @param the value element type + */ +final class UnmodifiableMultiValueMap implements MultiValueMap, Serializable { + + private static final long serialVersionUID = -8697084563854098920L; + + + private final MultiValueMap delegate; + + @Nullable + private transient Set keySet; + + @Nullable + private transient Set>> entrySet; + + @Nullable + private transient Collection> values; + + + @SuppressWarnings("unchecked") + public UnmodifiableMultiValueMap(MultiValueMap delegate) { + Assert.notNull(delegate, "Delegate must not be null"); + this.delegate = (MultiValueMap) delegate; + } + + // delegation + + @Override + public int size() { + return this.delegate.size(); + } + + @Override + public boolean isEmpty() { + return this.delegate.isEmpty(); + } + + @Override + public boolean containsKey(Object key) { + return this.delegate.containsKey(key); + } + + @Override + public boolean containsValue(Object value) { + return this.delegate.containsValue(value); + } + + @Override + @Nullable + public List get(Object key) { + List result = this.delegate.get(key); + return result != null ? Collections.unmodifiableList(result) : null; + } + + @Override + public V getFirst(K key) { + return this.delegate.getFirst(key); + } + + @Override + public List getOrDefault(Object key, List defaultValue) { + List result = this.delegate.getOrDefault(key, defaultValue); + if (result != defaultValue) { + result = Collections.unmodifiableList(result); + } + return result; + } + + @Override + public void forEach(BiConsumer> action) { + this.delegate.forEach((k, vs) -> action.accept(k, Collections.unmodifiableList(vs))); + } + + @Override + public Map toSingleValueMap() { + return this.delegate.toSingleValueMap(); + } + + @Override + public int hashCode() { + return this.delegate.hashCode(); + } + + @Override + public boolean equals(Object obj) { + return this == obj || this.delegate.equals(obj); + } + + @Override + public String toString() { + return this.delegate.toString(); + } + + // lazy init + + @Override + public Set keySet() { + if (this.keySet == null) { + this.keySet = Collections.unmodifiableSet(this.delegate.keySet()); + } + return this.keySet; + } + + @Override + public Set>> entrySet() { + if (this.entrySet == null) { + this.entrySet = new UnmodifiableEntrySet<>(this.delegate.entrySet()); + } + return this.entrySet; + } + + @Override + public Collection> values() { + if (this.values == null) { + this.values = new UnmodifiableValueCollection<>(this.delegate.values()); + } + return this.values; + } + + // unsupported + + @Nullable + @Override + public List put(K key, List value) { + throw new UnsupportedOperationException(); + } + + @Override + public List putIfAbsent(K key, List value) { + throw new UnsupportedOperationException(); + } + + @Override + public void putAll(Map> m) { + throw new UnsupportedOperationException(); + } + + @Override + public List remove(Object key) { + throw new UnsupportedOperationException(); + } + + @Override + public void add(K key, @Nullable V value) { + throw new UnsupportedOperationException(); + } + + @Override + public void addAll(K key, List values) { + throw new UnsupportedOperationException(); + } + + @Override + public void addAll(MultiValueMap values) { + throw new UnsupportedOperationException(); + } + + @Override + public void addIfAbsent(K key, @Nullable V value) { + throw new UnsupportedOperationException(); + } + + @Override + public void set(K key, @Nullable V value) { + throw new UnsupportedOperationException(); + } + + @Override + public void setAll(Map values) { + throw new UnsupportedOperationException(); + } + + @Override + public void replaceAll(BiFunction, ? extends List> function) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean remove(Object key, Object value) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean replace(K key, List oldValue, List newValue) { + throw new UnsupportedOperationException(); + } + + @Override + public List replace(K key, List value) { + throw new UnsupportedOperationException(); + } + + @Override + public List computeIfAbsent(K key, Function> mappingFunction) { + throw new UnsupportedOperationException(); + } + + @Override + public List computeIfPresent(K key, + BiFunction, ? extends List> remappingFunction) { + throw new UnsupportedOperationException(); + } + + @Override + public List compute(K key, + BiFunction, ? extends List> remappingFunction) { + throw new UnsupportedOperationException(); + } + + @Override + public List merge(K key, List value, + BiFunction, ? super List, ? extends List> remappingFunction) { + throw new UnsupportedOperationException(); + } + + @Override + public void clear() { + throw new UnsupportedOperationException(); + } + + + private static class UnmodifiableEntrySet implements Set>>, Serializable { + + private static final long serialVersionUID = 2407578793783925203L; + + + private final Set>> delegate; + + + @SuppressWarnings("unchecked") + public UnmodifiableEntrySet(Set>> delegate) { + this.delegate = (Set>>) delegate; + } + + // delegation + + @Override + public int size() { + return this.delegate.size(); + } + + @Override + public boolean isEmpty() { + return this.delegate.isEmpty(); + } + + @Override + public boolean contains(Object o) { + return this.delegate.contains(o); + } + + @Override + public boolean containsAll(Collection c) { + return this.delegate.containsAll(c); + } + + @Override + public Iterator>> iterator() { + Iterator>> iterator = this.delegate.iterator(); + return new Iterator<>() { + @Override + public boolean hasNext() { + return iterator.hasNext(); + } + + @Override + public Entry> next() { + return new UnmodifiableEntry<>(iterator.next()); + } + }; + } + + @Override + public Object[] toArray() { + Object[] result = this.delegate.toArray(); + filterArray(result); + return result; + } + + @Override + public T[] toArray(T[] a) { + T[] result = this.delegate.toArray(a); + filterArray(result); + return result; + } + + @SuppressWarnings("unchecked") + private void filterArray(Object[] result) { + for (int i = 0; i < result.length; i++) { + if (result[i] instanceof Map.Entry entry) { + result[i] = new UnmodifiableEntry<>((Entry>) entry); + } + } + } + + @Override + public void forEach(Consumer>> action) { + this.delegate.forEach(e -> action.accept(new UnmodifiableEntry<>(e))); + } + + @Override + public Stream>> stream() { + return StreamSupport.stream(spliterator(), false); + } + + @Override + public Stream>> parallelStream() { + return StreamSupport.stream(spliterator(), true); + } + + @Override + public Spliterator>> spliterator() { + return new UnmodifiableEntrySpliterator<>(this.delegate.spliterator()); + } + + @Override + public int hashCode() { + return this.delegate.hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + else if (obj instanceof Set other) { + return other.size() == this.delegate.size() && + containsAll(other); + } + return false; + } + + @Override + public String toString() { + return this.delegate.toString(); + } + + // unsupported + + @Override + public boolean add(Entry> kListEntry) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean remove(Object o) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean removeIf(Predicate>> filter) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean addAll(Collection>> c) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean retainAll(Collection c) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean removeAll(Collection c) { + throw new UnsupportedOperationException(); + } + + @Override + public void clear() { + throw new UnsupportedOperationException(); + } + + + private static class UnmodifiableEntrySpliterator implements Spliterator>> { + + private final Spliterator>> delegate; + + + @SuppressWarnings("unchecked") + public UnmodifiableEntrySpliterator( + Spliterator>> delegate) { + this.delegate = (Spliterator>>) delegate; + } + + @Override + public boolean tryAdvance(Consumer>> action) { + return this.delegate.tryAdvance(entry -> action.accept(new UnmodifiableEntry<>(entry))); + } + + @Override + public void forEachRemaining(Consumer>> action) { + this.delegate.forEachRemaining(entry -> action.accept(new UnmodifiableEntry<>(entry))); + } + + @Override + @Nullable + public Spliterator>> trySplit() { + Spliterator>> split = this.delegate.trySplit(); + if (split != null) { + return new UnmodifiableEntrySpliterator<>(split); + } + else { + return null; + } + } + + @Override + public long estimateSize() { + return this.delegate.estimateSize(); + } + + @Override + public long getExactSizeIfKnown() { + return this.delegate.getExactSizeIfKnown(); + } + + @Override + public int characteristics() { + return this.delegate.characteristics(); + } + + @Override + public boolean hasCharacteristics(int characteristics) { + return this.delegate.hasCharacteristics(characteristics); + } + + @Override + public Comparator>> getComparator() { + return this.delegate.getComparator(); + } + } + + + private static class UnmodifiableEntry implements Map.Entry> { + + private final Entry> delegate; + + + @SuppressWarnings("unchecked") + public UnmodifiableEntry(Entry> delegate) { + Assert.notNull(delegate, "Delegate must not be null"); + this.delegate = (Entry>) delegate; + } + + @Override + public K getKey() { + return this.delegate.getKey(); + } + + @Override + public List getValue() { + return Collections.unmodifiableList(this.delegate.getValue()); + } + + @Override + public List setValue(List value) { + throw new UnsupportedOperationException(); + } + + @Override + public int hashCode() { + return this.delegate.hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + else if (obj instanceof Map.Entry other) { + return getKey().equals(other.getKey()) && + getValue().equals(other.getValue()); + } + return false; + } + + @Override + public String toString() { + return this.delegate.toString(); + } + } + } + + + private static class UnmodifiableValueCollection implements Collection>, Serializable { + + private static final long serialVersionUID = 5518377583904339588L; + + private final Collection> delegate; + + + public UnmodifiableValueCollection(Collection> delegate) { + this.delegate = delegate; + } + + // delegation + + @Override + public int size() { + return this.delegate.size(); + } + + @Override + public boolean isEmpty() { + return this.delegate.isEmpty(); + } + + @Override + public boolean contains(Object o) { + return this.delegate.contains(o); + } + + @Override + public boolean containsAll(Collection c) { + return this.delegate.containsAll(c); + } + + @Override + public Object[] toArray() { + Object[] result = this.delegate.toArray(); + filterArray(result); + return result; + } + + @Override + public T[] toArray(T[] a) { + T[] result = this.delegate.toArray(a); + filterArray(result); + return result; + } + + private void filterArray(Object[] array) { + for (int i = 0; i < array.length; i++) { + if (array[i] instanceof List list) { + array[i] = Collections.unmodifiableList(list); + } + } + } + + @Override + public Iterator> iterator() { + Iterator> iterator = this.delegate.iterator(); + return new Iterator<>() { + @Override + public boolean hasNext() { + return iterator.hasNext(); + } + + @Override + public List next() { + return Collections.unmodifiableList(iterator.next()); + } + }; + } + + @Override + public void forEach(Consumer> action) { + this.delegate.forEach(list -> action.accept(Collections.unmodifiableList(list))); + } + + @Override + public Spliterator> spliterator() { + return new UnmodifiableValueSpliterator<>(this.delegate.spliterator()); + } + + @Override + public Stream> stream() { + return StreamSupport.stream(spliterator(), false); + } + + @Override + public Stream> parallelStream() { + return StreamSupport.stream(spliterator(), true); + } + + @Override + public int hashCode() { + return this.delegate.hashCode(); + } + + @Override + public boolean equals(Object obj) { + return this == obj || this.delegate.equals(obj); + } + + @Override + public String toString() { + return this.delegate.toString(); + } + + // unsupported + + @Override + public boolean add(List ts) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean remove(Object o) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean addAll(Collection> c) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean removeAll(Collection c) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean retainAll(Collection c) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean removeIf(Predicate> filter) { + throw new UnsupportedOperationException(); + } + + @Override + public void clear() { + throw new UnsupportedOperationException(); + } + + + private static class UnmodifiableValueSpliterator implements Spliterator> { + + private final Spliterator> delegate; + + + public UnmodifiableValueSpliterator(Spliterator> delegate) { + this.delegate = delegate; + } + + @Override + public boolean tryAdvance(Consumer> action) { + return this.delegate.tryAdvance(l -> action.accept(Collections.unmodifiableList(l))); + } + + @Override + public void forEachRemaining(Consumer> action) { + this.delegate.forEachRemaining(l -> action.accept(Collections.unmodifiableList(l))); + } + + @Override + @Nullable + public Spliterator> trySplit() { + Spliterator> split = this.delegate.trySplit(); + if (split != null) { + return new UnmodifiableValueSpliterator<>(split); + } + else { + return null; + } + } + + @Override + public long estimateSize() { + return this.delegate.estimateSize(); + } + + @Override + public long getExactSizeIfKnown() { + return this.delegate.getExactSizeIfKnown(); + } + + @Override + public int characteristics() { + return this.delegate.characteristics(); + } + + @Override + public boolean hasCharacteristics(int characteristics) { + return this.delegate.hasCharacteristics(characteristics); + } + + @Override + public Comparator> getComparator() { + return this.delegate.getComparator(); + } + } + + } +} diff --git a/spring-core/src/test/java/org/springframework/util/UnmodifiableMultiValueMapTests.java b/spring-core/src/test/java/org/springframework/util/UnmodifiableMultiValueMapTests.java new file mode 100644 index 00000000000..27c856eb564 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/util/UnmodifiableMultiValueMapTests.java @@ -0,0 +1,179 @@ +/* + * Copyright 2002-2021 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 + * + * https://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.util; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.assertj.core.api.ThrowableTypeAssert; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.entry; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +/** + * @author Arjen Poutsma + */ +class UnmodifiableMultiValueMapTests { + + @Test + void delegation() { + MultiValueMap mock = mock(MultiValueMap.class); + UnmodifiableMultiValueMap map = new UnmodifiableMultiValueMap<>(mock); + + given(mock.size()).willReturn(1); + assertThat(map.size()).isEqualTo(1); + + given(mock.isEmpty()).willReturn(false); + assertThat(map.isEmpty()).isFalse(); + + given(mock.containsKey("foo")).willReturn(true); + assertThat(map.containsKey("foo")).isTrue(); + + given(mock.containsValue(List.of("bar"))).willReturn(true); + assertThat(map.containsValue(List.of("bar"))).isTrue(); + + List list = new ArrayList<>(); + list.add("bar"); + given(mock.get("foo")).willReturn(list); + List result = map.get("foo"); + assertThat(result).isNotNull().containsExactly("bar"); + assertThatUnsupportedOperationException().isThrownBy(() -> result.add("baz")); + + given(mock.getOrDefault("foo", List.of("bar"))).willReturn(List.of("baz")); + assertThat(map.getOrDefault("foo", List.of("bar"))).containsExactly("baz"); + + given(mock.toSingleValueMap()).willReturn(Map.of("foo", "bar")); + assertThat(map.toSingleValueMap()).containsExactly(entry("foo", "bar")); + } + + @Test + void unsupported() { + UnmodifiableMultiValueMap map = new UnmodifiableMultiValueMap<>(new LinkedMultiValueMap<>()); + + assertThatUnsupportedOperationException().isThrownBy(() -> map.put("foo", List.of("bar"))); + assertThatUnsupportedOperationException().isThrownBy(() -> map.putIfAbsent("foo", List.of("bar"))); + assertThatUnsupportedOperationException().isThrownBy(() -> map.putAll(Map.of("foo", List.of("bar")))); + assertThatUnsupportedOperationException().isThrownBy(() -> map.remove("foo")); + assertThatUnsupportedOperationException().isThrownBy(() -> map.add("foo", "bar")); + assertThatUnsupportedOperationException().isThrownBy(() -> map.addAll("foo", List.of("bar"))); + assertThatUnsupportedOperationException().isThrownBy(() -> map.addAll(new LinkedMultiValueMap<>())); + assertThatUnsupportedOperationException().isThrownBy(() -> map.addIfAbsent("foo", "baz")); + assertThatUnsupportedOperationException().isThrownBy(() -> map.set("foo", "baz")); + assertThatUnsupportedOperationException().isThrownBy(() -> map.setAll(Map.of("foo", "baz"))); + assertThatUnsupportedOperationException().isThrownBy(() -> map.replaceAll((s, strings) -> strings)); + assertThatUnsupportedOperationException().isThrownBy(() -> map.remove("foo", List.of("bar"))); + assertThatUnsupportedOperationException().isThrownBy(() -> map.replace("foo", List.of("bar"))); + assertThatUnsupportedOperationException().isThrownBy(() -> map.replace("foo", List.of("bar"), List.of("baz"))); + assertThatUnsupportedOperationException().isThrownBy(() -> map.computeIfAbsent("foo", s -> List.of("bar"))); + assertThatUnsupportedOperationException().isThrownBy( + () -> map.computeIfPresent("foo", (s1, s2) -> List.of("bar"))); + assertThatUnsupportedOperationException().isThrownBy(() -> map.compute("foo", (s1, s2) -> List.of("bar"))); + assertThatUnsupportedOperationException().isThrownBy(() -> map.merge("foo", List.of("bar"), (s1, s2) -> s1)); + assertThatUnsupportedOperationException().isThrownBy(() -> map.clear()); + } + + @Test + void entrySetDelegation() { + MultiValueMap mockMap = mock(MultiValueMap.class); + Set>> mockSet = mock(Set.class); + given(mockMap.entrySet()).willReturn(mockSet); + Set>> set = new UnmodifiableMultiValueMap<>(mockMap).entrySet(); + + given(mockSet.size()).willReturn(1); + assertThat(set.size()).isEqualTo(1); + + given(mockSet.isEmpty()).willReturn(false); + assertThat(set.isEmpty()).isFalse(); + + given(mockSet.contains("foo")).willReturn(true); + assertThat(set.contains("foo")).isTrue(); + + List>> mockEntries = List.of(mock(Map.Entry.class)); + given(mockSet.containsAll(mockEntries)).willReturn(true); + assertThat(set.containsAll(mockEntries)).isTrue(); + + Iterator>> mockIterator = mock(Iterator.class); + given(mockSet.iterator()).willReturn(mockIterator); + given(mockIterator.hasNext()).willReturn(false); + assertThat(set.iterator()).isExhausted(); + } + + @Test + void entrySetUnsupported() { + Set>> set = new UnmodifiableMultiValueMap(new LinkedMultiValueMap<>()).entrySet(); + + assertThatUnsupportedOperationException().isThrownBy(() -> set.add(mock(Map.Entry.class))); + assertThatUnsupportedOperationException().isThrownBy(() -> set.remove("foo")); + assertThatUnsupportedOperationException().isThrownBy(() -> set.removeIf(e -> true)); + assertThatUnsupportedOperationException().isThrownBy(() -> set.addAll(mock(List.class))); + assertThatUnsupportedOperationException().isThrownBy(() -> set.retainAll(mock(List.class))); + assertThatUnsupportedOperationException().isThrownBy(() -> set.removeAll(mock(List.class))); + assertThatUnsupportedOperationException().isThrownBy(() -> set.clear()); + } + + @Test + void valuesDelegation() { + MultiValueMap mockMap = mock(MultiValueMap.class); + Collection> mockValues = mock(Collection.class); + given(mockMap.values()).willReturn(mockValues); + Collection> values = new UnmodifiableMultiValueMap<>(mockMap).values(); + + given(mockValues.size()).willReturn(1); + assertThat(values.size()).isEqualTo(1); + + given(mockValues.isEmpty()).willReturn(false); + assertThat(values.isEmpty()).isFalse(); + + given(mockValues.contains(List.of("foo"))).willReturn(true); + assertThat(mockValues.contains(List.of("foo"))).isTrue(); + + given(mockValues.containsAll(List.of(List.of("foo")))).willReturn(true); + assertThat(mockValues.containsAll(List.of(List.of("foo")))).isTrue(); + + Iterator> mockIterator = mock(Iterator.class); + given(mockValues.iterator()).willReturn(mockIterator); + given(mockIterator.hasNext()).willReturn(false); + assertThat(values.iterator()).isExhausted(); + } + + @Test + void valuesUnsupported() { + Collection> values = + new UnmodifiableMultiValueMap(new LinkedMultiValueMap<>()).values(); + + assertThatUnsupportedOperationException().isThrownBy(() -> values.add(List.of("foo"))); + assertThatUnsupportedOperationException().isThrownBy(() -> values.remove(List.of("foo"))); + assertThatUnsupportedOperationException().isThrownBy(() -> values.addAll(List.of(List.of("foo")))); + assertThatUnsupportedOperationException().isThrownBy(() -> values.removeAll(List.of(List.of("foo")))); + assertThatUnsupportedOperationException().isThrownBy(() -> values.retainAll(List.of(List.of("foo")))); + assertThatUnsupportedOperationException().isThrownBy(() -> values.removeIf(s -> true)); + assertThatUnsupportedOperationException().isThrownBy(() -> values.clear()); + } + + private static ThrowableTypeAssert assertThatUnsupportedOperationException() { + return assertThatExceptionOfType(UnsupportedOperationException.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 68c98562a28..37fbf44b7b9 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 @@ -479,8 +479,9 @@ public class UriComponentsBuilder implements UriBuilder, Cloneable { result = new OpaqueUriComponents(this.scheme, this.ssp, this.fragment); } else { + MultiValueMap queryParams = new LinkedMultiValueMap<>(this.queryParams); HierarchicalUriComponents uric = new HierarchicalUriComponents(this.scheme, this.fragment, - this.userInfo, this.host, this.port, this.pathBuilder.build(), this.queryParams, + this.userInfo, this.host, this.port, this.pathBuilder.build(), queryParams, hint == EncodingHint.FULLY_ENCODED); result = (hint == EncodingHint.ENCODE_TEMPLATE ? uric.encodeTemplate(this.charset) : uric); }