com.cedarsoftware.util.CaseInsensitiveMap Maven / Gradle / Ivy
package com.cedarsoftware.util;
import java.util.AbstractMap;
import java.util.AbstractSet;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.TreeMap;
import java.util.WeakHashMap;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.ConcurrentSkipListMap;
/**
* Useful Map that does not care about the case-sensitivity of keys
* when the key value is a String. Other key types can be used.
* String keys will be treated case insensitively, yet key case will
* be retained. Non-string keys will work as they normally would.
*
* The internal CaseInsensitiveString is never exposed externally
* from this class. When requesting the keys or entries of this map,
* or calling containsKey() or get() for example, use a String as you
* normally would. The returned Set of keys for the keySet() and
* entrySet() APIs return the original Strings, not the internally
* wrapped CaseInsensitiveString.
*
* As an added benefit, .keySet() returns a case-insensitive
* Set, however, again, the contents of the entries are actual Strings.
* Similarly, .entrySet() returns a case-insensitive entry set, such that
* .getKey() on the entry is case-insensitive when compared, but the
* returned key is a String.
*
* @author John DeRegnaucourt ([email protected])
*
* Copyright (c) Cedar Software LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* License
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
public class CaseInsensitiveMap implements Map
{
private final Map map;
public CaseInsensitiveMap()
{
map = new LinkedHashMap<>();
}
/**
* Use the constructor that takes two (2) Maps. The first Map may/may not contain any items to add,
* and the second Map is an empty Map configured the way you want it to be (load factor, capacity)
* and the type of Map you want. This Map is used by CaseInsenstiveMap internally to store entries.
*/
public CaseInsensitiveMap(int initialCapacity)
{
map = new LinkedHashMap<>(initialCapacity);
}
/**
* Use the constructor that takes two (2) Maps. The first Map may/may not contain any items to add,
* and the second Map is an empty Map configured the way you want it to be (load factor, capacity)
* and the type of Map you want. This Map is used by CaseInsenstiveMap internally to store entries.
*/
public CaseInsensitiveMap(int initialCapacity, float loadFactor)
{
map = new LinkedHashMap<>(initialCapacity, loadFactor);
}
/**
* Wrap the passed in Map with a CaseInsensitiveMap, allowing other Map types like
* TreeMap, ConcurrentHashMap, etc. to be case insensitive. The caller supplies
* the actual Map instance that will back the CaseInsensitiveMap.;
* @param source existing Map to supply the entries.
* @param mapInstance empty new Map to use. This lets you decide what Map to use to back the CaseInsensitiveMap.
*/
public CaseInsensitiveMap(Map source, Map mapInstance)
{
map = copy(source, mapInstance);
}
/**
* Wrap the passed in Map with a CaseInsensitiveMap, allowing other Map types like
* TreeMap, ConcurrentHashMap, etc. to be case insensitive.
* @param m Map to wrap.
*/
public CaseInsensitiveMap(Map m)
{
if (m instanceof TreeMap)
{
map = copy(m, new TreeMap<>());
}
else if (m instanceof LinkedHashMap)
{
map = copy(m, new LinkedHashMap<>(m.size()));
}
else if (m instanceof ConcurrentSkipListMap)
{
map = copy(m, new ConcurrentSkipListMap<>());
}
else if (m instanceof ConcurrentMap)
{
map = copy(m, new ConcurrentHashMap<>(m.size()));
}
else if (m instanceof WeakHashMap)
{
map = copy(m, new WeakHashMap<>(m.size()));
}
else if (m instanceof HashMap)
{
map = copy(m, new HashMap<>(m.size()));
}
else
{
map = copy(m, new LinkedHashMap<>(m.size()));
}
}
@SuppressWarnings("unchecked")
protected Map copy(Map source, Map dest)
{
for (Entry entry : source.entrySet())
{
// Get key from Entry, leaving it in it's original state (in case the key is a CaseInsensitiveString)
Object key;
if (isCaseInsenstiveEntry(entry))
{
key = ((CaseInsensitiveEntry)entry).getOriginalKey();
}
else
{
key = entry.getKey();
}
// Wrap any String keys with a CaseInsensitiveString. Keys that were already CaseInsensitiveStrings will
// remain as such.
K altKey;
if (key instanceof String)
{
altKey = (K) new CaseInsensitiveString((String)key);
}
else
{
altKey = (K)key;
}
dest.put(altKey, entry.getValue());
}
return dest;
}
private boolean isCaseInsenstiveEntry(Object o)
{
return CaseInsensitiveEntry.class.isInstance(o);
}
public V get(Object key)
{
if (key instanceof String)
{
String keyString = (String) key;
return map.get(new CaseInsensitiveString(keyString));
}
return map.get(key);
}
public boolean containsKey(Object key)
{
if (key instanceof String)
{
String keyString = (String) key;
return map.containsKey(new CaseInsensitiveString(keyString));
}
return map.containsKey(key);
}
@SuppressWarnings("unchecked")
public V put(K key, V value)
{
if (key instanceof String)
{
final CaseInsensitiveString newKey = new CaseInsensitiveString((String) key);
return map.put((K) newKey, value);
}
return map.put(key, value);
}
@SuppressWarnings("unchecked")
public Object putObject(Object key, Object value)
{ // not calling put() to save a little speed.
if (key instanceof String)
{
final CaseInsensitiveString newKey = new CaseInsensitiveString((String) key);
return map.put((K) newKey, (V)value);
}
return map.put((K)key, (V)value);
}
@SuppressWarnings("unchecked")
public void putAll(Map extends K, ? extends V> m)
{
if (MapUtilities.isEmpty(m))
{
return;
}
for (Entry extends K, ? extends V> entry : m.entrySet())
{
if (isCaseInsenstiveEntry(entry))
{
CaseInsensitiveEntry ciEntry = (CaseInsensitiveEntry) entry;
put(ciEntry.getOriginalKey(), entry.getValue());
}
else
{
put(entry.getKey(), entry.getValue());
}
}
}
public V remove(Object key)
{
if (key instanceof String)
{
String keyString = (String) key;
return map.remove(new CaseInsensitiveString(keyString));
}
return map.remove(key);
}
// delegates
public int size()
{
return map.size();
}
public boolean isEmpty()
{
return map.isEmpty();
}
public boolean equals(Object other)
{
if (other == this) return true;
if (!(other instanceof Map)) return false;
Map, ?> that = (Map, ?>) other;
if (that.size() != size())
{
return false;
}
for (Entry, ?> entry : that.entrySet())
{
final Object thatKey = entry.getKey();
if (!containsKey(thatKey))
{
return false;
}
Object thatValue = entry.getValue();
Object thisValue = get(thatKey);
if (!Objects.equals(thisValue, thatValue))
{
return false;
}
}
return true;
}
public int hashCode()
{
int h = 0;
for (Entry entry : map.entrySet())
{
Object key = entry.getKey();
int hKey = key == null ? 0 : key.hashCode();
Object value = entry.getValue();
int hValue = value == null ? 0 : value.hashCode();
h += hKey ^ hValue;
}
return h;
}
public String toString()
{
return map.toString();
}
public void clear()
{
map.clear();
}
public boolean containsValue(Object value)
{
return map.containsValue(value);
}
public Collection values()
{
return map.values();
}
@Deprecated
public Map minus(Object removeMe)
{
throw new UnsupportedOperationException("Unsupported operation [minus] or [-] between Maps. Use removeAll() or retainAll() instead.");
}
@Deprecated
public Map plus(Object right)
{
throw new UnsupportedOperationException("Unsupported operation [plus] or [+] between Maps. Use putAll() instead.");
}
public Map getWrappedMap()
{
return map;
}
/**
* Returns a Set view of the keys contained in this map.
* The set is backed by the map, so changes to the map are
* reflected in the set, and vice-versa. If the map is modified
* while an iteration over the set is in progress (except through
* the iterator's own remove operation), the results of
* the iteration are undefined. The set supports element removal,
* which removes the corresponding mapping from the map, via the
* Iterator.remove, Set.remove,
* removeAll, retainAll, and clear
* operations. It does not support the add or addAll
* operations.
*/
public Set keySet()
{
return new AbstractSet()
{
Iterator iter;
public boolean contains(Object o) { return CaseInsensitiveMap.this.containsKey(o); }
public boolean remove(Object o)
{
final int size = map.size();
CaseInsensitiveMap.this.remove(o);
return map.size() != size;
}
public boolean removeAll(Collection c)
{
int size = map.size();
for (Object o : c)
{
CaseInsensitiveMap.this.remove(o);
}
return map.size() != size;
}
@SuppressWarnings("unchecked")
public boolean retainAll(Collection> c)
{
Map other = new CaseInsensitiveMap<>();
for (Object o : c)
{
other.put((K)o, null);
}
final int size = map.size();
Iterator i = map.keySet().iterator();
while (i.hasNext())
{
K key = i.next();
if (!other.containsKey(key))
{
i.remove();
}
}
return map.size() != size;
}
public Object[] toArray()
{
Object[] items = new Object[size()];
int i = 0;
for (Object key : map.keySet())
{
items[i++] = key instanceof CaseInsensitiveString ? key.toString() : key;
}
return items;
}
public int size() { return map.size(); }
public void clear() { map.clear(); }
public int hashCode()
{
int h = 0;
// Use map.keySet() so that we walk through the CaseInsensitiveStrings generating a hashCode
// that is based on the lowerCase() value of the Strings (hashCode() on the CaseInsensitiveStrings
// with map.keySet() will return the hashCode of .toLowerCase() of those strings).
for (Object key : map.keySet())
{
if (key != null)
{
h += key.hashCode();
}
}
return h;
}
@SuppressWarnings("unchecked")
public Iterator iterator()
{
iter = map.keySet().iterator();
return new Iterator()
{
public void remove() { iter.remove(); }
public boolean hasNext() { return iter.hasNext(); }
public K next()
{
Object next = iter.next();
if (next instanceof CaseInsensitiveString)
{
next = next.toString();
}
return (K) next;
}
};
}
};
}
public Set> entrySet()
{
return new AbstractSet>()
{
Iterator> iter;
public int size() { return map.size(); }
public boolean isEmpty() { return map.isEmpty(); }
public void clear() { map.clear(); }
@SuppressWarnings("unchecked")
public boolean contains(Object o)
{
if (!(o instanceof Entry))
{
return false;
}
Entry that = (Entry) o;
if (CaseInsensitiveMap.this.containsKey(that.getKey()))
{
Object value = CaseInsensitiveMap.this.get(that.getKey());
return Objects.equals(value, that.getValue());
}
return false;
}
@SuppressWarnings("unchecked")
public boolean remove(Object o)
{
if (!(o instanceof Entry))
{
return false;
}
final int size = map.size();
Entry that = (Entry) o;
CaseInsensitiveMap.this.remove(that.getKey());
return map.size() != size;
}
/**
* This method is required. JDK method is broken, as it relies
* on iterator solution. This method is fast because contains()
* and remove() are both hashed O(1) look ups.
*/
@SuppressWarnings("unchecked")
public boolean removeAll(Collection c)
{
final int size = map.size();
for (Object o : c)
{
if (o instanceof Entry)
{
Entry that = (Entry) o;
CaseInsensitiveMap.this.remove(that.getKey());
}
}
return map.size() != size;
}
@SuppressWarnings("unchecked")
public boolean retainAll(Collection c)
{
// Create fast-access O(1) to all elements within passed in Collection
Map other = new CaseInsensitiveMap<>();
for (Object o : c)
{
if (o instanceof Entry)
{
other.put(((Entry)o).getKey(), ((Entry) o).getValue());
}
}
int origSize = size();
// Drop all items that are not in the passed in Collection
Iterator> i = map.entrySet().iterator();
while (i.hasNext())
{
Entry entry = i.next();
K key = entry.getKey();
V value = entry.getValue();
if (!other.containsKey(key))
{ // Key not even present, nuke the entry
i.remove();
}
else
{ // Key present, now check value match
Object v = other.get(key);
if (!Objects.equals(v, value))
{
i.remove();
}
}
}
return size() != origSize;
}
public Iterator> iterator()
{
iter = map.entrySet().iterator();
return new Iterator>()
{
public boolean hasNext() { return iter.hasNext(); }
public Entry next() { return new CaseInsensitiveEntry(iter.next()); }
public void remove() { iter.remove(); }
};
}
};
}
/**
* Entry implementation that will give back a String instead of a CaseInsensitiveString
* when .getKey() is called.
*
* Also, when the setValue() API is called on the Entry, it will 'write thru' to the
* underlying Map's value.
*/
public class CaseInsensitiveEntry extends AbstractMap.SimpleEntry
{
public CaseInsensitiveEntry(Entry entry)
{
super(entry);
}
@SuppressWarnings("unchecked")
public K getKey()
{
K superKey = super.getKey();
if (superKey instanceof CaseInsensitiveString)
{
return (K)((CaseInsensitiveString)superKey).original;
}
return superKey;
}
public K getOriginalKey()
{
return super.getKey();
}
public V setValue(V value)
{
return map.put(super.getKey(), value);
}
}
/**
* Class used to wrap String keys. This class ignores the
* case of Strings when they are compared. Based on known usage,
* null checks, proper instance, etc. are dropped.
*/
public static final class CaseInsensitiveString implements Comparable
{
private final String original;
private final int hash;
public CaseInsensitiveString(String string)
{
original = string;
hash = StringUtilities.hashCodeIgnoreCase(string); // no new String created unlike .toLowerCase()
}
public String toString()
{
return original;
}
public int hashCode()
{
return hash;
}
public boolean equals(Object other)
{
if (other == this)
{
return true;
}
if (other instanceof CaseInsensitiveString)
{
return hash == ((CaseInsensitiveString)other).hash &&
original.equalsIgnoreCase(((CaseInsensitiveString)other).original);
}
if (other instanceof String)
{
return original.equalsIgnoreCase((String)other);
}
return false;
}
public int compareTo(Object o)
{
if (o instanceof CaseInsensitiveString)
{
CaseInsensitiveString other = (CaseInsensitiveString) o;
return original.compareToIgnoreCase(other.original);
}
if (o instanceof String)
{
String other = (String)o;
return original.compareToIgnoreCase(other);
}
// Strings are less than non-Strings (come before)
return -1;
}
}
}