001/*
002 * Copyright 2002-2017 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.bind;
018
019import java.lang.reflect.Array;
020import java.util.Collection;
021import java.util.List;
022import java.util.Map;
023
024import org.springframework.beans.MutablePropertyValues;
025import org.springframework.beans.PropertyValue;
026import org.springframework.core.CollectionFactory;
027import org.springframework.validation.DataBinder;
028import org.springframework.web.multipart.MultipartFile;
029
030/**
031 * Special {@link DataBinder} for data binding from web request parameters
032 * to JavaBean objects. Designed for web environments, but not dependent on
033 * the Servlet API; serves as base class for more specific DataBinder variants,
034 * such as {@link org.springframework.web.bind.ServletRequestDataBinder}.
035 *
036 * <p>Includes support for field markers which address a common problem with
037 * HTML checkboxes and select options: detecting that a field was part of
038 * the form, but did not generate a request parameter because it was empty.
039 * A field marker allows to detect that state and reset the corresponding
040 * bean property accordingly. Default values, for parameters that are otherwise
041 * not present, can specify a value for the field other then empty.
042 *
043 * @author Juergen Hoeller
044 * @author Scott Andrews
045 * @author Brian Clozel
046 * @since 1.2
047 * @see #registerCustomEditor
048 * @see #setAllowedFields
049 * @see #setRequiredFields
050 * @see #setFieldMarkerPrefix
051 * @see #setFieldDefaultPrefix
052 * @see ServletRequestDataBinder
053 */
054public class WebDataBinder extends DataBinder {
055
056        /**
057         * Default prefix that field marker parameters start with, followed by the field
058         * name: e.g. "_subscribeToNewsletter" for a field "subscribeToNewsletter".
059         * <p>Such a marker parameter indicates that the field was visible, that is,
060         * existed in the form that caused the submission. If no corresponding field
061         * value parameter was found, the field will be reset. The value of the field
062         * marker parameter does not matter in this case; an arbitrary value can be used.
063         * This is particularly useful for HTML checkboxes and select options.
064         * @see #setFieldMarkerPrefix
065         */
066        public static final String DEFAULT_FIELD_MARKER_PREFIX = "_";
067
068        /**
069         * Default prefix that field default parameters start with, followed by the field
070         * name: e.g. "!subscribeToNewsletter" for a field "subscribeToNewsletter".
071         * <p>Default parameters differ from field markers in that they provide a default
072         * value instead of an empty value.
073         * @see #setFieldDefaultPrefix
074         */
075        public static final String DEFAULT_FIELD_DEFAULT_PREFIX = "!";
076
077        private String fieldMarkerPrefix = DEFAULT_FIELD_MARKER_PREFIX;
078
079        private String fieldDefaultPrefix = DEFAULT_FIELD_DEFAULT_PREFIX;
080
081        private boolean bindEmptyMultipartFiles = true;
082
083
084        /**
085         * Create a new WebDataBinder instance, with default object name.
086         * @param target the target object to bind onto (or {@code null}
087         * if the binder is just used to convert a plain parameter value)
088         * @see #DEFAULT_OBJECT_NAME
089         */
090        public WebDataBinder(Object target) {
091                super(target);
092        }
093
094        /**
095         * Create a new WebDataBinder instance.
096         * @param target the target object to bind onto (or {@code null}
097         * if the binder is just used to convert a plain parameter value)
098         * @param objectName the name of the target object
099         */
100        public WebDataBinder(Object target, String objectName) {
101                super(target, objectName);
102        }
103
104
105        /**
106         * Specify a prefix that can be used for parameters that mark potentially
107         * empty fields, having "prefix + field" as name. Such a marker parameter is
108         * checked by existence: You can send any value for it, for example "visible".
109         * This is particularly useful for HTML checkboxes and select options.
110         * <p>Default is "_", for "_FIELD" parameters (e.g. "_subscribeToNewsletter").
111         * Set this to null if you want to turn off the empty field check completely.
112         * <p>HTML checkboxes only send a value when they're checked, so it is not
113         * possible to detect that a formerly checked box has just been unchecked,
114         * at least not with standard HTML means.
115         * <p>One way to address this is to look for a checkbox parameter value if
116         * you know that the checkbox has been visible in the form, resetting the
117         * checkbox if no value found. In Spring web MVC, this typically happens
118         * in a custom {@code onBind} implementation.
119         * <p>This auto-reset mechanism addresses this deficiency, provided
120         * that a marker parameter is sent for each checkbox field, like
121         * "_subscribeToNewsletter" for a "subscribeToNewsletter" field.
122         * As the marker parameter is sent in any case, the data binder can
123         * detect an empty field and automatically reset its value.
124         * @see #DEFAULT_FIELD_MARKER_PREFIX
125         */
126        public void setFieldMarkerPrefix(String fieldMarkerPrefix) {
127                this.fieldMarkerPrefix = fieldMarkerPrefix;
128        }
129
130        /**
131         * Return the prefix for parameters that mark potentially empty fields.
132         */
133        public String getFieldMarkerPrefix() {
134                return this.fieldMarkerPrefix;
135        }
136
137        /**
138         * Specify a prefix that can be used for parameters that indicate default
139         * value fields, having "prefix + field" as name. The value of the default
140         * field is used when the field is not provided.
141         * <p>Default is "!", for "!FIELD" parameters (e.g. "!subscribeToNewsletter").
142         * Set this to null if you want to turn off the field defaults completely.
143         * <p>HTML checkboxes only send a value when they're checked, so it is not
144         * possible to detect that a formerly checked box has just been unchecked,
145         * at least not with standard HTML means.  A default field is especially
146         * useful when a checkbox represents a non-boolean value.
147         * <p>The presence of a default parameter preempts the behavior of a field
148         * marker for the given field.
149         * @see #DEFAULT_FIELD_DEFAULT_PREFIX
150         */
151        public void setFieldDefaultPrefix(String fieldDefaultPrefix) {
152                this.fieldDefaultPrefix = fieldDefaultPrefix;
153        }
154
155        /**
156         * Return the prefix for parameters that mark default fields.
157         */
158        public String getFieldDefaultPrefix() {
159                return this.fieldDefaultPrefix;
160        }
161
162        /**
163         * Set whether to bind empty MultipartFile parameters. Default is "true".
164         * <p>Turn this off if you want to keep an already bound MultipartFile
165         * when the user resubmits the form without choosing a different file.
166         * Else, the already bound MultipartFile will be replaced by an empty
167         * MultipartFile holder.
168         * @see org.springframework.web.multipart.MultipartFile
169         */
170        public void setBindEmptyMultipartFiles(boolean bindEmptyMultipartFiles) {
171                this.bindEmptyMultipartFiles = bindEmptyMultipartFiles;
172        }
173
174        /**
175         * Return whether to bind empty MultipartFile parameters.
176         */
177        public boolean isBindEmptyMultipartFiles() {
178                return this.bindEmptyMultipartFiles;
179        }
180
181
182        /**
183         * This implementation performs a field default and marker check
184         * before delegating to the superclass binding process.
185         * @see #checkFieldDefaults
186         * @see #checkFieldMarkers
187         */
188        @Override
189        protected void doBind(MutablePropertyValues mpvs) {
190                checkFieldDefaults(mpvs);
191                checkFieldMarkers(mpvs);
192                super.doBind(mpvs);
193        }
194
195        /**
196         * Check the given property values for field defaults,
197         * i.e. for fields that start with the field default prefix.
198         * <p>The existence of a field defaults indicates that the specified
199         * value should be used if the field is otherwise not present.
200         * @param mpvs the property values to be bound (can be modified)
201         * @see #getFieldDefaultPrefix
202         */
203        protected void checkFieldDefaults(MutablePropertyValues mpvs) {
204                String fieldDefaultPrefix = getFieldDefaultPrefix();
205                if (fieldDefaultPrefix != null) {
206                        PropertyValue[] pvArray = mpvs.getPropertyValues();
207                        for (PropertyValue pv : pvArray) {
208                                if (pv.getName().startsWith(fieldDefaultPrefix)) {
209                                        String field = pv.getName().substring(fieldDefaultPrefix.length());
210                                        if (getPropertyAccessor().isWritableProperty(field) && !mpvs.contains(field)) {
211                                                mpvs.add(field, pv.getValue());
212                                        }
213                                        mpvs.removePropertyValue(pv);
214                                }
215                        }
216                }
217        }
218
219        /**
220         * Check the given property values for field markers,
221         * i.e. for fields that start with the field marker prefix.
222         * <p>The existence of a field marker indicates that the specified
223         * field existed in the form. If the property values do not contain
224         * a corresponding field value, the field will be considered as empty
225         * and will be reset appropriately.
226         * @param mpvs the property values to be bound (can be modified)
227         * @see #getFieldMarkerPrefix
228         * @see #getEmptyValue(String, Class)
229         */
230        protected void checkFieldMarkers(MutablePropertyValues mpvs) {
231                String fieldMarkerPrefix = getFieldMarkerPrefix();
232                if (fieldMarkerPrefix != null) {
233                        PropertyValue[] pvArray = mpvs.getPropertyValues();
234                        for (PropertyValue pv : pvArray) {
235                                if (pv.getName().startsWith(fieldMarkerPrefix)) {
236                                        String field = pv.getName().substring(fieldMarkerPrefix.length());
237                                        if (getPropertyAccessor().isWritableProperty(field) && !mpvs.contains(field)) {
238                                                Class<?> fieldType = getPropertyAccessor().getPropertyType(field);
239                                                mpvs.add(field, getEmptyValue(field, fieldType));
240                                        }
241                                        mpvs.removePropertyValue(pv);
242                                }
243                        }
244                }
245        }
246
247        /**
248         * Determine an empty value for the specified field.
249         * <p>The default implementation returns:
250         * <ul>
251         * <li>{@code Boolean.FALSE} for boolean fields
252         * <li>an empty array for array types
253         * <li>Collection implementations for Collection types
254         * <li>Map implementations for Map types
255         * <li>else, {@code null} is used as default
256         * </ul>
257         * @param field the name of the field
258         * @param fieldType the type of the field
259         * @return the empty value (for most fields: null)
260         */
261        protected Object getEmptyValue(String field, Class<?> fieldType) {
262                if (fieldType != null) {
263                        try {
264                                if (boolean.class == fieldType || Boolean.class == fieldType) {
265                                        // Special handling of boolean property.
266                                        return Boolean.FALSE;
267                                }
268                                else if (fieldType.isArray()) {
269                                        // Special handling of array property.
270                                        return Array.newInstance(fieldType.getComponentType(), 0);
271                                }
272                                else if (Collection.class.isAssignableFrom(fieldType)) {
273                                        return CollectionFactory.createCollection(fieldType, 0);
274                                }
275                                else if (Map.class.isAssignableFrom(fieldType)) {
276                                        return CollectionFactory.createMap(fieldType, 0);
277                                }
278                        }
279                        catch (IllegalArgumentException ex) {
280                                if (logger.isDebugEnabled()) {
281                                        logger.debug("Failed to create default value - falling back to null: " + ex.getMessage());
282                                }
283                        }
284                }
285                // Default value: null.
286                return null;
287        }
288
289        /**
290         * Bind all multipart files contained in the given request, if any
291         * (in case of a multipart request). To be called by subclasses.
292         * <p>Multipart files will only be added to the property values if they
293         * are not empty or if we're configured to bind empty multipart files too.
294         * @param multipartFiles Map of field name String to MultipartFile object
295         * @param mpvs the property values to be bound (can be modified)
296         * @see org.springframework.web.multipart.MultipartFile
297         * @see #setBindEmptyMultipartFiles
298         */
299        protected void bindMultipart(Map<String, List<MultipartFile>> multipartFiles, MutablePropertyValues mpvs) {
300                for (Map.Entry<String, List<MultipartFile>> entry : multipartFiles.entrySet()) {
301                        String key = entry.getKey();
302                        List<MultipartFile> values = entry.getValue();
303                        if (values.size() == 1) {
304                                MultipartFile value = values.get(0);
305                                if (isBindEmptyMultipartFiles() || !value.isEmpty()) {
306                                        mpvs.add(key, value);
307                                }
308                        }
309                        else {
310                                mpvs.add(key, values);
311                        }
312                }
313        }
314
315}