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}