Skip to content

Commit c6b4b43

Browse files
committed
Merge branch '6.2.x'
2 parents 543390c + 335a2c4 commit c6b4b43

File tree

6 files changed

+263
-21
lines changed

6 files changed

+263
-21
lines changed

spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/TestBean.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,8 @@ public class TestBean implements BeanNameAware, BeanFactoryAware, ITestBean, IOt
6060

6161
private boolean jedi;
6262

63+
private String favoriteCafé;
64+
6365
private ITestBean spouse;
6466

6567
private String touchy;
@@ -210,6 +212,14 @@ public void setJedi(boolean jedi) {
210212
this.jedi = jedi;
211213
}
212214

215+
public String getFavoriteCafé() {
216+
return this.favoriteCafé;
217+
}
218+
219+
public void setFavoriteCafé(String favoriteCafé) {
220+
this.favoriteCafé = favoriteCafé;
221+
}
222+
213223
@Override
214224
public ITestBean getSpouse() {
215225
return this.spouse;

spring-webmvc/src/main/java/org/springframework/web/servlet/tags/form/OptionWriter.java

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import org.springframework.util.Assert;
2929
import org.springframework.util.CollectionUtils;
3030
import org.springframework.web.servlet.support.BindStatus;
31+
import org.springframework.web.util.HtmlUtils;
3132

3233
/**
3334
* Provides supporting functionality to render a list of '{@code option}'
@@ -100,18 +101,25 @@ class OptionWriter {
100101

101102
private final boolean htmlEscape;
102103

104+
private final @Nullable String encoding;
105+
103106

104107
/**
105-
* Create a new {@code OptionWriter} for the supplied {@code objectSource}.
108+
* Create a new {@code OptionWriter} for the supplied {@code optionSource}.
106109
* @param optionSource the source of the {@code options} (never {@code null})
107110
* @param bindStatus the {@link BindStatus} for the bound value (never {@code null})
108111
* @param valueProperty the name of the property used to render {@code option} values
109112
* (optional)
110113
* @param labelProperty the name of the property used to render {@code option} labels
111114
* (optional)
115+
* @param htmlEscape whether special characters should be converted into HTML
116+
* character references
117+
* @param encoding the character encoding to use, or {@code null} if response
118+
* encoding should not be used with HTML escaping
112119
*/
113120
public OptionWriter(Object optionSource, BindStatus bindStatus,
114-
@Nullable String valueProperty, @Nullable String labelProperty, boolean htmlEscape) {
121+
@Nullable String valueProperty, @Nullable String labelProperty,
122+
boolean htmlEscape, @Nullable String encoding) {
115123

116124
Assert.notNull(optionSource, "'optionSource' must not be null");
117125
Assert.notNull(bindStatus, "'bindStatus' must not be null");
@@ -120,6 +128,7 @@ public OptionWriter(Object optionSource, BindStatus bindStatus,
120128
this.valueProperty = valueProperty;
121129
this.labelProperty = labelProperty;
122130
this.htmlEscape = htmlEscape;
131+
this.encoding = encoding;
123132
}
124133

125134

@@ -248,7 +257,14 @@ private void renderOption(TagWriter tagWriter, Object item, @Nullable Object val
248257
*/
249258
private String getDisplayString(@Nullable Object value) {
250259
PropertyEditor editor = (value != null ? this.bindStatus.findEditor(value.getClass()) : null);
251-
return ValueFormatter.getDisplayString(value, editor, this.htmlEscape);
260+
String displayString = ValueFormatter.getDisplayString(value, editor, false);
261+
return (this.htmlEscape ? htmlEscape(displayString) : displayString);
262+
}
263+
264+
private String htmlEscape(String content) {
265+
return (this.encoding != null ?
266+
HtmlUtils.htmlEscape(content, this.encoding) :
267+
HtmlUtils.htmlEscape(content));
252268
}
253269

254270
/**

spring-webmvc/src/main/java/org/springframework/web/servlet/tags/form/OptionsTag.java

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,7 @@
187187
* @author Rob Harrop
188188
* @author Juergen Hoeller
189189
* @author Scott Andrews
190+
* @author Sam Brannen
190191
* @since 2.0
191192
*/
192193
@SuppressWarnings("serial")
@@ -306,7 +307,10 @@ protected int writeTagContent(TagWriter tagWriter) throws JspException {
306307
(itemValue != null ? ObjectUtils.getDisplayString(evaluate("itemValue", itemValue)) : null);
307308
String labelProperty =
308309
(itemLabel != null ? ObjectUtils.getDisplayString(evaluate("itemLabel", itemLabel)) : null);
309-
OptionsWriter optionWriter = new OptionsWriter(selectName, itemsObject, valueProperty, labelProperty);
310+
String encodingToUse =
311+
(isResponseEncodedHtmlEscape() ? this.pageContext.getResponse().getCharacterEncoding() : null);
312+
OptionsWriter optionWriter =
313+
new OptionsWriter(selectName, itemsObject, valueProperty, labelProperty, encodingToUse);
310314
optionWriter.writeOptions(tagWriter);
311315
}
312316
return SKIP_BODY;
@@ -345,9 +349,9 @@ private class OptionsWriter extends OptionWriter {
345349
private final @Nullable String selectName;
346350

347351
public OptionsWriter(@Nullable String selectName, Object optionSource,
348-
@Nullable String valueProperty, @Nullable String labelProperty) {
352+
@Nullable String valueProperty, @Nullable String labelProperty, @Nullable String encoding) {
349353

350-
super(optionSource, getBindStatus(), valueProperty, labelProperty, isHtmlEscape());
354+
super(optionSource, getBindStatus(), valueProperty, labelProperty, isHtmlEscape(), encoding);
351355
this.selectName = selectName;
352356
}
353357

spring-webmvc/src/main/java/org/springframework/web/servlet/tags/form/SelectTag.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,7 @@
235235
*
236236
* @author Rob Harrop
237237
* @author Juergen Hoeller
238+
* @author Sam Brannen
238239
* @since 2.0
239240
* @see OptionTag
240241
*/
@@ -407,8 +408,12 @@ protected int writeTagContent(TagWriter tagWriter) throws JspException {
407408
ObjectUtils.getDisplayString(evaluate("itemValue", getItemValue())) : null);
408409
String labelProperty = (getItemLabel() != null ?
409410
ObjectUtils.getDisplayString(evaluate("itemLabel", getItemLabel())) : null);
411+
String encodingToUse = (isResponseEncodedHtmlEscape() ?
412+
this.pageContext.getResponse().getCharacterEncoding() : null);
410413
OptionWriter optionWriter =
411-
new OptionWriter(itemsObject, getBindStatus(), valueProperty, labelProperty, isHtmlEscape()) {
414+
new OptionWriter(itemsObject, getBindStatus(), valueProperty, labelProperty,
415+
isHtmlEscape(), encodingToUse) {
416+
412417
@Override
413418
protected String processOptionValue(String resolvedValue) {
414419
return processFieldValue(selectName, resolvedValue, "option");

spring-webmvc/src/test/java/org/springframework/web/servlet/tags/form/OptionsTagTests.java

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import java.util.ArrayList;
2222
import java.util.Collections;
2323
import java.util.HashMap;
24+
import java.util.LinkedHashMap;
2425
import java.util.List;
2526
import java.util.Map;
2627

@@ -50,6 +51,7 @@
5051
* @author Juergen Hoeller
5152
* @author Scott Andrews
5253
* @author Jeremy Grelle
54+
* @author Sam Brannen
5355
*/
5456
@SuppressWarnings({ "rawtypes", "unchecked" })
5557
class OptionsTagTests extends AbstractHtmlElementTagTests {
@@ -115,6 +117,86 @@ void withCollection() throws Exception {
115117
assertThat(element.attribute("onclick").getValue()).isEqualTo("CLICK");
116118
}
117119

120+
@Test // gh-35783
121+
void withListWithHtmlEscaping() throws Exception {
122+
getPageContext().setAttribute(
123+
SelectTag.LIST_VALUE_PAGE_ATTRIBUTE, new BindStatus(getRequestContext(), "testBean.country", false));
124+
125+
this.tag.setItems(List.of("café", "Jane \"I Love Cafés\" Smith"));
126+
this.tag.setId("myOption");
127+
128+
var expectedOutput = """
129+
<option id="myOption1" value="caf&eacute;">caf&eacute;</option>
130+
<option id="myOption2" value="Jane &quot;I Love Caf&eacute;s&quot; Smith">Jane &quot;I Love Caf&eacute;s&quot; Smith</option>
131+
""".replace("\n", "");
132+
133+
assertThat(this.tag.doStartTag()).isEqualTo(Tag.SKIP_BODY);
134+
assertThat(getOutput()).isEqualTo(expectedOutput);
135+
}
136+
137+
@Test // gh-35783
138+
void withListWithHtmlEscapingAndCharacterEncoding() throws Exception {
139+
this.getPageContext().getResponse().setCharacterEncoding("UTF-8");
140+
141+
getPageContext().setAttribute(
142+
SelectTag.LIST_VALUE_PAGE_ATTRIBUTE, new BindStatus(getRequestContext(), "testBean.country", false));
143+
144+
this.tag.setItems(List.of("café", "Jane \"I Love Cafés\" Smith"));
145+
this.tag.setId("myOption");
146+
147+
var expectedOutput = """
148+
<option id="myOption1" value="café">café</option>
149+
<option id="myOption2" value="Jane &quot;I Love Cafés&quot; Smith">Jane &quot;I Love Cafés&quot; Smith</option>
150+
""".replace("\n", "");
151+
152+
assertThat(this.tag.doStartTag()).isEqualTo(Tag.SKIP_BODY);
153+
assertThat(getOutput()).isEqualTo(expectedOutput);
154+
}
155+
156+
@Test // gh-35783
157+
void withMapWithHtmlEscaping() throws Exception {
158+
getPageContext().setAttribute(
159+
SelectTag.LIST_VALUE_PAGE_ATTRIBUTE, new BindStatus(getRequestContext(), "testBean.country", false));
160+
161+
var map = new LinkedHashMap<String, String>();
162+
map.put("one", "Jane \"I Love Cafés\" Smith");
163+
map.put("two", "Joe Café");
164+
165+
this.tag.setItems(map);
166+
this.tag.setId("myOption");
167+
168+
var expectedOutput = """
169+
<option id="myOption1" value="one">Jane &quot;I Love Caf&eacute;s&quot; Smith</option>
170+
<option id="myOption2" value="two">Joe Caf&eacute;</option>
171+
""".replace("\n", "");
172+
173+
assertThat(this.tag.doStartTag()).isEqualTo(Tag.SKIP_BODY);
174+
assertThat(getOutput()).isEqualTo(expectedOutput);
175+
}
176+
177+
@Test // gh-35783
178+
void withMapWithHtmlEscapingAndCharacterEncoding() throws Exception {
179+
this.getPageContext().getResponse().setCharacterEncoding("UTF-8");
180+
181+
getPageContext().setAttribute(
182+
SelectTag.LIST_VALUE_PAGE_ATTRIBUTE, new BindStatus(getRequestContext(), "testBean.country", false));
183+
184+
var map = new LinkedHashMap<String, String>();
185+
map.put("one", "Jane \"I Love Cafés\" Smith");
186+
map.put("two", "Joe Café");
187+
188+
this.tag.setItems(map);
189+
this.tag.setId("myOption");
190+
191+
var expectedOutput = """
192+
<option id="myOption1" value="one">Jane &quot;I Love Cafés&quot; Smith</option>
193+
<option id="myOption2" value="two">Joe Café</option>
194+
""".replace("\n", "");
195+
196+
assertThat(this.tag.doStartTag()).isEqualTo(Tag.SKIP_BODY);
197+
assertThat(getOutput()).isEqualTo(expectedOutput);
198+
}
199+
118200
@Test
119201
void withCollectionAndDynamicAttributes() throws Exception {
120202
String dynamicAttribute1 = "attr1";

0 commit comments

Comments
 (0)