001/*
002 * Copyright 2002-2012 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;
022import javax.servlet.jsp.JspException;
023import javax.servlet.jsp.PageContext;
024import javax.servlet.jsp.tagext.BodyTag;
025
026import org.springframework.util.Assert;
027import org.springframework.util.ObjectUtils;
028import org.springframework.util.StringUtils;
029
030/**
031 * Form tag for displaying errors for a particular field or object.
032 *
033 * <p>This tag supports three main usage patterns:
034 *
035 * <ol>
036 *      <li>Field only - set '{@code path}' to the field name (or path)</li>
037 *      <li>Object errors only - omit '{@code path}'</li>
038 *      <li>All errors - set '{@code path}' to '{@code *}'</li>
039 * </ol>
040 *
041 * @author Rob Harrop
042 * @author Juergen Hoeller
043 * @author Rick Evans
044 * @since 2.0
045 */
046@SuppressWarnings("serial")
047public class ErrorsTag extends AbstractHtmlElementBodyTag implements BodyTag {
048
049        /**
050         * The key under which this tag exposes error messages in
051         * the {@link PageContext#PAGE_SCOPE page context scope}.
052         */
053        public static final String MESSAGES_ATTRIBUTE = "messages";
054
055        /**
056         * The HTML '{@code span}' tag.
057         */
058        public static final String SPAN_TAG = "span";
059
060
061        private String element = SPAN_TAG;
062
063        private String delimiter = "<br/>";
064
065        /**
066         * Stores any value that existed in the 'errors messages' before the tag was started.
067         */
068        private Object oldMessages;
069
070        private boolean errorMessagesWereExposed;
071
072
073        /**
074         * Set the HTML element must be used to render the error messages.
075         * <p>Defaults to an HTML '{@code <span/>}' tag.
076         */
077        public void setElement(String element) {
078                Assert.hasText(element, "'element' cannot be null or blank");
079                this.element = element;
080        }
081
082        /**
083         * Get the HTML element must be used to render the error messages.
084         */
085        public String getElement() {
086                return this.element;
087        }
088
089        /**
090         * Set the delimiter to be used between error messages.
091         * <p>Defaults to an HTML '{@code <br/>}' tag.
092         */
093        public void setDelimiter(String delimiter) {
094                this.delimiter = delimiter;
095        }
096
097        /**
098         * Return the delimiter to be used between error messages.
099         */
100        public String getDelimiter() {
101                return this.delimiter;
102        }
103
104
105        /**
106         * Get the value for the HTML '{@code id}' attribute.
107         * <p>Appends '{@code .errors}' to the value returned by {@link #getPropertyPath()}
108         * or to the model attribute name if the {@code <form:errors/>} tag's
109         * '{@code path}' attribute has been omitted.
110         * @return the value for the HTML '{@code id}' attribute
111         * @see #getPropertyPath()
112         */
113        @Override
114        protected String autogenerateId() throws JspException {
115                String path = getPropertyPath();
116                if ("".equals(path) || "*".equals(path)) {
117                        path = (String) this.pageContext.getAttribute(
118                                        FormTag.MODEL_ATTRIBUTE_VARIABLE_NAME, PageContext.REQUEST_SCOPE);
119                }
120                return StringUtils.deleteAny(path, "[]") + ".errors";
121        }
122
123        /**
124         * Get the value for the HTML '{@code name}' attribute.
125         * <p>Simply returns {@code null} because the '{@code name}' attribute
126         * is not a validate attribute for the '{@code span}' element.
127         */
128        @Override
129        protected String getName() throws JspException {
130                return null;
131        }
132
133        /**
134         * Should rendering of this tag proceed at all?
135         * <p>Only renders output when there are errors for the configured {@link #setPath path}.
136         * @return {@code true} only when there are errors for the configured {@link #setPath path}
137         */
138        @Override
139        protected boolean shouldRender() throws JspException {
140                try {
141                        return getBindStatus().isError();
142                }
143                catch (IllegalStateException ex) {
144                        // Neither BindingResult nor target object available.
145                        return false;
146                }
147        }
148
149        @Override
150        protected void renderDefaultContent(TagWriter tagWriter) throws JspException {
151                tagWriter.startTag(getElement());
152                writeDefaultAttributes(tagWriter);
153                String delimiter = ObjectUtils.getDisplayString(evaluate("delimiter", getDelimiter()));
154                String[] errorMessages = getBindStatus().getErrorMessages();
155                for (int i = 0; i < errorMessages.length; i++) {
156                        String errorMessage = errorMessages[i];
157                        if (i > 0) {
158                                tagWriter.appendValue(delimiter);
159                        }
160                        tagWriter.appendValue(getDisplayString(errorMessage));
161                }
162                tagWriter.endTag();
163        }
164
165        /**
166         * Exposes any bind status error messages under {@link #MESSAGES_ATTRIBUTE this key}
167         * in the {@link PageContext#PAGE_SCOPE}.
168         * <p>Only called if {@link #shouldRender()} returns {@code true}.
169         * @see #removeAttributes()
170         */
171        @Override
172        protected void exposeAttributes() throws JspException {
173                List<String> errorMessages = new ArrayList<String>();
174                errorMessages.addAll(Arrays.asList(getBindStatus().getErrorMessages()));
175                this.oldMessages = this.pageContext.getAttribute(MESSAGES_ATTRIBUTE, PageContext.PAGE_SCOPE);
176                this.pageContext.setAttribute(MESSAGES_ATTRIBUTE, errorMessages, PageContext.PAGE_SCOPE);
177                this.errorMessagesWereExposed = true;
178        }
179
180        /**
181         * Removes any bind status error messages that were previously stored under
182         * {@link #MESSAGES_ATTRIBUTE this key} in the {@link PageContext#PAGE_SCOPE}.
183         * @see #exposeAttributes()
184         */
185        @Override
186        protected void removeAttributes() {
187                if (this.errorMessagesWereExposed) {
188                        if (this.oldMessages != null) {
189                                this.pageContext.setAttribute(MESSAGES_ATTRIBUTE, this.oldMessages, PageContext.PAGE_SCOPE);
190                                this.oldMessages = null;
191                        }
192                        else {
193                                this.pageContext.removeAttribute(MESSAGES_ATTRIBUTE, PageContext.PAGE_SCOPE);
194                        }
195                }
196        }
197
198}