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}