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.support; 018 019import java.beans.PropertyEditor; 020import java.util.Arrays; 021import java.util.List; 022 023import org.springframework.beans.BeanWrapper; 024import org.springframework.beans.PropertyAccessorFactory; 025import org.springframework.context.NoSuchMessageException; 026import org.springframework.util.StringUtils; 027import org.springframework.validation.BindingResult; 028import org.springframework.validation.Errors; 029import org.springframework.validation.ObjectError; 030import org.springframework.web.util.HtmlUtils; 031 032/** 033 * Simple adapter to expose the bind status of a field or object. 034 * Set as a variable both by the JSP bind tag and Velocity/FreeMarker macros. 035 * 036 * <p>Obviously, object status representations (i.e. errors at the object level 037 * rather than the field level) do not have an expression and a value but only 038 * error codes and messages. For simplicity's sake and to be able to use the same 039 * tags and macros, the same status class is used for both scenarios. 040 * 041 * @author Rod Johnson 042 * @author Juergen Hoeller 043 * @author Darren Davison 044 * @see RequestContext#getBindStatus 045 * @see org.springframework.web.servlet.tags.BindTag 046 * @see org.springframework.web.servlet.view.AbstractTemplateView#setExposeSpringMacroHelpers 047 */ 048public class BindStatus { 049 050 private final RequestContext requestContext; 051 052 private final String path; 053 054 private final boolean htmlEscape; 055 056 private final String expression; 057 058 private final Errors errors; 059 060 private BindingResult bindingResult; 061 062 private Object value; 063 064 private Class<?> valueType; 065 066 private Object actualValue; 067 068 private PropertyEditor editor; 069 070 private List<? extends ObjectError> objectErrors; 071 072 private String[] errorCodes; 073 074 private String[] errorMessages; 075 076 077 /** 078 * Create a new BindStatus instance, representing a field or object status. 079 * @param requestContext the current RequestContext 080 * @param path the bean and property path for which values and errors 081 * will be resolved (e.g. "customer.address.street") 082 * @param htmlEscape whether to HTML-escape error messages and string values 083 * @throws IllegalStateException if no corresponding Errors object found 084 */ 085 public BindStatus(RequestContext requestContext, String path, boolean htmlEscape) throws IllegalStateException { 086 this.requestContext = requestContext; 087 this.path = path; 088 this.htmlEscape = htmlEscape; 089 090 // determine name of the object and property 091 String beanName; 092 int dotPos = path.indexOf('.'); 093 if (dotPos == -1) { 094 // property not set, only the object itself 095 beanName = path; 096 this.expression = null; 097 } 098 else { 099 beanName = path.substring(0, dotPos); 100 this.expression = path.substring(dotPos + 1); 101 } 102 103 this.errors = requestContext.getErrors(beanName, false); 104 105 if (this.errors != null) { 106 // Usual case: A BindingResult is available as request attribute. 107 // Can determine error codes and messages for the given expression. 108 // Can use a custom PropertyEditor, as registered by a form controller. 109 if (this.expression != null) { 110 if ("*".equals(this.expression)) { 111 this.objectErrors = this.errors.getAllErrors(); 112 } 113 else if (this.expression.endsWith("*")) { 114 this.objectErrors = this.errors.getFieldErrors(this.expression); 115 } 116 else { 117 this.objectErrors = this.errors.getFieldErrors(this.expression); 118 this.value = this.errors.getFieldValue(this.expression); 119 this.valueType = this.errors.getFieldType(this.expression); 120 if (this.errors instanceof BindingResult) { 121 this.bindingResult = (BindingResult) this.errors; 122 this.actualValue = this.bindingResult.getRawFieldValue(this.expression); 123 this.editor = this.bindingResult.findEditor(this.expression, null); 124 } 125 else { 126 this.actualValue = this.value; 127 } 128 } 129 } 130 else { 131 this.objectErrors = this.errors.getGlobalErrors(); 132 } 133 initErrorCodes(); 134 } 135 136 else { 137 // No BindingResult available as request attribute: 138 // Probably forwarded directly to a form view. 139 // Let's do the best we can: extract a plain target if appropriate. 140 Object target = requestContext.getModelObject(beanName); 141 if (target == null) { 142 throw new IllegalStateException("Neither BindingResult nor plain target object for bean name '" + 143 beanName + "' available as request attribute"); 144 } 145 if (this.expression != null && !"*".equals(this.expression) && !this.expression.endsWith("*")) { 146 BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(target); 147 this.value = bw.getPropertyValue(this.expression); 148 this.valueType = bw.getPropertyType(this.expression); 149 this.actualValue = this.value; 150 } 151 this.errorCodes = new String[0]; 152 this.errorMessages = new String[0]; 153 } 154 155 if (htmlEscape && this.value instanceof String) { 156 this.value = HtmlUtils.htmlEscape((String) this.value); 157 } 158 } 159 160 /** 161 * Extract the error codes from the ObjectError list. 162 */ 163 private void initErrorCodes() { 164 this.errorCodes = new String[this.objectErrors.size()]; 165 for (int i = 0; i < this.objectErrors.size(); i++) { 166 ObjectError error = this.objectErrors.get(i); 167 this.errorCodes[i] = error.getCode(); 168 } 169 } 170 171 /** 172 * Extract the error messages from the ObjectError list. 173 */ 174 private void initErrorMessages() throws NoSuchMessageException { 175 if (this.errorMessages == null) { 176 this.errorMessages = new String[this.objectErrors.size()]; 177 for (int i = 0; i < this.objectErrors.size(); i++) { 178 ObjectError error = this.objectErrors.get(i); 179 this.errorMessages[i] = this.requestContext.getMessage(error, this.htmlEscape); 180 } 181 } 182 } 183 184 185 /** 186 * Return the bean and property path for which values and errors 187 * will be resolved (e.g. "customer.address.street"). 188 */ 189 public String getPath() { 190 return this.path; 191 } 192 193 /** 194 * Return a bind expression that can be used in HTML forms as input name 195 * for the respective field, or {@code null} if not field-specific. 196 * <p>Returns a bind path appropriate for resubmission, e.g. "address.street". 197 * Note that the complete bind path as required by the bind tag is 198 * "customer.address.street", if bound to a "customer" bean. 199 */ 200 public String getExpression() { 201 return this.expression; 202 } 203 204 /** 205 * Return the current value of the field, i.e. either the property value 206 * or a rejected update, or {@code null} if not field-specific. 207 * <p>This value will be an HTML-escaped String if the original value 208 * already was a String. 209 */ 210 public Object getValue() { 211 return this.value; 212 } 213 214 /** 215 * Get the '{@code Class}' type of the field. Favor this instead of 216 * '{@code getValue().getClass()}' since '{@code getValue()}' may 217 * return '{@code null}'. 218 */ 219 public Class<?> getValueType() { 220 return this.valueType; 221 } 222 223 /** 224 * Return the actual value of the field, i.e. the raw property value, 225 * or {@code null} if not available. 226 */ 227 public Object getActualValue() { 228 return this.actualValue; 229 } 230 231 /** 232 * Return a suitable display value for the field, i.e. the stringified 233 * value if not null, and an empty string in case of a null value. 234 * <p>This value will be an HTML-escaped String if the original value 235 * was non-null: the {@code toString} result of the original value 236 * will get HTML-escaped. 237 */ 238 public String getDisplayValue() { 239 if (this.value instanceof String) { 240 return (String) this.value; 241 } 242 if (this.value != null) { 243 return (this.htmlEscape ? HtmlUtils.htmlEscape(this.value.toString()) : this.value.toString()); 244 } 245 return ""; 246 } 247 248 /** 249 * Return if this status represents a field or object error. 250 */ 251 public boolean isError() { 252 return (this.errorCodes != null && this.errorCodes.length > 0); 253 } 254 255 /** 256 * Return the error codes for the field or object, if any. 257 * Returns an empty array instead of null if none. 258 */ 259 public String[] getErrorCodes() { 260 return this.errorCodes; 261 } 262 263 /** 264 * Return the first error codes for the field or object, if any. 265 */ 266 public String getErrorCode() { 267 return (this.errorCodes.length > 0 ? this.errorCodes[0] : ""); 268 } 269 270 /** 271 * Return the resolved error messages for the field or object, 272 * if any. Returns an empty array instead of null if none. 273 */ 274 public String[] getErrorMessages() { 275 initErrorMessages(); 276 return this.errorMessages; 277 } 278 279 /** 280 * Return the first error message for the field or object, if any. 281 */ 282 public String getErrorMessage() { 283 initErrorMessages(); 284 return (this.errorMessages.length > 0 ? this.errorMessages[0] : ""); 285 } 286 287 /** 288 * Return an error message string, concatenating all messages 289 * separated by the given delimiter. 290 * @param delimiter separator string, e.g. ", " or "<br>" 291 * @return the error message string 292 */ 293 public String getErrorMessagesAsString(String delimiter) { 294 initErrorMessages(); 295 return StringUtils.arrayToDelimitedString(this.errorMessages, delimiter); 296 } 297 298 /** 299 * Return the Errors instance (typically a BindingResult) that this 300 * bind status is currently associated with. 301 * @return the current Errors instance, or {@code null} if none 302 * @see org.springframework.validation.BindingResult 303 */ 304 public Errors getErrors() { 305 return this.errors; 306 } 307 308 /** 309 * Return the PropertyEditor for the property that this bind status 310 * is currently bound to. 311 * @return the current PropertyEditor, or {@code null} if none 312 */ 313 public PropertyEditor getEditor() { 314 return this.editor; 315 } 316 317 /** 318 * Find a PropertyEditor for the given value class, associated with 319 * the property that this bound status is currently bound to. 320 * @param valueClass the value class that an editor is needed for 321 * @return the associated PropertyEditor, or {@code null} if none 322 */ 323 public PropertyEditor findEditor(Class<?> valueClass) { 324 return (this.bindingResult != null ? this.bindingResult.findEditor(this.expression, valueClass) : null); 325 } 326 327 328 @Override 329 public String toString() { 330 StringBuilder sb = new StringBuilder("BindStatus: "); 331 sb.append("expression=[").append(this.expression).append("]; "); 332 sb.append("value=[").append(this.value).append("]"); 333 if (isError()) { 334 sb.append("; errorCodes=").append(Arrays.asList(this.errorCodes)); 335 } 336 return sb.toString(); 337 } 338 339}