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}