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}