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; 021 022import javax.servlet.jsp.JspException; 023 024import org.springframework.lang.Nullable; 025import org.springframework.util.ObjectUtils; 026import org.springframework.web.bind.WebDataBinder; 027import org.springframework.web.servlet.support.BindStatus; 028 029/** 030 * The {@code <select>} tag renders an HTML 'select' element. 031 * Supports data binding to the selected option. 032 * 033 * <p>Inner '{@code option}' tags can be rendered using one of the 034 * approaches supported by the OptionWriter class. 035 * 036 * <p>Also supports the use of nested {@link OptionTag OptionTags} or 037 * (typically one) nested {@link OptionsTag}. 038 * 039 * <p> 040 * <table> 041 * <caption>Attribute Summary</caption> 042 * <thead> 043 * <tr> 044 * <th class="colFirst">Attribute</th> 045 * <th class="colOne">Required?</th> 046 * <th class="colOne">Runtime Expression?</th> 047 * <th class="colLast">Description</th> 048 * </tr> 049 * </thead> 050 * <tbody> 051 * <tr class="altColor"> 052 * <td><p>accesskey</p></td> 053 * <td><p>false</p></td> 054 * <td><p>true</p></td> 055 * <td><p>HTML Standard Attribute</p></td> 056 * </tr> 057 * <tr class="rowColor"> 058 * <td><p>cssClass</p></td> 059 * <td><p>false</p></td> 060 * <td><p>true</p></td> 061 * <td><p>HTML Optional Attribute</p></td> 062 * </tr> 063 * <tr class="altColor"> 064 * <td><p>cssErrorClass</p></td> 065 * <td><p>false</p></td> 066 * <td><p>true</p></td> 067 * <td><p>HTML Optional Attribute. Used when the bound field has errors.</p></td> 068 * </tr> 069 * <tr class="rowColor"> 070 * <td><p>cssStyle</p></td> 071 * <td><p>false</p></td> 072 * <td><p>true</p></td> 073 * <td><p>HTML Optional Attribute</p></td> 074 * </tr> 075 * <tr class="altColor"> 076 * <td><p>dir</p></td> 077 * <td><p>false</p></td> 078 * <td><p>true</p></td> 079 * <td><p>HTML Standard Attribute</p></td> 080 * </tr> 081 * <tr class="rowColor"> 082 * <td><p>disabled</p></td> 083 * <td><p>false</p></td> 084 * <td><p>true</p></td> 085 * <td><p>HTML Optional Attribute. Setting the value of this attribute to 'true' 086 * will disable the HTML element.</p></td> 087 * </tr> 088 * <tr class="altColor"> 089 * <td><p>htmlEscape</p></td> 090 * <td><p>false</p></td> 091 * <td><p>true</p></td> 092 * <td><p>Enable/disable HTML escaping of rendered values.</p></td> 093 * </tr> 094 * <tr class="rowColor"> 095 * <td><p>id</p></td> 096 * <td><p>false</p></td> 097 * <td><p>true</p></td> 098 * <td><p>HTML Standard Attribute</p></td> 099 * </tr> 100 * <tr class="altColor"> 101 * <td><p>itemLabel</p></td> 102 * <td><p>false</p></td> 103 * <td><p>true</p></td> 104 * <td><p>Name of the property mapped to the inner text of the 'option' tag</p></td> 105 * </tr> 106 * <tr class="rowColor"> 107 * <td><p>items</p></td> 108 * <td><p>false</p></td> 109 * <td><p>true</p></td> 110 * <td><p>The Collection, Map or array of objects used to generate the inner 111 * 'option' tags</p></td> 112 * </tr> 113 * <tr class="altColor"> 114 * <td><p>itemValue</p></td> 115 * <td><p>false</p></td> 116 * <td><p>true</p></td> 117 * <td><p>Name of the property mapped to 'value' attribute of the 'option' 118 * tag</p></td> 119 * </tr> 120 * <tr class="rowColor"> 121 * <td><p>lang</p></td> 122 * <td><p>false</p></td> 123 * <td><p>true</p></td> 124 * <td><p>HTML Standard Attribute</p></td> 125 * </tr> 126 * <tr class="altColor"> 127 * <td><p>multiple</p></td> 128 * <td><p>false</p></td> 129 * <td><p>true</p></td> 130 * <td><p>HTML Optional Attribute</p></td> 131 * </tr> 132 * <tr class="rowColor"> 133 * <td><p>onblur</p></td> 134 * <td><p>false</p></td> 135 * <td><p>true</p></td> 136 * <td><p>HTML Event Attribute</p></td> 137 * </tr> 138 * <tr class="altColor"> 139 * <td><p>onchange</p></td> 140 * <td><p>false</p></td> 141 * <td><p>true</p></td> 142 * <td><p>HTML Event Attribute</p></td> 143 * </tr> 144 * <tr class="rowColor"> 145 * <td><p>onclick</p></td> 146 * <td><p>false</p></td> 147 * <td><p>true</p></td> 148 * <td><p>HTML Event Attribute</p></td> 149 * </tr> 150 * <tr class="altColor"> 151 * <td><p>ondblclick</p></td> 152 * <td><p>false</p></td> 153 * <td><p>true</p></td> 154 * <td><p>HTML Event Attribute</p></td> 155 * </tr> 156 * <tr class="rowColor"> 157 * <td><p>onfocus</p></td> 158 * <td><p>false</p></td> 159 * <td><p>true</p></td> 160 * <td><p>HTML Event Attribute</p></td> 161 * </tr> 162 * <tr class="altColor"> 163 * <td><p>onkeydown</p></td> 164 * <td><p>false</p></td> 165 * <td><p>true</p></td> 166 * <td><p>HTML Event Attribute</p></td> 167 * </tr> 168 * <tr class="rowColor"> 169 * <td><p>onkeypress</p></td> 170 * <td><p>false</p></td> 171 * <td><p>true</p></td> 172 * <td><p>HTML Event Attribute</p></td> 173 * </tr> 174 * <tr class="altColor"> 175 * <td><p>onkeyup</p></td> 176 * <td><p>false</p></td> 177 * <td><p>true</p></td> 178 * <td><p>HTML Event Attribute</p></td> 179 * </tr> 180 * <tr class="rowColor"> 181 * <td><p>onmousedown</p></td> 182 * <td><p>false</p></td> 183 * <td><p>true</p></td> 184 * <td><p>HTML Event Attribute</p></td> 185 * </tr> 186 * <tr class="altColor"> 187 * <td><p>onmousemove</p></td> 188 * <td><p>false</p></td> 189 * <td><p>true</p></td> 190 * <td><p>HTML Event Attribute</p></td> 191 * </tr> 192 * <tr class="rowColor"> 193 * <td><p>onmouseout</p></td> 194 * <td><p>false</p></td> 195 * <td><p>true</p></td> 196 * <td><p>HTML Event Attribute</p></td> 197 * </tr> 198 * <tr class="altColor"> 199 * <td><p>onmouseover</p></td> 200 * <td><p>false</p></td> 201 * <td><p>true</p></td> 202 * <td><p>HTML Event Attribute</p></td> 203 * </tr> 204 * <tr class="rowColor"> 205 * <td><p>onmouseup</p></td> 206 * <td><p>false</p></td> 207 * <td><p>true</p></td> 208 * <td><p>HTML Event Attribute</p></td> 209 * </tr> 210 * <tr class="altColor"> 211 * <td><p>path</p></td> 212 * <td><p>true</p></td> 213 * <td><p>true</p></td> 214 * <td><p>Path to property for data binding</p></td> 215 * </tr> 216 * <tr class="rowColor"> 217 * <td><p>size</p></td> 218 * <td><p>false</p></td> 219 * <td><p>true</p></td> 220 * <td><p>HTML Optional Attribute</p></td> 221 * </tr> 222 * <tr class="altColor"> 223 * <td><p>tabindex</p></td> 224 * <td><p>false</p></td> 225 * <td><p>true</p></td> 226 * <td><p>HTML Standard Attribute</p></td> 227 * </tr> 228 * <tr class="rowColor"> 229 * <td><p>title</p></td> 230 * <td><p>false</p></td> 231 * <td><p>true</p></td> 232 * <td><p>HTML Standard Attribute</p></td> 233 * </tr> 234 * </tbody> 235 * </table> 236 * 237 * @author Rob Harrop 238 * @author Juergen Hoeller 239 * @since 2.0 240 * @see OptionTag 241 */ 242@SuppressWarnings("serial") 243public class SelectTag extends AbstractHtmlInputElementTag { 244 245 /** 246 * The {@link javax.servlet.jsp.PageContext} attribute under 247 * which the bound value is exposed to inner {@link OptionTag OptionTags}. 248 */ 249 public static final String LIST_VALUE_PAGE_ATTRIBUTE = 250 "org.springframework.web.servlet.tags.form.SelectTag.listValue"; 251 252 /** 253 * Marker object for items that have been specified but resolve to null. 254 * Allows to differentiate between 'set but null' and 'not set at all'. 255 */ 256 private static final Object EMPTY = new Object(); 257 258 259 /** 260 * The {@link Collection}, {@link Map} or array of objects used to generate 261 * the inner '{@code option}' tags. 262 */ 263 @Nullable 264 private Object items; 265 266 /** 267 * The name of the property mapped to the '{@code value}' attribute 268 * of the '{@code option}' tag. 269 */ 270 @Nullable 271 private String itemValue; 272 273 /** 274 * The name of the property mapped to the inner text of the 275 * '{@code option}' tag. 276 */ 277 @Nullable 278 private String itemLabel; 279 280 /** 281 * The value of the HTML '{@code size}' attribute rendered 282 * on the final '{@code select}' element. 283 */ 284 @Nullable 285 private String size; 286 287 /** 288 * Indicates whether or not the '{@code select}' tag allows 289 * multiple-selections. 290 */ 291 @Nullable 292 private Object multiple; 293 294 /** 295 * The {@link TagWriter} instance that the output is being written. 296 * <p>Only used in conjunction with nested {@link OptionTag OptionTags}. 297 */ 298 @Nullable 299 private TagWriter tagWriter; 300 301 302 /** 303 * Set the {@link Collection}, {@link Map} or array of objects used to 304 * generate the inner '{@code option}' tags. 305 * <p>Required when wishing to render '{@code option}' tags from 306 * an array, {@link Collection} or {@link Map}. 307 * <p>Typically a runtime expression. 308 * @param items the items that comprise the options of this selection 309 */ 310 public void setItems(@Nullable Object items) { 311 this.items = (items != null ? items : EMPTY); 312 } 313 314 /** 315 * Get the value of the '{@code items}' attribute. 316 * <p>May be a runtime expression. 317 */ 318 @Nullable 319 protected Object getItems() { 320 return this.items; 321 } 322 323 /** 324 * Set the name of the property mapped to the '{@code value}' 325 * attribute of the '{@code option}' tag. 326 * <p>Required when wishing to render '{@code option}' tags from 327 * an array or {@link Collection}. 328 * <p>May be a runtime expression. 329 */ 330 public void setItemValue(String itemValue) { 331 this.itemValue = itemValue; 332 } 333 334 /** 335 * Get the value of the '{@code itemValue}' attribute. 336 * <p>May be a runtime expression. 337 */ 338 @Nullable 339 protected String getItemValue() { 340 return this.itemValue; 341 } 342 343 /** 344 * Set the name of the property mapped to the label (inner text) of the 345 * '{@code option}' tag. 346 * <p>May be a runtime expression. 347 */ 348 public void setItemLabel(String itemLabel) { 349 this.itemLabel = itemLabel; 350 } 351 352 /** 353 * Get the value of the '{@code itemLabel}' attribute. 354 * <p>May be a runtime expression. 355 */ 356 @Nullable 357 protected String getItemLabel() { 358 return this.itemLabel; 359 } 360 361 /** 362 * Set the value of the HTML '{@code size}' attribute rendered 363 * on the final '{@code select}' element. 364 */ 365 public void setSize(String size) { 366 this.size = size; 367 } 368 369 /** 370 * Get the value of the '{@code size}' attribute. 371 */ 372 @Nullable 373 protected String getSize() { 374 return this.size; 375 } 376 377 /** 378 * Set the value of the HTML '{@code multiple}' attribute rendered 379 * on the final '{@code select}' element. 380 */ 381 public void setMultiple(Object multiple) { 382 this.multiple = multiple; 383 } 384 385 /** 386 * Get the value of the HTML '{@code multiple}' attribute rendered 387 * on the final '{@code select}' element. 388 */ 389 @Nullable 390 protected Object getMultiple() { 391 return this.multiple; 392 } 393 394 395 /** 396 * Renders the HTML '{@code select}' tag to the supplied 397 * {@link TagWriter}. 398 * <p>Renders nested '{@code option}' tags if the 399 * {@link #setItems items} property is set, otherwise exposes the 400 * bound value for the nested {@link OptionTag OptionTags}. 401 */ 402 @Override 403 protected int writeTagContent(TagWriter tagWriter) throws JspException { 404 tagWriter.startTag("select"); 405 writeDefaultAttributes(tagWriter); 406 if (isMultiple()) { 407 tagWriter.writeAttribute("multiple", "multiple"); 408 } 409 tagWriter.writeOptionalAttributeValue("size", getDisplayString(evaluate("size", getSize()))); 410 411 Object items = getItems(); 412 if (items != null) { 413 // Items specified, but might still be empty... 414 if (items != EMPTY) { 415 Object itemsObject = evaluate("items", items); 416 if (itemsObject != null) { 417 final String selectName = getName(); 418 String valueProperty = (getItemValue() != null ? 419 ObjectUtils.getDisplayString(evaluate("itemValue", getItemValue())) : null); 420 String labelProperty = (getItemLabel() != null ? 421 ObjectUtils.getDisplayString(evaluate("itemLabel", getItemLabel())) : null); 422 OptionWriter optionWriter = 423 new OptionWriter(itemsObject, getBindStatus(), valueProperty, labelProperty, isHtmlEscape()) { 424 @Override 425 protected String processOptionValue(String resolvedValue) { 426 return processFieldValue(selectName, resolvedValue, "option"); 427 } 428 }; 429 optionWriter.writeOptions(tagWriter); 430 } 431 } 432 tagWriter.endTag(true); 433 writeHiddenTagIfNecessary(tagWriter); 434 return SKIP_BODY; 435 } 436 else { 437 // Using nested <form:option/> tags, so just expose the value in the PageContext... 438 tagWriter.forceBlock(); 439 this.tagWriter = tagWriter; 440 this.pageContext.setAttribute(LIST_VALUE_PAGE_ATTRIBUTE, getBindStatus()); 441 return EVAL_BODY_INCLUDE; 442 } 443 } 444 445 /** 446 * If using a multi-select, a hidden element is needed to make sure all 447 * items are correctly unselected on the server-side in response to a 448 * {@code null} post. 449 */ 450 private void writeHiddenTagIfNecessary(TagWriter tagWriter) throws JspException { 451 if (isMultiple()) { 452 tagWriter.startTag("input"); 453 tagWriter.writeAttribute("type", "hidden"); 454 String name = WebDataBinder.DEFAULT_FIELD_MARKER_PREFIX + getName(); 455 tagWriter.writeAttribute("name", name); 456 tagWriter.writeAttribute("value", processFieldValue(name, "1", "hidden")); 457 tagWriter.endTag(); 458 } 459 } 460 461 private boolean isMultiple() throws JspException { 462 Object multiple = getMultiple(); 463 if (multiple != null) { 464 String stringValue = multiple.toString(); 465 return ("multiple".equalsIgnoreCase(stringValue) || Boolean.parseBoolean(stringValue)); 466 } 467 return forceMultiple(); 468 } 469 470 /** 471 * Returns '{@code true}' if the bound value requires the 472 * resultant '{@code select}' tag to be multi-select. 473 */ 474 private boolean forceMultiple() throws JspException { 475 BindStatus bindStatus = getBindStatus(); 476 Class<?> valueType = bindStatus.getValueType(); 477 if (valueType != null && typeRequiresMultiple(valueType)) { 478 return true; 479 } 480 else if (bindStatus.getEditor() != null) { 481 Object editorValue = bindStatus.getEditor().getValue(); 482 if (editorValue != null && typeRequiresMultiple(editorValue.getClass())) { 483 return true; 484 } 485 } 486 return false; 487 } 488 489 /** 490 * Returns '{@code true}' for arrays, {@link Collection Collections} 491 * and {@link Map Maps}. 492 */ 493 private static boolean typeRequiresMultiple(Class<?> type) { 494 return (type.isArray() || Collection.class.isAssignableFrom(type) || Map.class.isAssignableFrom(type)); 495 } 496 497 /** 498 * Closes any block tag that might have been opened when using 499 * nested {@link OptionTag options}. 500 */ 501 @Override 502 public int doEndTag() throws JspException { 503 if (this.tagWriter != null) { 504 this.tagWriter.endTag(); 505 writeHiddenTagIfNecessary(this.tagWriter); 506 } 507 return EVAL_PAGE; 508 } 509 510 /** 511 * Clears the {@link TagWriter} that might have been left over when using 512 * nested {@link OptionTag options}. 513 */ 514 @Override 515 public void doFinally() { 516 super.doFinally(); 517 this.tagWriter = null; 518 this.pageContext.removeAttribute(LIST_VALUE_PAGE_ATTRIBUTE); 519 } 520 521}