001/* 002 * Copyright 2002-2018 the original author or authors. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * https://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016 017package org.springframework.web.servlet.tags.form; 018 019import java.util.Collection; 020import java.util.Map; 021import javax.servlet.jsp.JspException; 022 023import org.springframework.util.ObjectUtils; 024import org.springframework.web.bind.WebDataBinder; 025import org.springframework.web.servlet.support.BindStatus; 026 027/** 028 * Databinding-aware JSP tag that renders an HTML '{@code select}' 029 * element. 030 * 031 * <p>Inner '{@code option}' tags can be rendered using one of the 032 * approaches supported by the OptionWriter class. 033 * 034 * <p>Also supports the use of nested {@link OptionTag OptionTags} or 035 * (typically one) nested {@link OptionsTag}. 036 * 037 * @author Rob Harrop 038 * @author Juergen Hoeller 039 * @since 2.0 040 * @see OptionTag 041 */ 042@SuppressWarnings("serial") 043public class SelectTag extends AbstractHtmlInputElementTag { 044 045 /** 046 * The {@link javax.servlet.jsp.PageContext} attribute under 047 * which the bound value is exposed to inner {@link OptionTag OptionTags}. 048 */ 049 public static final String LIST_VALUE_PAGE_ATTRIBUTE = 050 "org.springframework.web.servlet.tags.form.SelectTag.listValue"; 051 052 /** 053 * Marker object for items that have been specified but resolve to null. 054 * Allows to differentiate between 'set but null' and 'not set at all'. 055 */ 056 private static final Object EMPTY = new Object(); 057 058 059 /** 060 * The {@link Collection}, {@link Map} or array of objects used to generate 061 * the inner '{@code option}' tags. 062 */ 063 private Object items; 064 065 /** 066 * The name of the property mapped to the '{@code value}' attribute 067 * of the '{@code option}' tag. 068 */ 069 private String itemValue; 070 071 /** 072 * The name of the property mapped to the inner text of the 073 * '{@code option}' tag. 074 */ 075 private String itemLabel; 076 077 /** 078 * The value of the HTML '{@code size}' attribute rendered 079 * on the final '{@code select}' element. 080 */ 081 private String size; 082 083 /** 084 * Indicates whether or not the '{@code select}' tag allows 085 * multiple-selections. 086 */ 087 private Object multiple; 088 089 /** 090 * The {@link TagWriter} instance that the output is being written. 091 * <p>Only used in conjunction with nested {@link OptionTag OptionTags}. 092 */ 093 private TagWriter tagWriter; 094 095 096 /** 097 * Set the {@link Collection}, {@link Map} or array of objects used to 098 * generate the inner '{@code option}' tags. 099 * <p>Required when wishing to render '{@code option}' tags from 100 * an array, {@link Collection} or {@link Map}. 101 * <p>Typically a runtime expression. 102 * @param items the items that comprise the options of this selection 103 */ 104 public void setItems(Object items) { 105 this.items = (items != null ? items : EMPTY); 106 } 107 108 /** 109 * Get the value of the '{@code items}' attribute. 110 * <p>May be a runtime expression. 111 */ 112 protected Object getItems() { 113 return this.items; 114 } 115 116 /** 117 * Set the name of the property mapped to the '{@code value}' 118 * attribute of the '{@code option}' tag. 119 * <p>Required when wishing to render '{@code option}' tags from 120 * an array or {@link Collection}. 121 * <p>May be a runtime expression. 122 */ 123 public void setItemValue(String itemValue) { 124 this.itemValue = itemValue; 125 } 126 127 /** 128 * Get the value of the '{@code itemValue}' attribute. 129 * <p>May be a runtime expression. 130 */ 131 protected String getItemValue() { 132 return this.itemValue; 133 } 134 135 /** 136 * Set the name of the property mapped to the label (inner text) of the 137 * '{@code option}' tag. 138 * <p>May be a runtime expression. 139 */ 140 public void setItemLabel(String itemLabel) { 141 this.itemLabel = itemLabel; 142 } 143 144 /** 145 * Get the value of the '{@code itemLabel}' attribute. 146 * <p>May be a runtime expression. 147 */ 148 protected String getItemLabel() { 149 return this.itemLabel; 150 } 151 152 /** 153 * Set the value of the HTML '{@code size}' attribute rendered 154 * on the final '{@code select}' element. 155 */ 156 public void setSize(String size) { 157 this.size = size; 158 } 159 160 /** 161 * Get the value of the '{@code size}' attribute. 162 */ 163 protected String getSize() { 164 return this.size; 165 } 166 167 /** 168 * Set the value of the HTML '{@code multiple}' attribute rendered 169 * on the final '{@code select}' element. 170 */ 171 public void setMultiple(Object multiple) { 172 this.multiple = multiple; 173 } 174 175 /** 176 * Get the value of the HTML '{@code multiple}' attribute rendered 177 * on the final '{@code select}' element. 178 */ 179 protected Object getMultiple() { 180 return this.multiple; 181 } 182 183 184 /** 185 * Renders the HTML '{@code select}' tag to the supplied 186 * {@link TagWriter}. 187 * <p>Renders nested '{@code option}' tags if the 188 * {@link #setItems items} property is set, otherwise exposes the 189 * bound value for the nested {@link OptionTag OptionTags}. 190 */ 191 @Override 192 protected int writeTagContent(TagWriter tagWriter) throws JspException { 193 tagWriter.startTag("select"); 194 writeDefaultAttributes(tagWriter); 195 if (isMultiple()) { 196 tagWriter.writeAttribute("multiple", "multiple"); 197 } 198 tagWriter.writeOptionalAttributeValue("size", getDisplayString(evaluate("size", getSize()))); 199 200 Object items = getItems(); 201 if (items != null) { 202 // Items specified, but might still be empty... 203 if (items != EMPTY) { 204 Object itemsObject = evaluate("items", items); 205 if (itemsObject != null) { 206 final String selectName = getName(); 207 String valueProperty = (getItemValue() != null ? 208 ObjectUtils.getDisplayString(evaluate("itemValue", getItemValue())) : null); 209 String labelProperty = (getItemLabel() != null ? 210 ObjectUtils.getDisplayString(evaluate("itemLabel", getItemLabel())) : null); 211 OptionWriter optionWriter = 212 new OptionWriter(itemsObject, getBindStatus(), valueProperty, labelProperty, isHtmlEscape()) { 213 @Override 214 protected String processOptionValue(String resolvedValue) { 215 return processFieldValue(selectName, resolvedValue, "option"); 216 } 217 }; 218 optionWriter.writeOptions(tagWriter); 219 } 220 } 221 tagWriter.endTag(true); 222 writeHiddenTagIfNecessary(tagWriter); 223 return SKIP_BODY; 224 } 225 else { 226 // Using nested <form:option/> tags, so just expose the value in the PageContext... 227 tagWriter.forceBlock(); 228 this.tagWriter = tagWriter; 229 this.pageContext.setAttribute(LIST_VALUE_PAGE_ATTRIBUTE, getBindStatus()); 230 return EVAL_BODY_INCLUDE; 231 } 232 } 233 234 /** 235 * If using a multi-select, a hidden element is needed to make sure all 236 * items are correctly unselected on the server-side in response to a 237 * {@code null} post. 238 */ 239 private void writeHiddenTagIfNecessary(TagWriter tagWriter) throws JspException { 240 if (isMultiple()) { 241 tagWriter.startTag("input"); 242 tagWriter.writeAttribute("type", "hidden"); 243 String name = WebDataBinder.DEFAULT_FIELD_MARKER_PREFIX + getName(); 244 tagWriter.writeAttribute("name", name); 245 tagWriter.writeAttribute("value", processFieldValue(name, "1", "hidden")); 246 tagWriter.endTag(); 247 } 248 } 249 250 private boolean isMultiple() throws JspException { 251 Object multiple = getMultiple(); 252 if (multiple != null) { 253 String stringValue = multiple.toString(); 254 return ("multiple".equalsIgnoreCase(stringValue) || Boolean.parseBoolean(stringValue)); 255 } 256 return forceMultiple(); 257 } 258 259 /** 260 * Returns '{@code true}' if the bound value requires the 261 * resultant '{@code select}' tag to be multi-select. 262 */ 263 private boolean forceMultiple() throws JspException { 264 BindStatus bindStatus = getBindStatus(); 265 Class<?> valueType = bindStatus.getValueType(); 266 if (valueType != null && typeRequiresMultiple(valueType)) { 267 return true; 268 } 269 else if (bindStatus.getEditor() != null) { 270 Object editorValue = bindStatus.getEditor().getValue(); 271 if (editorValue != null && typeRequiresMultiple(editorValue.getClass())) { 272 return true; 273 } 274 } 275 return false; 276 } 277 278 /** 279 * Returns '{@code true}' for arrays, {@link Collection Collections} 280 * and {@link Map Maps}. 281 */ 282 private static boolean typeRequiresMultiple(Class<?> type) { 283 return (type.isArray() || Collection.class.isAssignableFrom(type) || Map.class.isAssignableFrom(type)); 284 } 285 286 /** 287 * Closes any block tag that might have been opened when using 288 * nested {@link OptionTag options}. 289 */ 290 @Override 291 public int doEndTag() throws JspException { 292 if (this.tagWriter != null) { 293 this.tagWriter.endTag(); 294 writeHiddenTagIfNecessary(this.tagWriter); 295 } 296 return EVAL_PAGE; 297 } 298 299 /** 300 * Clears the {@link TagWriter} that might have been left over when using 301 * nested {@link OptionTag options}. 302 */ 303 @Override 304 public void doFinally() { 305 super.doFinally(); 306 this.tagWriter = null; 307 this.pageContext.removeAttribute(LIST_VALUE_PAGE_ATTRIBUTE); 308 } 309 310}