From 1714a0049280eb48123eb4edca7aa3f736b22461 Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Mon, 10 Nov 2025 14:13:34 +0100 Subject: [PATCH 1/2] Suppress warnings in Gradle build --- .../client/HttpComponentsClientHttpRequestFactoryTests.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spring-web/src/test/java/org/springframework/http/client/HttpComponentsClientHttpRequestFactoryTests.java b/spring-web/src/test/java/org/springframework/http/client/HttpComponentsClientHttpRequestFactoryTests.java index 0dc4675db99..517d8ab946c 100644 --- a/spring-web/src/test/java/org/springframework/http/client/HttpComponentsClientHttpRequestFactoryTests.java +++ b/spring-web/src/test/java/org/springframework/http/client/HttpComponentsClientHttpRequestFactoryTests.java @@ -61,7 +61,7 @@ class HttpComponentsClientHttpRequestFactoryTests extends AbstractHttpRequestFac } @Test - @SuppressWarnings("removal") + @SuppressWarnings({ "removal", "deprecation" }) void assertCustomConfig() throws Exception { HttpClient httpClient = HttpClientBuilder.create().build(); HttpComponentsClientHttpRequestFactory hrf = new HttpComponentsClientHttpRequestFactory(httpClient); @@ -103,7 +103,7 @@ class HttpComponentsClientHttpRequestFactoryTests extends AbstractHttpRequestFac } @Test - @SuppressWarnings("removal") + @SuppressWarnings({ "removal", "deprecation" }) void localSettingsOverrideClientDefaultSettings() throws Exception { RequestConfig defaultConfig = RequestConfig.custom() .setConnectTimeout(1234, MILLISECONDS) From 335a2c4e21939a921ac33dcbb4ba830c4567bda3 Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Mon, 10 Nov 2025 15:21:12 +0100 Subject: [PATCH 2/2] Support response encoding in `select` and `options` JSP form tags Prior to this commit, Spring Framework's JSP form tags supported the response encoding in most places; however, and still did not support the response character encoding. To address that, this commit updates SelectTag, OptionsTag, and OptionWriter to provide support for response character encoding in the `select` and `options` JSP form tags. See gh-33023 Closes gh-35783 --- .../beans/testfixture/beans/TestBean.java | 10 ++ .../web/servlet/tags/form/OptionWriter.java | 23 ++- .../web/servlet/tags/form/OptionsTag.java | 10 +- .../web/servlet/tags/form/SelectTag.java | 7 +- .../servlet/tags/form/OptionsTagTests.java | 82 ++++++++++ .../web/servlet/tags/form/SelectTagTests.java | 153 ++++++++++++++++-- 6 files changed, 264 insertions(+), 21 deletions(-) diff --git a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/TestBean.java b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/TestBean.java index 5801191af96..72aad792d97 100644 --- a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/TestBean.java +++ b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/TestBean.java @@ -59,6 +59,8 @@ public class TestBean implements BeanNameAware, BeanFactoryAware, ITestBean, IOt private boolean jedi; + private String favoriteCafé; + private ITestBean spouse; private String touchy; @@ -209,6 +211,14 @@ public class TestBean implements BeanNameAware, BeanFactoryAware, ITestBean, IOt this.jedi = jedi; } + public String getFavoriteCafé() { + return this.favoriteCafé; + } + + public void setFavoriteCafé(String favoriteCafé) { + this.favoriteCafé = favoriteCafé; + } + @Override public ITestBean getSpouse() { return this.spouse; diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/tags/form/OptionWriter.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/tags/form/OptionWriter.java index 88f70dca68f..8812a5e4387 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/tags/form/OptionWriter.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/tags/form/OptionWriter.java @@ -28,6 +28,7 @@ import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; import org.springframework.web.servlet.support.BindStatus; +import org.springframework.web.util.HtmlUtils; /** * Provides supporting functionality to render a list of '{@code option}' @@ -102,18 +103,26 @@ class OptionWriter { private final boolean htmlEscape; + @Nullable + private final String encoding; + /** - * Create a new {@code OptionWriter} for the supplied {@code objectSource}. + * Create a new {@code OptionWriter} for the supplied {@code optionSource}. * @param optionSource the source of the {@code options} (never {@code null}) * @param bindStatus the {@link BindStatus} for the bound value (never {@code null}) * @param valueProperty the name of the property used to render {@code option} values * (optional) * @param labelProperty the name of the property used to render {@code option} labels * (optional) + * @param htmlEscape whether special characters should be converted into HTML + * character references + * @param encoding the character encoding to use, or {@code null} if response + * encoding should not be used with HTML escaping */ public OptionWriter(Object optionSource, BindStatus bindStatus, - @Nullable String valueProperty, @Nullable String labelProperty, boolean htmlEscape) { + @Nullable String valueProperty, @Nullable String labelProperty, + boolean htmlEscape, @Nullable String encoding) { Assert.notNull(optionSource, "'optionSource' must not be null"); Assert.notNull(bindStatus, "'bindStatus' must not be null"); @@ -122,6 +131,7 @@ class OptionWriter { this.valueProperty = valueProperty; this.labelProperty = labelProperty; this.htmlEscape = htmlEscape; + this.encoding = encoding; } @@ -250,7 +260,14 @@ class OptionWriter { */ private String getDisplayString(@Nullable Object value) { PropertyEditor editor = (value != null ? this.bindStatus.findEditor(value.getClass()) : null); - return ValueFormatter.getDisplayString(value, editor, this.htmlEscape); + String displayString = ValueFormatter.getDisplayString(value, editor, false); + return (this.htmlEscape ? htmlEscape(displayString) : displayString); + } + + private String htmlEscape(String content) { + return (this.encoding != null ? + HtmlUtils.htmlEscape(content, this.encoding) : + HtmlUtils.htmlEscape(content)); } /** diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/tags/form/OptionsTag.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/tags/form/OptionsTag.java index e1badc1205c..bef51ed5b7d 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/tags/form/OptionsTag.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/tags/form/OptionsTag.java @@ -187,6 +187,7 @@ import org.springframework.web.util.TagUtils; * @author Rob Harrop * @author Juergen Hoeller * @author Scott Andrews + * @author Sam Brannen * @since 2.0 */ @SuppressWarnings("serial") @@ -312,7 +313,10 @@ public class OptionsTag extends AbstractHtmlElementTag { (itemValue != null ? ObjectUtils.getDisplayString(evaluate("itemValue", itemValue)) : null); String labelProperty = (itemLabel != null ? ObjectUtils.getDisplayString(evaluate("itemLabel", itemLabel)) : null); - OptionsWriter optionWriter = new OptionsWriter(selectName, itemsObject, valueProperty, labelProperty); + String encodingToUse = + (isResponseEncodedHtmlEscape() ? this.pageContext.getResponse().getCharacterEncoding() : null); + OptionsWriter optionWriter = + new OptionsWriter(selectName, itemsObject, valueProperty, labelProperty, encodingToUse); optionWriter.writeOptions(tagWriter); } return SKIP_BODY; @@ -353,9 +357,9 @@ public class OptionsTag extends AbstractHtmlElementTag { private final String selectName; public OptionsWriter(@Nullable String selectName, Object optionSource, - @Nullable String valueProperty, @Nullable String labelProperty) { + @Nullable String valueProperty, @Nullable String labelProperty, @Nullable String encoding) { - super(optionSource, getBindStatus(), valueProperty, labelProperty, isHtmlEscape()); + super(optionSource, getBindStatus(), valueProperty, labelProperty, isHtmlEscape(), encoding); this.selectName = selectName; } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/tags/form/SelectTag.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/tags/form/SelectTag.java index a4f7220126d..292eb051d98 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/tags/form/SelectTag.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/tags/form/SelectTag.java @@ -235,6 +235,7 @@ import org.springframework.web.servlet.support.BindStatus; * * @author Rob Harrop * @author Juergen Hoeller + * @author Sam Brannen * @since 2.0 * @see OptionTag */ @@ -418,8 +419,12 @@ public class SelectTag extends AbstractHtmlInputElementTag { ObjectUtils.getDisplayString(evaluate("itemValue", getItemValue())) : null); String labelProperty = (getItemLabel() != null ? ObjectUtils.getDisplayString(evaluate("itemLabel", getItemLabel())) : null); + String encodingToUse = (isResponseEncodedHtmlEscape() ? + this.pageContext.getResponse().getCharacterEncoding() : null); OptionWriter optionWriter = - new OptionWriter(itemsObject, getBindStatus(), valueProperty, labelProperty, isHtmlEscape()) { + new OptionWriter(itemsObject, getBindStatus(), valueProperty, labelProperty, + isHtmlEscape(), encodingToUse) { + @Override protected String processOptionValue(String resolvedValue) { return processFieldValue(selectName, resolvedValue, "option"); diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/tags/form/OptionsTagTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/tags/form/OptionsTagTests.java index fede271ef88..11009812c7e 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/tags/form/OptionsTagTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/tags/form/OptionsTagTests.java @@ -21,6 +21,7 @@ import java.io.StringReader; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -50,6 +51,7 @@ import static org.assertj.core.api.Assertions.assertThat; * @author Juergen Hoeller * @author Scott Andrews * @author Jeremy Grelle + * @author Sam Brannen */ @SuppressWarnings({ "rawtypes", "unchecked" }) class OptionsTagTests extends AbstractHtmlElementTagTests { @@ -114,6 +116,86 @@ class OptionsTagTests extends AbstractHtmlElementTagTests { assertThat(element.attribute("onclick").getValue()).isEqualTo("CLICK"); } + @Test // gh-35783 + void withListWithHtmlEscaping() throws Exception { + getPageContext().setAttribute( + SelectTag.LIST_VALUE_PAGE_ATTRIBUTE, new BindStatus(getRequestContext(), "testBean.country", false)); + + this.tag.setItems(List.of("café", "Jane \"I Love Cafés\" Smith")); + this.tag.setId("myOption"); + + var expectedOutput = """ + + + """.replace("\n", ""); + + assertThat(this.tag.doStartTag()).isEqualTo(Tag.SKIP_BODY); + assertThat(getOutput()).isEqualTo(expectedOutput); + } + + @Test // gh-35783 + void withListWithHtmlEscapingAndCharacterEncoding() throws Exception { + this.getPageContext().getResponse().setCharacterEncoding("UTF-8"); + + getPageContext().setAttribute( + SelectTag.LIST_VALUE_PAGE_ATTRIBUTE, new BindStatus(getRequestContext(), "testBean.country", false)); + + this.tag.setItems(List.of("café", "Jane \"I Love Cafés\" Smith")); + this.tag.setId("myOption"); + + var expectedOutput = """ + + + """.replace("\n", ""); + + assertThat(this.tag.doStartTag()).isEqualTo(Tag.SKIP_BODY); + assertThat(getOutput()).isEqualTo(expectedOutput); + } + + @Test // gh-35783 + void withMapWithHtmlEscaping() throws Exception { + getPageContext().setAttribute( + SelectTag.LIST_VALUE_PAGE_ATTRIBUTE, new BindStatus(getRequestContext(), "testBean.country", false)); + + var map = new LinkedHashMap(); + map.put("one", "Jane \"I Love Cafés\" Smith"); + map.put("two", "Joe Café"); + + this.tag.setItems(map); + this.tag.setId("myOption"); + + var expectedOutput = """ + + + """.replace("\n", ""); + + assertThat(this.tag.doStartTag()).isEqualTo(Tag.SKIP_BODY); + assertThat(getOutput()).isEqualTo(expectedOutput); + } + + @Test // gh-35783 + void withMapWithHtmlEscapingAndCharacterEncoding() throws Exception { + this.getPageContext().getResponse().setCharacterEncoding("UTF-8"); + + getPageContext().setAttribute( + SelectTag.LIST_VALUE_PAGE_ATTRIBUTE, new BindStatus(getRequestContext(), "testBean.country", false)); + + var map = new LinkedHashMap(); + map.put("one", "Jane \"I Love Cafés\" Smith"); + map.put("two", "Joe Café"); + + this.tag.setItems(map); + this.tag.setId("myOption"); + + var expectedOutput = """ + + + """.replace("\n", ""); + + assertThat(this.tag.doStartTag()).isEqualTo(Tag.SKIP_BODY); + assertThat(getOutput()).isEqualTo(expectedOutput); + } + @Test void withCollectionAndDynamicAttributes() throws Exception { String dynamicAttribute1 = "attr1"; diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/tags/form/SelectTagTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/tags/form/SelectTagTests.java index f2cfed9c2bd..b91152c589b 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/tags/form/SelectTagTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/tags/form/SelectTagTests.java @@ -23,6 +23,7 @@ import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.List; import java.util.Locale; import java.util.Map; @@ -54,6 +55,7 @@ import static org.assertj.core.api.Assertions.assertThatExceptionOfType; * @author Juergen Hoeller * @author Jeremy Grelle * @author Dave Syer + * @author Sam Brannen */ @SuppressWarnings({ "rawtypes", "unchecked" }) public class SelectTagTests extends AbstractFormTagTests { @@ -77,6 +79,18 @@ public class SelectTagTests extends AbstractFormTagTests { this.tag.setPageContext(getPageContext()); } + @Override + protected TestBean createTestBean() { + this.bean = new TestBeanWithRealCountry(); + this.bean.setName("Rob"); + this.bean.setCountry("UK"); + this.bean.setSex("M"); + this.bean.setMyFloat(Float.valueOf("12.34")); + this.bean.setSomeIntegerArray(new Integer[]{12, 34}); + return this.bean; + } + + @Test void dynamicAttributes() throws JspException { String dynamicAttribute1 = "attr1"; @@ -132,6 +146,78 @@ public class SelectTagTests extends AbstractFormTagTests { assertList(true); } + @Test // gh-33023 + void withListWithHtmlEscapingInPath() throws Exception { + this.tag.setPath("favoriteCafé"); + this.tag.setItems(List.of("Cup of Joe", "Jane's Coffee Shop")); + this.tag.setSize("2"); + + var expectedOutput = """ + + """.replace("\n", ""); + + assertThat(this.tag.doStartTag()).isEqualTo(Tag.SKIP_BODY); + assertThat(getOutput()).isEqualTo(expectedOutput); + } + + @Test // gh-33023 + void withListWithHtmlEscapingAndCharacterEncodingInPath() throws Exception { + this.getPageContext().getResponse().setCharacterEncoding("UTF-8"); + + this.tag.setPath("favoriteCafé"); + this.tag.setItems(List.of("Cup of Joe", "Jane's Coffee Shop")); + this.tag.setSize("2"); + + var expectedOutput = """ + + """.replace("\n", ""); + + assertThat(this.tag.doStartTag()).isEqualTo(Tag.SKIP_BODY); + assertThat(getOutput()).isEqualTo(expectedOutput); + } + + @Test // gh-35783 + void withListWithHtmlEscapingInOptions() throws Exception { + this.tag.setPath("name"); + this.tag.setItems(List.of("café", "Jane \"I Love Cafés\" Smith")); + this.tag.setSize("2"); + + var expectedOutput = """ + + """.replace("\n", ""); + + assertThat(this.tag.doStartTag()).isEqualTo(Tag.SKIP_BODY); + assertThat(getOutput()).isEqualTo(expectedOutput); + } + + @Test // gh-35783 + void withListWithHtmlEscapingAndCharacterEncodingInOptions() throws Exception { + this.getPageContext().getResponse().setCharacterEncoding("UTF-8"); + + this.tag.setPath("name"); + this.tag.setItems(List.of("café", "Jane \"I Love Cafés\" Smith")); + this.tag.setSize("2"); + + var expectedOutput = """ + + """.replace("\n", ""); + + assertThat(this.tag.doStartTag()).isEqualTo(Tag.SKIP_BODY); + assertThat(getOutput()).isEqualTo(expectedOutput); + } + @Test void withResolvedList() throws Exception { this.tag.setPath("country"); @@ -335,8 +421,58 @@ public class SelectTagTests extends AbstractFormTagTests { void withMap() throws Exception { this.tag.setPath("sex"); this.tag.setItems(getSexes()); - int result = this.tag.doStartTag(); - assertThat(result).isEqualTo(Tag.SKIP_BODY); + + var expectedOutput = """ + + """.replace("\n", ""); + + assertThat(this.tag.doStartTag()).isEqualTo(Tag.SKIP_BODY); + assertThat(getOutput()).isEqualTo(expectedOutput); + } + + @Test // gh-35783 + void withMapWithHtmlEscapingInOptions() throws Exception { + var map = new LinkedHashMap(); + map.put("F", "Jane \"I Love Cafés\" Smith"); + map.put("M", "Joe Café"); + + this.tag.setPath("sex"); + this.tag.setItems(map); + + var expectedOutput = """ + + """.replace("\n", ""); + + assertThat(this.tag.doStartTag()).isEqualTo(Tag.SKIP_BODY); + assertThat(getOutput()).isEqualTo(expectedOutput); + } + + @Test // gh-35783 + void withMapWithHtmlEscapingAndCharacterEncodingInOptions() throws Exception { + this.getPageContext().getResponse().setCharacterEncoding("UTF-8"); + + var map = new LinkedHashMap(); + map.put("F", "Jane \"I Love Cafés\" Smith"); + map.put("M", "Joe Café"); + + this.tag.setPath("sex"); + this.tag.setItems(map); + + var expectedOutput = """ + + """.replace("\n", ""); + + assertThat(this.tag.doStartTag()).isEqualTo(Tag.SKIP_BODY); + assertThat(getOutput()).isEqualTo(expectedOutput); } @Test @@ -958,7 +1094,7 @@ public class SelectTagTests extends AbstractFormTagTests { } private Map getSexes() { - Map sexes = new HashMap<>(); + Map sexes = new LinkedHashMap<>(); sexes.put("F", "Female"); sexes.put("M", "Male"); return sexes; @@ -996,17 +1132,6 @@ public class SelectTagTests extends AbstractFormTagTests { } } - @Override - protected TestBean createTestBean() { - this.bean = new TestBeanWithRealCountry(); - this.bean.setName("Rob"); - this.bean.setCountry("UK"); - this.bean.setSex("M"); - this.bean.setMyFloat(Float.valueOf("12.34")); - this.bean.setSomeIntegerArray(new Integer[]{12, 34}); - return this.bean; - } - private TestBean getTestBean() { return (TestBean) getPageContext().getRequest().getAttribute(COMMAND_NAME); }