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