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