From ed13afa0d402fd66a7054b1b0cac2d3095454848 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Deleuze?= Date: Wed, 8 Jul 2026 15:08:51 +0200 Subject: [PATCH 1/5] Ensure consistent ButtonTag value attribute processing Closes gh-37017 --- .../web/servlet/tags/form/ButtonTag.java | 4 +++- .../web/servlet/tags/form/ButtonTagTests.java | 15 +++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/tags/form/ButtonTag.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/tags/form/ButtonTag.java index b3908e46f6a..9ecf8fc335e 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/tags/form/ButtonTag.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/tags/form/ButtonTag.java @@ -67,6 +67,7 @@ import org.springframework.web.servlet.support.RequestDataValueProcessor; * * * @author Rossen Stoyanchev + * @author Sebastien Deleuze * @since 3.1 */ @SuppressWarnings("serial") @@ -152,7 +153,8 @@ public class ButtonTag extends AbstractHtmlElementTag { */ protected void writeValue(TagWriter tagWriter) throws JspException { String valueToUse = (getValue() != null ? getValue() : getDefaultValue()); - tagWriter.writeAttribute("value", processFieldValue(getName(), valueToUse, getType())); + tagWriter.writeAttribute("value", + processFieldValue(getName(), getDisplayString(valueToUse), getType())); } /** diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/tags/form/ButtonTagTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/tags/form/ButtonTagTests.java index c4c3431fccb..82f9effc565 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/tags/form/ButtonTagTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/tags/form/ButtonTagTests.java @@ -27,6 +27,7 @@ import static org.assertj.core.api.Assertions.assertThat; /** * @author Rossen Stoyanchev + * @author Sebastien Deleuze */ class ButtonTagTests extends AbstractFormTagTests { @@ -58,6 +59,20 @@ class ButtonTagTests extends AbstractFormTagTests { assertAttributeNotPresent(output, "disabled"); } + @Test + void value() throws Exception { + this.tag.setValue("\"My Button\""); + + assertThat(this.tag.doStartTag()).isEqualTo(Tag.EVAL_BODY_INCLUDE); + assertThat(this.tag.doEndTag()).isEqualTo(Tag.EVAL_PAGE); + + String output = getOutput(); + assertTagOpened(output); + assertTagClosed(output); + + assertContainsAttribute(output, "value", ""My Button""); + } + @Test void disabled() throws Exception { this.tag.setDisabled(true); From 7e0818f4e99b86dafef988e04f02a2c3a576e7f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Deleuze?= Date: Wed, 8 Jul 2026 15:11:42 +0200 Subject: [PATCH 2/5] Align domainToAscii with current WhatWG spec The WhatWG URL Standard changed its "domain to ASCII" algorithm when beStrict is false and the domain is an ASCII string, it now returns the domain lowercased regardless of Unicode ToASCII's outcome, for web compatibility. Invalid or ambiguous "xn--" (ACE) labels are no longer rejected or validated; they are lowercased and accepted, matching browsers and the web-platform-tests URL cases. This supersedes the earlier spec revision that only lowercased ASCII domains whose labels did not start with "xn--". Drop the now-obsolete "xn--" label detection (which was dead code anyway due to a typo) and unconditionally lowercase ASCII domains. Closes gh-37018 --- .../web/util/WhatWgUrlParser.java | 30 ++++--------------- .../web/util/WhatWgUrlParserTests.java | 20 +++++++++++++ 2 files changed, 26 insertions(+), 24 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/web/util/WhatWgUrlParser.java b/spring-web/src/main/java/org/springframework/web/util/WhatWgUrlParser.java index d2d10298745..6b5c508eb95 100644 --- a/spring-web/src/main/java/org/springframework/web/util/WhatWgUrlParser.java +++ b/spring-web/src/main/java/org/springframework/web/util/WhatWgUrlParser.java @@ -55,6 +55,7 @@ import org.springframework.util.Assert; * with {@code EXTRA}. * * @author Arjen Poutsma + * @author Sebastien Deleuze * @since 6.2 */ @SuppressWarnings({"SameParameterValue", "BooleanMethodIsAlwaysInverted"}) @@ -277,31 +278,12 @@ final class WhatWgUrlParser { } private static String domainToAscii(String domain, boolean beStrict) { - // If beStrict is false, domain is an ASCII string, and strictly splitting domain on U+002E (.) - // does not produce any item that starts with an ASCII case-insensitive match for "xn--", - // this step is equivalent to ASCII lowercasing domain. + // If beStrict is false and domain is an ASCII string, the algorithm returns domain lowercased + // regardless of Unicode ToASCII's outcome, due to web compatibility. In particular, the WhatWG + // spec deliberately does not reject invalid or ambiguous "xn--" (ACE) labels here. See the note in + // https://url.spec.whatwg.org/#concept-domain-to-ascii and web-platform-tests url cases if (!beStrict && containsOnlyAscii(domain)) { - int dotIdx = domain.indexOf('.'); - boolean onlyLowerCase = true; - while (dotIdx != -1) { - if (domain.length() - dotIdx > 4) { - // ASCII case-insensitive match for "xn--" - int ch0 = domain.codePointAt(dotIdx + 1); - int ch1 = domain.codePointAt(dotIdx + 2); - int ch2 = domain.codePointAt(dotIdx + 3); - int ch3 = domain.codePointAt(dotIdx + 4); - if ((ch0 == 'x' || ch0 == 'X') && - (ch1 == 'n' || ch1 == 'N') && - ch2 == '-' && ch3 == '_') { - onlyLowerCase = false; - break; - } - } - dotIdx = domain.indexOf('.', dotIdx + 1); - } - if (onlyLowerCase) { - return domain.toLowerCase(Locale.ENGLISH); - } + return domain.toLowerCase(Locale.ENGLISH); } // Let result be the result of running Unicode ToASCII (https://www.unicode.org/reports/tr46/#ToASCII) // with domain_name set to domain, UseSTD3ASCIIRules set to beStrict, CheckHyphens set to false, diff --git a/spring-web/src/test/java/org/springframework/web/util/WhatWgUrlParserTests.java b/spring-web/src/test/java/org/springframework/web/util/WhatWgUrlParserTests.java index c4fadb5f0fa..ad4c4e697a3 100644 --- a/spring-web/src/test/java/org/springframework/web/util/WhatWgUrlParserTests.java +++ b/spring-web/src/test/java/org/springframework/web/util/WhatWgUrlParserTests.java @@ -23,6 +23,7 @@ import static org.assertj.core.api.Assertions.assertThat; /** * @author Arjen Poutsma + * @author Sebastien Deleuze */ class WhatWgUrlParserTests { @@ -43,6 +44,25 @@ class WhatWgUrlParserTests { testParse("//other.info/parent/../foo/bar", "", "other.info", null, "/foo/bar", null, null); } + @Test + void parseAsciiHost() { + // Pure ASCII host is lowercased + testParse("https://EXAMPLE.com/foo", "https", "example.com", null, "/foo", null, null); + // ASCII "xn--" (ACE) labels are accepted and lowercased (in any case), not validated or rejected. + // See https://url.spec.whatwg.org/#concept-domain-to-ascii and web-platform-tests url cases. + testParse("https://a.b.c.xn--pokxncvks", "https", "a.b.c.xn--pokxncvks", null, "", null, null); + testParse("https://a.b.c.XN--pokxncvks", "https", "a.b.c.xn--pokxncvks", null, "", null, null); + testParse("https://a.b.c.Xn--pokxncvks", "https", "a.b.c.xn--pokxncvks", null, "", null, null); + // A trailing non-numeric "xn--" label keeps a numeric-looking host as a domain, not an IPv4 address. + testParse("https://10.0.0.xn--pokxncvks", "https", "10.0.0.xn--pokxncvks", null, "", null, null); + testParse("https://10.0.0.XN--pokxncvks", "https", "10.0.0.xn--pokxncvks", null, "", null, null); + testParse("https://10.0.0.xN--pokxncvks", "https", "10.0.0.xn--pokxncvks", null, "", null, null); + // Leading ACE label is handled too, and an empty "xn--" label is accepted (not a failure) + testParse("https://XN--pokxncvks.example", "https", "xn--pokxncvks.example", null, "", null, null); + testParse("https://xn--/", "https", "xn--", null, "/", null, null); + testParse("file://xn--/p", "file", "xn--", null, "/p", null, null); + } + private void testParse(String input, String scheme, @Nullable String host, @Nullable String port, String path, @Nullable String query, @Nullable String fragment) { WhatWgUrlParser.UrlRecord result = WhatWgUrlParser.parse(input, EMPTY_URL_RECORD, null, null); assertThat(result.scheme()).as("Invalid scheme").isEqualTo(scheme); From 9726c7ed5f8ef573289cfab345f36e4e0ccf5a62 Mon Sep 17 00:00:00 2001 From: Yanming Zhou Date: Mon, 15 Jun 2026 15:49:06 +0800 Subject: [PATCH 3/5] Let composite/filtered collections accept null elements Closes gh-36923 Signed-off-by: Yanming Zhou --- .../springframework/util/CompositeCollection.java | 3 ++- .../springframework/util/CompositeIterator.java | 5 ++++- .../org/springframework/util/CompositeMap.java | 15 ++++++++------- .../org/springframework/util/CompositeSet.java | 3 ++- .../springframework/util/FilteredCollection.java | 5 ++++- .../springframework/util/FilteredIterator.java | 6 +++--- .../org/springframework/util/FilteredMap.java | 3 ++- .../org/springframework/util/FilteredSet.java | 3 ++- .../util/CompositeCollectionTests.java | 15 +++++++++++++++ .../util/CompositeIteratorTests.java | 11 +++++++++++ .../springframework/util/CompositeMapTests.java | 13 +++++++++++++ .../springframework/util/CompositeSetTests.java | 14 ++++++++++++++ .../util/FilteredCollectionTests.java | 10 ++++++++++ .../util/FilteredIteratorTests.java | 11 +++++++++++ .../springframework/util/FilteredMapTests.java | 13 +++++++++++++ .../springframework/util/FilteredSetTests.java | 14 ++++++++++++++ 16 files changed, 128 insertions(+), 16 deletions(-) diff --git a/spring-core/src/main/java/org/springframework/util/CompositeCollection.java b/spring-core/src/main/java/org/springframework/util/CompositeCollection.java index 1559e49174a..42d3bcf8655 100644 --- a/spring-core/src/main/java/org/springframework/util/CompositeCollection.java +++ b/spring-core/src/main/java/org/springframework/util/CompositeCollection.java @@ -28,10 +28,11 @@ import org.jspecify.annotations.Nullable; * exposed through {@link CompositeMap#values()}. * * @author Arjen Poutsma + * @author Yanming Zhou * @since 6.2 * @param the type of elements maintained by this collection */ -class CompositeCollection implements Collection { +class CompositeCollection implements Collection { private final Collection first; diff --git a/spring-core/src/main/java/org/springframework/util/CompositeIterator.java b/spring-core/src/main/java/org/springframework/util/CompositeIterator.java index 662a402c191..cc711c4de7c 100644 --- a/spring-core/src/main/java/org/springframework/util/CompositeIterator.java +++ b/spring-core/src/main/java/org/springframework/util/CompositeIterator.java @@ -21,6 +21,8 @@ import java.util.LinkedHashSet; import java.util.NoSuchElementException; import java.util.Set; +import org.jspecify.annotations.Nullable; + /** * Composite iterator that combines multiple other iterators, * as registered via {@link #add(Iterator)}. @@ -30,10 +32,11 @@ import java.util.Set; * * @author Erwin Vervaet * @author Juergen Hoeller + * @author Yanming Zhou * @since 3.0 * @param the element type */ -public class CompositeIterator implements Iterator { +public class CompositeIterator implements Iterator { private final Set> iterators = new LinkedHashSet<>(); diff --git a/spring-core/src/main/java/org/springframework/util/CompositeMap.java b/spring-core/src/main/java/org/springframework/util/CompositeMap.java index 29e7476d1bf..37e9f25034b 100644 --- a/spring-core/src/main/java/org/springframework/util/CompositeMap.java +++ b/spring-core/src/main/java/org/springframework/util/CompositeMap.java @@ -32,17 +32,18 @@ import org.jspecify.annotations.Nullable; * {@link CollectionUtils#compositeMap(Map, Map, BiFunction, Consumer)}. * * @author Arjen Poutsma + * @author Yanming Zhou * @since 6.2 * @param the type of keys maintained by this map * @param the type of mapped values */ -final class CompositeMap implements Map { +final class CompositeMap implements Map { - private final Map first; + private final Map first; - private final Map second; + private final Map second; - private final @Nullable BiFunction putFunction; + private final @Nullable BiFunction putFunction; private final @Nullable Consumer> putAllFunction; @@ -53,7 +54,7 @@ final class CompositeMap implements Map { CompositeMap(Map first, Map second, @Nullable BiFunction putFunction, - @Nullable Consumer> putAllFunction) { + @Nullable Consumer> putAllFunction) { Assert.notNull(first, "First must not be null"); Assert.notNull(second, "Second must not be null"); @@ -106,7 +107,7 @@ final class CompositeMap implements Map { } @Override - public @Nullable V put(K key, V value) { + public V put(K key, V value) { if (this.putFunction == null) { throw new UnsupportedOperationException(); } @@ -116,7 +117,7 @@ final class CompositeMap implements Map { } @Override - public @Nullable V remove(Object key) { + public V remove(Object key) { V firstResult = this.first.remove(key); V secondResult = this.second.remove(key); if (firstResult != null) { diff --git a/spring-core/src/main/java/org/springframework/util/CompositeSet.java b/spring-core/src/main/java/org/springframework/util/CompositeSet.java index 2137bc54673..1d30371cd86 100644 --- a/spring-core/src/main/java/org/springframework/util/CompositeSet.java +++ b/spring-core/src/main/java/org/springframework/util/CompositeSet.java @@ -25,10 +25,11 @@ import org.jspecify.annotations.Nullable; * {@link CompositeMap#keySet()} and {@link CompositeMap#entrySet()}. * * @author Arjen Poutsma + * @author Yanming Zhou * @since 6.2 * @param the type of elements maintained by this set */ -final class CompositeSet extends CompositeCollection implements Set { +final class CompositeSet extends CompositeCollection implements Set { CompositeSet(Set first, Set second) { super(first, second); diff --git a/spring-core/src/main/java/org/springframework/util/FilteredCollection.java b/spring-core/src/main/java/org/springframework/util/FilteredCollection.java index 33a68c89ad7..bcb5ca9d1b5 100644 --- a/spring-core/src/main/java/org/springframework/util/FilteredCollection.java +++ b/spring-core/src/main/java/org/springframework/util/FilteredCollection.java @@ -21,15 +21,18 @@ import java.util.Collection; import java.util.Iterator; import java.util.function.Predicate; +import org.jspecify.annotations.Nullable; + /** * Collection that filters out values that do not match a predicate. * This type is used by {@link CompositeMap}. * * @author Arjen Poutsma + * @author Yanming Zhou * @since 6.2 * @param the type of elements maintained by this collection */ -class FilteredCollection extends AbstractCollection { +class FilteredCollection extends AbstractCollection { private final Collection delegate; diff --git a/spring-core/src/main/java/org/springframework/util/FilteredIterator.java b/spring-core/src/main/java/org/springframework/util/FilteredIterator.java index eeaa0dc52c5..fab7088364b 100644 --- a/spring-core/src/main/java/org/springframework/util/FilteredIterator.java +++ b/spring-core/src/main/java/org/springframework/util/FilteredIterator.java @@ -28,10 +28,11 @@ import org.jspecify.annotations.Nullable; *

This type is used by {@link CompositeMap}. * * @author Arjen Poutsma + * @author Yanming Zhou * @since 6.2 * @param the type of elements returned by this iterator */ -final class FilteredIterator implements Iterator { +final class FilteredIterator implements Iterator { private final Iterator delegate; @@ -56,12 +57,11 @@ final class FilteredIterator implements Iterator { } @Override - public E next() { + public @Nullable E next() { if (!this.hasNext && !setNext()) { throw new NoSuchElementException(); } this.hasNext = false; - Assert.state(this.next != null, "Next should not be null"); return this.next; } diff --git a/spring-core/src/main/java/org/springframework/util/FilteredMap.java b/spring-core/src/main/java/org/springframework/util/FilteredMap.java index de6a05be032..57200d9baeb 100644 --- a/spring-core/src/main/java/org/springframework/util/FilteredMap.java +++ b/spring-core/src/main/java/org/springframework/util/FilteredMap.java @@ -28,11 +28,12 @@ import org.jspecify.annotations.Nullable; * This type is used by {@link CompositeMap}. * * @author Arjen Poutsma + * @author Yanming Zhou * @since 6.2 * @param the type of keys maintained by this map * @param the type of mapped values */ -final class FilteredMap extends AbstractMap { +final class FilteredMap extends AbstractMap { private final Map delegate; diff --git a/spring-core/src/main/java/org/springframework/util/FilteredSet.java b/spring-core/src/main/java/org/springframework/util/FilteredSet.java index 5292cde2a41..81ef333589e 100644 --- a/spring-core/src/main/java/org/springframework/util/FilteredSet.java +++ b/spring-core/src/main/java/org/springframework/util/FilteredSet.java @@ -26,10 +26,11 @@ import org.jspecify.annotations.Nullable; * This type is used by {@link CompositeMap}. * * @author Arjen Poutsma + * @author Yanming Zhou * @since 6.2 * @param the type of elements maintained by this set */ -final class FilteredSet extends FilteredCollection implements Set { +final class FilteredSet extends FilteredCollection implements Set { public FilteredSet(Set delegate, Predicate filter) { super(delegate, filter); diff --git a/spring-core/src/test/java/org/springframework/util/CompositeCollectionTests.java b/spring-core/src/test/java/org/springframework/util/CompositeCollectionTests.java index 62859e28c24..cd158eaeab9 100644 --- a/spring-core/src/test/java/org/springframework/util/CompositeCollectionTests.java +++ b/spring-core/src/test/java/org/springframework/util/CompositeCollectionTests.java @@ -21,6 +21,7 @@ import java.util.Collections; import java.util.Iterator; import java.util.List; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; @@ -28,6 +29,7 @@ import static org.assertj.core.api.Assertions.assertThatExceptionOfType; /** * @author Arjen Poutsma + * @author Yanming Zhou */ class CompositeCollectionTests { @@ -192,4 +194,17 @@ class CompositeCollectionTests { assertThat(first).isEmpty(); assertThat(second).isEmpty(); } + + @Test + void nullable() { + List<@Nullable String> first = new ArrayList<>(); + first.add("foo"); + first.add(null); + List<@Nullable String> second = new ArrayList<>(); + second.add("bar"); + second.add(null); + CompositeCollection<@Nullable String> composite = new CompositeCollection<>(first, second); + + assertThat(composite).containsExactly("foo", null, "bar", null); + } } diff --git a/spring-core/src/test/java/org/springframework/util/CompositeIteratorTests.java b/spring-core/src/test/java/org/springframework/util/CompositeIteratorTests.java index 730c6f6af4b..b2b4d5984a8 100644 --- a/spring-core/src/test/java/org/springframework/util/CompositeIteratorTests.java +++ b/spring-core/src/test/java/org/springframework/util/CompositeIteratorTests.java @@ -21,6 +21,7 @@ import java.util.Iterator; import java.util.List; import java.util.NoSuchElementException; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; @@ -33,6 +34,7 @@ import static org.assertj.core.api.Assertions.assertThatIllegalStateException; * * @author Erwin Vervaet * @author Juergen Hoeller + * @author Yanming Zhou */ class CompositeIteratorTests { @@ -99,4 +101,13 @@ class CompositeIteratorTests { it.add(iterator)); } + @Test + void nullable() { + List<@Nullable String> first = Arrays.asList("1", null); + List<@Nullable String> second = Arrays.asList("2", null); + CompositeIterator it = new CompositeIterator<>(); + it.add(first.iterator()); + it.add(second.iterator()); + assertThat(it).toIterable().containsExactly("1", null, "2", null); + } } diff --git a/spring-core/src/test/java/org/springframework/util/CompositeMapTests.java b/spring-core/src/test/java/org/springframework/util/CompositeMapTests.java index 8501c4c93e9..aa998eebf5a 100644 --- a/spring-core/src/test/java/org/springframework/util/CompositeMapTests.java +++ b/spring-core/src/test/java/org/springframework/util/CompositeMapTests.java @@ -23,6 +23,7 @@ import java.util.Map; import java.util.Set; import java.util.concurrent.atomic.AtomicBoolean; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; @@ -32,6 +33,7 @@ import static org.assertj.core.api.Assertions.entry; /** * @author Arjen Poutsma + * @author Yanming Zhou */ class CompositeMapTests { @@ -220,6 +222,17 @@ class CompositeMapTests { assertThat(entries).containsExactly(entry("foo", "bar"), entry("baz", "qux")); } + @Test + void nullable() { + Map first = new HashMap<>(); + first.put("foo", "bar"); + Map second = new HashMap<>(); + second.put("baz", null); + CompositeMap composite = new CompositeMap<>(first, second); + + assertThat(composite).containsExactly(entry("foo", "bar"), entry("baz", null)); + } + @Nested class CollisionTests { diff --git a/spring-core/src/test/java/org/springframework/util/CompositeSetTests.java b/spring-core/src/test/java/org/springframework/util/CompositeSetTests.java index dfd4124834d..6b7487b5aab 100644 --- a/spring-core/src/test/java/org/springframework/util/CompositeSetTests.java +++ b/spring-core/src/test/java/org/springframework/util/CompositeSetTests.java @@ -20,12 +20,14 @@ import java.util.Collections; import java.util.HashSet; import java.util.Set; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; /** * @author Arjen Poutsma + * @author Yanming Zhou */ class CompositeSetTests { @@ -44,4 +46,16 @@ class CompositeSetTests { assertThat(composite).isNotEqualTo(Collections.emptySet()); } + @Test + void nullable() { + Set<@Nullable String> first = new HashSet<>(); + first.add("foo"); + first.add(null); + Set<@Nullable String> second = new HashSet<>(); + second.add("bar"); + first.add(null); + CompositeSet<@Nullable String> composite = new CompositeSet<>(first, second); + + assertThat(composite).containsExactlyInAnyOrder("foo", null, "bar"); + } } diff --git a/spring-core/src/test/java/org/springframework/util/FilteredCollectionTests.java b/spring-core/src/test/java/org/springframework/util/FilteredCollectionTests.java index bfb1ec6c03c..c9d06110b6e 100644 --- a/spring-core/src/test/java/org/springframework/util/FilteredCollectionTests.java +++ b/spring-core/src/test/java/org/springframework/util/FilteredCollectionTests.java @@ -17,14 +17,17 @@ package org.springframework.util; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; /** * @author Arjen Poutsma + * @author Yanming Zhou */ class FilteredCollectionTests { @@ -74,4 +77,11 @@ class FilteredCollectionTests { assertThat(contained).isFalse(); } + @Test + void nullable() { + List<@Nullable String> list = Arrays.asList("foo", "bar", null); + FilteredCollection<@Nullable String> filtered = new FilteredCollection<>(list, s -> !"bar".equals(s)); + + assertThat(filtered).containsExactlyInAnyOrder("foo", null); + } } diff --git a/spring-core/src/test/java/org/springframework/util/FilteredIteratorTests.java b/spring-core/src/test/java/org/springframework/util/FilteredIteratorTests.java index 3ac253ed2b9..e0eb8373770 100644 --- a/spring-core/src/test/java/org/springframework/util/FilteredIteratorTests.java +++ b/spring-core/src/test/java/org/springframework/util/FilteredIteratorTests.java @@ -16,14 +16,17 @@ package org.springframework.util; +import java.util.Arrays; import java.util.List; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; /** * @author Arjen Poutsma + * @author Yanming Zhou */ final class FilteredIteratorTests { @@ -35,4 +38,12 @@ final class FilteredIteratorTests { assertThat(filtered).toIterable().containsExactly("foo", "baz"); } + @Test + void nullable() { + List<@Nullable String> list = Arrays.asList("foo", "bar", null); + FilteredIterator<@Nullable String> filtered = new FilteredIterator<>(list.iterator(), s -> !"bar".equals(s)); + + assertThat(filtered).toIterable().containsExactly("foo", null); + } + } diff --git a/spring-core/src/test/java/org/springframework/util/FilteredMapTests.java b/spring-core/src/test/java/org/springframework/util/FilteredMapTests.java index 9fe5b739332..f4cfe4728fa 100644 --- a/spring-core/src/test/java/org/springframework/util/FilteredMapTests.java +++ b/spring-core/src/test/java/org/springframework/util/FilteredMapTests.java @@ -20,6 +20,7 @@ import java.util.HashMap; import java.util.Map; import java.util.Set; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Test; import static java.util.Map.entry; @@ -27,6 +28,7 @@ import static org.assertj.core.api.Assertions.assertThat; /** * @author Arjen Poutsma + * @author Yanming Zhou */ class FilteredMapTests { @@ -100,4 +102,15 @@ class FilteredMapTests { Set keySet = filtered.keySet(); assertThat(keySet).containsExactlyInAnyOrder("foo", "quux"); } + + @Test + void nullable() { + Map map = new HashMap<>(); + map.put("foo", null); + map.put("bar", "bar"); + map.put("baz", "baz"); + FilteredMap filtered = new FilteredMap<>(map, s -> !s.equals("baz")); + + assertThat(filtered).containsEntry("foo", null).containsEntry("bar", "bar").doesNotContainKeys("baz"); + } } diff --git a/spring-core/src/test/java/org/springframework/util/FilteredSetTests.java b/spring-core/src/test/java/org/springframework/util/FilteredSetTests.java index 8490420c5a8..de0c7c2db55 100644 --- a/spring-core/src/test/java/org/springframework/util/FilteredSetTests.java +++ b/spring-core/src/test/java/org/springframework/util/FilteredSetTests.java @@ -17,14 +17,17 @@ package org.springframework.util; import java.util.Collections; +import java.util.HashSet; import java.util.Set; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; /** * @author Arjen Poutsma + * @author Yanming Zhou */ class FilteredSetTests { @@ -39,4 +42,15 @@ class FilteredSetTests { assertThat(filtered).isNotEqualTo(set); assertThat(filtered).isNotEqualTo(Collections.emptySet()); } + + @Test + void nullable() { + Set<@Nullable String> set = new HashSet<>(); + set.add("foo"); + set.add("bar"); + set.add(null); + FilteredSet<@Nullable String> filtered = new FilteredSet<>(set, s -> !"bar".equals(s)); + + assertThat(filtered).containsExactlyInAnyOrder("foo", null); + } } From 516a2ca51165c81d63443e0a02f38915a364cf5c Mon Sep 17 00:00:00 2001 From: Chris Date: Tue, 7 Jul 2026 15:07:42 +0200 Subject: [PATCH 4/5] Make batchArgs contents nullable in batchUpdate Updated batchUpdate method signatures to allow nullable Object arrays. See gh-37012 Signed-off-by: Chris --- .../java/org/springframework/jdbc/core/JdbcTemplate.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java index 6c91d5a2c02..33b94792ddc 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java @@ -1046,12 +1046,12 @@ public class JdbcTemplate extends JdbcAccessor implements JdbcOperations { } @Override - public int[] batchUpdate(String sql, List batchArgs) throws DataAccessException { + public int[] batchUpdate(String sql, List<@Nullable Object[]> batchArgs) throws DataAccessException { return batchUpdate(sql, batchArgs, new int[0]); } @Override - public int[] batchUpdate(String sql, List batchArgs, int[] argTypes) throws DataAccessException { + public int[] batchUpdate(String sql, List<@Nullable Object[]> batchArgs, int[] argTypes) throws DataAccessException { if (batchArgs.isEmpty()) { return new int[0]; } @@ -1061,9 +1061,9 @@ public class JdbcTemplate extends JdbcAccessor implements JdbcOperations { new BatchPreparedStatementSetter() { @Override public void setValues(PreparedStatement ps, int i) throws SQLException { - Object[] values = batchArgs.get(i); + @Nullable Object[] values = batchArgs.get(i); int colIndex = 0; - for (Object value : values) { + for (@Nullable Object value : values) { colIndex++; if (value instanceof SqlParameterValue paramValue) { StatementCreatorUtils.setParameterValue(ps, colIndex, paramValue, paramValue.getValue()); From 16e82cd6934bc6aa56cb6538a2ee06d81acbafe7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Deleuze?= Date: Wed, 8 Jul 2026 15:27:26 +0200 Subject: [PATCH 5/5] Refine JdbcTemplate nullability contribution Closes gh-37012 --- .../jdbc/core/JdbcOperations.java | 4 +- .../jdbc/core/JdbcTemplate.java | 4 +- .../core/JdbcOperationsExtensionsTests.kt | 38 +++++++++++++++++++ 3 files changed, 42 insertions(+), 4 deletions(-) diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/JdbcOperations.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/JdbcOperations.java index be172510202..601c83a77cd 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/JdbcOperations.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/JdbcOperations.java @@ -1013,7 +1013,7 @@ public interface JdbcOperations { * {@link java.sql.Statement#SUCCESS_NO_INFO}/{@link java.sql.Statement#EXECUTE_FAILED}) * @throws DataAccessException if there is any problem issuing the update */ - int[] batchUpdate(String sql, List batchArgs) throws DataAccessException; + int[] batchUpdate(String sql, List batchArgs) throws DataAccessException; /** * Execute a batch using the supplied SQL statement with the batch of supplied arguments. @@ -1026,7 +1026,7 @@ public interface JdbcOperations { * {@link java.sql.Statement#SUCCESS_NO_INFO}/{@link java.sql.Statement#EXECUTE_FAILED}) * @throws DataAccessException if there is any problem issuing the update */ - int[] batchUpdate(String sql, List batchArgs, int[] argTypes) throws DataAccessException; + int[] batchUpdate(String sql, List batchArgs, int[] argTypes) throws DataAccessException; /** * Execute multiple batches using the supplied SQL statement with the collect of supplied diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java index 33b94792ddc..0eec243d061 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java @@ -1046,12 +1046,12 @@ public class JdbcTemplate extends JdbcAccessor implements JdbcOperations { } @Override - public int[] batchUpdate(String sql, List<@Nullable Object[]> batchArgs) throws DataAccessException { + public int[] batchUpdate(String sql, List batchArgs) throws DataAccessException { return batchUpdate(sql, batchArgs, new int[0]); } @Override - public int[] batchUpdate(String sql, List<@Nullable Object[]> batchArgs, int[] argTypes) throws DataAccessException { + public int[] batchUpdate(String sql, List batchArgs, int[] argTypes) throws DataAccessException { if (batchArgs.isEmpty()) { return new int[0]; } diff --git a/spring-jdbc/src/test/kotlin/org/springframework/jdbc/core/JdbcOperationsExtensionsTests.kt b/spring-jdbc/src/test/kotlin/org/springframework/jdbc/core/JdbcOperationsExtensionsTests.kt index 91c2c55092b..18cef980ef9 100644 --- a/spring-jdbc/src/test/kotlin/org/springframework/jdbc/core/JdbcOperationsExtensionsTests.kt +++ b/spring-jdbc/src/test/kotlin/org/springframework/jdbc/core/JdbcOperationsExtensionsTests.kt @@ -223,4 +223,42 @@ class JdbcOperationsExtensionsTests { verify { template.query(sql, ofType>(), 3) } } + @Test // gh-37012 + fun `batchUpdate with non-null batchArgs`() { + val batchArgs: List> = listOf(arrayOf(1, "a"), arrayOf(2, "b")) + val result = intArrayOf(1, 1) + every { template.batchUpdate(sql, batchArgs) } returns result + assertThat(template.batchUpdate(sql, batchArgs)).isEqualTo(result) + verify { template.batchUpdate(sql, batchArgs) } + } + + @Test // gh-37012 + fun `batchUpdate with nullable batchArgs`() { + val batchArgs: List> = listOf(arrayOf(1, null), arrayOf(null, "b")) + val result = intArrayOf(1, 1) + every { template.batchUpdate(sql, batchArgs) } returns result + assertThat(template.batchUpdate(sql, batchArgs)).isEqualTo(result) + verify { template.batchUpdate(sql, batchArgs) } + } + + @Test // gh-37012 + fun `batchUpdate with non-null batchArgs and argTypes`() { + val batchArgs: List> = listOf(arrayOf(1, "a"), arrayOf(2, "b")) + val argTypes = intArrayOf(JDBCType.INTEGER.vendorTypeNumber, JDBCType.VARCHAR.vendorTypeNumber) + val result = intArrayOf(1, 1) + every { template.batchUpdate(sql, batchArgs, argTypes) } returns result + assertThat(template.batchUpdate(sql, batchArgs, argTypes)).isEqualTo(result) + verify { template.batchUpdate(sql, batchArgs, argTypes) } + } + + @Test // gh-37012 + fun `batchUpdate with nullable batchArgs and argTypes`() { + val batchArgs: List> = listOf(arrayOf(1, null), arrayOf(null, "b")) + val argTypes = intArrayOf(JDBCType.INTEGER.vendorTypeNumber, JDBCType.VARCHAR.vendorTypeNumber) + val result = intArrayOf(1, 1) + every { template.batchUpdate(sql, batchArgs, argTypes) } returns result + assertThat(template.batchUpdate(sql, batchArgs, argTypes)).isEqualTo(result) + verify { template.batchUpdate(sql, batchArgs, argTypes) } + } + }