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.core;
018
019import java.util.ArrayList;
020import java.util.Collection;
021import java.util.EnumMap;
022import java.util.EnumSet;
023import java.util.HashMap;
024import java.util.HashSet;
025import java.util.LinkedHashMap;
026import java.util.LinkedHashSet;
027import java.util.LinkedList;
028import java.util.List;
029import java.util.Map;
030import java.util.NavigableMap;
031import java.util.NavigableSet;
032import java.util.Properties;
033import java.util.Set;
034import java.util.SortedMap;
035import java.util.SortedSet;
036import java.util.TreeMap;
037import java.util.TreeSet;
038
039import org.springframework.lang.Nullable;
040import org.springframework.util.Assert;
041import org.springframework.util.LinkedMultiValueMap;
042import org.springframework.util.MultiValueMap;
043import org.springframework.util.ReflectionUtils;
044
045/**
046 * Factory for collections that is aware of common Java and Spring collection types.
047 *
048 * <p>Mainly for internal use within the framework.
049 *
050 * @author Juergen Hoeller
051 * @author Arjen Poutsma
052 * @author Oliver Gierke
053 * @author Sam Brannen
054 * @since 1.1.1
055 */
056public final class CollectionFactory {
057
058        private static final Set<Class<?>> approximableCollectionTypes = new HashSet<>();
059
060        private static final Set<Class<?>> approximableMapTypes = new HashSet<>();
061
062
063        static {
064                // Standard collection interfaces
065                approximableCollectionTypes.add(Collection.class);
066                approximableCollectionTypes.add(List.class);
067                approximableCollectionTypes.add(Set.class);
068                approximableCollectionTypes.add(SortedSet.class);
069                approximableCollectionTypes.add(NavigableSet.class);
070                approximableMapTypes.add(Map.class);
071                approximableMapTypes.add(SortedMap.class);
072                approximableMapTypes.add(NavigableMap.class);
073
074                // Common concrete collection classes
075                approximableCollectionTypes.add(ArrayList.class);
076                approximableCollectionTypes.add(LinkedList.class);
077                approximableCollectionTypes.add(HashSet.class);
078                approximableCollectionTypes.add(LinkedHashSet.class);
079                approximableCollectionTypes.add(TreeSet.class);
080                approximableCollectionTypes.add(EnumSet.class);
081                approximableMapTypes.add(HashMap.class);
082                approximableMapTypes.add(LinkedHashMap.class);
083                approximableMapTypes.add(TreeMap.class);
084                approximableMapTypes.add(EnumMap.class);
085        }
086
087
088        private CollectionFactory() {
089        }
090
091
092        /**
093         * Determine whether the given collection type is an <em>approximable</em> type,
094         * i.e. a type that {@link #createApproximateCollection} can approximate.
095         * @param collectionType the collection type to check
096         * @return {@code true} if the type is <em>approximable</em>
097         */
098        public static boolean isApproximableCollectionType(@Nullable Class<?> collectionType) {
099                return (collectionType != null && approximableCollectionTypes.contains(collectionType));
100        }
101
102        /**
103         * Create the most approximate collection for the given collection.
104         * <p><strong>Warning</strong>: Since the parameterized type {@code E} is
105         * not bound to the type of elements contained in the supplied
106         * {@code collection}, type safety cannot be guaranteed if the supplied
107         * {@code collection} is an {@link EnumSet}. In such scenarios, the caller
108         * is responsible for ensuring that the element type for the supplied
109         * {@code collection} is an enum type matching type {@code E}. As an
110         * alternative, the caller may wish to treat the return value as a raw
111         * collection or collection of {@link Object}.
112         * @param collection the original collection object, potentially {@code null}
113         * @param capacity the initial capacity
114         * @return a new, empty collection instance
115         * @see #isApproximableCollectionType
116         * @see java.util.LinkedList
117         * @see java.util.ArrayList
118         * @see java.util.EnumSet
119         * @see java.util.TreeSet
120         * @see java.util.LinkedHashSet
121         */
122        @SuppressWarnings({"rawtypes", "unchecked", "cast"})
123        public static <E> Collection<E> createApproximateCollection(@Nullable Object collection, int capacity) {
124                if (collection instanceof LinkedList) {
125                        return new LinkedList<>();
126                }
127                else if (collection instanceof List) {
128                        return new ArrayList<>(capacity);
129                }
130                else if (collection instanceof EnumSet) {
131                        // Cast is necessary for compilation in Eclipse 4.4.1.
132                        Collection<E> enumSet = (Collection<E>) EnumSet.copyOf((EnumSet) collection);
133                        enumSet.clear();
134                        return enumSet;
135                }
136                else if (collection instanceof SortedSet) {
137                        return new TreeSet<>(((SortedSet<E>) collection).comparator());
138                }
139                else {
140                        return new LinkedHashSet<>(capacity);
141                }
142        }
143
144        /**
145         * Create the most appropriate collection for the given collection type.
146         * <p>Delegates to {@link #createCollection(Class, Class, int)} with a
147         * {@code null} element type.
148         * @param collectionType the desired type of the target collection (never {@code null})
149         * @param capacity the initial capacity
150         * @return a new collection instance
151         * @throws IllegalArgumentException if the supplied {@code collectionType}
152         * is {@code null} or of type {@link EnumSet}
153         */
154        public static <E> Collection<E> createCollection(Class<?> collectionType, int capacity) {
155                return createCollection(collectionType, null, capacity);
156        }
157
158        /**
159         * Create the most appropriate collection for the given collection type.
160         * <p><strong>Warning</strong>: Since the parameterized type {@code E} is
161         * not bound to the supplied {@code elementType}, type safety cannot be
162         * guaranteed if the desired {@code collectionType} is {@link EnumSet}.
163         * In such scenarios, the caller is responsible for ensuring that the
164         * supplied {@code elementType} is an enum type matching type {@code E}.
165         * As an alternative, the caller may wish to treat the return value as a
166         * raw collection or collection of {@link Object}.
167         * @param collectionType the desired type of the target collection (never {@code null})
168         * @param elementType the collection's element type, or {@code null} if unknown
169         * (note: only relevant for {@link EnumSet} creation)
170         * @param capacity the initial capacity
171         * @return a new collection instance
172         * @since 4.1.3
173         * @see java.util.LinkedHashSet
174         * @see java.util.ArrayList
175         * @see java.util.TreeSet
176         * @see java.util.EnumSet
177         * @throws IllegalArgumentException if the supplied {@code collectionType} is
178         * {@code null}; or if the desired {@code collectionType} is {@link EnumSet} and
179         * the supplied {@code elementType} is not a subtype of {@link Enum}
180         */
181        @SuppressWarnings({"unchecked", "cast"})
182        public static <E> Collection<E> createCollection(Class<?> collectionType, @Nullable Class<?> elementType, int capacity) {
183                Assert.notNull(collectionType, "Collection type must not be null");
184                if (collectionType.isInterface()) {
185                        if (Set.class == collectionType || Collection.class == collectionType) {
186                                return new LinkedHashSet<>(capacity);
187                        }
188                        else if (List.class == collectionType) {
189                                return new ArrayList<>(capacity);
190                        }
191                        else if (SortedSet.class == collectionType || NavigableSet.class == collectionType) {
192                                return new TreeSet<>();
193                        }
194                        else {
195                                throw new IllegalArgumentException("Unsupported Collection interface: " + collectionType.getName());
196                        }
197                }
198                else if (EnumSet.class.isAssignableFrom(collectionType)) {
199                        Assert.notNull(elementType, "Cannot create EnumSet for unknown element type");
200                        // Cast is necessary for compilation in Eclipse 4.4.1.
201                        return (Collection<E>) EnumSet.noneOf(asEnumType(elementType));
202                }
203                else {
204                        if (!Collection.class.isAssignableFrom(collectionType)) {
205                                throw new IllegalArgumentException("Unsupported Collection type: " + collectionType.getName());
206                        }
207                        try {
208                                return (Collection<E>) ReflectionUtils.accessibleConstructor(collectionType).newInstance();
209                        }
210                        catch (Throwable ex) {
211                                throw new IllegalArgumentException(
212                                        "Could not instantiate Collection type: " + collectionType.getName(), ex);
213                        }
214                }
215        }
216
217        /**
218         * Determine whether the given map type is an <em>approximable</em> type,
219         * i.e. a type that {@link #createApproximateMap} can approximate.
220         * @param mapType the map type to check
221         * @return {@code true} if the type is <em>approximable</em>
222         */
223        public static boolean isApproximableMapType(@Nullable Class<?> mapType) {
224                return (mapType != null && approximableMapTypes.contains(mapType));
225        }
226
227        /**
228         * Create the most approximate map for the given map.
229         * <p><strong>Warning</strong>: Since the parameterized type {@code K} is
230         * not bound to the type of keys contained in the supplied {@code map},
231         * type safety cannot be guaranteed if the supplied {@code map} is an
232         * {@link EnumMap}. In such scenarios, the caller is responsible for
233         * ensuring that the key type in the supplied {@code map} is an enum type
234         * matching type {@code K}. As an alternative, the caller may wish to
235         * treat the return value as a raw map or map keyed by {@link Object}.
236         * @param map the original map object, potentially {@code null}
237         * @param capacity the initial capacity
238         * @return a new, empty map instance
239         * @see #isApproximableMapType
240         * @see java.util.EnumMap
241         * @see java.util.TreeMap
242         * @see java.util.LinkedHashMap
243         */
244        @SuppressWarnings({"rawtypes", "unchecked"})
245        public static <K, V> Map<K, V> createApproximateMap(@Nullable Object map, int capacity) {
246                if (map instanceof EnumMap) {
247                        EnumMap enumMap = new EnumMap((EnumMap) map);
248                        enumMap.clear();
249                        return enumMap;
250                }
251                else if (map instanceof SortedMap) {
252                        return new TreeMap<>(((SortedMap<K, V>) map).comparator());
253                }
254                else {
255                        return new LinkedHashMap<>(capacity);
256                }
257        }
258
259        /**
260         * Create the most appropriate map for the given map type.
261         * <p>Delegates to {@link #createMap(Class, Class, int)} with a
262         * {@code null} key type.
263         * @param mapType the desired type of the target map
264         * @param capacity the initial capacity
265         * @return a new map instance
266         * @throws IllegalArgumentException if the supplied {@code mapType} is
267         * {@code null} or of type {@link EnumMap}
268         */
269        public static <K, V> Map<K, V> createMap(Class<?> mapType, int capacity) {
270                return createMap(mapType, null, capacity);
271        }
272
273        /**
274         * Create the most appropriate map for the given map type.
275         * <p><strong>Warning</strong>: Since the parameterized type {@code K}
276         * is not bound to the supplied {@code keyType}, type safety cannot be
277         * guaranteed if the desired {@code mapType} is {@link EnumMap}. In such
278         * scenarios, the caller is responsible for ensuring that the {@code keyType}
279         * is an enum type matching type {@code K}. As an alternative, the caller
280         * may wish to treat the return value as a raw map or map keyed by
281         * {@link Object}. Similarly, type safety cannot be enforced if the
282         * desired {@code mapType} is {@link MultiValueMap}.
283         * @param mapType the desired type of the target map (never {@code null})
284         * @param keyType the map's key type, or {@code null} if unknown
285         * (note: only relevant for {@link EnumMap} creation)
286         * @param capacity the initial capacity
287         * @return a new map instance
288         * @since 4.1.3
289         * @see java.util.LinkedHashMap
290         * @see java.util.TreeMap
291         * @see org.springframework.util.LinkedMultiValueMap
292         * @see java.util.EnumMap
293         * @throws IllegalArgumentException if the supplied {@code mapType} is
294         * {@code null}; or if the desired {@code mapType} is {@link EnumMap} and
295         * the supplied {@code keyType} is not a subtype of {@link Enum}
296         */
297        @SuppressWarnings({"rawtypes", "unchecked"})
298        public static <K, V> Map<K, V> createMap(Class<?> mapType, @Nullable Class<?> keyType, int capacity) {
299                Assert.notNull(mapType, "Map type must not be null");
300                if (mapType.isInterface()) {
301                        if (Map.class == mapType) {
302                                return new LinkedHashMap<>(capacity);
303                        }
304                        else if (SortedMap.class == mapType || NavigableMap.class == mapType) {
305                                return new TreeMap<>();
306                        }
307                        else if (MultiValueMap.class == mapType) {
308                                return new LinkedMultiValueMap();
309                        }
310                        else {
311                                throw new IllegalArgumentException("Unsupported Map interface: " + mapType.getName());
312                        }
313                }
314                else if (EnumMap.class == mapType) {
315                        Assert.notNull(keyType, "Cannot create EnumMap for unknown key type");
316                        return new EnumMap(asEnumType(keyType));
317                }
318                else {
319                        if (!Map.class.isAssignableFrom(mapType)) {
320                                throw new IllegalArgumentException("Unsupported Map type: " + mapType.getName());
321                        }
322                        try {
323                                return (Map<K, V>) ReflectionUtils.accessibleConstructor(mapType).newInstance();
324                        }
325                        catch (Throwable ex) {
326                                throw new IllegalArgumentException("Could not instantiate Map type: " + mapType.getName(), ex);
327                        }
328                }
329        }
330
331        /**
332         * Create a variant of {@link java.util.Properties} that automatically adapts
333         * non-String values to String representations in {@link Properties#getProperty}.
334         * <p>In addition, the returned {@code Properties} instance sorts properties
335         * alphanumerically based on their keys.
336         * @return a new {@code Properties} instance
337         * @since 4.3.4
338         * @see #createSortedProperties(boolean)
339         * @see #createSortedProperties(Properties, boolean)
340         */
341        @SuppressWarnings("serial")
342        public static Properties createStringAdaptingProperties() {
343                return new SortedProperties(false) {
344                        @Override
345                        @Nullable
346                        public String getProperty(String key) {
347                                Object value = get(key);
348                                return (value != null ? value.toString() : null);
349                        }
350                };
351        }
352
353        /**
354         * Create a variant of {@link java.util.Properties} that sorts properties
355         * alphanumerically based on their keys.
356         * <p>This can be useful when storing the {@link Properties} instance in a
357         * properties file, since it allows such files to be generated in a repeatable
358         * manner with consistent ordering of properties. Comments in generated
359         * properties files can also be optionally omitted.
360         * @param omitComments {@code true} if comments should be omitted when
361         * storing properties in a file
362         * @return a new {@code Properties} instance
363         * @since 5.2
364         * @see #createStringAdaptingProperties()
365         * @see #createSortedProperties(Properties, boolean)
366         */
367        public static Properties createSortedProperties(boolean omitComments) {
368                return new SortedProperties(omitComments);
369        }
370
371        /**
372         * Create a variant of {@link java.util.Properties} that sorts properties
373         * alphanumerically based on their keys.
374         * <p>This can be useful when storing the {@code Properties} instance in a
375         * properties file, since it allows such files to be generated in a repeatable
376         * manner with consistent ordering of properties. Comments in generated
377         * properties files can also be optionally omitted.
378         * <p>The returned {@code Properties} instance will be populated with
379         * properties from the supplied {@code properties} object, but default
380         * properties from the supplied {@code properties} object will not be copied.
381         * @param properties the {@code Properties} object from which to copy the
382         * initial properties
383         * @param omitComments {@code true} if comments should be omitted when
384         * storing properties in a file
385         * @return a new {@code Properties} instance
386         * @since 5.2
387         * @see #createStringAdaptingProperties()
388         * @see #createSortedProperties(boolean)
389         */
390        public static Properties createSortedProperties(Properties properties, boolean omitComments) {
391                return new SortedProperties(properties, omitComments);
392        }
393
394        /**
395         * Cast the given type to a subtype of {@link Enum}.
396         * @param enumType the enum type, never {@code null}
397         * @return the given type as subtype of {@link Enum}
398         * @throws IllegalArgumentException if the given type is not a subtype of {@link Enum}
399         */
400        @SuppressWarnings("rawtypes")
401        private static Class<? extends Enum> asEnumType(Class<?> enumType) {
402                Assert.notNull(enumType, "Enum type must not be null");
403                if (!Enum.class.isAssignableFrom(enumType)) {
404                        throw new IllegalArgumentException("Supplied type is not an enum: " + enumType.getName());
405                }
406                return enumType.asSubclass(Enum.class);
407        }
408
409}