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}