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