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}