001/*
002 * Copyright 2002-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 *      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.propertyeditors;
018
019import java.beans.PropertyEditorSupport;
020import java.util.LinkedHashMap;
021import java.util.Map;
022import java.util.SortedMap;
023import java.util.TreeMap;
024
025/**
026 * Property editor for Maps, converting any source Map
027 * to a given target Map type.
028 *
029 * @author Juergen Hoeller
030 * @since 2.0.1
031 * @see java.util.Map
032 * @see java.util.SortedMap
033 */
034public class CustomMapEditor extends PropertyEditorSupport {
035
036        @SuppressWarnings("rawtypes")
037        private final Class<? extends Map> mapType;
038
039        private final boolean nullAsEmptyMap;
040
041
042        /**
043         * Create a new CustomMapEditor for the given target type,
044         * keeping an incoming {@code null} as-is.
045         * @param mapType the target type, which needs to be a
046         * sub-interface of Map or a concrete Map class
047         * @see java.util.Map
048         * @see java.util.HashMap
049         * @see java.util.TreeMap
050         * @see java.util.LinkedHashMap
051         */
052        @SuppressWarnings("rawtypes")
053        public CustomMapEditor(Class<? extends Map> mapType) {
054                this(mapType, false);
055        }
056
057        /**
058         * Create a new CustomMapEditor for the given target type.
059         * <p>If the incoming value is of the given type, it will be used as-is.
060         * If it is a different Map type or an array, it will be converted
061         * to a default implementation of the given Map type.
062         * If the value is anything else, a target Map with that single
063         * value will be created.
064         * <p>The default Map implementations are: TreeMap for SortedMap,
065         * and LinkedHashMap for Map.
066         * @param mapType the target type, which needs to be a
067         * sub-interface of Map or a concrete Map class
068         * @param nullAsEmptyMap ap whether to convert an incoming {@code null}
069         * value to an empty Map (of the appropriate type)
070         * @see java.util.Map
071         * @see java.util.TreeMap
072         * @see java.util.LinkedHashMap
073         */
074        @SuppressWarnings("rawtypes")
075        public CustomMapEditor(Class<? extends Map> mapType, boolean nullAsEmptyMap) {
076                if (mapType == null) {
077                        throw new IllegalArgumentException("Map type is required");
078                }
079                if (!Map.class.isAssignableFrom(mapType)) {
080                        throw new IllegalArgumentException(
081                                        "Map type [" + mapType.getName() + "] does not implement [java.util.Map]");
082                }
083                this.mapType = mapType;
084                this.nullAsEmptyMap = nullAsEmptyMap;
085        }
086
087
088        /**
089         * Convert the given text value to a Map with a single element.
090         */
091        @Override
092        public void setAsText(String text) throws IllegalArgumentException {
093                setValue(text);
094        }
095
096        /**
097         * Convert the given value to a Map of the target type.
098         */
099        @Override
100        public void setValue(Object value) {
101                if (value == null && this.nullAsEmptyMap) {
102                        super.setValue(createMap(this.mapType, 0));
103                }
104                else if (value == null || (this.mapType.isInstance(value) && !alwaysCreateNewMap())) {
105                        // Use the source value as-is, as it matches the target type.
106                        super.setValue(value);
107                }
108                else if (value instanceof Map) {
109                        // Convert Map elements.
110                        Map<?, ?> source = (Map<?, ?>) value;
111                        Map<Object, Object> target = createMap(this.mapType, source.size());
112                        for (Map.Entry<?, ?> entry : source.entrySet()) {
113                                target.put(convertKey(entry.getKey()), convertValue(entry.getValue()));
114                        }
115                        super.setValue(target);
116                }
117                else {
118                        throw new IllegalArgumentException("Value cannot be converted to Map: " + value);
119                }
120        }
121
122        /**
123         * Create a Map of the given type, with the given
124         * initial capacity (if supported by the Map type).
125         * @param mapType a sub-interface of Map
126         * @param initialCapacity the initial capacity
127         * @return the new Map instance
128         */
129        @SuppressWarnings({ "rawtypes", "unchecked" })
130        protected Map<Object, Object> createMap(Class<? extends Map> mapType, int initialCapacity) {
131                if (!mapType.isInterface()) {
132                        try {
133                                return mapType.newInstance();
134                        }
135                        catch (Throwable ex) {
136                                throw new IllegalArgumentException(
137                                                "Could not instantiate map class: " + mapType.getName(), ex);
138                        }
139                }
140                else if (SortedMap.class == mapType) {
141                        return new TreeMap<Object, Object>();
142                }
143                else {
144                        return new LinkedHashMap<Object, Object>(initialCapacity);
145                }
146        }
147
148        /**
149         * Return whether to always create a new Map,
150         * even if the type of the passed-in Map already matches.
151         * <p>Default is "false"; can be overridden to enforce creation of a
152         * new Map, for example to convert elements in any case.
153         * @see #convertKey
154         * @see #convertValue
155         */
156        protected boolean alwaysCreateNewMap() {
157                return false;
158        }
159
160        /**
161         * Hook to convert each encountered Map key.
162         * The default implementation simply returns the passed-in key as-is.
163         * <p>Can be overridden to perform conversion of certain keys,
164         * for example from String to Integer.
165         * <p>Only called if actually creating a new Map!
166         * This is by default not the case if the type of the passed-in Map
167         * already matches. Override {@link #alwaysCreateNewMap()} to
168         * enforce creating a new Map in every case.
169         * @param key the source key
170         * @return the key to be used in the target Map
171         * @see #alwaysCreateNewMap
172         */
173        protected Object convertKey(Object key) {
174                return key;
175        }
176
177        /**
178         * Hook to convert each encountered Map value.
179         * The default implementation simply returns the passed-in value as-is.
180         * <p>Can be overridden to perform conversion of certain values,
181         * for example from String to Integer.
182         * <p>Only called if actually creating a new Map!
183         * This is by default not the case if the type of the passed-in Map
184         * already matches. Override {@link #alwaysCreateNewMap()} to
185         * enforce creating a new Map in every case.
186         * @param value the source value
187         * @return the value to be used in the target Map
188         * @see #alwaysCreateNewMap
189         */
190        protected Object convertValue(Object value) {
191                return value;
192        }
193
194
195        /**
196         * This implementation returns {@code null} to indicate that
197         * there is no appropriate text representation.
198         */
199        @Override
200        public String getAsText() {
201                return null;
202        }
203
204}