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 javax.servlet.jsp.JspException;
020
021import org.springframework.lang.Nullable;
022import org.springframework.util.Assert;
023import org.springframework.util.ObjectUtils;
024import org.springframework.util.StringUtils;
025import org.springframework.web.servlet.support.BindStatus;
026import org.springframework.web.util.TagUtils;
027
028/**
029 * The {@code <options>} tag renders a list of HTML 'option' tags.
030 * Sets 'selected' as appropriate based on bound value.
031 *
032 * <p><i>Must</i> be used within a {@link SelectTag 'select' tag}.
033 *
034 * <p>
035 * <table>
036 * <caption>Attribute Summary</caption>
037 * <thead>
038 * <tr>
039 * <th class="colFirst">Attribute</th>
040 * <th class="colOne">Required?</th>
041 * <th class="colOne">Runtime Expression?</th>
042 * <th class="colLast">Description</th>
043 * </tr>
044 * </thead>
045 * <tbody>
046 * <tr class="altColor">
047 * <td><p>cssClass</p></td>
048 * <td><p>false</p></td>
049 * <td><p>true</p></td>
050 * <td><p>HTML Optional Attribute</p></td>
051 * </tr>
052 * <tr class="rowColor">
053 * <td><p>cssErrorClass</p></td>
054 * <td><p>false</p></td>
055 * <td><p>true</p></td>
056 * <td><p>HTML Optional Attribute. Used when the bound field has errors.</p></td>
057 * </tr>
058 * <tr class="altColor">
059 * <td><p>cssStyle</p></td>
060 * <td><p>false</p></td>
061 * <td><p>true</p></td>
062 * <td><p>HTML Optional Attribute</p></td>
063 * </tr>
064 * <tr class="rowColor">
065 * <td><p>dir</p></td>
066 * <td><p>false</p></td>
067 * <td><p>true</p></td>
068 * <td><p>HTML Standard Attribute</p></td>
069 * </tr>
070 * <tr class="altColor">
071 * <td><p>disabled</p></td>
072 * <td><p>false</p></td>
073 * <td><p>true</p></td>
074 * <td><p>HTML Optional Attribute. Setting the value of this attribute
075 * to 'true' will disable the HTML element.</p></td>
076 * </tr>
077 * <tr class="rowColor">
078 * <td><p>htmlEscape</p></td>
079 * <td><p>false</p></td>
080 * <td><p>true</p></td>
081 * <td><p>Enable/disable HTML escaping of rendered values.</p></td>
082 * </tr>
083 * <tr class="altColor">
084 * <td><p>id</p></td>
085 * <td><p>false</p></td>
086 * <td><p>true</p></td>
087 * <td><p>HTML Standard Attribute</p></td>
088 * </tr>
089 * <tr class="rowColor">
090 * <td><p>itemLabel</p></td>
091 * <td><p>false</p></td>
092 * <td><p>true</p></td>
093 * <td><p>Name of the property mapped to the inner text of the 'option' tag</p></td>
094 * </tr>
095 * <tr class="altColor">
096 * <td><p>items</p></td>
097 * <td><p>true</p></td>
098 * <td><p>true</p></td>
099 * <td><p>The Collection, Map or array of objects used to generate the inner 'option' tags</p></td>
100 * </tr>
101 * <tr class="rowColor">
102 * <td><p>itemValue</p></td>
103 * <td><p>false</p></td>
104 * <td><p>true</p></td>
105 * <td><p>Name of the property mapped to 'value' attribute of the 'option' tag</p></td>
106 * </tr>
107 * <tr class="altColor">
108 * <td><p>lang</p></td>
109 * <td><p>false</p></td>
110 * <td><p>true</p></td>
111 * <td><p>HTML Standard Attribute</p></td>
112 * </tr>
113 * <tr class="rowColor">
114 * <td><p>onclick</p></td>
115 * <td><p>false</p></td>
116 * <td><p>true</p></td>
117 * <td><p>HTML Event Attribute</p></td>
118 * </tr>
119 * <tr class="altColor">
120 * <td><p>ondblclick</p></td>
121 * <td><p>false</p></td>
122 * <td><p>true</p></td>
123 * <td><p>HTML Event Attribute</p></td>
124 * </tr>
125 * <tr class="rowColor">
126 * <td><p>onkeydown</p></td>
127 * <td><p>false</p></td>
128 * <td><p>true</p></td>
129 * <td><p>HTML Event Attribute</p></td>
130 * </tr>
131 * <tr class="altColor">
132 * <td><p>onkeypress</p></td>
133 * <td><p>false</p></td>
134 * <td><p>true</p></td>
135 * <td><p>HTML Event Attribute</p></td>
136 * </tr>
137 * <tr class="rowColor">
138 * <td><p>onkeyup</p></td>
139 * <td><p>false</p></td>
140 * <td><p>true</p></td>
141 * <td><p>HTML Event Attribute</p></td>
142 * </tr>
143 * <tr class="altColor">
144 * <td><p>onmousedown</p></td>
145 * <td><p>false</p></td>
146 * <td><p>true</p></td>
147 * <td><p>HTML Event Attribute</p></td>
148 * </tr>
149 * <tr class="rowColor">
150 * <td><p>onmousemove</p></td>
151 * <td><p>false</p></td>
152 * <td><p>true</p></td>
153 * <td><p>HTML Event Attribute</p></td>
154 * </tr>
155 * <tr class="altColor">
156 * <td><p>onmouseout</p></td>
157 * <td><p>false</p></td>
158 * <td><p>true</p></td>
159 * <td><p>HTML Event Attribute</p></td>
160 * </tr>
161 * <tr class="rowColor">
162 * <td><p>onmouseover</p></td>
163 * <td><p>false</p></td>
164 * <td><p>true</p></td>
165 * <td><p>HTML Event Attribute</p></td>
166 * </tr>
167 * <tr class="altColor">
168 * <td><p>onmouseup</p></td>
169 * <td><p>false</p></td>
170 * <td><p>true</p></td>
171 * <td><p>HTML Event Attribute</p></td>
172 * </tr>
173 * <tr class="rowColor">
174 * <td><p>tabindex</p></td>
175 * <td><p>false</p></td>
176 * <td><p>true</p></td>
177 * <td><p>HTML Standard Attribute</p></td>
178 * </tr>
179 * <tr class="altColor">
180 * <td><p>title</p></td>
181 * <td><p>false</p></td>
182 * <td><p>true</p></td>
183 * <td><p>HTML Standard Attribute</p></td>
184 * </tr>
185 * </tbody>
186 * </table>
187 *
188 * @author Rob Harrop
189 * @author Juergen Hoeller
190 * @author Scott Andrews
191 * @since 2.0
192 */
193@SuppressWarnings("serial")
194public class OptionsTag extends AbstractHtmlElementTag {
195
196        /**
197         * The {@link java.util.Collection}, {@link java.util.Map} or array of
198         * objects used to generate the inner '{@code option}' tags.
199         */
200        @Nullable
201        private Object items;
202
203        /**
204         * The name of the property mapped to the '{@code value}' attribute
205         * of the '{@code option}' tag.
206         */
207        @Nullable
208        private String itemValue;
209
210        /**
211         * The name of the property mapped to the inner text of the
212         * '{@code option}' tag.
213         */
214        @Nullable
215        private String itemLabel;
216
217        private boolean disabled;
218
219
220        /**
221         * Set the {@link java.util.Collection}, {@link java.util.Map} or array
222         * of objects used to generate the inner '{@code option}' tags.
223         * <p>Required when wishing to render '{@code option}' tags from an
224         * array, {@link java.util.Collection} or {@link java.util.Map}.
225         * <p>Typically a runtime expression.
226         */
227        public void setItems(Object items) {
228                this.items = items;
229        }
230
231        /**
232         * Get the {@link java.util.Collection}, {@link java.util.Map} or array
233         * of objects used to generate the inner '{@code option}' tags.
234         * <p>Typically a runtime expression.
235         */
236        @Nullable
237        protected Object getItems() {
238                return this.items;
239        }
240
241        /**
242         * Set the name of the property mapped to the '{@code value}'
243         * attribute of the '{@code option}' tag.
244         * <p>Required when wishing to render '{@code option}' tags from
245         * an array or {@link java.util.Collection}.
246         */
247        public void setItemValue(String itemValue) {
248                Assert.hasText(itemValue, "'itemValue' must not be empty");
249                this.itemValue = itemValue;
250        }
251
252        /**
253         * Return the name of the property mapped to the '{@code value}'
254         * attribute of the '{@code option}' tag.
255         */
256        @Nullable
257        protected String getItemValue() {
258                return this.itemValue;
259        }
260
261        /**
262         * Set the name of the property mapped to the label (inner text) of the
263         * '{@code option}' tag.
264         */
265        public void setItemLabel(String itemLabel) {
266                Assert.hasText(itemLabel, "'itemLabel' must not be empty");
267                this.itemLabel = itemLabel;
268        }
269
270        /**
271         * Get the name of the property mapped to the label (inner text) of the
272         * '{@code option}' tag.
273         */
274        @Nullable
275        protected String getItemLabel() {
276                return this.itemLabel;
277        }
278
279        /**
280         * Set the value of the '{@code disabled}' attribute.
281         */
282        public void setDisabled(boolean disabled) {
283                this.disabled = disabled;
284        }
285
286        /**
287         * Get the value of the '{@code disabled}' attribute.
288         */
289        protected boolean isDisabled() {
290                return this.disabled;
291        }
292
293
294        @Override
295        protected int writeTagContent(TagWriter tagWriter) throws JspException {
296                SelectTag selectTag = getSelectTag();
297                Object items = getItems();
298                Object itemsObject = null;
299                if (items != null) {
300                        itemsObject = (items instanceof String ? evaluate("items", items) : items);
301                }
302                else {
303                        Class<?> selectTagBoundType = selectTag.getBindStatus().getValueType();
304                        if (selectTagBoundType != null && selectTagBoundType.isEnum()) {
305                                itemsObject = selectTagBoundType.getEnumConstants();
306                        }
307                }
308                if (itemsObject != null) {
309                        String selectName = selectTag.getName();
310                        String itemValue = getItemValue();
311                        String itemLabel = getItemLabel();
312                        String valueProperty =
313                                        (itemValue != null ? ObjectUtils.getDisplayString(evaluate("itemValue", itemValue)) : null);
314                        String labelProperty =
315                                        (itemLabel != null ? ObjectUtils.getDisplayString(evaluate("itemLabel", itemLabel)) : null);
316                        OptionsWriter optionWriter = new OptionsWriter(selectName, itemsObject, valueProperty, labelProperty);
317                        optionWriter.writeOptions(tagWriter);
318                }
319                return SKIP_BODY;
320        }
321
322        /**
323         * Appends a counter to a specified id,
324         * since we're dealing with multiple HTML elements.
325         */
326        @Override
327        protected String resolveId() throws JspException {
328                Object id = evaluate("id", getId());
329                if (id != null) {
330                        String idString = id.toString();
331                        return (StringUtils.hasText(idString) ? TagIdGenerator.nextId(idString, this.pageContext) : null);
332                }
333                return null;
334        }
335
336        private SelectTag getSelectTag() {
337                TagUtils.assertHasAncestorOfType(this, SelectTag.class, "options", "select");
338                return (SelectTag) findAncestorWithClass(this, SelectTag.class);
339        }
340
341        @Override
342        protected BindStatus getBindStatus() {
343                return (BindStatus) this.pageContext.getAttribute(SelectTag.LIST_VALUE_PAGE_ATTRIBUTE);
344        }
345
346
347        /**
348         * Inner class that adapts OptionWriter for multiple options to be rendered.
349         */
350        private class OptionsWriter extends OptionWriter {
351
352                @Nullable
353                private final String selectName;
354
355                public OptionsWriter(@Nullable String selectName, Object optionSource,
356                                @Nullable String valueProperty, @Nullable String labelProperty) {
357
358                        super(optionSource, getBindStatus(), valueProperty, labelProperty, isHtmlEscape());
359                        this.selectName = selectName;
360                }
361
362                @Override
363                protected boolean isOptionDisabled() throws JspException {
364                        return isDisabled();
365                }
366
367                @Override
368                protected void writeCommonAttributes(TagWriter tagWriter) throws JspException {
369                        writeOptionalAttribute(tagWriter, "id", resolveId());
370                        writeOptionalAttributes(tagWriter);
371                }
372
373                @Override
374                protected String processOptionValue(String value) {
375                        return processFieldValue(this.selectName, value, "option");
376                }
377        }
378
379}