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.reactive.result.view; 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 by FreeMarker macros and other tag libraries. 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 Rossen Stoyanchev 044 * @author Juergen Hoeller 045 * @since 5.0 046 * @see RequestContext#getBindStatus 047 */ 048public class BindStatus { 049 050 private final RequestContext requestContext; 051 052 private final String path; 053 054 private final boolean htmlEscape; 055 056 @Nullable 057 private final String expression; 058 059 @Nullable 060 private final Errors errors; 061 062 private final String[] errorCodes; 063 064 @Nullable 065 private String[] errorMessages; 066 067 @Nullable 068 private List<? extends ObjectError> objectErrors; 069 070 @Nullable 071 private Object value; 072 073 @Nullable 074 private Class<?> valueType; 075 076 @Nullable 077 private Object actualValue; 078 079 @Nullable 080 private PropertyEditor editor; 081 082 @Nullable 083 private BindingResult bindingResult; 084 085 086 /** 087 * Create a new BindStatus instance, representing a field or object status. 088 * @param requestContext the current RequestContext 089 * @param path the bean and property path for which values and errors 090 * will be resolved (e.g. "customer.address.street") 091 * @param htmlEscape whether to HTML-escape error messages and string values 092 * @throws IllegalStateException if no corresponding Errors object found 093 */ 094 public BindStatus(RequestContext requestContext, String path, boolean htmlEscape) throws IllegalStateException { 095 this.requestContext = requestContext; 096 this.path = path; 097 this.htmlEscape = htmlEscape; 098 099 // determine name of the object and property 100 String beanName; 101 int dotPos = path.indexOf('.'); 102 if (dotPos == -1) { 103 // property not set, only the object itself 104 beanName = path; 105 this.expression = null; 106 } 107 else { 108 beanName = path.substring(0, dotPos); 109 this.expression = path.substring(dotPos + 1); 110 } 111 112 this.errors = requestContext.getErrors(beanName, false); 113 114 if (this.errors != null) { 115 // Usual case: A BindingResult is available as request attribute. 116 // Can determine error codes and messages for the given expression. 117 // Can use a custom PropertyEditor, as registered by a form controller. 118 if (this.expression != null) { 119 if ("*".equals(this.expression)) { 120 this.objectErrors = this.errors.getAllErrors(); 121 } 122 else if (this.expression.endsWith("*")) { 123 this.objectErrors = this.errors.getFieldErrors(this.expression); 124 } 125 else { 126 this.objectErrors = this.errors.getFieldErrors(this.expression); 127 this.value = this.errors.getFieldValue(this.expression); 128 this.valueType = this.errors.getFieldType(this.expression); 129 if (this.errors instanceof BindingResult) { 130 this.bindingResult = (BindingResult) this.errors; 131 this.actualValue = this.bindingResult.getRawFieldValue(this.expression); 132 this.editor = this.bindingResult.findEditor(this.expression, null); 133 } 134 else { 135 this.actualValue = this.value; 136 } 137 } 138 } 139 else { 140 this.objectErrors = this.errors.getGlobalErrors(); 141 } 142 this.errorCodes = initErrorCodes(this.objectErrors); 143 } 144 145 else { 146 // No BindingResult available as request attribute: 147 // Probably forwarded directly to a form view. 148 // Let's do the best we can: extract a plain target if appropriate. 149 Object target = requestContext.getModelObject(beanName); 150 if (target == null) { 151 throw new IllegalStateException( 152 "Neither BindingResult nor plain target object for bean name '" + 153 beanName + "' available as request attribute"); 154 } 155 if (this.expression != null && !"*".equals(this.expression) && !this.expression.endsWith("*")) { 156 BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(target); 157 this.value = bw.getPropertyValue(this.expression); 158 this.valueType = bw.getPropertyType(this.expression); 159 this.actualValue = this.value; 160 } 161 this.errorCodes = new String[0]; 162 this.errorMessages = new String[0]; 163 } 164 165 if (htmlEscape && this.value instanceof String) { 166 this.value = HtmlUtils.htmlEscape((String) this.value); 167 } 168 } 169 170 /** 171 * Extract the error codes from the ObjectError list. 172 */ 173 private static String[] initErrorCodes(List<? extends ObjectError> objectErrors) { 174 String[] errorCodes = new String[objectErrors.size()]; 175 for (int i = 0; i < objectErrors.size(); i++) { 176 ObjectError error = objectErrors.get(i); 177 errorCodes[i] = error.getCode(); 178 } 179 return errorCodes; 180 } 181 182 183 /** 184 * Return the bean and property path for which values and errors 185 * will be resolved (e.g. "customer.address.street"). 186 */ 187 public String getPath() { 188 return this.path; 189 } 190 191 /** 192 * Return a bind expression that can be used in HTML forms as input name 193 * for the respective field, or {@code null} if not field-specific. 194 * <p>Returns a bind path appropriate for resubmission, e.g. "address.street". 195 * Note that the complete bind path as required by the bind tag is 196 * "customer.address.street", if bound to a "customer" bean. 197 */ 198 @Nullable 199 public String getExpression() { 200 return this.expression; 201 } 202 203 /** 204 * Return the current value of the field, i.e. either the property value 205 * or a rejected update, or {@code null} if not field-specific. 206 * <p>This value will be an HTML-escaped String if the original value 207 * already was a String. 208 */ 209 @Nullable 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 @Nullable 220 public Class<?> getValueType() { 221 return this.valueType; 222 } 223 224 /** 225 * Return the actual value of the field, i.e. the raw property value, 226 * or {@code null} if not available. 227 */ 228 @Nullable 229 public Object getActualValue() { 230 return this.actualValue; 231 } 232 233 /** 234 * Return a suitable display value for the field, i.e. the stringified 235 * value if not null, and an empty string in case of a null value. 236 * <p>This value will be an HTML-escaped String if the original value 237 * was non-null: the {@code toString} result of the original value 238 * will get HTML-escaped. 239 */ 240 public String getDisplayValue() { 241 if (this.value instanceof String) { 242 return (String) this.value; 243 } 244 if (this.value != null) { 245 return (this.htmlEscape ? 246 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 (!ObjectUtils.isEmpty(this.errorCodes) ? 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 ? 348 this.bindingResult.findEditor(this.expression, valueClass) : null); 349 } 350 351 352 @Override 353 public String toString() { 354 StringBuilder sb = new StringBuilder("BindStatus: "); 355 sb.append("expression=[").append(this.expression).append("]; "); 356 sb.append("value=[").append(this.value).append("]"); 357 if (!ObjectUtils.isEmpty(this.errorCodes)) { 358 sb.append("; errorCodes=").append(Arrays.asList(this.errorCodes)); 359 } 360 return sb.toString(); 361 } 362 363}