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<String, String> map = new CaseInsensitiveMap<String, String>(); 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.</strong> 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 * <p> 055 * <strong>Note that CaseInsensitiveMap is not synchronized and is not thread-safe.</strong> 056 * If you wish to use this map from multiple threads concurrently, you must use 057 * appropriate synchronization. The simplest approach is to wrap this map 058 * using {@link java.util.Collections#synchronizedMap(Map)}. This class may throw 059 * exceptions when accessed by concurrent threads without synchronization. 060 * </p> 061 * 062 * @param <K> the type of the keys in this map 063 * @param <V> the type of the values in this map 064 * @since 3.0 065 */ 066public class CaseInsensitiveMap<K, V> extends AbstractHashedMap<K, V> implements Serializable, Cloneable { 067 068 /** Serialisation version */ 069 private static final long serialVersionUID = -7074655917369299456L; 070 071 /** 072 * Constructs a new empty map with default size and load factor. 073 */ 074 public CaseInsensitiveMap() { 075 super(DEFAULT_CAPACITY, DEFAULT_LOAD_FACTOR, DEFAULT_THRESHOLD); 076 } 077 078 /** 079 * Constructs a new, empty map with the specified initial capacity. 080 * 081 * @param initialCapacity the initial capacity 082 * @throws IllegalArgumentException if the initial capacity is negative 083 */ 084 public CaseInsensitiveMap(final int initialCapacity) { 085 super(initialCapacity); 086 } 087 088 /** 089 * Constructs a new, empty map with the specified initial capacity and 090 * load factor. 091 * 092 * @param initialCapacity the initial capacity 093 * @param loadFactor the load factor 094 * @throws IllegalArgumentException if the initial capacity is negative 095 * @throws IllegalArgumentException if the load factor is less than zero 096 */ 097 public CaseInsensitiveMap(final int initialCapacity, final float loadFactor) { 098 super(initialCapacity, loadFactor); 099 } 100 101 /** 102 * Constructor copying elements from another map. 103 * <p> 104 * Keys will be converted to lower case strings, which may cause 105 * some entries to be removed (if string representation of keys differ 106 * only by character case). 107 * 108 * @param map the map to copy 109 * @throws NullPointerException if the map is null 110 */ 111 public CaseInsensitiveMap(final Map<? extends K, ? extends V> map) { 112 super(map); 113 } 114 115 //----------------------------------------------------------------------- 116 /** 117 * Overrides convertKey() from {@link AbstractHashedMap} to convert keys to 118 * lower case. 119 * <p> 120 * Returns {@link AbstractHashedMap#NULL} if key is null. 121 * 122 * @param key the key convert 123 * @return the converted key 124 */ 125 @Override 126 protected Object convertKey(final Object key) { 127 if (key != null) { 128 final char[] chars = key.toString().toCharArray(); 129 for (int i = chars.length - 1; i >= 0; i--) { 130 chars[i] = Character.toLowerCase(Character.toUpperCase(chars[i])); 131 } 132 return new String(chars); 133 } 134 return AbstractHashedMap.NULL; 135 } 136 137 //----------------------------------------------------------------------- 138 /** 139 * Clones the map without cloning the keys or values. 140 * 141 * @return a shallow clone 142 */ 143 @Override 144 public CaseInsensitiveMap<K, V> clone() { 145 return (CaseInsensitiveMap<K, V>) super.clone(); 146 } 147 148 /** 149 * Write the map out using a custom routine. 150 * 151 * @param out the output stream 152 * @throws IOException if an error occurs while writing to the stream 153 */ 154 private void writeObject(final ObjectOutputStream out) throws IOException { 155 out.defaultWriteObject(); 156 doWriteObject(out); 157 } 158 159 /** 160 * Read the map in using a custom routine. 161 * 162 * @param in the input stream 163 * @throws IOException if an error occurs while reading from the stream 164 * @throws ClassNotFoundException if an object read from the stream can not be loaded 165 */ 166 private void readObject(final ObjectInputStream in) throws IOException, ClassNotFoundException { 167 in.defaultReadObject(); 168 doReadObject(in); 169 } 170 171}