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, <form:select> and
<form:options> 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
This commit is contained in:
Sam Brannen
2025-11-10 15:21:12 +01:00
parent 1714a00492
commit 335a2c4e21
6 changed files with 264 additions and 21 deletions
@@ -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 = """
<option id="myOption1" value="caf&eacute;">caf&eacute;</option>
<option id="myOption2" value="Jane &quot;I Love Caf&eacute;s&quot; Smith">Jane &quot;I Love Caf&eacute;s&quot; Smith</option>
""".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 = """
<option id="myOption1" value="café">café</option>
<option id="myOption2" value="Jane &quot;I Love Cafés&quot; Smith">Jane &quot;I Love Cafés&quot; Smith</option>
""".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<String, String>();
map.put("one", "Jane \"I Love Cafés\" Smith");
map.put("two", "Joe Café");
this.tag.setItems(map);
this.tag.setId("myOption");
var expectedOutput = """
<option id="myOption1" value="one">Jane &quot;I Love Caf&eacute;s&quot; Smith</option>
<option id="myOption2" value="two">Joe Caf&eacute;</option>
""".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<String, String>();
map.put("one", "Jane \"I Love Cafés\" Smith");
map.put("two", "Joe Café");
this.tag.setItems(map);
this.tag.setId("myOption");
var expectedOutput = """
<option id="myOption1" value="one">Jane &quot;I Love Cafés&quot; Smith</option>
<option id="myOption2" value="two">Joe Café</option>
""".replace("\n", "");
assertThat(this.tag.doStartTag()).isEqualTo(Tag.SKIP_BODY);
assertThat(getOutput()).isEqualTo(expectedOutput);
}
@Test
void withCollectionAndDynamicAttributes() throws Exception {
String dynamicAttribute1 = "attr1";
@@ -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 = """
<select id="favoriteCaf&eacute;" name="favoriteCaf&eacute;" size="2">
<option value="Cup of Joe">Cup of Joe</option>
<option value="Jane&#39;s Coffee Shop">Jane&#39;s Coffee Shop</option>
</select>
""".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 = """
<select id="favoriteCafé" name="favoriteCafé" size="2">
<option value="Cup of Joe">Cup of Joe</option>
<option value="Jane&#39;s Coffee Shop">Jane&#39;s Coffee Shop</option>
</select>
""".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 = """
<select id="name" name="name" size="2">
<option value="caf&eacute;">caf&eacute;</option>
<option value="Jane &quot;I Love Caf&eacute;s&quot; Smith">Jane &quot;I Love Caf&eacute;s&quot; Smith</option>
</select>
""".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 = """
<select id="name" name="name" size="2">
<option value="café">café</option>
<option value="Jane &quot;I Love Cafés&quot; Smith">Jane &quot;I Love Cafés&quot; Smith</option>
</select>
""".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 = """
<select id="sex" name="sex">
<option value="F">Female</option>
<option value="M" selected="selected">Male</option>
</select>
""".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<String, String>();
map.put("F", "Jane \"I Love Cafés\" Smith");
map.put("M", "Joe Café");
this.tag.setPath("sex");
this.tag.setItems(map);
var expectedOutput = """
<select id="sex" name="sex">
<option value="F">Jane &quot;I Love Caf&eacute;s&quot; Smith</option>
<option value="M" selected="selected">Joe Caf&eacute;</option>
</select>
""".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<String, String>();
map.put("F", "Jane \"I Love Cafés\" Smith");
map.put("M", "Joe Café");
this.tag.setPath("sex");
this.tag.setItems(map);
var expectedOutput = """
<select id="sex" name="sex">
<option value="F">Jane &quot;I Love Cafés&quot; Smith</option>
<option value="M" selected="selected">Joe Café</option>
</select>
""".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<String, String> sexes = new HashMap<>();
Map<String, String> 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);
}