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 @@ -60,6 +60,8 @@ public class TestBean implements BeanNameAware, BeanFactoryAware, ITestBean, IOt
private boolean jedi;
private String favoriteCafé;
private ITestBean spouse;
private String touchy;
@ -210,6 +212,14 @@ public class TestBean implements BeanNameAware, BeanFactoryAware, ITestBean, IOt @@ -210,6 +212,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;

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

@ -28,6 +28,7 @@ import org.springframework.beans.PropertyAccessorFactory; @@ -28,6 +28,7 @@ import org.springframework.beans.PropertyAccessorFactory;
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}'
@ -100,18 +101,25 @@ class OptionWriter { @@ -100,18 +101,25 @@ class OptionWriter {
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 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");
@ -120,6 +128,7 @@ class OptionWriter { @@ -120,6 +128,7 @@ class OptionWriter {
this.valueProperty = valueProperty;
this.labelProperty = labelProperty;
this.htmlEscape = htmlEscape;
this.encoding = encoding;
}
@ -248,7 +257,14 @@ class OptionWriter { @@ -248,7 +257,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));
}
/**

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

@ -187,6 +187,7 @@ import org.springframework.web.util.TagUtils; @@ -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")
@ -306,7 +307,10 @@ public class OptionsTag extends AbstractHtmlElementTag { @@ -306,7 +307,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;
@ -345,9 +349,9 @@ public class OptionsTag extends AbstractHtmlElementTag { @@ -345,9 +349,9 @@ public class OptionsTag extends AbstractHtmlElementTag {
private final @Nullable 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;
}

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; @@ -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
*/
@ -407,8 +408,12 @@ public class SelectTag extends AbstractHtmlInputElementTag { @@ -407,8 +408,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
spring-webmvc/src/test/java/org/springframework/web/servlet/tags/form/OptionsTagTests.java

@ -21,6 +21,7 @@ import java.io.StringReader; @@ -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; @@ -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 {
@ -115,6 +117,86 @@ class OptionsTagTests extends AbstractHtmlElementTagTests { @@ -115,6 +117,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";

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

@ -23,6 +23,7 @@ import java.util.ArrayList; @@ -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; @@ -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 {
@ -78,6 +80,18 @@ public class SelectTagTests extends AbstractFormTagTests { @@ -78,6 +80,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";
@ -133,6 +147,78 @@ public class SelectTagTests extends AbstractFormTagTests { @@ -133,6 +147,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");
@ -336,8 +422,58 @@ public class SelectTagTests extends AbstractFormTagTests { @@ -336,8 +422,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
@ -959,7 +1095,7 @@ public class SelectTagTests extends AbstractFormTagTests { @@ -959,7 +1095,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;
@ -997,17 +1133,6 @@ public class SelectTagTests extends AbstractFormTagTests { @@ -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() {
return (TestBean) getPageContext().getRequest().getAttribute(COMMAND_NAME);
}

Loading…
Cancel
Save