001/* 002 * Copyright 2012-2016 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 * http://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.boot.bind; 018 019import java.net.InetAddress; 020import java.util.ArrayList; 021import java.util.Collection; 022import java.util.Collections; 023import java.util.HashSet; 024import java.util.LinkedHashMap; 025import java.util.LinkedList; 026import java.util.List; 027import java.util.Map; 028import java.util.Properties; 029import java.util.Set; 030 031import org.springframework.beans.BeanWrapper; 032import org.springframework.beans.BeanWrapperImpl; 033import org.springframework.beans.BeansException; 034import org.springframework.beans.InvalidPropertyException; 035import org.springframework.beans.MutablePropertyValues; 036import org.springframework.beans.NotWritablePropertyException; 037import org.springframework.beans.PropertyValue; 038import org.springframework.core.convert.ConversionService; 039import org.springframework.core.convert.TypeDescriptor; 040import org.springframework.core.env.StandardEnvironment; 041import org.springframework.util.LinkedMultiValueMap; 042import org.springframework.util.MultiValueMap; 043import org.springframework.util.StringUtils; 044import org.springframework.validation.AbstractPropertyBindingResult; 045import org.springframework.validation.BeanPropertyBindingResult; 046import org.springframework.validation.DataBinder; 047 048/** 049 * Binder implementation that allows caller to bind to maps and also allows property names 050 * to match a bit loosely (if underscores or dashes are removed and replaced with camel 051 * case for example). 052 * 053 * @author Dave Syer 054 * @author Phillip Webb 055 * @author Stephane Nicoll 056 * @author Andy Wilkinson 057 * @see RelaxedNames 058 */ 059public class RelaxedDataBinder extends DataBinder { 060 061 private static final Object BLANK = new Object(); 062 063 private String namePrefix; 064 065 private boolean ignoreNestedProperties; 066 067 private MultiValueMap<String, String> nameAliases = new LinkedMultiValueMap<String, String>(); 068 069 /** 070 * Create a new {@link RelaxedDataBinder} instance. 071 * @param target the target into which properties are bound 072 */ 073 public RelaxedDataBinder(Object target) { 074 super(wrapTarget(target)); 075 } 076 077 /** 078 * Create a new {@link RelaxedDataBinder} instance. 079 * @param target the target into which properties are bound 080 * @param namePrefix An optional prefix to be used when reading properties 081 */ 082 public RelaxedDataBinder(Object target, String namePrefix) { 083 super(wrapTarget(target), 084 (StringUtils.hasLength(namePrefix) ? namePrefix : DEFAULT_OBJECT_NAME)); 085 this.namePrefix = cleanNamePrefix(namePrefix); 086 } 087 088 private String cleanNamePrefix(String namePrefix) { 089 if (!StringUtils.hasLength(namePrefix)) { 090 return null; 091 } 092 return (namePrefix.endsWith(".") ? namePrefix : namePrefix + "."); 093 } 094 095 /** 096 * Flag to disable binding of nested properties (i.e. those with period separators in 097 * their paths). Can be useful to disable this if the name prefix is empty and you 098 * don't want to ignore unknown fields. 099 * @param ignoreNestedProperties the flag to set (default false) 100 */ 101 public void setIgnoreNestedProperties(boolean ignoreNestedProperties) { 102 this.ignoreNestedProperties = ignoreNestedProperties; 103 } 104 105 /** 106 * Set name aliases. 107 * @param aliases a map of property name to aliases 108 */ 109 public void setNameAliases(Map<String, List<String>> aliases) { 110 this.nameAliases = new LinkedMultiValueMap<String, String>(aliases); 111 } 112 113 /** 114 * Add aliases to the {@link DataBinder}. 115 * @param name the property name to alias 116 * @param alias aliases for the property names 117 * @return this instance 118 */ 119 public RelaxedDataBinder withAlias(String name, String... alias) { 120 for (String value : alias) { 121 this.nameAliases.add(name, value); 122 } 123 return this; 124 } 125 126 @Override 127 protected void doBind(MutablePropertyValues propertyValues) { 128 super.doBind(modifyProperties(propertyValues, getTarget())); 129 } 130 131 /** 132 * Modify the property values so that period separated property paths are valid for 133 * map keys. Also creates new maps for properties of map type that are null (assuming 134 * all maps are potentially nested). The standard bracket {@code[...]} dereferencing 135 * is also accepted. 136 * @param propertyValues the property values 137 * @param target the target object 138 * @return modified property values 139 */ 140 private MutablePropertyValues modifyProperties(MutablePropertyValues propertyValues, 141 Object target) { 142 propertyValues = getPropertyValuesForNamePrefix(propertyValues); 143 if (target instanceof MapHolder) { 144 propertyValues = addMapPrefix(propertyValues); 145 } 146 BeanWrapper wrapper = new BeanWrapperImpl(target); 147 wrapper.setConversionService( 148 new RelaxedConversionService(getConversionService())); 149 wrapper.setAutoGrowNestedPaths(true); 150 List<PropertyValue> sortedValues = new ArrayList<PropertyValue>(); 151 Set<String> modifiedNames = new HashSet<String>(); 152 List<String> sortedNames = getSortedPropertyNames(propertyValues); 153 for (String name : sortedNames) { 154 PropertyValue propertyValue = propertyValues.getPropertyValue(name); 155 PropertyValue modifiedProperty = modifyProperty(wrapper, propertyValue); 156 if (modifiedNames.add(modifiedProperty.getName())) { 157 sortedValues.add(modifiedProperty); 158 } 159 } 160 return new MutablePropertyValues(sortedValues); 161 } 162 163 private List<String> getSortedPropertyNames(MutablePropertyValues propertyValues) { 164 List<String> names = new LinkedList<String>(); 165 for (PropertyValue propertyValue : propertyValues.getPropertyValueList()) { 166 names.add(propertyValue.getName()); 167 } 168 sortPropertyNames(names); 169 return names; 170 } 171 172 /** 173 * Sort by name so that parent properties get processed first (e.g. 'foo.bar' before 174 * 'foo.bar.spam'). Don't use Collections.sort() because the order might be 175 * significant for other property names (it shouldn't be but who knows what people 176 * might be relying on, e.g. HSQL has a JDBCXADataSource where "databaseName" is a 177 * synonym for "url"). 178 * @param names the names to sort 179 */ 180 private void sortPropertyNames(List<String> names) { 181 for (String name : new ArrayList<String>(names)) { 182 int propertyIndex = names.indexOf(name); 183 BeanPath path = new BeanPath(name); 184 for (String prefix : path.prefixes()) { 185 int prefixIndex = names.indexOf(prefix); 186 if (prefixIndex >= propertyIndex) { 187 // The child property has a parent in the list in the wrong order 188 names.remove(name); 189 names.add(prefixIndex, name); 190 } 191 } 192 } 193 } 194 195 private MutablePropertyValues addMapPrefix(MutablePropertyValues propertyValues) { 196 MutablePropertyValues rtn = new MutablePropertyValues(); 197 for (PropertyValue pv : propertyValues.getPropertyValues()) { 198 rtn.add("map." + pv.getName(), pv.getValue()); 199 } 200 return rtn; 201 } 202 203 private MutablePropertyValues getPropertyValuesForNamePrefix( 204 MutablePropertyValues propertyValues) { 205 if (!StringUtils.hasText(this.namePrefix) && !this.ignoreNestedProperties) { 206 return propertyValues; 207 } 208 MutablePropertyValues rtn = new MutablePropertyValues(); 209 for (PropertyValue value : propertyValues.getPropertyValues()) { 210 String name = value.getName(); 211 for (String prefix : new RelaxedNames(stripLastDot(this.namePrefix))) { 212 for (String separator : new String[] { ".", "_" }) { 213 String candidate = (StringUtils.hasLength(prefix) ? prefix + separator 214 : prefix); 215 if (name.startsWith(candidate)) { 216 name = name.substring(candidate.length()); 217 if (!(this.ignoreNestedProperties && name.contains("."))) { 218 PropertyOrigin propertyOrigin = OriginCapablePropertyValue 219 .getOrigin(value); 220 rtn.addPropertyValue(new OriginCapablePropertyValue(name, 221 value.getValue(), propertyOrigin)); 222 } 223 } 224 } 225 } 226 } 227 return rtn; 228 } 229 230 private String stripLastDot(String string) { 231 if (StringUtils.hasLength(string) && string.endsWith(".")) { 232 string = string.substring(0, string.length() - 1); 233 } 234 return string; 235 } 236 237 private PropertyValue modifyProperty(BeanWrapper target, 238 PropertyValue propertyValue) { 239 String name = propertyValue.getName(); 240 String normalizedName = normalizePath(target, name); 241 if (!normalizedName.equals(name)) { 242 return new PropertyValue(normalizedName, propertyValue.getValue()); 243 } 244 return propertyValue; 245 } 246 247 /** 248 * Normalize a bean property path to a format understood by a BeanWrapper. This is 249 * used so that 250 * <ul> 251 * <li>Fuzzy matching can be employed for bean property names</li> 252 * <li>Period separators can be used instead of indexing ([...]) for map keys</li> 253 * </ul> 254 * @param wrapper a bean wrapper for the object to bind 255 * @param path the bean path to bind 256 * @return a transformed path with correct bean wrapper syntax 257 */ 258 protected String normalizePath(BeanWrapper wrapper, String path) { 259 return initializePath(wrapper, new BeanPath(path), 0); 260 } 261 262 @Override 263 protected AbstractPropertyBindingResult createBeanPropertyBindingResult() { 264 return new RelaxedBeanPropertyBindingResult(getTarget(), getObjectName(), 265 isAutoGrowNestedPaths(), getAutoGrowCollectionLimit(), 266 getConversionService()); 267 } 268 269 private String initializePath(BeanWrapper wrapper, BeanPath path, int index) { 270 String prefix = path.prefix(index); 271 String key = path.name(index); 272 if (path.isProperty(index)) { 273 key = getActualPropertyName(wrapper, prefix, key); 274 path.rename(index, key); 275 } 276 if (path.name(++index) == null) { 277 return path.toString(); 278 } 279 String name = path.prefix(index); 280 TypeDescriptor descriptor = wrapper.getPropertyTypeDescriptor(name); 281 if (descriptor == null || descriptor.isMap()) { 282 if (isMapValueStringType(descriptor) 283 || isBlanked(wrapper, name, path.name(index))) { 284 path.collapseKeys(index); 285 } 286 path.mapIndex(index); 287 extendMapIfNecessary(wrapper, path, index); 288 } 289 else if (descriptor.isCollection()) { 290 extendCollectionIfNecessary(wrapper, path, index); 291 } 292 else if (descriptor.getType().equals(Object.class)) { 293 if (isBlanked(wrapper, name, path.name(index))) { 294 path.collapseKeys(index); 295 } 296 path.mapIndex(index); 297 if (path.isLastNode(index)) { 298 wrapper.setPropertyValue(path.toString(), BLANK); 299 } 300 else { 301 String next = path.prefix(index + 1); 302 if (wrapper.getPropertyValue(next) == null) { 303 wrapper.setPropertyValue(next, new LinkedHashMap<String, Object>()); 304 } 305 } 306 } 307 return initializePath(wrapper, path, index); 308 } 309 310 private boolean isMapValueStringType(TypeDescriptor descriptor) { 311 if (descriptor == null || descriptor.getMapValueTypeDescriptor() == null) { 312 return false; 313 } 314 if (Properties.class.isAssignableFrom(descriptor.getObjectType())) { 315 // Properties is declared as Map<Object,Object> but we know it's really 316 // Map<String,String> 317 return true; 318 } 319 Class<?> valueType = descriptor.getMapValueTypeDescriptor().getObjectType(); 320 return (valueType != null && CharSequence.class.isAssignableFrom(valueType)); 321 } 322 323 @SuppressWarnings("rawtypes") 324 private boolean isBlanked(BeanWrapper wrapper, String propertyName, String key) { 325 Object value = (wrapper.isReadableProperty(propertyName) 326 ? wrapper.getPropertyValue(propertyName) : null); 327 if (value instanceof Map) { 328 if (((Map) value).get(key) == BLANK) { 329 return true; 330 } 331 } 332 return false; 333 } 334 335 private void extendCollectionIfNecessary(BeanWrapper wrapper, BeanPath path, 336 int index) { 337 String name = path.prefix(index); 338 TypeDescriptor elementDescriptor = wrapper.getPropertyTypeDescriptor(name) 339 .getElementTypeDescriptor(); 340 if (!elementDescriptor.isMap() && !elementDescriptor.isCollection() 341 && !elementDescriptor.getType().equals(Object.class)) { 342 return; 343 } 344 Object extend = new LinkedHashMap<String, Object>(); 345 if (!elementDescriptor.isMap() && path.isArrayIndex(index)) { 346 extend = new ArrayList<Object>(); 347 } 348 wrapper.setPropertyValue(path.prefix(index + 1), extend); 349 } 350 351 private void extendMapIfNecessary(BeanWrapper wrapper, BeanPath path, int index) { 352 String name = path.prefix(index); 353 TypeDescriptor parent = wrapper.getPropertyTypeDescriptor(name); 354 if (parent == null) { 355 return; 356 } 357 TypeDescriptor descriptor = parent.getMapValueTypeDescriptor(); 358 if (descriptor == null) { 359 descriptor = TypeDescriptor.valueOf(Object.class); 360 } 361 if (!descriptor.isMap() && !descriptor.isCollection() 362 && !descriptor.getType().equals(Object.class)) { 363 return; 364 } 365 String extensionName = path.prefix(index + 1); 366 if (wrapper.isReadableProperty(extensionName)) { 367 Object currentValue = wrapper.getPropertyValue(extensionName); 368 if ((descriptor.isCollection() && currentValue instanceof Collection) 369 || (!descriptor.isCollection() && currentValue instanceof Map)) { 370 return; 371 } 372 } 373 Object extend = new LinkedHashMap<String, Object>(); 374 if (descriptor.isCollection()) { 375 extend = new ArrayList<Object>(); 376 } 377 if (descriptor.getType().equals(Object.class) && path.isLastNode(index)) { 378 extend = BLANK; 379 } 380 wrapper.setPropertyValue(extensionName, extend); 381 } 382 383 private String getActualPropertyName(BeanWrapper target, String prefix, String name) { 384 String propertyName = resolvePropertyName(target, prefix, name); 385 if (propertyName == null) { 386 propertyName = resolveNestedPropertyName(target, prefix, name); 387 } 388 return (propertyName == null ? name : propertyName); 389 } 390 391 private String resolveNestedPropertyName(BeanWrapper target, String prefix, 392 String name) { 393 StringBuilder candidate = new StringBuilder(); 394 for (String field : name.split("[_\\-\\.]")) { 395 candidate.append(candidate.length() > 0 ? "." : ""); 396 candidate.append(field); 397 String nested = resolvePropertyName(target, prefix, candidate.toString()); 398 if (nested != null) { 399 Class<?> type = target.getPropertyType(nested); 400 if ((type != null) && Map.class.isAssignableFrom(type)) { 401 // Special case for map property (gh-3836). 402 return nested + "[" + name.substring(candidate.length() + 1) + "]"; 403 } 404 String propertyName = resolvePropertyName(target, 405 joinString(prefix, nested), 406 name.substring(candidate.length() + 1)); 407 if (propertyName != null) { 408 return joinString(nested, propertyName); 409 } 410 } 411 } 412 return null; 413 } 414 415 private String resolvePropertyName(BeanWrapper target, String prefix, String name) { 416 Iterable<String> names = getNameAndAliases(name); 417 for (String nameOrAlias : names) { 418 for (String candidate : new RelaxedNames(nameOrAlias)) { 419 try { 420 if (target.getPropertyType(joinString(prefix, candidate)) != null) { 421 return candidate; 422 } 423 } 424 catch (InvalidPropertyException ex) { 425 // swallow and continue 426 } 427 } 428 } 429 return null; 430 } 431 432 private String joinString(String prefix, String name) { 433 return (StringUtils.hasLength(prefix) ? prefix + "." + name : name); 434 } 435 436 private Iterable<String> getNameAndAliases(String name) { 437 List<String> aliases = this.nameAliases.get(name); 438 if (aliases == null) { 439 return Collections.singleton(name); 440 } 441 List<String> nameAndAliases = new ArrayList<String>(aliases.size() + 1); 442 nameAndAliases.add(name); 443 nameAndAliases.addAll(aliases); 444 return nameAndAliases; 445 } 446 447 private static Object wrapTarget(Object target) { 448 if (target instanceof Map) { 449 @SuppressWarnings("unchecked") 450 Map<String, Object> map = (Map<String, Object>) target; 451 target = new MapHolder(map); 452 } 453 return target; 454 } 455 456 /** 457 * Holder to allow Map targets to be bound. 458 */ 459 static class MapHolder { 460 461 private Map<String, Object> map; 462 463 MapHolder(Map<String, Object> map) { 464 this.map = map; 465 } 466 467 public void setMap(Map<String, Object> map) { 468 this.map = map; 469 } 470 471 public Map<String, Object> getMap() { 472 return this.map; 473 } 474 475 } 476 477 /** 478 * A path though properties of a bean. 479 */ 480 private static class BeanPath { 481 482 private List<PathNode> nodes; 483 484 BeanPath(String path) { 485 this.nodes = splitPath(path); 486 } 487 488 public List<String> prefixes() { 489 List<String> prefixes = new ArrayList<String>(); 490 for (int index = 1; index < this.nodes.size(); index++) { 491 prefixes.add(prefix(index)); 492 } 493 return prefixes; 494 } 495 496 public boolean isLastNode(int index) { 497 return index >= this.nodes.size() - 1; 498 } 499 500 private List<PathNode> splitPath(String path) { 501 List<PathNode> nodes = new ArrayList<PathNode>(); 502 String current = extractIndexedPaths(path, nodes); 503 for (String name : StringUtils.delimitedListToStringArray(current, ".")) { 504 if (StringUtils.hasText(name)) { 505 nodes.add(new PropertyNode(name)); 506 } 507 } 508 return nodes; 509 } 510 511 private String extractIndexedPaths(String path, List<PathNode> nodes) { 512 int startRef = path.indexOf("["); 513 String current = path; 514 while (startRef >= 0) { 515 if (startRef > 0) { 516 nodes.addAll(splitPath(current.substring(0, startRef))); 517 } 518 int endRef = current.indexOf("]", startRef); 519 if (endRef > 0) { 520 String sub = current.substring(startRef + 1, endRef); 521 if (sub.matches("[0-9]+")) { 522 nodes.add(new ArrayIndexNode(sub)); 523 } 524 else { 525 nodes.add(new MapIndexNode(sub)); 526 } 527 } 528 current = current.substring(endRef + 1); 529 startRef = current.indexOf("["); 530 } 531 return current; 532 } 533 534 public void collapseKeys(int index) { 535 List<PathNode> revised = new ArrayList<PathNode>(); 536 for (int i = 0; i < index; i++) { 537 revised.add(this.nodes.get(i)); 538 } 539 StringBuilder builder = new StringBuilder(); 540 for (int i = index; i < this.nodes.size(); i++) { 541 if (i > index) { 542 builder.append("."); 543 } 544 builder.append(this.nodes.get(i).name); 545 } 546 revised.add(new PropertyNode(builder.toString())); 547 this.nodes = revised; 548 } 549 550 public void mapIndex(int index) { 551 PathNode node = this.nodes.get(index); 552 if (node instanceof PropertyNode) { 553 node = ((PropertyNode) node).mapIndex(); 554 } 555 this.nodes.set(index, node); 556 } 557 558 public String prefix(int index) { 559 return range(0, index); 560 } 561 562 public void rename(int index, String name) { 563 this.nodes.get(index).name = name; 564 } 565 566 public String name(int index) { 567 if (index < this.nodes.size()) { 568 return this.nodes.get(index).name; 569 } 570 return null; 571 } 572 573 private String range(int start, int end) { 574 StringBuilder builder = new StringBuilder(); 575 for (int i = start; i < end; i++) { 576 PathNode node = this.nodes.get(i); 577 builder.append(node); 578 } 579 if (builder.toString().startsWith(("."))) { 580 builder.replace(0, 1, ""); 581 } 582 return builder.toString(); 583 } 584 585 public boolean isArrayIndex(int index) { 586 return this.nodes.get(index) instanceof ArrayIndexNode; 587 } 588 589 public boolean isProperty(int index) { 590 return this.nodes.get(index) instanceof PropertyNode; 591 } 592 593 @Override 594 public String toString() { 595 return prefix(this.nodes.size()); 596 } 597 598 private static class PathNode { 599 600 protected String name; 601 602 PathNode(String name) { 603 this.name = name; 604 } 605 606 } 607 608 private static class ArrayIndexNode extends PathNode { 609 610 ArrayIndexNode(String name) { 611 super(name); 612 } 613 614 @Override 615 public String toString() { 616 return "[" + this.name + "]"; 617 } 618 619 } 620 621 private static class MapIndexNode extends PathNode { 622 623 MapIndexNode(String name) { 624 super(name); 625 } 626 627 @Override 628 public String toString() { 629 return "[" + this.name + "]"; 630 } 631 632 } 633 634 private static class PropertyNode extends PathNode { 635 636 PropertyNode(String name) { 637 super(name); 638 } 639 640 public MapIndexNode mapIndex() { 641 return new MapIndexNode(this.name); 642 } 643 644 @Override 645 public String toString() { 646 return "." + this.name; 647 } 648 649 } 650 651 } 652 653 /** 654 * Extended version of {@link BeanPropertyBindingResult} to support relaxed binding. 655 */ 656 private static class RelaxedBeanPropertyBindingResult 657 extends BeanPropertyBindingResult { 658 659 private RelaxedConversionService conversionService; 660 661 RelaxedBeanPropertyBindingResult(Object target, String objectName, 662 boolean autoGrowNestedPaths, int autoGrowCollectionLimit, 663 ConversionService conversionService) { 664 super(target, objectName, autoGrowNestedPaths, autoGrowCollectionLimit); 665 this.conversionService = new RelaxedConversionService(conversionService); 666 } 667 668 @Override 669 protected BeanWrapper createBeanWrapper() { 670 BeanWrapper beanWrapper = new RelaxedBeanWrapper(getTarget()); 671 beanWrapper.setConversionService(this.conversionService); 672 beanWrapper.registerCustomEditor(InetAddress.class, new InetAddressEditor()); 673 return beanWrapper; 674 } 675 676 } 677 678 /** 679 * Extended version of {@link BeanWrapperImpl} to support relaxed binding. 680 */ 681 private static class RelaxedBeanWrapper extends BeanWrapperImpl { 682 683 private static final Set<String> BENIGN_PROPERTY_SOURCE_NAMES; 684 685 static { 686 Set<String> names = new HashSet<String>(); 687 names.add(StandardEnvironment.SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME); 688 names.add(StandardEnvironment.SYSTEM_PROPERTIES_PROPERTY_SOURCE_NAME); 689 BENIGN_PROPERTY_SOURCE_NAMES = Collections.unmodifiableSet(names); 690 } 691 692 RelaxedBeanWrapper(Object target) { 693 super(target); 694 } 695 696 @Override 697 public void setPropertyValue(PropertyValue pv) throws BeansException { 698 try { 699 super.setPropertyValue(pv); 700 } 701 catch (NotWritablePropertyException ex) { 702 PropertyOrigin origin = OriginCapablePropertyValue.getOrigin(pv); 703 if (isBenign(origin)) { 704 logger.debug("Ignoring benign property binding failure", ex); 705 return; 706 } 707 if (origin == null) { 708 throw ex; 709 } 710 throw new RelaxedBindingNotWritablePropertyException(ex, origin); 711 } 712 } 713 714 private boolean isBenign(PropertyOrigin origin) { 715 String name = (origin == null ? null : origin.getSource().getName()); 716 return BENIGN_PROPERTY_SOURCE_NAMES.contains(name); 717 } 718 719 } 720 721}