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