001/*
002 * Copyright 2002-2019 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
025import org.springframework.lang.Nullable;
026import org.springframework.util.Assert;
027import org.springframework.util.ReflectionUtils;
028
029/**
030 * Property editor for Maps, converting any source Map
031 * to a given target Map type.
032 *
033 * @author Juergen Hoeller
034 * @since 2.0.1
035 * @see java.util.Map
036 * @see java.util.SortedMap
037 */
038public class CustomMapEditor extends PropertyEditorSupport {
039
040        @SuppressWarnings("rawtypes")
041        private final Class<? extends Map> mapType;
042
043        private final boolean nullAsEmptyMap;
044
045
046        /**
047         * Create a new CustomMapEditor for the given target type,
048         * keeping an incoming {@code null} as-is.
049         * @param mapType the target type, which needs to be a
050         * sub-interface of Map or a concrete Map class
051         * @see java.util.Map
052         * @see java.util.HashMap
053         * @see java.util.TreeMap
054         * @see java.util.LinkedHashMap
055         */
056        @SuppressWarnings("rawtypes")
057        public CustomMapEditor(Class<? extends Map> mapType) {
058                this(mapType, false);
059        }
060
061        /**
062         * Create a new CustomMapEditor for the given target type.
063         * <p>If the incoming value is of the given type, it will be used as-is.
064         * If it is a different Map type or an array, it will be converted
065         * to a default implementation of the given Map type.
066         * If the value is anything else, a target Map with that single
067         * value will be created.
068         * <p>The default Map implementations are: TreeMap for SortedMap,
069         * and LinkedHashMap for Map.
070         * @param mapType the target type, which needs to be a
071         * sub-interface of Map or a concrete Map class
072         * @param nullAsEmptyMap ap whether to convert an incoming {@code null}
073         * value to an empty Map (of the appropriate type)
074         * @see java.util.Map
075         * @see java.util.TreeMap
076         * @see java.util.LinkedHashMap
077         */
078        @SuppressWarnings("rawtypes")
079        public CustomMapEditor(Class<? extends Map> mapType, boolean nullAsEmptyMap) {
080                Assert.notNull(mapType, "Map type is required");
081                if (!Map.class.isAssignableFrom(mapType)) {
082                        throw new IllegalArgumentException(
083                                        "Map type [" + mapType.getName() + "] does not implement [java.util.Map]");
084                }
085                this.mapType = mapType;
086                this.nullAsEmptyMap = nullAsEmptyMap;
087        }
088
089
090        /**
091         * Convert the given text value to a Map with a single element.
092         */
093        @Override
094        public void setAsText(String text) throws IllegalArgumentException {
095                setValue(text);
096        }
097
098        /**
099         * Convert the given value to a Map of the target type.
100         */
101        @Override
102        public void setValue(@Nullable Object value) {
103                if (value == null && this.nullAsEmptyMap) {
104                        super.setValue(createMap(this.mapType, 0));
105                }
106                else if (value == null || (this.mapType.isInstance(value) && !alwaysCreateNewMap())) {
107                        // Use the source value as-is, as it matches the target type.
108                        super.setValue(value);
109                }
110                else if (value instanceof Map) {
111                        // Convert Map elements.
112                        Map<?, ?> source = (Map<?, ?>) value;
113                        Map<Object, Object> target = createMap(this.mapType, source.size());
114                        source.forEach((key, val) -> target.put(convertKey(key), convertValue(val)));
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 ReflectionUtils.accessibleConstructor(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<>();
142                }
143                else {
144                        return new LinkedHashMap<>(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        @Nullable
201        public String getAsText() {
202                return null;
203        }
204
205}