Skip to content

Commit 8e261e5

Browse files
committed
SPR-3389 Nicer handling of Java 5 enums by the Spring MVC form taglib.
The form:options and form:radiobuttons tags will now render a set of options automatically if the bind target is an Enum and items are not otherwise specified. The values of the enum are converted into form inputs where by default the form value is the enum's name() and the form label is the enum's toString().
1 parent ad2cc34 commit 8e261e5

File tree

7 files changed

+219
-19
lines changed

7 files changed

+219
-19
lines changed

org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/tags/form/AbstractMultiCheckedElementTag.java

+16-1
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
* of '<code>checkbox</code>' or '<code>radio</code>'.
3636
*
3737
* @author Juergen Hoeller
38+
* @author Scott Andrews
3839
* @since 2.5.2
3940
*/
4041
public abstract class AbstractMultiCheckedElementTag extends AbstractCheckedElementTag {
@@ -191,6 +192,11 @@ protected int writeTagContent(TagWriter tagWriter) throws JspException {
191192
String labelProperty =
192193
(itemLabel != null ? ObjectUtils.getDisplayString(evaluate("itemLabel", itemLabel)) : null);
193194

195+
Class<?> boundType = getBindStatus().getValueType();
196+
if (itemsObject == null && boundType != null && boundType.isEnum()) {
197+
itemsObject = boundType.getEnumConstants();
198+
}
199+
194200
if (itemsObject == null) {
195201
throw new IllegalArgumentException("Attribute 'items' is required and must be a Collection, an Array or a Map");
196202
}
@@ -229,7 +235,16 @@ private void writeObjectEntry(TagWriter tagWriter, String valueProperty,
229235
String labelProperty, Object item, int itemIndex) throws JspException {
230236

231237
BeanWrapper wrapper = PropertyAccessorFactory.forBeanPropertyAccess(item);
232-
Object renderValue = (valueProperty != null ? wrapper.getPropertyValue(valueProperty) : item);
238+
Object renderValue;
239+
if (valueProperty != null) {
240+
renderValue = wrapper.getPropertyValue(valueProperty);
241+
}
242+
else if (item instanceof Enum) {
243+
renderValue = ((Enum<?>) item).name();
244+
}
245+
else {
246+
renderValue = item;
247+
}
233248
Object renderLabel = (labelProperty != null ? wrapper.getPropertyValue(labelProperty) : item);
234249
writeElementTag(tagWriter, item, renderValue, renderLabel, itemIndex);
235250
}

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

+24-2
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,8 @@
4242
* the <code>labelProperty</code>). These properties are then used when
4343
* rendering each element of the array/{@link Collection} as an '<code>option</code>'.
4444
* If either property name is omitted, the value of {@link Object#toString()} of
45-
* the corresponding array/{@link Collection} element is used instead.
45+
* the corresponding array/{@link Collection} element is used instead. However,
46+
* if the item is an enum, {@link Enum#name()} is used as the default value.
4647
* </p>
4748
* <h3>Using a {@link Map}:</h3>
4849
* <p>
@@ -83,6 +84,7 @@
8384
* @author Rob Harrop
8485
* @author Juergen Hoeller
8586
* @author Sam Brannen
87+
* @author Scott Andrews
8688
* @since 2.0
8789
*/
8890
class OptionWriter {
@@ -134,6 +136,9 @@ else if (this.optionSource instanceof Collection) {
134136
else if (this.optionSource instanceof Map) {
135137
renderFromMap(tagWriter);
136138
}
139+
else if (this.optionSource instanceof Class && this.optionSource.getClass().isEnum()) {
140+
renderFromEnum(tagWriter);
141+
}
137142
else {
138143
throw new JspException(
139144
"Type [" + this.optionSource.getClass().getName() + "] is not valid for option items");
@@ -177,6 +182,14 @@ private void renderFromCollection(TagWriter tagWriter) throws JspException {
177182
doRenderFromCollection((Collection) this.optionSource, tagWriter);
178183
}
179184

185+
/**
186+
* Renders the inner '<code>option</code>' tags using the {@link #optionSource}.
187+
* @see #doRenderFromCollection(java.util.Collection, TagWriter)
188+
*/
189+
private void renderFromEnum(final TagWriter tagWriter) throws JspException {
190+
doRenderFromCollection(CollectionUtils.arrayToList(((Class) this.optionSource).getEnumConstants()), tagWriter);
191+
}
192+
180193
/**
181194
* Renders the inner '<code>option</code>' tags using the supplied {@link Collection} of
182195
* objects as the source. The value of the {@link #valueProperty} field is used
@@ -187,7 +200,16 @@ private void doRenderFromCollection(Collection optionCollection, TagWriter tagWr
187200
for (Iterator it = optionCollection.iterator(); it.hasNext();) {
188201
Object item = it.next();
189202
BeanWrapper wrapper = PropertyAccessorFactory.forBeanPropertyAccess(item);
190-
Object value = (this.valueProperty != null ? wrapper.getPropertyValue(this.valueProperty) : item);
203+
Object value;
204+
if (this.valueProperty != null) {
205+
value = wrapper.getPropertyValue(this.valueProperty);
206+
}
207+
else if (item instanceof Enum) {
208+
value = ((Enum<?>) item).name();
209+
}
210+
else {
211+
value = item;
212+
}
191213
Object label = (this.labelProperty != null ? wrapper.getPropertyValue(this.labelProperty) : item);
192214
renderOption(tagWriter, item, value, label);
193215
}

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

+11-1
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
*
3434
* @author Rob Harrop
3535
* @author Juergen Hoeller
36+
* @author Scott Andrews
3637
* @since 2.0
3738
*/
3839
public class OptionsTag extends AbstractHtmlElementTag {
@@ -146,7 +147,16 @@ protected boolean isDisabled() {
146147
protected int writeTagContent(TagWriter tagWriter) throws JspException {
147148
assertUnderSelectTag();
148149
Object items = getItems();
149-
Object itemsObject = (items instanceof String ? evaluate("items", (String) items) : items);
150+
Object itemsObject = null;
151+
if (items != null) {
152+
itemsObject = (items instanceof String ? evaluate("items", (String) items) : items);
153+
} else {
154+
Class<?> selectTagBoundType = ((SelectTag) findAncestorWithClass(this, SelectTag.class))
155+
.getBindStatus().getValueType();
156+
if (selectTagBoundType != null && selectTagBoundType.isEnum()) {
157+
itemsObject = selectTagBoundType.getEnumConstants();
158+
}
159+
}
150160
if (itemsObject != null) {
151161
String itemValue = getItemValue();
152162
String itemLabel = getItemLabel();

org.springframework.web.servlet/src/main/resources/META-INF/spring-form.tld

+4-4
Original file line numberDiff line numberDiff line change
@@ -971,9 +971,9 @@
971971
</attribute>
972972
<attribute>
973973
<name>items</name>
974-
<required>true</required>
974+
<required>false</required>
975975
<rtexprvalue>true</rtexprvalue>
976-
<description>The Collection, Map or array of objects used to generate the inner 'option' tags</description>
976+
<description>The Collection, Map or array of objects used to generate the inner 'option' tags. This attribute is required unless the containing select's property for data binding is an Enum, in which case the enum's values are used.</description>
977977
</attribute>
978978
<attribute>
979979
<name>itemValue</name>
@@ -1433,9 +1433,9 @@
14331433
<!-- radiobuttons specific attributes -->
14341434
<attribute>
14351435
<name>items</name>
1436-
<required>true</required>
1436+
<required>false</required>
14371437
<rtexprvalue>true</rtexprvalue>
1438-
<description>The Collection, Map or array of objects used to generate the 'input' tags with type 'radio'</description>
1438+
<description>The Collection, Map or array of objects used to generate the 'input' tags with type 'radio'. This attribute is required unless the property for data binding is an Enum, in which case the enum's values are used.</description>
14391439
</attribute>
14401440
<attribute>
14411441
<name>itemValue</name>

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

+75-9
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,13 @@
2424
import java.util.Map;
2525

2626
import javax.servlet.http.HttpServletRequest;
27+
import javax.servlet.jsp.tagext.BodyTag;
2728
import javax.servlet.jsp.tagext.Tag;
2829

2930
import org.dom4j.Document;
3031
import org.dom4j.Element;
32+
import org.dom4j.Node;
3133
import org.dom4j.io.SAXReader;
32-
3334
import org.springframework.beans.TestBean;
3435
import org.springframework.mock.web.MockHttpServletRequest;
3536
import org.springframework.mock.web.MockPageContext;
@@ -43,11 +44,13 @@
4344
/**
4445
* @author Rob Harrop
4546
* @author Juergen Hoeller
47+
* @author Scott Andrews
4648
*/
47-
public class OptionsTagTests extends AbstractHtmlElementTagTests {
49+
public final class OptionsTagTests extends AbstractHtmlElementTagTests {
4850

4951
private static final String COMMAND_NAME = "testBean";
5052

53+
private SelectTag selectTag;
5154
private OptionsTag tag;
5255

5356
protected void onSetUp() {
@@ -56,7 +59,13 @@ protected TagWriter createTagWriter() {
5659
return new TagWriter(getWriter());
5760
}
5861
};
59-
this.tag.setParent(new SelectTag());
62+
selectTag = new SelectTag() {
63+
protected TagWriter createTagWriter() {
64+
return new TagWriter(getWriter());
65+
}
66+
};
67+
selectTag.setPageContext(getPageContext());
68+
this.tag.setParent(selectTag);
6069
this.tag.setPageContext(getPageContext());
6170
}
6271

@@ -148,24 +157,81 @@ public void testWithItemsNullReference() throws Exception {
148157
}
149158

150159
public void testWithoutItems() throws Exception {
151-
getPageContext().setAttribute(
152-
SelectTag.LIST_VALUE_PAGE_ATTRIBUTE, new BindStatus(getRequestContext(), "testBean.country", false));
153-
154160
this.tag.setItemValue("isoCode");
155161
this.tag.setItemLabel("name");
162+
this.selectTag.setPath("testBean");
163+
164+
this.selectTag.doStartTag();
156165
int result = this.tag.doStartTag();
157166
assertEquals(Tag.SKIP_BODY, result);
167+
this.tag.doEndTag();
168+
this.selectTag.doEndTag();
169+
158170
String output = getOutput();
159-
output = "<doc>" + output + "</doc>";
160-
161171
SAXReader reader = new SAXReader();
162172
Document document = reader.read(new StringReader(output));
163173
Element rootElement = document.getRootElement();
164-
174+
165175
List children = rootElement.elements();
166176
assertEquals("Incorrect number of children", 0, children.size());
167177
}
168178

179+
public void testWithoutItemsEnumParent() throws Exception {
180+
BeanWithEnum testBean = new BeanWithEnum();
181+
testBean.setTestEnum(TestEnum.VALUE_2);
182+
getPageContext().getRequest().setAttribute("testBean", testBean);
183+
184+
this.selectTag.setPath("testBean.testEnum");
185+
186+
this.selectTag.doStartTag();
187+
int result = this.tag.doStartTag();
188+
assertEquals(BodyTag.SKIP_BODY, result);
189+
result = this.tag.doEndTag();
190+
assertEquals(Tag.EVAL_PAGE, result);
191+
this.selectTag.doEndTag();
192+
193+
String output = getWriter().toString();
194+
SAXReader reader = new SAXReader();
195+
Document document = reader.read(new StringReader(output));
196+
Element rootElement = document.getRootElement();
197+
198+
assertEquals(2, rootElement.elements().size());
199+
Node value1 = rootElement.selectSingleNode("option[@value = 'VALUE_1']");
200+
Node value2 = rootElement.selectSingleNode("option[@value = 'VALUE_2']");
201+
assertEquals("TestEnum: VALUE_1", value1.getText());
202+
assertEquals("TestEnum: VALUE_2", value2.getText());
203+
assertEquals(value2, rootElement.selectSingleNode("option[@selected]"));
204+
}
205+
206+
public void testWithoutItemsEnumParentWithExplicitLabelsAndValues() throws Exception {
207+
BeanWithEnum testBean = new BeanWithEnum();
208+
testBean.setTestEnum(TestEnum.VALUE_2);
209+
getPageContext().getRequest().setAttribute("testBean", testBean);
210+
211+
this.selectTag.setPath("testBean.testEnum");
212+
this.tag.setItemLabel("enumLabel");
213+
this.tag.setItemValue("enumValue");
214+
215+
this.selectTag.doStartTag();
216+
int result = this.tag.doStartTag();
217+
assertEquals(BodyTag.SKIP_BODY, result);
218+
result = this.tag.doEndTag();
219+
assertEquals(Tag.EVAL_PAGE, result);
220+
this.selectTag.doEndTag();
221+
222+
String output = getWriter().toString();
223+
SAXReader reader = new SAXReader();
224+
Document document = reader.read(new StringReader(output));
225+
Element rootElement = document.getRootElement();
226+
227+
assertEquals(2, rootElement.elements().size());
228+
Node value1 = rootElement.selectSingleNode("option[@value = 'Value: VALUE_1']");
229+
Node value2 = rootElement.selectSingleNode("option[@value = 'Value: VALUE_2']");
230+
assertEquals("Label: VALUE_1", value1.getText());
231+
assertEquals("Label: VALUE_2", value2.getText());
232+
assertEquals(value2, rootElement.selectSingleNode("option[@selected]"));
233+
}
234+
169235
protected void extendRequest(MockHttpServletRequest request) {
170236
TestBean bean = new TestBean();
171237
bean.setName("foo");

org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/tags/form/RadioButtonsTagTests.java

+49-2
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,8 @@
3131

3232
import org.dom4j.Document;
3333
import org.dom4j.Element;
34+
import org.dom4j.Node;
3435
import org.dom4j.io.SAXReader;
35-
3636
import org.springframework.beans.Colour;
3737
import org.springframework.beans.Pet;
3838
import org.springframework.beans.TestBean;
@@ -43,8 +43,9 @@
4343
/**
4444
* @author Thomas Risberg
4545
* @author Juergen Hoeller
46+
* @author Scott Andrews
4647
*/
47-
public class RadioButtonsTagTests extends AbstractFormTagTests {
48+
public final class RadioButtonsTagTests extends AbstractFormTagTests {
4849

4950
private RadioButtonsTag tag;
5051

@@ -404,6 +405,52 @@ public void testCollectionOfPetsWithEditor() throws Exception {
404405
assertEquals("Mufty", radioButtonElement5.attribute("value").getValue());
405406
assertEquals("MUFTY", spanElement5.getStringValue());
406407
}
408+
409+
public void testWithoutItemsEnumBindTarget() throws Exception {
410+
BeanWithEnum testBean = new BeanWithEnum();
411+
testBean.setTestEnum(TestEnum.VALUE_2);
412+
getPageContext().getRequest().setAttribute("testBean", testBean);
413+
414+
this.tag.setPath("testEnum");
415+
int result = this.tag.doStartTag();
416+
assertEquals(Tag.SKIP_BODY, result);
417+
418+
String output = "<div>" + getOutput() + "</div>";
419+
SAXReader reader = new SAXReader();
420+
Document document = reader.read(new StringReader(output));
421+
Element rootElement = document.getRootElement();
422+
423+
assertEquals(2, rootElement.elements().size());
424+
Node value1 = rootElement.selectSingleNode("//input[@value = 'VALUE_1']");
425+
Node value2 = rootElement.selectSingleNode("//input[@value = 'VALUE_2']");
426+
assertEquals("TestEnum: VALUE_1", rootElement.selectSingleNode("//label[@for = '" + value1.valueOf("@id") + "']").getText());
427+
assertEquals("TestEnum: VALUE_2", rootElement.selectSingleNode("//label[@for = '" + value2.valueOf("@id") + "']").getText());
428+
assertEquals(value2, rootElement.selectSingleNode("//input[@checked]"));
429+
}
430+
431+
public void testWithoutItemsEnumBindTargetWithExplicitLabelsAndValues() throws Exception {
432+
BeanWithEnum testBean = new BeanWithEnum();
433+
testBean.setTestEnum(TestEnum.VALUE_2);
434+
getPageContext().getRequest().setAttribute("testBean", testBean);
435+
436+
this.tag.setPath("testEnum");
437+
this.tag.setItemLabel("enumLabel");
438+
this.tag.setItemValue("enumValue");
439+
int result = this.tag.doStartTag();
440+
assertEquals(Tag.SKIP_BODY, result);
441+
442+
String output = "<div>" + getOutput() + "</div>";
443+
SAXReader reader = new SAXReader();
444+
Document document = reader.read(new StringReader(output));
445+
Element rootElement = document.getRootElement();
446+
447+
assertEquals(2, rootElement.elements().size());
448+
Node value1 = rootElement.selectSingleNode("//input[@value = 'Value: VALUE_1']");
449+
Node value2 = rootElement.selectSingleNode("//input[@value = 'Value: VALUE_2']");
450+
assertEquals("Label: VALUE_1", rootElement.selectSingleNode("//label[@for = '" + value1.valueOf("@id") + "']").getText());
451+
assertEquals("Label: VALUE_2", rootElement.selectSingleNode("//label[@for = '" + value2.valueOf("@id") + "']").getText());
452+
assertEquals(value2, rootElement.selectSingleNode("//input[@checked]"));
453+
}
407454

408455
public void testWithNullValue() throws Exception {
409456
try {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package org.springframework.web.servlet.tags.form;
2+
3+
/**
4+
* Test related data types for <code>org.springframework.web.servlet.tags.form</code> package.
5+
*
6+
* @author Scott Andrews
7+
*/
8+
public class TestTypes { }
9+
10+
class BeanWithEnum {
11+
12+
private TestEnum testEnum;
13+
14+
public TestEnum getTestEnum() {
15+
return testEnum;
16+
}
17+
18+
public void setTestEnum(TestEnum customEnum) {
19+
this.testEnum = customEnum;
20+
}
21+
22+
}
23+
24+
enum TestEnum {
25+
26+
VALUE_1, VALUE_2;
27+
28+
public String getEnumLabel() {
29+
return "Label: " + name();
30+
}
31+
32+
public String getEnumValue() {
33+
return "Value: " + name();
34+
}
35+
36+
public String toString() {
37+
return "TestEnum: " + name();
38+
}
39+
40+
}

0 commit comments

Comments
 (0)