mirror of
https://github.com/spring-projects/spring-framework.git
synced 2026-09-28 01:59:04 +00:00
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:
+10
@@ -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;
|
||||
|
||||
+20
-3
@@ -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));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+7
-3
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
+6
-1
@@ -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");
|
||||
|
||||
+82
@@ -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é">café</option>
|
||||
<option id="myOption2" value="Jane "I Love Cafés" Smith">Jane "I Love Cafés" 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 "I Love Cafés" Smith">Jane "I Love Cafés" 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 "I Love Cafés" 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 // 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 "I Love Cafés" 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";
|
||||
|
||||
+139
-14
@@ -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é" name="favoriteCafé" size="2">
|
||||
<option value="Cup of Joe">Cup of Joe</option>
|
||||
<option value="Jane's Coffee Shop">Jane'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's Coffee Shop">Jane'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é">café</option>
|
||||
<option value="Jane "I Love Cafés" Smith">Jane "I Love Cafés" 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 "I Love Cafés" Smith">Jane "I Love Cafés" 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 "I Love Cafés" 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 // 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 "I Love Cafés" 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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user