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}