From 8e261e5833e24a31564fc386cf1def2166e8a4d7 Mon Sep 17 00:00:00 2001 From: Scott Andrews Date: Thu, 22 Jan 2009 20:36:36 +0000 Subject: [PATCH] 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(). --- .../form/AbstractMultiCheckedElementTag.java | 17 +++- .../web/servlet/tags/form/OptionWriter.java | 26 +++++- .../web/servlet/tags/form/OptionsTag.java | 12 ++- .../main/resources/META-INF/spring-form.tld | 8 +- .../servlet/tags/form/OptionsTagTests.java | 84 +++++++++++++++++-- .../tags/form/RadioButtonsTagTests.java | 51 ++++++++++- .../web/servlet/tags/form/TestTypes.java | 40 +++++++++ 7 files changed, 219 insertions(+), 19 deletions(-) create mode 100644 org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/tags/form/TestTypes.java 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 a529ccae0c..06a83f02b8 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 20a50ec649..3b3b6e5214 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 a9585a54a8..2017fdfd10 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 fa16e7a0b5..3f4ae1dc2a 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 de9d2b909a..09a023bfae 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 8c72cd4473..f44ed3b4c0 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 0000000000..e15e78ffdc --- /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 -- GitLab