diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/tags/form/AbstractMultiCheckedElementTag.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/tags/form/AbstractMultiCheckedElementTag.java index a529ccae0c119ef8328344a28d7f84aa92ca0dda..06a83f02b84d4eef059e5e2a7074d9489b05b85b 100644 --- a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/tags/form/AbstractMultiCheckedElementTag.java +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/tags/form/AbstractMultiCheckedElementTag.java @@ -35,6 +35,7 @@ import org.springframework.util.StringUtils; * of 'checkbox' or 'radio'. * * @author Juergen Hoeller + * @author Scott Andrews * @since 2.5.2 */ public abstract class AbstractMultiCheckedElementTag extends AbstractCheckedElementTag { @@ -191,6 +192,11 @@ public abstract class AbstractMultiCheckedElementTag extends AbstractCheckedElem String labelProperty = (itemLabel != null ? ObjectUtils.getDisplayString(evaluate("itemLabel", itemLabel)) : null); + Class boundType = getBindStatus().getValueType(); + if (itemsObject == null && boundType != null && boundType.isEnum()) { + itemsObject = boundType.getEnumConstants(); + } + if (itemsObject == null) { throw new IllegalArgumentException("Attribute 'items' is required and must be a Collection, an Array or a Map"); } @@ -229,7 +235,16 @@ public abstract class AbstractMultiCheckedElementTag extends AbstractCheckedElem String labelProperty, Object item, int itemIndex) throws JspException { BeanWrapper wrapper = PropertyAccessorFactory.forBeanPropertyAccess(item); - Object renderValue = (valueProperty != null ? wrapper.getPropertyValue(valueProperty) : item); + Object renderValue; + if (valueProperty != null) { + renderValue = wrapper.getPropertyValue(valueProperty); + } + else if (item instanceof Enum) { + renderValue = ((Enum) item).name(); + } + else { + renderValue = item; + } Object renderLabel = (labelProperty != null ? wrapper.getPropertyValue(labelProperty) : item); writeElementTag(tagWriter, item, renderValue, renderLabel, itemIndex); } diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/tags/form/OptionWriter.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/tags/form/OptionWriter.java index 20a50ec649ddc31fd57e9411f6bb5111e47be10d..3b3b6e521445a05a2e50aa000b89eee1c0096835 100644 --- a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/tags/form/OptionWriter.java +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/tags/form/OptionWriter.java @@ -42,7 +42,8 @@ import org.springframework.web.servlet.support.BindStatus; * the labelProperty). These properties are then used when * rendering each element of the array/{@link Collection} as an 'option'. * If either property name is omitted, the value of {@link Object#toString()} of - * the corresponding array/{@link Collection} element is used instead. + * the corresponding array/{@link Collection} element is used instead. However, + * if the item is an enum, {@link Enum#name()} is used as the default value. *

*

Using a {@link Map}:

*

@@ -83,6 +84,7 @@ import org.springframework.web.servlet.support.BindStatus; * @author Rob Harrop * @author Juergen Hoeller * @author Sam Brannen + * @author Scott Andrews * @since 2.0 */ class OptionWriter { @@ -134,6 +136,9 @@ class OptionWriter { else if (this.optionSource instanceof Map) { renderFromMap(tagWriter); } + else if (this.optionSource instanceof Class && this.optionSource.getClass().isEnum()) { + renderFromEnum(tagWriter); + } else { throw new JspException( "Type [" + this.optionSource.getClass().getName() + "] is not valid for option items"); @@ -177,6 +182,14 @@ class OptionWriter { doRenderFromCollection((Collection) this.optionSource, tagWriter); } + /** + * Renders the inner 'option' tags using the {@link #optionSource}. + * @see #doRenderFromCollection(java.util.Collection, TagWriter) + */ + private void renderFromEnum(final TagWriter tagWriter) throws JspException { + doRenderFromCollection(CollectionUtils.arrayToList(((Class) this.optionSource).getEnumConstants()), tagWriter); + } + /** * Renders the inner 'option' tags using the supplied {@link Collection} of * objects as the source. The value of the {@link #valueProperty} field is used @@ -187,7 +200,16 @@ class OptionWriter { for (Iterator it = optionCollection.iterator(); it.hasNext();) { Object item = it.next(); BeanWrapper wrapper = PropertyAccessorFactory.forBeanPropertyAccess(item); - Object value = (this.valueProperty != null ? wrapper.getPropertyValue(this.valueProperty) : item); + Object value; + if (this.valueProperty != null) { + value = wrapper.getPropertyValue(this.valueProperty); + } + else if (item instanceof Enum) { + value = ((Enum) item).name(); + } + else { + value = item; + } Object label = (this.labelProperty != null ? wrapper.getPropertyValue(this.labelProperty) : item); renderOption(tagWriter, item, value, label); } diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/tags/form/OptionsTag.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/tags/form/OptionsTag.java index a9585a54a814a5dc692339712d5adeb3ebb308f6..2017fdfd10e6ba8c75599559b98baea74e114bf4 100644 --- a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/tags/form/OptionsTag.java +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/tags/form/OptionsTag.java @@ -33,6 +33,7 @@ import org.springframework.web.util.TagUtils; * * @author Rob Harrop * @author Juergen Hoeller + * @author Scott Andrews * @since 2.0 */ public class OptionsTag extends AbstractHtmlElementTag { @@ -146,7 +147,16 @@ public class OptionsTag extends AbstractHtmlElementTag { protected int writeTagContent(TagWriter tagWriter) throws JspException { assertUnderSelectTag(); Object items = getItems(); - Object itemsObject = (items instanceof String ? evaluate("items", (String) items) : items); + Object itemsObject = null; + if (items != null) { + itemsObject = (items instanceof String ? evaluate("items", (String) items) : items); + } else { + Class selectTagBoundType = ((SelectTag) findAncestorWithClass(this, SelectTag.class)) + .getBindStatus().getValueType(); + if (selectTagBoundType != null && selectTagBoundType.isEnum()) { + itemsObject = selectTagBoundType.getEnumConstants(); + } + } if (itemsObject != null) { String itemValue = getItemValue(); String itemLabel = getItemLabel(); diff --git a/org.springframework.web.servlet/src/main/resources/META-INF/spring-form.tld b/org.springframework.web.servlet/src/main/resources/META-INF/spring-form.tld index fa16e7a0b538ccaafa01f77b7f811779ea67a861..3f4ae1dc2a89b5a03da844590906d87b354c51b3 100644 --- a/org.springframework.web.servlet/src/main/resources/META-INF/spring-form.tld +++ b/org.springframework.web.servlet/src/main/resources/META-INF/spring-form.tld @@ -971,9 +971,9 @@ items - true + false true - The Collection, Map or array of objects used to generate the inner 'option' tags + 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. itemValue @@ -1433,9 +1433,9 @@ items - true + false true - The Collection, Map or array of objects used to generate the 'input' tags with type 'radio' + 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. itemValue diff --git a/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/tags/form/OptionsTagTests.java b/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/tags/form/OptionsTagTests.java index de9d2b909af3217af208ab0705209f86249efd0a..09a023bfae491d6fd6b2defb55b86b403e31142b 100644 --- a/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/tags/form/OptionsTagTests.java +++ b/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/tags/form/OptionsTagTests.java @@ -24,12 +24,13 @@ import java.util.List; import java.util.Map; import javax.servlet.http.HttpServletRequest; +import javax.servlet.jsp.tagext.BodyTag; import javax.servlet.jsp.tagext.Tag; import org.dom4j.Document; import org.dom4j.Element; +import org.dom4j.Node; import org.dom4j.io.SAXReader; - import org.springframework.beans.TestBean; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockPageContext; @@ -43,11 +44,13 @@ import org.springframework.web.servlet.tags.RequestContextAwareTag; /** * @author Rob Harrop * @author Juergen Hoeller + * @author Scott Andrews */ -public class OptionsTagTests extends AbstractHtmlElementTagTests { +public final class OptionsTagTests extends AbstractHtmlElementTagTests { private static final String COMMAND_NAME = "testBean"; + private SelectTag selectTag; private OptionsTag tag; protected void onSetUp() { @@ -56,7 +59,13 @@ public class OptionsTagTests extends AbstractHtmlElementTagTests { return new TagWriter(getWriter()); } }; - this.tag.setParent(new SelectTag()); + selectTag = new SelectTag() { + protected TagWriter createTagWriter() { + return new TagWriter(getWriter()); + } + }; + selectTag.setPageContext(getPageContext()); + this.tag.setParent(selectTag); this.tag.setPageContext(getPageContext()); } @@ -148,24 +157,81 @@ public class OptionsTagTests extends AbstractHtmlElementTagTests { } public void testWithoutItems() throws Exception { - getPageContext().setAttribute( - SelectTag.LIST_VALUE_PAGE_ATTRIBUTE, new BindStatus(getRequestContext(), "testBean.country", false)); - this.tag.setItemValue("isoCode"); this.tag.setItemLabel("name"); + this.selectTag.setPath("testBean"); + + this.selectTag.doStartTag(); int result = this.tag.doStartTag(); assertEquals(Tag.SKIP_BODY, result); + this.tag.doEndTag(); + this.selectTag.doEndTag(); + String output = getOutput(); - output = "" + output + ""; - SAXReader reader = new SAXReader(); Document document = reader.read(new StringReader(output)); Element rootElement = document.getRootElement(); - + List children = rootElement.elements(); assertEquals("Incorrect number of children", 0, children.size()); } + public void testWithoutItemsEnumParent() throws Exception { + BeanWithEnum testBean = new BeanWithEnum(); + testBean.setTestEnum(TestEnum.VALUE_2); + getPageContext().getRequest().setAttribute("testBean", testBean); + + this.selectTag.setPath("testBean.testEnum"); + + this.selectTag.doStartTag(); + int result = this.tag.doStartTag(); + assertEquals(BodyTag.SKIP_BODY, result); + result = this.tag.doEndTag(); + assertEquals(Tag.EVAL_PAGE, result); + this.selectTag.doEndTag(); + + String output = getWriter().toString(); + SAXReader reader = new SAXReader(); + Document document = reader.read(new StringReader(output)); + Element rootElement = document.getRootElement(); + + assertEquals(2, rootElement.elements().size()); + Node value1 = rootElement.selectSingleNode("option[@value = 'VALUE_1']"); + Node value2 = rootElement.selectSingleNode("option[@value = 'VALUE_2']"); + assertEquals("TestEnum: VALUE_1", value1.getText()); + assertEquals("TestEnum: VALUE_2", value2.getText()); + assertEquals(value2, rootElement.selectSingleNode("option[@selected]")); + } + + public void testWithoutItemsEnumParentWithExplicitLabelsAndValues() throws Exception { + BeanWithEnum testBean = new BeanWithEnum(); + testBean.setTestEnum(TestEnum.VALUE_2); + getPageContext().getRequest().setAttribute("testBean", testBean); + + this.selectTag.setPath("testBean.testEnum"); + this.tag.setItemLabel("enumLabel"); + this.tag.setItemValue("enumValue"); + + this.selectTag.doStartTag(); + int result = this.tag.doStartTag(); + assertEquals(BodyTag.SKIP_BODY, result); + result = this.tag.doEndTag(); + assertEquals(Tag.EVAL_PAGE, result); + this.selectTag.doEndTag(); + + String output = getWriter().toString(); + SAXReader reader = new SAXReader(); + Document document = reader.read(new StringReader(output)); + Element rootElement = document.getRootElement(); + + assertEquals(2, rootElement.elements().size()); + Node value1 = rootElement.selectSingleNode("option[@value = 'Value: VALUE_1']"); + Node value2 = rootElement.selectSingleNode("option[@value = 'Value: VALUE_2']"); + assertEquals("Label: VALUE_1", value1.getText()); + assertEquals("Label: VALUE_2", value2.getText()); + assertEquals(value2, rootElement.selectSingleNode("option[@selected]")); + } + protected void extendRequest(MockHttpServletRequest request) { TestBean bean = new TestBean(); bean.setName("foo"); diff --git a/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/tags/form/RadioButtonsTagTests.java b/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/tags/form/RadioButtonsTagTests.java index 8c72cd4473729c6eaa41f2e9a544e266bfaf5bba..f44ed3b4c0611beb8fc6f5fc2b83a620f4968f5c 100644 --- a/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/tags/form/RadioButtonsTagTests.java +++ b/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/tags/form/RadioButtonsTagTests.java @@ -31,8 +31,8 @@ import javax.servlet.jsp.tagext.Tag; import org.dom4j.Document; import org.dom4j.Element; +import org.dom4j.Node; import org.dom4j.io.SAXReader; - import org.springframework.beans.Colour; import org.springframework.beans.Pet; import org.springframework.beans.TestBean; @@ -43,8 +43,9 @@ import org.springframework.validation.BindingResult; /** * @author Thomas Risberg * @author Juergen Hoeller + * @author Scott Andrews */ -public class RadioButtonsTagTests extends AbstractFormTagTests { +public final class RadioButtonsTagTests extends AbstractFormTagTests { private RadioButtonsTag tag; @@ -404,6 +405,52 @@ public class RadioButtonsTagTests extends AbstractFormTagTests { assertEquals("Mufty", radioButtonElement5.attribute("value").getValue()); assertEquals("MUFTY", spanElement5.getStringValue()); } + + public void testWithoutItemsEnumBindTarget() throws Exception { + BeanWithEnum testBean = new BeanWithEnum(); + testBean.setTestEnum(TestEnum.VALUE_2); + getPageContext().getRequest().setAttribute("testBean", testBean); + + this.tag.setPath("testEnum"); + int result = this.tag.doStartTag(); + assertEquals(Tag.SKIP_BODY, result); + + String output = "

" + getOutput() + "
"; + SAXReader reader = new SAXReader(); + Document document = reader.read(new StringReader(output)); + Element rootElement = document.getRootElement(); + + assertEquals(2, rootElement.elements().size()); + Node value1 = rootElement.selectSingleNode("//input[@value = 'VALUE_1']"); + Node value2 = rootElement.selectSingleNode("//input[@value = 'VALUE_2']"); + assertEquals("TestEnum: VALUE_1", rootElement.selectSingleNode("//label[@for = '" + value1.valueOf("@id") + "']").getText()); + assertEquals("TestEnum: VALUE_2", rootElement.selectSingleNode("//label[@for = '" + value2.valueOf("@id") + "']").getText()); + assertEquals(value2, rootElement.selectSingleNode("//input[@checked]")); + } + + public void testWithoutItemsEnumBindTargetWithExplicitLabelsAndValues() throws Exception { + BeanWithEnum testBean = new BeanWithEnum(); + testBean.setTestEnum(TestEnum.VALUE_2); + getPageContext().getRequest().setAttribute("testBean", testBean); + + this.tag.setPath("testEnum"); + this.tag.setItemLabel("enumLabel"); + this.tag.setItemValue("enumValue"); + int result = this.tag.doStartTag(); + assertEquals(Tag.SKIP_BODY, result); + + String output = "
" + getOutput() + "
"; + SAXReader reader = new SAXReader(); + Document document = reader.read(new StringReader(output)); + Element rootElement = document.getRootElement(); + + assertEquals(2, rootElement.elements().size()); + Node value1 = rootElement.selectSingleNode("//input[@value = 'Value: VALUE_1']"); + Node value2 = rootElement.selectSingleNode("//input[@value = 'Value: VALUE_2']"); + assertEquals("Label: VALUE_1", rootElement.selectSingleNode("//label[@for = '" + value1.valueOf("@id") + "']").getText()); + assertEquals("Label: VALUE_2", rootElement.selectSingleNode("//label[@for = '" + value2.valueOf("@id") + "']").getText()); + assertEquals(value2, rootElement.selectSingleNode("//input[@checked]")); + } public void testWithNullValue() throws Exception { try { diff --git a/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/tags/form/TestTypes.java b/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/tags/form/TestTypes.java new file mode 100644 index 0000000000000000000000000000000000000000..e15e78ffdce355ef651df71de92c92153a96a455 --- /dev/null +++ b/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/tags/form/TestTypes.java @@ -0,0 +1,40 @@ +package org.springframework.web.servlet.tags.form; + +/** + * Test related data types for org.springframework.web.servlet.tags.form package. + * + * @author Scott Andrews + */ +public class TestTypes { } + +class BeanWithEnum { + + private TestEnum testEnum; + + public TestEnum getTestEnum() { + return testEnum; + } + + public void setTestEnum(TestEnum customEnum) { + this.testEnum = customEnum; + } + +} + +enum TestEnum { + + VALUE_1, VALUE_2; + + public String getEnumLabel() { + return "Label: " + name(); + } + + public String getEnumValue() { + return "Value: " + name(); + } + + public String toString() { + return "TestEnum: " + name(); + } + +} \ No newline at end of file