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); + } } 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 6c91d5a2c02..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 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 batchArgs, int[] argTypes) throws DataAccessException { + public int[] batchUpdate(String sql, List 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()); 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) } + } + } 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); 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);