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</code>.
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 * Null keys are supported.
033 * <p>
034 * The <code>keySet()</code> method returns all lowercase keys, or nulls.
035 * <p>
036 * Example:
037 * <pre><code>
038 *  Map&lt;String, String&gt; map = new CaseInsensitiveMap&lt;String, String&gt;();
039 *  map.put("One", "One");
040 *  map.put("Two", "Two");
041 *  map.put(null, "Three");
042 *  map.put("one", "Four");
043 * </code></pre>
044 * creates a <code>CaseInsensitiveMap</code> with three entries.<br>
045 * <code>map.get(null)</code> returns <code>"Three"</code> and <code>map.get("ONE")</code>
046 * returns <code>"Four".</code>  The <code>Set</code> returned by <code>keySet()</code>
047 * equals <code>{"one", "two", null}.</code>
048 * <p>
049 * <strong>This map will violate the detail of various Map and map view contracts.</note>
050 * As a general rule, don't compare this map to other maps. In particular, you can't
051 * use decorators like {@link ListOrderedMap} on it, which silently assume that these
052 * contracts are fulfilled.
053 * <p>
054 * <strong>Note that CaseInsensitiveMap is not synchronized and is not thread-safe.</strong>
055 * If you wish to use this map from multiple threads concurrently, you must use
056 * appropriate synchronization. The simplest approach is to wrap this map
057 * using {@link java.util.Collections#synchronizedMap(Map)}. This class may throw
058 * exceptions when accessed by concurrent threads without synchronization.
059 *
060 * @since 3.0
061 * @version $Id: CaseInsensitiveMap.html 972421 2015-11-14 20:00:04Z tn $
062 */
063public class CaseInsensitiveMap<K, V> extends AbstractHashedMap<K, V> implements Serializable, Cloneable {
064
065    /** Serialisation version */
066    private static final long serialVersionUID = -7074655917369299456L;
067
068    /**
069     * Constructs a new empty map with default size and load factor.
070     */
071    public CaseInsensitiveMap() {
072        super(DEFAULT_CAPACITY, DEFAULT_LOAD_FACTOR, DEFAULT_THRESHOLD);
073    }
074
075    /**
076     * Constructs a new, empty map with the specified initial capacity.
077     *
078     * @param initialCapacity  the initial capacity
079     * @throws IllegalArgumentException if the initial capacity is negative
080     */
081    public CaseInsensitiveMap(final int initialCapacity) {
082        super(initialCapacity);
083    }
084
085    /**
086     * Constructs a new, empty map with the specified initial capacity and
087     * load factor.
088     *
089     * @param initialCapacity  the initial capacity
090     * @param loadFactor  the load factor
091     * @throws IllegalArgumentException if the initial capacity is negative
092     * @throws IllegalArgumentException if the load factor is less than zero
093     */
094    public CaseInsensitiveMap(final int initialCapacity, final float loadFactor) {
095        super(initialCapacity, loadFactor);
096    }
097
098    /**
099     * Constructor copying elements from another map.
100     * <p>
101     * Keys will be converted to lower case strings, which may cause
102     * some entries to be removed (if string representation of keys differ
103     * only by character case).
104     *
105     * @param map  the map to copy
106     * @throws NullPointerException if the map is null
107     */
108    public CaseInsensitiveMap(final Map<? extends K, ? extends V> map) {
109        super(map);
110    }
111
112    //-----------------------------------------------------------------------
113    /**
114     * Overrides convertKey() from {@link AbstractHashedMap} to convert keys to
115     * lower case.
116     * <p>
117     * Returns {@link AbstractHashedMap#NULL} if key is null.
118     *
119     * @param key  the key convert
120     * @return the converted key
121     */
122    @Override
123    protected Object convertKey(final Object key) {
124        if (key != null) {
125            final char[] chars = key.toString().toCharArray();
126            for (int i = chars.length - 1; i >= 0; i--) {
127                chars[i] = Character.toLowerCase(Character.toUpperCase(chars[i]));
128            }
129            return new String(chars);
130        }
131        return AbstractHashedMap.NULL;
132    }
133
134    //-----------------------------------------------------------------------
135    /**
136     * Clones the map without cloning the keys or values.
137     *
138     * @return a shallow clone
139     */
140    @Override
141    public CaseInsensitiveMap<K, V> clone() {
142        return (CaseInsensitiveMap<K, V>) super.clone();
143    }
144
145    /**
146     * Write the map out using a custom routine.
147     */
148    private void writeObject(final ObjectOutputStream out) throws IOException {
149        out.defaultWriteObject();
150        doWriteObject(out);
151    }
152
153    /**
154     * Read the map in using a custom routine.
155     */
156    private void readObject(final ObjectInputStream in) throws IOException, ClassNotFoundException {
157        in.defaultReadObject();
158        doReadObject(in);
159    }
160
161}