Browse Source

Merge branch '6.2.x'

pull/34993/merge
Sam Brannen 1 month ago
parent
commit
c6b4b43076
  1. 10
      spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/TestBean.java
  2. 22
      spring-webmvc/src/main/java/org/springframework/web/servlet/tags/form/OptionWriter.java
  3. 10
      spring-webmvc/src/main/java/org/springframework/web/servlet/tags/form/OptionsTag.java
  4. 7
      spring-webmvc/src/main/java/org/springframework/web/servlet/tags/form/SelectTag.java
  5. 82
      spring-webmvc/src/test/java/org/springframework/web/servlet/tags/form/OptionsTagTests.java
  6. 153
      spring-webmvc/src/test/java/org/springframework/web/servlet/tags/form/SelectTagTests.java

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

@ -60,6 +60,8 @@ public class TestBean implements BeanNameAware, BeanFactoryAware, ITestBean, IOt
private boolean jedi; private boolean jedi;
private String favoriteCafé;
private ITestBean spouse; private ITestBean spouse;
private String touchy; private String touchy;
@ -210,6 +212,14 @@ public class TestBean implements BeanNameAware, BeanFactoryAware, ITestBean, IOt
this.jedi = jedi; this.jedi = jedi;
} }
public String getFavoriteCafé() {
return this.favoriteCafé;
}
public void setFavoriteCafé(String favoriteCafé) {
this.favoriteCafé = favoriteCafé;
}
@Override @Override
public ITestBean getSpouse() { public ITestBean getSpouse() {
return this.spouse; return this.spouse;

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

@ -28,6 +28,7 @@ import org.springframework.beans.PropertyAccessorFactory;
import org.springframework.util.Assert; import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils; import org.springframework.util.CollectionUtils;
import org.springframework.web.servlet.support.BindStatus; import org.springframework.web.servlet.support.BindStatus;
import org.springframework.web.util.HtmlUtils;
/** /**
* Provides supporting functionality to render a list of '{@code option}' * Provides supporting functionality to render a list of '{@code option}'
@ -100,18 +101,25 @@ class OptionWriter {
private final boolean htmlEscape; private final boolean htmlEscape;
private final @Nullable 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 optionSource the source of the {@code options} (never {@code null})
* @param bindStatus the {@link BindStatus} for the bound value (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 * @param valueProperty the name of the property used to render {@code option} values
* (optional) * (optional)
* @param labelProperty the name of the property used to render {@code option} labels * @param labelProperty the name of the property used to render {@code option} labels
* (optional) * (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, 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(optionSource, "'optionSource' must not be null");
Assert.notNull(bindStatus, "'bindStatus' must not be null"); Assert.notNull(bindStatus, "'bindStatus' must not be null");
@ -120,6 +128,7 @@ class OptionWriter {
this.valueProperty = valueProperty; this.valueProperty = valueProperty;
this.labelProperty = labelProperty; this.labelProperty = labelProperty;
this.htmlEscape = htmlEscape; this.htmlEscape = htmlEscape;
this.encoding = encoding;
} }
@ -248,7 +257,14 @@ class OptionWriter {
*/ */
private String getDisplayString(@Nullable Object value) { private String getDisplayString(@Nullable Object value) {
PropertyEditor editor = (value != null ? this.bindStatus.findEditor(value.getClass()) : null); 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));
} }
/** /**

10
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 Rob Harrop
* @author Juergen Hoeller * @author Juergen Hoeller
* @author Scott Andrews * @author Scott Andrews
* @author Sam Brannen
* @since 2.0 * @since 2.0
*/ */
@SuppressWarnings("serial") @SuppressWarnings("serial")
@ -306,7 +307,10 @@ public class OptionsTag extends AbstractHtmlElementTag {
(itemValue != null ? ObjectUtils.getDisplayString(evaluate("itemValue", itemValue)) : null); (itemValue != null ? ObjectUtils.getDisplayString(evaluate("itemValue", itemValue)) : null);
String labelProperty = String labelProperty =
(itemLabel != null ? ObjectUtils.getDisplayString(evaluate("itemLabel", itemLabel)) : null); (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); optionWriter.writeOptions(tagWriter);
} }
return SKIP_BODY; return SKIP_BODY;
@ -345,9 +349,9 @@ public class OptionsTag extends AbstractHtmlElementTag {
private final @Nullable String selectName; private final @Nullable String selectName;
public OptionsWriter(@Nullable String selectName, Object optionSource, 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; this.selectName = selectName;
} }

7
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 Rob Harrop
* @author Juergen Hoeller * @author Juergen Hoeller
* @author Sam Brannen
* @since 2.0 * @since 2.0
* @see OptionTag * @see OptionTag
*/ */
@ -407,8 +408,12 @@ public class SelectTag extends AbstractHtmlInputElementTag {
ObjectUtils.getDisplayString(evaluate("itemValue", getItemValue())) : null); ObjectUtils.getDisplayString(evaluate("itemValue", getItemValue())) : null);
String labelProperty = (getItemLabel() != null ? String labelProperty = (getItemLabel() != null ?
ObjectUtils.getDisplayString(evaluate("itemLabel", getItemLabel())) : null); ObjectUtils.getDisplayString(evaluate("itemLabel", getItemLabel())) : null);
String encodingToUse = (isResponseEncodedHtmlEscape() ?
this.pageContext.getResponse().getCharacterEncoding() : null);
OptionWriter optionWriter = OptionWriter optionWriter =
new OptionWriter(itemsObject, getBindStatus(), valueProperty, labelProperty, isHtmlEscape()) { new OptionWriter(itemsObject, getBindStatus(), valueProperty, labelProperty,
isHtmlEscape(), encodingToUse) {
@Override @Override
protected String processOptionValue(String resolvedValue) { protected String processOptionValue(String resolvedValue) {
return processFieldValue(selectName, resolvedValue, "option"); return processFieldValue(selectName, resolvedValue, "option");

82
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.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@ -50,6 +51,7 @@ import static org.assertj.core.api.Assertions.assertThat;
* @author Juergen Hoeller * @author Juergen Hoeller
* @author Scott Andrews * @author Scott Andrews
* @author Jeremy Grelle * @author Jeremy Grelle
* @author Sam Brannen
*/ */
@SuppressWarnings({ "rawtypes", "unchecked" }) @SuppressWarnings({ "rawtypes", "unchecked" })
class OptionsTagTests extends AbstractHtmlElementTagTests { class OptionsTagTests extends AbstractHtmlElementTagTests {
@ -115,6 +117,86 @@ class OptionsTagTests extends AbstractHtmlElementTagTests {
assertThat(element.attribute("onclick").getValue()).isEqualTo("CLICK"); 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 @Test
void withCollectionAndDynamicAttributes() throws Exception { void withCollectionAndDynamicAttributes() throws Exception {
String dynamicAttribute1 = "attr1"; String dynamicAttribute1 = "attr1";

153
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.Collections;
import java.util.Comparator; import java.util.Comparator;
import java.util.HashMap; import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.Map; import java.util.Map;
@ -54,6 +55,7 @@ import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
* @author Juergen Hoeller * @author Juergen Hoeller
* @author Jeremy Grelle * @author Jeremy Grelle
* @author Dave Syer * @author Dave Syer
* @author Sam Brannen
*/ */
@SuppressWarnings({ "rawtypes", "unchecked" }) @SuppressWarnings({ "rawtypes", "unchecked" })
public class SelectTagTests extends AbstractFormTagTests { public class SelectTagTests extends AbstractFormTagTests {
@ -78,6 +80,18 @@ public class SelectTagTests extends AbstractFormTagTests {
this.tag.setPageContext(getPageContext()); 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 @Test
void dynamicAttributes() throws JspException { void dynamicAttributes() throws JspException {
String dynamicAttribute1 = "attr1"; String dynamicAttribute1 = "attr1";
@ -133,6 +147,78 @@ public class SelectTagTests extends AbstractFormTagTests {
assertList(true); 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 @Test
void withResolvedList() throws Exception { void withResolvedList() throws Exception {
this.tag.setPath("country"); this.tag.setPath("country");
@ -336,8 +422,58 @@ public class SelectTagTests extends AbstractFormTagTests {
void withMap() throws Exception { void withMap() throws Exception {
this.tag.setPath("sex"); this.tag.setPath("sex");
this.tag.setItems(getSexes()); 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 @Test
@ -959,7 +1095,7 @@ public class SelectTagTests extends AbstractFormTagTests {
} }
private Map getSexes() { private Map getSexes() {
Map<String, String> sexes = new HashMap<>(); Map<String, String> sexes = new LinkedHashMap<>();
sexes.put("F", "Female"); sexes.put("F", "Female");
sexes.put("M", "Male"); sexes.put("M", "Male");
return sexes; return sexes;
@ -997,17 +1133,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() { private TestBean getTestBean() {
return (TestBean) getPageContext().getRequest().getAttribute(COMMAND_NAME); return (TestBean) getPageContext().getRequest().getAttribute(COMMAND_NAME);
} }

Loading…
Cancel
Save