001/*
002 * Copyright 2002-2020 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.ArrayList;
020import java.util.Arrays;
021import java.util.List;
022
023import javax.servlet.jsp.JspException;
024import javax.servlet.jsp.PageContext;
025import javax.servlet.jsp.tagext.BodyTag;
026
027import org.springframework.lang.Nullable;
028import org.springframework.util.Assert;
029import org.springframework.util.ObjectUtils;
030import org.springframework.util.StringUtils;
031
032/**
033 * The {@code <errors>} tag renders field errors in an HTML 'span' tag.
034 * Displays errors for either an object or a particular field.
035 *
036 * <p>This tag supports three main usage patterns:
037 *
038 * <ol>
039 *      <li>Field only - set '{@code path}' to the field name (or path)</li>
040 *      <li>Object errors only - omit '{@code path}'</li>
041 *      <li>All errors - set '{@code path}' to '{@code *}'</li>
042 * </ol>
043 *
044 * <p>
045 * <table>
046 * <caption>Attribute Summary</caption>
047 * <thead>
048 * <tr>
049 * <th class="colFirst">Attribute</th>
050 * <th class="colOne">Required?</th>
051 * <th class="colOne">Runtime Expression?</th>
052 * <th class="colLast">Description</th>
053 * </tr>
054 * </thead>
055 * <tbody>
056 * <tr class="altColor">
057 * <td><p>cssClass</p></td>
058 * <td><p>false</p></td>
059 * <td><p>true</p></td>
060 * <td><p>HTML Optional Attribute</p></td>
061 * </tr>
062 * <tr class="rowColor">
063 * <td><p>cssStyle</p></td>
064 * <td><p>false</p></td>
065 * <td><p>true</p></td>
066 * <td><p>HTML Optional Attribute</p></td>
067 * </tr>
068 * <tr class="altColor">
069 * <td><p>delimiter</p></td>
070 * <td><p>false</p></td>
071 * <td><p>true</p></td>
072 * <td><p>Delimiter for displaying multiple error messages.
073 * Defaults to the br tag.</p></td>
074 * </tr>
075 * <tr class="rowColor">
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="altColor">
082 * <td><p>element</p></td>
083 * <td><p>false</p></td>
084 * <td><p>true</p></td>
085 * <td><p>Specifies the HTML element that is used to render the enclosing
086 * errors.</p></td>
087 * </tr>
088 * <tr class="rowColor">
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="altColor">
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="rowColor">
101 * <td><p>lang</p></td>
102 * <td><p>false</p></td>
103 * <td><p>true</p></td>
104 * <td><p>HTML Standard Attribute</p></td>
105 * </tr>
106 * <tr class="altColor">
107 * <td><p>onclick</p></td>
108 * <td><p>false</p></td>
109 * <td><p>true</p></td>
110 * <td><p>HTML Event Attribute</p></td>
111 * </tr>
112 * <tr class="rowColor">
113 * <td><p>ondblclick</p></td>
114 * <td><p>false</p></td>
115 * <td><p>true</p></td>
116 * <td><p>HTML Event Attribute</p></td>
117 * </tr>
118 * <tr class="altColor">
119 * <td><p>onkeydown</p></td>
120 * <td><p>false</p></td>
121 * <td><p>true</p></td>
122 * <td><p>HTML Event Attribute</p></td>
123 * </tr>
124 * <tr class="rowColor">
125 * <td><p>onkeypress</p></td>
126 * <td><p>false</p></td>
127 * <td><p>true</p></td>
128 * <td><p>HTML Event Attribute</p></td>
129 * </tr>
130 * <tr class="altColor">
131 * <td><p>onkeyup</p></td>
132 * <td><p>false</p></td>
133 * <td><p>true</p></td>
134 * <td><p>HTML Event Attribute</p></td>
135 * </tr>
136 * <tr class="rowColor">
137 * <td><p>onmousedown</p></td>
138 * <td><p>false</p></td>
139 * <td><p>true</p></td>
140 * <td><p>HTML Event Attribute</p></td>
141 * </tr>
142 * <tr class="altColor">
143 * <td><p>onmousemove</p></td>
144 * <td><p>false</p></td>
145 * <td><p>true</p></td>
146 * <td><p>HTML Event Attribute</p></td>
147 * </tr>
148 * <tr class="rowColor">
149 * <td><p>onmouseout</p></td>
150 * <td><p>false</p></td>
151 * <td><p>true</p></td>
152 * <td><p>HTML Event Attribute</p></td>
153 * </tr>
154 * <tr class="altColor">
155 * <td><p>onmouseover</p></td>
156 * <td><p>false</p></td>
157 * <td><p>true</p></td>
158 * <td><p>HTML Event Attribute</p></td>
159 * </tr>
160 * <tr class="rowColor">
161 * <td><p>onmouseup</p></td>
162 * <td><p>false</p></td>
163 * <td><p>true</p></td>
164 * <td><p>HTML Event Attribute</p></td>
165 * </tr>
166 * <tr class="altColor">
167 * <td><p>path</p></td>
168 * <td><p>false</p></td>
169 * <td><p>true</p></td>
170 * <td><p>Path to errors object for data binding</p></td>
171 * </tr>
172 * <tr class="rowColor">
173 * <td><p>tabindex</p></td>
174 * <td><p>false</p></td>
175 * <td><p>true</p></td>
176 * <td><p>HTML Standard Attribute</p></td>
177 * </tr>
178 * <tr class="altColor">
179 * <td><p>title</p></td>
180 * <td><p>false</p></td>
181 * <td><p>true</p></td>
182 * <td><p>HTML Standard Attribute</p></td>
183 * </tr>
184 * </tbody>
185 * </table>
186 *
187 * @author Rob Harrop
188 * @author Juergen Hoeller
189 * @author Rick Evans
190 * @since 2.0
191 */
192@SuppressWarnings("serial")
193public class ErrorsTag extends AbstractHtmlElementBodyTag implements BodyTag {
194
195        /**
196         * The key under which this tag exposes error messages in
197         * the {@link PageContext#PAGE_SCOPE page context scope}.
198         */
199        public static final String MESSAGES_ATTRIBUTE = "messages";
200
201        /**
202         * The HTML '{@code span}' tag.
203         */
204        public static final String SPAN_TAG = "span";
205
206
207        private String element = SPAN_TAG;
208
209        private String delimiter = "<br/>";
210
211        /**
212         * Stores any value that existed in the 'errors messages' before the tag was started.
213         */
214        @Nullable
215        private Object oldMessages;
216
217        private boolean errorMessagesWereExposed;
218
219
220        /**
221         * Set the HTML element must be used to render the error messages.
222         * <p>Defaults to an HTML '{@code <span/>}' tag.
223         */
224        public void setElement(String element) {
225                Assert.hasText(element, "'element' cannot be null or blank");
226                this.element = element;
227        }
228
229        /**
230         * Get the HTML element must be used to render the error messages.
231         */
232        public String getElement() {
233                return this.element;
234        }
235
236        /**
237         * Set the delimiter to be used between error messages.
238         * <p>Defaults to an HTML '{@code <br/>}' tag.
239         */
240        public void setDelimiter(String delimiter) {
241                this.delimiter = delimiter;
242        }
243
244        /**
245         * Return the delimiter to be used between error messages.
246         */
247        public String getDelimiter() {
248                return this.delimiter;
249        }
250
251
252        /**
253         * Get the value for the HTML '{@code id}' attribute.
254         * <p>Appends '{@code .errors}' to the value returned by {@link #getPropertyPath()}
255         * or to the model attribute name if the {@code <form:errors/>} tag's
256         * '{@code path}' attribute has been omitted.
257         * @return the value for the HTML '{@code id}' attribute
258         * @see #getPropertyPath()
259         */
260        @Override
261        protected String autogenerateId() throws JspException {
262                String path = getPropertyPath();
263                if (!StringUtils.hasLength(path) || "*".equals(path)) {
264                        path = (String) this.pageContext.getAttribute(
265                                        FormTag.MODEL_ATTRIBUTE_VARIABLE_NAME, PageContext.REQUEST_SCOPE);
266                }
267                return StringUtils.deleteAny(path, "[]") + ".errors";
268        }
269
270        /**
271         * Get the value for the HTML '{@code name}' attribute.
272         * <p>Simply returns {@code null} because the '{@code name}' attribute
273         * is not a validate attribute for the '{@code span}' element.
274         */
275        @Override
276        @Nullable
277        protected String getName() throws JspException {
278                return null;
279        }
280
281        /**
282         * Should rendering of this tag proceed at all?
283         * <p>Only renders output when there are errors for the configured {@link #setPath path}.
284         * @return {@code true} only when there are errors for the configured {@link #setPath path}
285         */
286        @Override
287        protected boolean shouldRender() throws JspException {
288                try {
289                        return getBindStatus().isError();
290                }
291                catch (IllegalStateException ex) {
292                        // Neither BindingResult nor target object available.
293                        return false;
294                }
295        }
296
297        @Override
298        protected void renderDefaultContent(TagWriter tagWriter) throws JspException {
299                tagWriter.startTag(getElement());
300                writeDefaultAttributes(tagWriter);
301                String delimiter = ObjectUtils.getDisplayString(evaluate("delimiter", getDelimiter()));
302                String[] errorMessages = getBindStatus().getErrorMessages();
303                for (int i = 0; i < errorMessages.length; i++) {
304                        String errorMessage = errorMessages[i];
305                        if (i > 0) {
306                                tagWriter.appendValue(delimiter);
307                        }
308                        tagWriter.appendValue(getDisplayString(errorMessage));
309                }
310                tagWriter.endTag();
311        }
312
313        /**
314         * Exposes any bind status error messages under {@link #MESSAGES_ATTRIBUTE this key}
315         * in the {@link PageContext#PAGE_SCOPE}.
316         * <p>Only called if {@link #shouldRender()} returns {@code true}.
317         * @see #removeAttributes()
318         */
319        @Override
320        protected void exposeAttributes() throws JspException {
321                List<String> errorMessages = new ArrayList<>(Arrays.asList(getBindStatus().getErrorMessages()));
322                this.oldMessages = this.pageContext.getAttribute(MESSAGES_ATTRIBUTE, PageContext.PAGE_SCOPE);
323                this.pageContext.setAttribute(MESSAGES_ATTRIBUTE, errorMessages, PageContext.PAGE_SCOPE);
324                this.errorMessagesWereExposed = true;
325        }
326
327        /**
328         * Removes any bind status error messages that were previously stored under
329         * {@link #MESSAGES_ATTRIBUTE this key} in the {@link PageContext#PAGE_SCOPE}.
330         * @see #exposeAttributes()
331         */
332        @Override
333        protected void removeAttributes() {
334                if (this.errorMessagesWereExposed) {
335                        if (this.oldMessages != null) {
336                                this.pageContext.setAttribute(MESSAGES_ATTRIBUTE, this.oldMessages, PageContext.PAGE_SCOPE);
337                                this.oldMessages = null;
338                        }
339                        else {
340                                this.pageContext.removeAttribute(MESSAGES_ATTRIBUTE, PageContext.PAGE_SCOPE);
341                        }
342                }
343        }
344
345}