001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.commons.collections4.map;
018
019import java.io.IOException;
020import java.io.ObjectInputStream;
021import java.io.ObjectOutputStream;
022import java.io.Serializable;
023import java.util.Map;
024
025/**
026 * A case-insensitive {@code Map}.
027 * <p>
028 * Before keys are added to the map or compared to other existing keys, they are converted
029 * to all lowercase in a locale-independent fashion by using information from the Unicode
030 * data file.
031 * </p>
032 * <p>
033 * Null keys are supported.
034 * </p>
035 * <p>
036 * The {@code keySet()} method returns all lowercase keys, or nulls.
037 * </p>
038 * <p>
039 * Example:
040 * </p>
041 * <pre><code>
042 *  Map&lt;String, String&gt; map = new CaseInsensitiveMap&lt;String, String&gt;();
043 *  map.put("One", "One");
044 *  map.put("Two", "Two");
045 *  map.put(null, "Three");
046 *  map.put("one", "Four");
047 * </code></pre>
048 * <p>
049 * The example above creates a {@code CaseInsensitiveMap} with three entries.
050 * </p>
051 * <p>
052 * {@code map.get(null)} returns {@code "Three"} and {@code map.get("ONE")}
053 * returns {@code "Four".}  The {@code Set} returned by {@code keySet()}
054 * equals {@code {"one", "two", null}.}
055 * </p>
056 * <p>
057 * <strong>This map will violate the detail of various Map and map view contracts.</strong>
058 * As a general rule, don't compare this map to other maps. In particular, you can't
059 * use decorators like {@link ListOrderedMap} on it, which silently assume that these
060 * contracts are fulfilled.
061 * </p>
062 * <p>
063 * <strong>Note that CaseInsensitiveMap is not synchronized and is not thread-safe.</strong>
064 * If you wish to use this map from multiple threads concurrently, you must use
065 * appropriate synchronization. The simplest approach is to wrap this map
066 * using {@link java.util.Collections#synchronizedMap(Map)}. This class may throw
067 * exceptions when accessed by concurrent threads without synchronization.
068 * </p>
069 *
070 * @param <K> the type of the keys in this map
071 * @param <V> the type of the values in this map
072 * @since 3.0
073 */
074public class CaseInsensitiveMap<K, V> extends AbstractHashedMap<K, V> implements Serializable, Cloneable {
075
076    /** Serialisation version */
077    private static final long serialVersionUID = -7074655917369299456L;
078
079    /**
080     * Constructs a new empty map with default size and load factor.
081     */
082    public CaseInsensitiveMap() {
083        super(DEFAULT_CAPACITY, DEFAULT_LOAD_FACTOR, DEFAULT_THRESHOLD);
084    }
085
086    /**
087     * Constructs a new, empty map with the specified initial capacity.
088     *
089     * @param initialCapacity  the initial capacity
090     * @throws IllegalArgumentException if the initial capacity is negative
091     */
092    public CaseInsensitiveMap(final int initialCapacity) {
093        super(initialCapacity);
094    }
095
096    /**
097     * Constructs a new, empty map with the specified initial capacity and
098     * load factor.
099     *
100     * @param initialCapacity  the initial capacity
101     * @param loadFactor  the load factor
102     * @throws IllegalArgumentException if the initial capacity is negative
103     * @throws IllegalArgumentException if the load factor is less than zero
104     */
105    public CaseInsensitiveMap(final int initialCapacity, final float loadFactor) {
106        super(initialCapacity, loadFactor);
107    }
108
109    /**
110     * Constructor copying elements from another map.
111     * <p>
112     * Keys will be converted to lower case strings, which may cause
113     * some entries to be removed (if string representation of keys differ
114     * only by character case).
115     *
116     * @param map  the map to copy
117     * @throws NullPointerException if the map is null
118     */
119    public CaseInsensitiveMap(final Map<? extends K, ? extends V> map) {
120        super(map);
121    }
122
123    /**
124     * Clones the map without cloning the keys or values.
125     *
126     * @return a shallow clone
127     */
128    @Override
129    public CaseInsensitiveMap<K, V> clone() {
130        return (CaseInsensitiveMap<K, V>) super.clone();
131    }
132
133    /**
134     * Overrides convertKey() from {@link AbstractHashedMap} to convert keys to
135     * lower case.
136     * <p>
137     * Returns {@link AbstractHashedMap#NULL} if key is null.
138     *
139     * @param key  the key convert
140     * @return the converted key
141     */
142    @Override
143    protected Object convertKey(final Object key) {
144        if (key != null) {
145            final char[] chars = key.toString().toCharArray();
146            for (int i = chars.length - 1; i >= 0; i--) {
147                chars[i] = Character.toLowerCase(Character.toUpperCase(chars[i]));
148            }
149            return new String(chars);
150        }
151        return AbstractHashedMap.NULL;
152    }
153
154    /**
155     * Read the map in using a custom routine.
156     *
157     * @param in the input stream
158     * @throws IOException if an error occurs while reading from the stream
159     * @throws ClassNotFoundException if an object read from the stream can not be loaded
160     */
161    private void readObject(final ObjectInputStream in) throws IOException, ClassNotFoundException {
162        in.defaultReadObject();
163        doReadObject(in);
164    }
165
166    /**
167     * Write the map out using a custom routine.
168     *
169     * @param out  the output stream
170     * @throws IOException if an error occurs while writing to the stream
171     */
172    private void writeObject(final ObjectOutputStream out) throws IOException {
173        out.defaultWriteObject();
174        doWriteObject(out);
175    }
176
177}