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}