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.beans; 018 019import java.beans.PropertyEditor; 020import java.io.File; 021import java.io.InputStream; 022import java.io.Reader; 023import java.math.BigDecimal; 024import java.math.BigInteger; 025import java.net.URI; 026import java.net.URL; 027import java.nio.charset.Charset; 028import java.nio.file.Path; 029import java.time.ZoneId; 030import java.util.ArrayList; 031import java.util.Collection; 032import java.util.Currency; 033import java.util.HashMap; 034import java.util.Iterator; 035import java.util.LinkedHashMap; 036import java.util.List; 037import java.util.Locale; 038import java.util.Map; 039import java.util.Properties; 040import java.util.Set; 041import java.util.SortedMap; 042import java.util.SortedSet; 043import java.util.TimeZone; 044import java.util.UUID; 045import java.util.regex.Pattern; 046 047import org.xml.sax.InputSource; 048 049import org.springframework.beans.propertyeditors.ByteArrayPropertyEditor; 050import org.springframework.beans.propertyeditors.CharArrayPropertyEditor; 051import org.springframework.beans.propertyeditors.CharacterEditor; 052import org.springframework.beans.propertyeditors.CharsetEditor; 053import org.springframework.beans.propertyeditors.ClassArrayEditor; 054import org.springframework.beans.propertyeditors.ClassEditor; 055import org.springframework.beans.propertyeditors.CurrencyEditor; 056import org.springframework.beans.propertyeditors.CustomBooleanEditor; 057import org.springframework.beans.propertyeditors.CustomCollectionEditor; 058import org.springframework.beans.propertyeditors.CustomMapEditor; 059import org.springframework.beans.propertyeditors.CustomNumberEditor; 060import org.springframework.beans.propertyeditors.FileEditor; 061import org.springframework.beans.propertyeditors.InputSourceEditor; 062import org.springframework.beans.propertyeditors.InputStreamEditor; 063import org.springframework.beans.propertyeditors.LocaleEditor; 064import org.springframework.beans.propertyeditors.PathEditor; 065import org.springframework.beans.propertyeditors.PatternEditor; 066import org.springframework.beans.propertyeditors.PropertiesEditor; 067import org.springframework.beans.propertyeditors.ReaderEditor; 068import org.springframework.beans.propertyeditors.StringArrayPropertyEditor; 069import org.springframework.beans.propertyeditors.TimeZoneEditor; 070import org.springframework.beans.propertyeditors.URIEditor; 071import org.springframework.beans.propertyeditors.URLEditor; 072import org.springframework.beans.propertyeditors.UUIDEditor; 073import org.springframework.beans.propertyeditors.ZoneIdEditor; 074import org.springframework.core.convert.ConversionService; 075import org.springframework.core.io.Resource; 076import org.springframework.core.io.support.ResourceArrayPropertyEditor; 077import org.springframework.lang.Nullable; 078import org.springframework.util.ClassUtils; 079 080/** 081 * Base implementation of the {@link PropertyEditorRegistry} interface. 082 * Provides management of default editors and custom editors. 083 * Mainly serves as base class for {@link BeanWrapperImpl}. 084 * 085 * @author Juergen Hoeller 086 * @author Rob Harrop 087 * @since 1.2.6 088 * @see java.beans.PropertyEditorManager 089 * @see java.beans.PropertyEditorSupport#setAsText 090 * @see java.beans.PropertyEditorSupport#setValue 091 */ 092public class PropertyEditorRegistrySupport implements PropertyEditorRegistry { 093 094 @Nullable 095 private ConversionService conversionService; 096 097 private boolean defaultEditorsActive = false; 098 099 private boolean configValueEditorsActive = false; 100 101 @Nullable 102 private Map<Class<?>, PropertyEditor> defaultEditors; 103 104 @Nullable 105 private Map<Class<?>, PropertyEditor> overriddenDefaultEditors; 106 107 @Nullable 108 private Map<Class<?>, PropertyEditor> customEditors; 109 110 @Nullable 111 private Map<String, CustomEditorHolder> customEditorsForPath; 112 113 @Nullable 114 private Map<Class<?>, PropertyEditor> customEditorCache; 115 116 117 /** 118 * Specify a Spring 3.0 ConversionService to use for converting 119 * property values, as an alternative to JavaBeans PropertyEditors. 120 */ 121 public void setConversionService(@Nullable ConversionService conversionService) { 122 this.conversionService = conversionService; 123 } 124 125 /** 126 * Return the associated ConversionService, if any. 127 */ 128 @Nullable 129 public ConversionService getConversionService() { 130 return this.conversionService; 131 } 132 133 134 //--------------------------------------------------------------------- 135 // Management of default editors 136 //--------------------------------------------------------------------- 137 138 /** 139 * Activate the default editors for this registry instance, 140 * allowing for lazily registering default editors when needed. 141 */ 142 protected void registerDefaultEditors() { 143 this.defaultEditorsActive = true; 144 } 145 146 /** 147 * Activate config value editors which are only intended for configuration purposes, 148 * such as {@link org.springframework.beans.propertyeditors.StringArrayPropertyEditor}. 149 * <p>Those editors are not registered by default simply because they are in 150 * general inappropriate for data binding purposes. Of course, you may register 151 * them individually in any case, through {@link #registerCustomEditor}. 152 */ 153 public void useConfigValueEditors() { 154 this.configValueEditorsActive = true; 155 } 156 157 /** 158 * Override the default editor for the specified type with the given property editor. 159 * <p>Note that this is different from registering a custom editor in that the editor 160 * semantically still is a default editor. A ConversionService will override such a 161 * default editor, whereas custom editors usually override the ConversionService. 162 * @param requiredType the type of the property 163 * @param propertyEditor the editor to register 164 * @see #registerCustomEditor(Class, PropertyEditor) 165 */ 166 public void overrideDefaultEditor(Class<?> requiredType, PropertyEditor propertyEditor) { 167 if (this.overriddenDefaultEditors == null) { 168 this.overriddenDefaultEditors = new HashMap<>(); 169 } 170 this.overriddenDefaultEditors.put(requiredType, propertyEditor); 171 } 172 173 /** 174 * Retrieve the default editor for the given property type, if any. 175 * <p>Lazily registers the default editors, if they are active. 176 * @param requiredType type of the property 177 * @return the default editor, or {@code null} if none found 178 * @see #registerDefaultEditors 179 */ 180 @Nullable 181 public PropertyEditor getDefaultEditor(Class<?> requiredType) { 182 if (!this.defaultEditorsActive) { 183 return null; 184 } 185 if (this.overriddenDefaultEditors != null) { 186 PropertyEditor editor = this.overriddenDefaultEditors.get(requiredType); 187 if (editor != null) { 188 return editor; 189 } 190 } 191 if (this.defaultEditors == null) { 192 createDefaultEditors(); 193 } 194 return this.defaultEditors.get(requiredType); 195 } 196 197 /** 198 * Actually register the default editors for this registry instance. 199 */ 200 private void createDefaultEditors() { 201 this.defaultEditors = new HashMap<>(64); 202 203 // Simple editors, without parameterization capabilities. 204 // The JDK does not contain a default editor for any of these target types. 205 this.defaultEditors.put(Charset.class, new CharsetEditor()); 206 this.defaultEditors.put(Class.class, new ClassEditor()); 207 this.defaultEditors.put(Class[].class, new ClassArrayEditor()); 208 this.defaultEditors.put(Currency.class, new CurrencyEditor()); 209 this.defaultEditors.put(File.class, new FileEditor()); 210 this.defaultEditors.put(InputStream.class, new InputStreamEditor()); 211 this.defaultEditors.put(InputSource.class, new InputSourceEditor()); 212 this.defaultEditors.put(Locale.class, new LocaleEditor()); 213 this.defaultEditors.put(Path.class, new PathEditor()); 214 this.defaultEditors.put(Pattern.class, new PatternEditor()); 215 this.defaultEditors.put(Properties.class, new PropertiesEditor()); 216 this.defaultEditors.put(Reader.class, new ReaderEditor()); 217 this.defaultEditors.put(Resource[].class, new ResourceArrayPropertyEditor()); 218 this.defaultEditors.put(TimeZone.class, new TimeZoneEditor()); 219 this.defaultEditors.put(URI.class, new URIEditor()); 220 this.defaultEditors.put(URL.class, new URLEditor()); 221 this.defaultEditors.put(UUID.class, new UUIDEditor()); 222 this.defaultEditors.put(ZoneId.class, new ZoneIdEditor()); 223 224 // Default instances of collection editors. 225 // Can be overridden by registering custom instances of those as custom editors. 226 this.defaultEditors.put(Collection.class, new CustomCollectionEditor(Collection.class)); 227 this.defaultEditors.put(Set.class, new CustomCollectionEditor(Set.class)); 228 this.defaultEditors.put(SortedSet.class, new CustomCollectionEditor(SortedSet.class)); 229 this.defaultEditors.put(List.class, new CustomCollectionEditor(List.class)); 230 this.defaultEditors.put(SortedMap.class, new CustomMapEditor(SortedMap.class)); 231 232 // Default editors for primitive arrays. 233 this.defaultEditors.put(byte[].class, new ByteArrayPropertyEditor()); 234 this.defaultEditors.put(char[].class, new CharArrayPropertyEditor()); 235 236 // The JDK does not contain a default editor for char! 237 this.defaultEditors.put(char.class, new CharacterEditor(false)); 238 this.defaultEditors.put(Character.class, new CharacterEditor(true)); 239 240 // Spring's CustomBooleanEditor accepts more flag values than the JDK's default editor. 241 this.defaultEditors.put(boolean.class, new CustomBooleanEditor(false)); 242 this.defaultEditors.put(Boolean.class, new CustomBooleanEditor(true)); 243 244 // The JDK does not contain default editors for number wrapper types! 245 // Override JDK primitive number editors with our own CustomNumberEditor. 246 this.defaultEditors.put(byte.class, new CustomNumberEditor(Byte.class, false)); 247 this.defaultEditors.put(Byte.class, new CustomNumberEditor(Byte.class, true)); 248 this.defaultEditors.put(short.class, new CustomNumberEditor(Short.class, false)); 249 this.defaultEditors.put(Short.class, new CustomNumberEditor(Short.class, true)); 250 this.defaultEditors.put(int.class, new CustomNumberEditor(Integer.class, false)); 251 this.defaultEditors.put(Integer.class, new CustomNumberEditor(Integer.class, true)); 252 this.defaultEditors.put(long.class, new CustomNumberEditor(Long.class, false)); 253 this.defaultEditors.put(Long.class, new CustomNumberEditor(Long.class, true)); 254 this.defaultEditors.put(float.class, new CustomNumberEditor(Float.class, false)); 255 this.defaultEditors.put(Float.class, new CustomNumberEditor(Float.class, true)); 256 this.defaultEditors.put(double.class, new CustomNumberEditor(Double.class, false)); 257 this.defaultEditors.put(Double.class, new CustomNumberEditor(Double.class, true)); 258 this.defaultEditors.put(BigDecimal.class, new CustomNumberEditor(BigDecimal.class, true)); 259 this.defaultEditors.put(BigInteger.class, new CustomNumberEditor(BigInteger.class, true)); 260 261 // Only register config value editors if explicitly requested. 262 if (this.configValueEditorsActive) { 263 StringArrayPropertyEditor sae = new StringArrayPropertyEditor(); 264 this.defaultEditors.put(String[].class, sae); 265 this.defaultEditors.put(short[].class, sae); 266 this.defaultEditors.put(int[].class, sae); 267 this.defaultEditors.put(long[].class, sae); 268 } 269 } 270 271 /** 272 * Copy the default editors registered in this instance to the given target registry. 273 * @param target the target registry to copy to 274 */ 275 protected void copyDefaultEditorsTo(PropertyEditorRegistrySupport target) { 276 target.defaultEditorsActive = this.defaultEditorsActive; 277 target.configValueEditorsActive = this.configValueEditorsActive; 278 target.defaultEditors = this.defaultEditors; 279 target.overriddenDefaultEditors = this.overriddenDefaultEditors; 280 } 281 282 283 //--------------------------------------------------------------------- 284 // Management of custom editors 285 //--------------------------------------------------------------------- 286 287 @Override 288 public void registerCustomEditor(Class<?> requiredType, PropertyEditor propertyEditor) { 289 registerCustomEditor(requiredType, null, propertyEditor); 290 } 291 292 @Override 293 public void registerCustomEditor(@Nullable Class<?> requiredType, @Nullable String propertyPath, PropertyEditor propertyEditor) { 294 if (requiredType == null && propertyPath == null) { 295 throw new IllegalArgumentException("Either requiredType or propertyPath is required"); 296 } 297 if (propertyPath != null) { 298 if (this.customEditorsForPath == null) { 299 this.customEditorsForPath = new LinkedHashMap<>(16); 300 } 301 this.customEditorsForPath.put(propertyPath, new CustomEditorHolder(propertyEditor, requiredType)); 302 } 303 else { 304 if (this.customEditors == null) { 305 this.customEditors = new LinkedHashMap<>(16); 306 } 307 this.customEditors.put(requiredType, propertyEditor); 308 this.customEditorCache = null; 309 } 310 } 311 312 @Override 313 @Nullable 314 public PropertyEditor findCustomEditor(@Nullable Class<?> requiredType, @Nullable String propertyPath) { 315 Class<?> requiredTypeToUse = requiredType; 316 if (propertyPath != null) { 317 if (this.customEditorsForPath != null) { 318 // Check property-specific editor first. 319 PropertyEditor editor = getCustomEditor(propertyPath, requiredType); 320 if (editor == null) { 321 List<String> strippedPaths = new ArrayList<>(); 322 addStrippedPropertyPaths(strippedPaths, "", propertyPath); 323 for (Iterator<String> it = strippedPaths.iterator(); it.hasNext() && editor == null;) { 324 String strippedPath = it.next(); 325 editor = getCustomEditor(strippedPath, requiredType); 326 } 327 } 328 if (editor != null) { 329 return editor; 330 } 331 } 332 if (requiredType == null) { 333 requiredTypeToUse = getPropertyType(propertyPath); 334 } 335 } 336 // No property-specific editor -> check type-specific editor. 337 return getCustomEditor(requiredTypeToUse); 338 } 339 340 /** 341 * Determine whether this registry contains a custom editor 342 * for the specified array/collection element. 343 * @param elementType the target type of the element 344 * (can be {@code null} if not known) 345 * @param propertyPath the property path (typically of the array/collection; 346 * can be {@code null} if not known) 347 * @return whether a matching custom editor has been found 348 */ 349 public boolean hasCustomEditorForElement(@Nullable Class<?> elementType, @Nullable String propertyPath) { 350 if (propertyPath != null && this.customEditorsForPath != null) { 351 for (Map.Entry<String, CustomEditorHolder> entry : this.customEditorsForPath.entrySet()) { 352 if (PropertyAccessorUtils.matchesProperty(entry.getKey(), propertyPath) && 353 entry.getValue().getPropertyEditor(elementType) != null) { 354 return true; 355 } 356 } 357 } 358 // No property-specific editor -> check type-specific editor. 359 return (elementType != null && this.customEditors != null && this.customEditors.containsKey(elementType)); 360 } 361 362 /** 363 * Determine the property type for the given property path. 364 * <p>Called by {@link #findCustomEditor} if no required type has been specified, 365 * to be able to find a type-specific editor even if just given a property path. 366 * <p>The default implementation always returns {@code null}. 367 * BeanWrapperImpl overrides this with the standard {@code getPropertyType} 368 * method as defined by the BeanWrapper interface. 369 * @param propertyPath the property path to determine the type for 370 * @return the type of the property, or {@code null} if not determinable 371 * @see BeanWrapper#getPropertyType(String) 372 */ 373 @Nullable 374 protected Class<?> getPropertyType(String propertyPath) { 375 return null; 376 } 377 378 /** 379 * Get custom editor that has been registered for the given property. 380 * @param propertyName the property path to look for 381 * @param requiredType the type to look for 382 * @return the custom editor, or {@code null} if none specific for this property 383 */ 384 @Nullable 385 private PropertyEditor getCustomEditor(String propertyName, @Nullable Class<?> requiredType) { 386 CustomEditorHolder holder = 387 (this.customEditorsForPath != null ? this.customEditorsForPath.get(propertyName) : null); 388 return (holder != null ? holder.getPropertyEditor(requiredType) : null); 389 } 390 391 /** 392 * Get custom editor for the given type. If no direct match found, 393 * try custom editor for superclass (which will in any case be able 394 * to render a value as String via {@code getAsText}). 395 * @param requiredType the type to look for 396 * @return the custom editor, or {@code null} if none found for this type 397 * @see java.beans.PropertyEditor#getAsText() 398 */ 399 @Nullable 400 private PropertyEditor getCustomEditor(@Nullable Class<?> requiredType) { 401 if (requiredType == null || this.customEditors == null) { 402 return null; 403 } 404 // Check directly registered editor for type. 405 PropertyEditor editor = this.customEditors.get(requiredType); 406 if (editor == null) { 407 // Check cached editor for type, registered for superclass or interface. 408 if (this.customEditorCache != null) { 409 editor = this.customEditorCache.get(requiredType); 410 } 411 if (editor == null) { 412 // Find editor for superclass or interface. 413 for (Iterator<Class<?>> it = this.customEditors.keySet().iterator(); it.hasNext() && editor == null;) { 414 Class<?> key = it.next(); 415 if (key.isAssignableFrom(requiredType)) { 416 editor = this.customEditors.get(key); 417 // Cache editor for search type, to avoid the overhead 418 // of repeated assignable-from checks. 419 if (this.customEditorCache == null) { 420 this.customEditorCache = new HashMap<>(); 421 } 422 this.customEditorCache.put(requiredType, editor); 423 } 424 } 425 } 426 } 427 return editor; 428 } 429 430 /** 431 * Guess the property type of the specified property from the registered 432 * custom editors (provided that they were registered for a specific type). 433 * @param propertyName the name of the property 434 * @return the property type, or {@code null} if not determinable 435 */ 436 @Nullable 437 protected Class<?> guessPropertyTypeFromEditors(String propertyName) { 438 if (this.customEditorsForPath != null) { 439 CustomEditorHolder editorHolder = this.customEditorsForPath.get(propertyName); 440 if (editorHolder == null) { 441 List<String> strippedPaths = new ArrayList<>(); 442 addStrippedPropertyPaths(strippedPaths, "", propertyName); 443 for (Iterator<String> it = strippedPaths.iterator(); it.hasNext() && editorHolder == null;) { 444 String strippedName = it.next(); 445 editorHolder = this.customEditorsForPath.get(strippedName); 446 } 447 } 448 if (editorHolder != null) { 449 return editorHolder.getRegisteredType(); 450 } 451 } 452 return null; 453 } 454 455 /** 456 * Copy the custom editors registered in this instance to the given target registry. 457 * @param target the target registry to copy to 458 * @param nestedProperty the nested property path of the target registry, if any. 459 * If this is non-null, only editors registered for a path below this nested property 460 * will be copied. If this is null, all editors will be copied. 461 */ 462 protected void copyCustomEditorsTo(PropertyEditorRegistry target, @Nullable String nestedProperty) { 463 String actualPropertyName = 464 (nestedProperty != null ? PropertyAccessorUtils.getPropertyName(nestedProperty) : null); 465 if (this.customEditors != null) { 466 this.customEditors.forEach(target::registerCustomEditor); 467 } 468 if (this.customEditorsForPath != null) { 469 this.customEditorsForPath.forEach((editorPath, editorHolder) -> { 470 if (nestedProperty != null) { 471 int pos = PropertyAccessorUtils.getFirstNestedPropertySeparatorIndex(editorPath); 472 if (pos != -1) { 473 String editorNestedProperty = editorPath.substring(0, pos); 474 String editorNestedPath = editorPath.substring(pos + 1); 475 if (editorNestedProperty.equals(nestedProperty) || editorNestedProperty.equals(actualPropertyName)) { 476 target.registerCustomEditor( 477 editorHolder.getRegisteredType(), editorNestedPath, editorHolder.getPropertyEditor()); 478 } 479 } 480 } 481 else { 482 target.registerCustomEditor( 483 editorHolder.getRegisteredType(), editorPath, editorHolder.getPropertyEditor()); 484 } 485 }); 486 } 487 } 488 489 490 /** 491 * Add property paths with all variations of stripped keys and/or indexes. 492 * Invokes itself recursively with nested paths. 493 * @param strippedPaths the result list to add to 494 * @param nestedPath the current nested path 495 * @param propertyPath the property path to check for keys/indexes to strip 496 */ 497 private void addStrippedPropertyPaths(List<String> strippedPaths, String nestedPath, String propertyPath) { 498 int startIndex = propertyPath.indexOf(PropertyAccessor.PROPERTY_KEY_PREFIX_CHAR); 499 if (startIndex != -1) { 500 int endIndex = propertyPath.indexOf(PropertyAccessor.PROPERTY_KEY_SUFFIX_CHAR); 501 if (endIndex != -1) { 502 String prefix = propertyPath.substring(0, startIndex); 503 String key = propertyPath.substring(startIndex, endIndex + 1); 504 String suffix = propertyPath.substring(endIndex + 1); 505 // Strip the first key. 506 strippedPaths.add(nestedPath + prefix + suffix); 507 // Search for further keys to strip, with the first key stripped. 508 addStrippedPropertyPaths(strippedPaths, nestedPath + prefix, suffix); 509 // Search for further keys to strip, with the first key not stripped. 510 addStrippedPropertyPaths(strippedPaths, nestedPath + prefix + key, suffix); 511 } 512 } 513 } 514 515 516 /** 517 * Holder for a registered custom editor with property name. 518 * Keeps the PropertyEditor itself plus the type it was registered for. 519 */ 520 private static final class CustomEditorHolder { 521 522 private final PropertyEditor propertyEditor; 523 524 @Nullable 525 private final Class<?> registeredType; 526 527 private CustomEditorHolder(PropertyEditor propertyEditor, @Nullable Class<?> registeredType) { 528 this.propertyEditor = propertyEditor; 529 this.registeredType = registeredType; 530 } 531 532 private PropertyEditor getPropertyEditor() { 533 return this.propertyEditor; 534 } 535 536 @Nullable 537 private Class<?> getRegisteredType() { 538 return this.registeredType; 539 } 540 541 @Nullable 542 private PropertyEditor getPropertyEditor(@Nullable Class<?> requiredType) { 543 // Special case: If no required type specified, which usually only happens for 544 // Collection elements, or required type is not assignable to registered type, 545 // which usually only happens for generic properties of type Object - 546 // then return PropertyEditor if not registered for Collection or array type. 547 // (If not registered for Collection or array, it is assumed to be intended 548 // for elements.) 549 if (this.registeredType == null || 550 (requiredType != null && 551 (ClassUtils.isAssignable(this.registeredType, requiredType) || 552 ClassUtils.isAssignable(requiredType, this.registeredType))) || 553 (requiredType == null && 554 (!Collection.class.isAssignableFrom(this.registeredType) && !this.registeredType.isArray()))) { 555 return this.propertyEditor; 556 } 557 else { 558 return null; 559 } 560 } 561 } 562 563}