/* * Copyright 2002-2008 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.springframework.web.servlet.tags.form; import java.util.Collection; import java.util.Map; import javax.servlet.jsp.JspException; import org.springframework.util.ObjectUtils; import org.springframework.web.bind.WebDataBinder; import org.springframework.web.servlet.support.BindStatus; /** * Databinding-aware JSP tag that renders an HTML '<code>select</code>' * element. * * <p>Inner '<code>option</code>' tags can be rendered using one of the * approaches supported by the OptionWriter class. * * <p>Also supports the use of nested {@link OptionTag OptionTags} or * (typically one) nested {@link OptionsTag}. * * @author Rob Harrop * @author Juergen Hoeller * @since 2.0 * @see OptionTag */ public class SelectTag extends AbstractHtmlInputElementTag { /** * The {@link javax.servlet.jsp.PageContext} attribute under * which the bound value is exposed to inner {@link OptionTag OptionTags}. */ public static final String LIST_VALUE_PAGE_ATTRIBUTE = "org.springframework.web.servlet.tags.form.SelectTag.listValue"; /** * Marker object for items that have been specified but resolve to null. * Allows to differentiate between 'set but null' and 'not set at all'. */ private static final Object EMPTY = new Object(); /** * The {@link Collection}, {@link Map} or array of objects used to generate the inner * '<code>option</code>' tags. */ private Object items; /** * The name of the property mapped to the '<code>value</code>' attribute * of the '<code>option</code>' tag. */ private String itemValue; /** * The name of the property mapped to the inner text of the * '<code>option</code>' tag. */ private String itemLabel; /** * The value of the HTML '<code>size</code>' attribute rendered * on the final '<code>select</code>' element. */ private String size; /** * Indicates whether or not the '<code>select</code>' tag allows * multiple-selections. */ private Object multiple = Boolean.FALSE; /** * The {@link TagWriter} instance that the output is being written. * <p>Only used in conjunction with nested {@link OptionTag OptionTags}. */ private TagWriter tagWriter; /** * Set the {@link Collection}, {@link Map} or array of objects used to * generate the inner '<code>option</code>' tags. * <p>Required when wishing to render '<code>option</code>' tags from * an array, {@link Collection} or {@link Map}. * <p>Typically a runtime expression. * @param items the items that comprise the options of this selection */ public void setItems(Object items) { this.items = (items != null ? items : EMPTY); } /** * Get the value of the '<code>items</code>' attribute. * <p>May be a runtime expression. */ protected Object getItems() { return this.items; } /** * Set the name of the property mapped to the '<code>value</code>' * attribute of the '<code>option</code>' tag. * <p>Required when wishing to render '<code>option</code>' tags from * an array or {@link Collection}. * <p>May be a runtime expression. */ public void setItemValue(String itemValue) { this.itemValue = itemValue; } /** * Get the value of the '<code>itemValue</code>' attribute. * <p>May be a runtime expression. */ protected String getItemValue() { return this.itemValue; } /** * Set the name of the property mapped to the label (inner text) of the * '<code>option</code>' tag. * <p>May be a runtime expression. */ public void setItemLabel(String itemLabel) { this.itemLabel = itemLabel; } /** * Get the value of the '<code>itemLabel</code>' attribute. * <p>May be a runtime expression. */ protected String getItemLabel() { return this.itemLabel; } /** * Set the value of the HTML '<code>size</code>' attribute rendered * on the final '<code>select</code>' element. * <p>May be a runtime expression. * @param size the desired value of the '<code>size</code>' attribute */ public void setSize(String size) { this.size = size; } /** * Get the value of the '<code>size</code>' attribute. * <p>May be a runtime expression. */ protected String getSize() { return this.size; } /** * Set the value of the HTML '<code>multiple</code>' attribute rendered * on the final '<code>select</code>' element. * <p>May be a runtime expression. */ public void setMultiple(Object multiple) { this.multiple = multiple; } /** * Get the value of the HTML '<code>multiple</code>' attribute rendered * on the final '<code>select</code>' element. * <p>May be a runtime expression. */ protected Object getMultiple() { return this.multiple; } /** * Renders the HTML '<code>select</code>' tag to the supplied * {@link TagWriter}. * <p>Renders nested '<code>option</code>' tags if the * {@link #setItems items} property is set, otherwise exposes the * bound value for the nested {@link OptionTag OptionTags}. */ protected int writeTagContent(TagWriter tagWriter) throws JspException { tagWriter.startTag("select"); writeDefaultAttributes(tagWriter); if (isMultiple()) { tagWriter.writeAttribute("multiple", "multiple"); } tagWriter.writeOptionalAttributeValue("size", getDisplayString(evaluate("size", getSize()))); Object items = getItems(); if (items != null) { // Items specified, but might still be empty... if (items != EMPTY) { Object itemsObject = (items instanceof String ? evaluate("items", (String) items) : items); if (itemsObject != null) { String valueProperty = (getItemValue() != null ? ObjectUtils.getDisplayString(evaluate("itemValue", getItemValue())) : null); String labelProperty = (getItemLabel() != null ? ObjectUtils.getDisplayString(evaluate("itemLabel", getItemLabel())) : null); OptionWriter optionWriter = new OptionWriter(itemsObject, getBindStatus(), valueProperty, labelProperty, isHtmlEscape()); optionWriter.writeOptions(tagWriter); } } tagWriter.endTag(true); writeHiddenTagIfNecessary(tagWriter); return SKIP_BODY; } else { // Using nested <form:option/> tags, so just expose the value in the PageContext... tagWriter.forceBlock(); this.tagWriter = tagWriter; this.pageContext.setAttribute(LIST_VALUE_PAGE_ATTRIBUTE, getBindStatus()); return EVAL_BODY_INCLUDE; } } /** * If using a multi-select, a hidden element is needed to make sure all * items are correctly unselected on the server-side in response to a * <code>null</code> post. */ private void writeHiddenTagIfNecessary(TagWriter tagWriter) throws JspException { if (isMultiple()) { tagWriter.startTag("input"); tagWriter.writeAttribute("type", "hidden"); tagWriter.writeAttribute("name", WebDataBinder.DEFAULT_FIELD_MARKER_PREFIX + getName()); tagWriter.writeAttribute("value", "1"); tagWriter.endTag(); } } private boolean isMultiple() throws JspException { Object multiple = getMultiple(); if (Boolean.TRUE.equals(multiple) || "true".equals(multiple) || "multiple".equals(multiple)) { return true; } else if (this.multiple instanceof String) { Object evaluatedValue = evaluate("multiple", multiple); return Boolean.TRUE.equals(evaluatedValue); } return forceMultiple(); } /** * Returns '<code>true</code>' if the bound value requires the * resultant '<code>select</code>' tag to be multi-select. */ private boolean forceMultiple() throws JspException { BindStatus bindStatus = getBindStatus(); Class valueType = bindStatus.getValueType(); if (valueType != null && typeRequiresMultiple(valueType)) { return true; } else if (bindStatus.getEditor() != null) { Object editorValue = bindStatus.getEditor().getValue(); if (editorValue != null && typeRequiresMultiple(editorValue.getClass())) { return true; } } return false; } /** * Returns '<code>true</code>' for arrays, {@link Collection Collections} * and {@link Map Maps}. */ private static boolean typeRequiresMultiple(Class type) { return (type.isArray() || Collection.class.isAssignableFrom(type) || Map.class.isAssignableFrom(type)); } /** * Closes any block tag that might have been opened when using * nested {@link OptionTag options}. */ public int doEndTag() throws JspException { if (this.tagWriter != null) { this.tagWriter.endTag(); writeHiddenTagIfNecessary(tagWriter); } return EVAL_PAGE; } /** * Clears the {@link TagWriter} that might have been left over when using * nested {@link OptionTag options}. */ public void doFinally() { super.doFinally(); this.tagWriter = null; this.pageContext.removeAttribute(LIST_VALUE_PAGE_ATTRIBUTE); } }