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}