/*
 * Copyright 2002-2018 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.springframework.web.servlet.support;

import java.beans.PropertyEditor;
import java.util.Arrays;
import java.util.List;

import org.springframework.beans.BeanWrapper;
import org.springframework.beans.PropertyAccessorFactory;
import org.springframework.context.NoSuchMessageException;
import org.springframework.lang.Nullable;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
import org.springframework.validation.BindingResult;
import org.springframework.validation.Errors;
import org.springframework.validation.ObjectError;
import org.springframework.web.util.HtmlUtils;

Simple adapter to expose the bind status of a field or object. Set as a variable both by the JSP bind tag and FreeMarker macros.

Obviously, object status representations (i.e. errors at the object level rather than the field level) do not have an expression and a value but only error codes and messages. For simplicity's sake and to be able to use the same tags and macros, the same status class is used for both scenarios.

Author:Rod Johnson, Juergen Hoeller, Darren Davison
See Also:
/** * Simple adapter to expose the bind status of a field or object. * Set as a variable both by the JSP bind tag and FreeMarker macros. * * <p>Obviously, object status representations (i.e. errors at the object level * rather than the field level) do not have an expression and a value but only * error codes and messages. For simplicity's sake and to be able to use the same * tags and macros, the same status class is used for both scenarios. * * @author Rod Johnson * @author Juergen Hoeller * @author Darren Davison * @see RequestContext#getBindStatus * @see org.springframework.web.servlet.tags.BindTag * @see org.springframework.web.servlet.view.AbstractTemplateView#setExposeSpringMacroHelpers */
public class BindStatus { private final RequestContext requestContext; private final String path; private final boolean htmlEscape; @Nullable private final String expression; @Nullable private final Errors errors; private final String[] errorCodes; @Nullable private String[] errorMessages; @Nullable private List<? extends ObjectError> objectErrors; @Nullable private Object value; @Nullable private Class<?> valueType; @Nullable private Object actualValue; @Nullable private PropertyEditor editor; @Nullable private BindingResult bindingResult;
Create a new BindStatus instance, representing a field or object status.
Params:
  • requestContext – the current RequestContext
  • path – the bean and property path for which values and errors will be resolved (e.g. "customer.address.street")
  • htmlEscape – whether to HTML-escape error messages and string values
Throws:
/** * Create a new BindStatus instance, representing a field or object status. * @param requestContext the current RequestContext * @param path the bean and property path for which values and errors * will be resolved (e.g. "customer.address.street") * @param htmlEscape whether to HTML-escape error messages and string values * @throws IllegalStateException if no corresponding Errors object found */
public BindStatus(RequestContext requestContext, String path, boolean htmlEscape) throws IllegalStateException { this.requestContext = requestContext; this.path = path; this.htmlEscape = htmlEscape; // determine name of the object and property String beanName; int dotPos = path.indexOf('.'); if (dotPos == -1) { // property not set, only the object itself beanName = path; this.expression = null; } else { beanName = path.substring(0, dotPos); this.expression = path.substring(dotPos + 1); } this.errors = requestContext.getErrors(beanName, false); if (this.errors != null) { // Usual case: A BindingResult is available as request attribute. // Can determine error codes and messages for the given expression. // Can use a custom PropertyEditor, as registered by a form controller. if (this.expression != null) { if ("*".equals(this.expression)) { this.objectErrors = this.errors.getAllErrors(); } else if (this.expression.endsWith("*")) { this.objectErrors = this.errors.getFieldErrors(this.expression); } else { this.objectErrors = this.errors.getFieldErrors(this.expression); this.value = this.errors.getFieldValue(this.expression); this.valueType = this.errors.getFieldType(this.expression); if (this.errors instanceof BindingResult) { this.bindingResult = (BindingResult) this.errors; this.actualValue = this.bindingResult.getRawFieldValue(this.expression); this.editor = this.bindingResult.findEditor(this.expression, null); } else { this.actualValue = this.value; } } } else { this.objectErrors = this.errors.getGlobalErrors(); } this.errorCodes = initErrorCodes(this.objectErrors); } else { // No BindingResult available as request attribute: // Probably forwarded directly to a form view. // Let's do the best we can: extract a plain target if appropriate. Object target = requestContext.getModelObject(beanName); if (target == null) { throw new IllegalStateException("Neither BindingResult nor plain target object for bean name '" + beanName + "' available as request attribute"); } if (this.expression != null && !"*".equals(this.expression) && !this.expression.endsWith("*")) { BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(target); this.value = bw.getPropertyValue(this.expression); this.valueType = bw.getPropertyType(this.expression); this.actualValue = this.value; } this.errorCodes = new String[0]; this.errorMessages = new String[0]; } if (htmlEscape && this.value instanceof String) { this.value = HtmlUtils.htmlEscape((String) this.value); } }
Extract the error codes from the ObjectError list.
/** * Extract the error codes from the ObjectError list. */
private static String[] initErrorCodes(List<? extends ObjectError> objectErrors) { String[] errorCodes = new String[objectErrors.size()]; for (int i = 0; i < objectErrors.size(); i++) { ObjectError error = objectErrors.get(i); errorCodes[i] = error.getCode(); } return errorCodes; }
Return the bean and property path for which values and errors will be resolved (e.g. "customer.address.street").
/** * Return the bean and property path for which values and errors * will be resolved (e.g. "customer.address.street"). */
public String getPath() { return this.path; }
Return a bind expression that can be used in HTML forms as input name for the respective field, or null if not field-specific.

Returns a bind path appropriate for resubmission, e.g. "address.street". Note that the complete bind path as required by the bind tag is "customer.address.street", if bound to a "customer" bean.

/** * Return a bind expression that can be used in HTML forms as input name * for the respective field, or {@code null} if not field-specific. * <p>Returns a bind path appropriate for resubmission, e.g. "address.street". * Note that the complete bind path as required by the bind tag is * "customer.address.street", if bound to a "customer" bean. */
@Nullable public String getExpression() { return this.expression; }
Return the current value of the field, i.e. either the property value or a rejected update, or null if not field-specific.

This value will be an HTML-escaped String if the original value already was a String.

/** * Return the current value of the field, i.e. either the property value * or a rejected update, or {@code null} if not field-specific. * <p>This value will be an HTML-escaped String if the original value * already was a String. */
@Nullable public Object getValue() { return this.value; }
Get the 'Class' type of the field. Favor this instead of 'getValue().getClass()' since 'getValue()' may return 'null'.
/** * Get the '{@code Class}' type of the field. Favor this instead of * '{@code getValue().getClass()}' since '{@code getValue()}' may * return '{@code null}'. */
@Nullable public Class<?> getValueType() { return this.valueType; }
Return the actual value of the field, i.e. the raw property value, or null if not available.
/** * Return the actual value of the field, i.e. the raw property value, * or {@code null} if not available. */
@Nullable public Object getActualValue() { return this.actualValue; }
Return a suitable display value for the field, i.e. the stringified value if not null, and an empty string in case of a null value.

This value will be an HTML-escaped String if the original value was non-null: the toString result of the original value will get HTML-escaped.

/** * Return a suitable display value for the field, i.e. the stringified * value if not null, and an empty string in case of a null value. * <p>This value will be an HTML-escaped String if the original value * was non-null: the {@code toString} result of the original value * will get HTML-escaped. */
public String getDisplayValue() { if (this.value instanceof String) { return (String) this.value; } if (this.value != null) { return (this.htmlEscape ? HtmlUtils.htmlEscape(this.value.toString()) : this.value.toString()); } return ""; }
Return if this status represents a field or object error.
/** * Return if this status represents a field or object error. */
public boolean isError() { return (this.errorCodes.length > 0); }
Return the error codes for the field or object, if any. Returns an empty array instead of null if none.
/** * Return the error codes for the field or object, if any. * Returns an empty array instead of null if none. */
public String[] getErrorCodes() { return this.errorCodes; }
Return the first error codes for the field or object, if any.
/** * Return the first error codes for the field or object, if any. */
public String getErrorCode() { return (this.errorCodes.length > 0 ? this.errorCodes[0] : ""); }
Return the resolved error messages for the field or object, if any. Returns an empty array instead of null if none.
/** * Return the resolved error messages for the field or object, * if any. Returns an empty array instead of null if none. */
public String[] getErrorMessages() { return initErrorMessages(); }
Return the first error message for the field or object, if any.
/** * Return the first error message for the field or object, if any. */
public String getErrorMessage() { String[] errorMessages = initErrorMessages(); return (errorMessages.length > 0 ? errorMessages[0] : ""); }
Return an error message string, concatenating all messages separated by the given delimiter.
Params:
  • delimiter – separator string, e.g. ", " or "
    "
Returns:the error message string
/** * Return an error message string, concatenating all messages * separated by the given delimiter. * @param delimiter separator string, e.g. ", " or "<br>" * @return the error message string */
public String getErrorMessagesAsString(String delimiter) { return StringUtils.arrayToDelimitedString(initErrorMessages(), delimiter); }
Extract the error messages from the ObjectError list.
/** * Extract the error messages from the ObjectError list. */
private String[] initErrorMessages() throws NoSuchMessageException { if (this.errorMessages == null) { if (this.objectErrors != null) { this.errorMessages = new String[this.objectErrors.size()]; for (int i = 0; i < this.objectErrors.size(); i++) { ObjectError error = this.objectErrors.get(i); this.errorMessages[i] = this.requestContext.getMessage(error, this.htmlEscape); } } else { this.errorMessages = new String[0]; } } return this.errorMessages; }
Return the Errors instance (typically a BindingResult) that this bind status is currently associated with.
See Also:
Returns:the current Errors instance, or null if none
/** * Return the Errors instance (typically a BindingResult) that this * bind status is currently associated with. * @return the current Errors instance, or {@code null} if none * @see org.springframework.validation.BindingResult */
@Nullable public Errors getErrors() { return this.errors; }
Return the PropertyEditor for the property that this bind status is currently bound to.
Returns:the current PropertyEditor, or null if none
/** * Return the PropertyEditor for the property that this bind status * is currently bound to. * @return the current PropertyEditor, or {@code null} if none */
@Nullable public PropertyEditor getEditor() { return this.editor; }
Find a PropertyEditor for the given value class, associated with the property that this bound status is currently bound to.
Params:
  • valueClass – the value class that an editor is needed for
Returns:the associated PropertyEditor, or null if none
/** * Find a PropertyEditor for the given value class, associated with * the property that this bound status is currently bound to. * @param valueClass the value class that an editor is needed for * @return the associated PropertyEditor, or {@code null} if none */
@Nullable public PropertyEditor findEditor(Class<?> valueClass) { return (this.bindingResult != null ? this.bindingResult.findEditor(this.expression, valueClass) : null); } @Override public String toString() { StringBuilder sb = new StringBuilder("BindStatus: "); sb.append("expression=[").append(this.expression).append("]; "); sb.append("value=[").append(this.value).append("]"); if (!ObjectUtils.isEmpty(this.errorCodes)) { sb.append("; errorCodes=").append(Arrays.asList(this.errorCodes)); } return sb.toString(); } }