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}