/* * Copyright 2002-2014 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}' * element. * * <p>Inner '{@code option}' 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 */ @SuppressWarnings("serial") 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}' tags. */ private Object items; /** * The name of the property mapped to the '{@code value}' attribute * of the '{@code option}' tag. */ private String itemValue; /** * The name of the property mapped to the inner text of the * '{@code option}' tag. */ private String itemLabel; /** * The value of the HTML '{@code size}' attribute rendered * on the final '{@code select}' element. */ private String size; /** * Indicates whether or not the '{@code select}' tag allows * multiple-selections. */ private Object multiple; /** * 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}' tags. * <p>Required when wishing to render '{@code option}' 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}' attribute. * <p>May be a runtime expression. */ protected Object getItems() { return this.items; } /** * Set the name of the property mapped to the '{@code value}' * attribute of the '{@code option}' tag. * <p>Required when wishing to render '{@code option}' 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}' 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}' tag. * <p>May be a runtime expression. */ public void setItemLabel(String itemLabel) { this.itemLabel = itemLabel; } /** * Get the value of the '{@code itemLabel}' attribute. * <p>May be a runtime expression. */ protected String getItemLabel() { return this.itemLabel; } /** * Set the value of the HTML '{@code size}' attribute rendered * on the final '{@code select}' element. */ public void setSize(String size) { this.size = size; } /** * Get the value of the '{@code size}' attribute. */ protected String getSize() { return this.size; } /** * Set the value of the HTML '{@code multiple}' attribute rendered * on the final '{@code select}' element. */ public void setMultiple(Object multiple) { this.multiple = multiple; } /** * Get the value of the HTML '{@code multiple}' attribute rendered * on the final '{@code select}' element. */ protected Object getMultiple() { return this.multiple; } /** * Renders the HTML '{@code select}' tag to the supplied * {@link TagWriter}. * <p>Renders nested '{@code option}' tags if the * {@link #setItems items} property is set, otherwise exposes the * bound value for the nested {@link OptionTag OptionTags}. */ @Override 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 = evaluate("items", items); if (itemsObject != null) { final String selectName = getName(); 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()) { @Override protected String processOptionValue(String resolvedValue) { return processFieldValue(selectName, resolvedValue, "option"); } }; 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} post. */ private void writeHiddenTagIfNecessary(TagWriter tagWriter) throws JspException { if (isMultiple()) { tagWriter.startTag("input"); tagWriter.writeAttribute("type", "hidden"); String name = WebDataBinder.DEFAULT_FIELD_MARKER_PREFIX + getName(); tagWriter.writeAttribute("name", name); tagWriter.writeAttribute("value", processFieldValue(name, "1", "hidden")); tagWriter.endTag(); } } private boolean isMultiple() throws JspException { Object multiple = getMultiple(); if (multiple != null) { String stringValue = multiple.toString(); return ("multiple".equalsIgnoreCase(stringValue) || Boolean.parseBoolean(stringValue)); } return forceMultiple(); } /** * Returns '{@code true}' if the bound value requires the * resultant '{@code select}' 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}' 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}. */ @Override 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}. */ @Override public void doFinally() { super.doFinally(); this.tagWriter = null; this.pageContext.removeAttribute(LIST_VALUE_PAGE_ATTRIBUTE); } }