package com.cedarsoftware.util;
import java.util.AbstractMap;
import java.util.AbstractSet;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.Map;
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;
import static com.cedarsoftware.util.StringUtilities.hashCodeIgnoreCase;
/**
* 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.
* <p>
* 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-insenstive
* 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 (john@cedarsoftware.com)
* <br>
* Copyright (c) Cedar Software LLC
* <br><br>
* 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
* <br><br>
* http://www.apache.org/licenses/LICENSE-2.0
* <br><br>
* 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<K, V> implements Map<K, V>
{
private final Map<K, V> map;
public CaseInsensitiveMap()
{
map = new LinkedHashMap<>();
}
public CaseInsensitiveMap(int initialCapacity)
{
map = new LinkedHashMap<>(initialCapacity);
}
/**
* 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<K, V> 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
{
map = copy(m, new HashMap(m.size()));
}
}
protected Map<K, V> copy(Map<K, V> source, Map dest)
{
for (Map.Entry<K, V> entry : source.entrySet())
{
K key = entry.getKey();
K altKey;
if (key instanceof String)
{
altKey = (K) new CaseInsensitiveString((String)key);
}
else
{
altKey = key;
}
dest.put(altKey, entry.getValue());
}
return dest;
}
public CaseInsensitiveMap(int initialCapacity, float loadFactor)
{
map = new LinkedHashMap<>(initialCapacity, loadFactor);
}
public V get(Object key)
{
if (key instanceof String)
{
String keyString = (String) key;
return map.get(new CaseInsensitiveString(keyString));
}
return map.get(key);
}
public V put(K key, V value)
{
if (key instanceof String)
{ // Must remove entry because the key case can change
final CaseInsensitiveString newKey = new CaseInsensitiveString((String) key);
return map.put((K) newKey, value);
}
return map.put(key, value);
}
public boolean containsKey(Object key)
{
if (key instanceof String)
{
String keyString = (String) key;
return map.containsKey(new CaseInsensitiveString(keyString));
}
return map.containsKey(key);
}
public void putAll(Map<? extends K, ? extends V> m)
{
if (m == null)
{
return;
}
for (Map.Entry entry : m.entrySet())
{
put((K) entry.getKey(), (V) 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 (Map.Entry entry : that.entrySet())
{
final Object thatKey = entry.getKey();
if (!containsKey(thatKey))
{
return false;
}
Object thatValue = entry.getValue();
Object thisValue = get(thatKey);
if (thatValue == null || thisValue == null)
{ // Perform null checks
if (thatValue != thisValue)
{
return false;
}
}
else if (!thisValue.equals(thatValue))
{
return false;
}
}
return true;
}
public int hashCode()
{
int h = 0;
for (Map.Entry<K, V> 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<V> values()
{
return map.values();
}
/**
* Returns a {@link 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 <tt>remove</tt> operation), the results of
* the iteration are undefined. The set supports element removal,
* which removes the corresponding mapping from the map, via the
* <tt>Iterator.remove</tt>, <tt>Set.remove</tt>,
* <tt>removeAll</tt>, <tt>retainAll</tt>, and <tt>clear</tt>
* operations. It does not support the <tt>add</tt> or <tt>addAll</tt>
* operations.
*/
public Set<K> keySet()
{
return new LocalSet();
}
public Map<K, V> getWrappedMap()
{
return map;
}
private class LocalSet extends AbstractSet<K>
{
final Map<K, V> localMap = CaseInsensitiveMap.this;
Iterator iter;
public LocalSet()
{ }
public boolean contains(Object o)
{
return localMap.containsKey(o);
}
public boolean remove(Object o)
{
final int size = map.size();
localMap.remove(o);
return map.size() != size;
}
public boolean removeAll(Collection c)
{
int size = map.size();
for (Object o : c)
{
if (contains(o))
{
remove(o);
}
}
return map.size() != size;
}
public boolean retainAll(Collection c)
{
Map other = new CaseInsensitiveMap();
for (Object o : c)
{
other.put(o, null);
}
final int size = map.size();
Iterator<Map.Entry<K, V>> i = map.entrySet().iterator();
while (i.hasNext())
{
Map.Entry<K, V> entry = i.next();
if (!other.containsKey(entry.getKey()))
{
i.remove();
}
}
return map.size() != size;
}
public boolean add(K o)
{
throw new UnsupportedOperationException("Cannot add() to a 'view' of a Map. See JavaDoc for Map.keySet()");
}
public boolean addAll(Collection c)
{
throw new UnsupportedOperationException("Cannot addAll() to a 'view' of a Map. See JavaDoc for Map.keySet()");
}
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 <T> T[] toArray(T[] a)
{
if (a.length < size())
{
// Make a new array of a's runtime type, but my contents:
return (T[]) Arrays.copyOf(toArray(), size(), a.getClass());
}
System.arraycopy(toArray(), 0, a, 0, size());
if (a.length > size())
{
a[size()] = null;
}
return a;
}
public int size()
{
return map.size();
}
public boolean isEmpty()
{
return map.isEmpty();
}
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;
}
public Iterator<K> iterator()
{
iter = map.keySet().iterator();
return new Iterator<K>()
{
Object lastReturned = null;
public boolean hasNext()
{
return iter.hasNext();
}
public K next()
{
lastReturned = iter.next();
if (lastReturned instanceof CaseInsensitiveString)
{
lastReturned = lastReturned.toString();
}
return (K) lastReturned;
}
public void remove()
{
iter.remove();
}
};
}
}
public Set<Map.Entry<K, V>> entrySet()
{
return new EntrySet<>();
}
private class EntrySet<E> extends LinkedHashSet<E>
{
final Map<K, V> localMap = CaseInsensitiveMap.this;
Iterator<Map.Entry<K, V>> iter;
EntrySet() { }
public int size()
{
return map.size();
}
public boolean isEmpty()
{
return map.isEmpty();
}
public void clear()
{
map.clear();
}
public boolean contains(Object o)
{
if (!(o instanceof Map.Entry))
{
return false;
}
Map.Entry that = (Map.Entry) o;
if (localMap.containsKey(that.getKey()))
{
Object value = localMap.get(that.getKey());
if (value == null)
{
return that.getValue() == null;
}
return value.equals(that.getValue());
}
return false;
}
public boolean remove(Object o)
{
final int size = map.size();
Map.Entry that = (Map.Entry) o;
localMap.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.
*/
public boolean removeAll(Collection c)
{
final int size = map.size();
for (Object o : c)
{
if (contains(o))
{
remove(o);
}
}
return map.size() != size;
}
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 Map.Entry)
{
other.put(((Map.Entry)o).getKey(), ((Map.Entry) o).getValue());
}
}
int origSize = size();
// Drop all items that are not in the passed in Collection
Iterator<Map.Entry<K,V>> i = map.entrySet().iterator();
while (i.hasNext())
{
Map.Entry<K, V> entry = i.next();
Object key = entry.getKey();
Object 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 (v == null)
{
if (value != null)
{
i.remove();
}
}
else
{
if (!v.equals(value))
{
i.remove();
}
}
}
}
return size() != origSize;
}
public boolean add(E o)
{
throw new UnsupportedOperationException("Cannot add() to a 'view' of a Map. See JavaDoc for Map.entrySet()");
}
public boolean addAll(Collection c)
{
throw new UnsupportedOperationException("Cannot addAll() to a 'view' of a Map. See JavaDoc for Map.entrySet()");
}
public Iterator<E> iterator()
{
iter = map.entrySet().iterator();
return new Iterator<E>()
{
Map.Entry lastReturned = null;
public boolean hasNext()
{
return iter.hasNext();
}
public E next()
{
lastReturned = iter.next();
return (E) new CaseInsensitiveEntry<>(lastReturned);
}
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<KK, VV> extends AbstractMap.SimpleEntry<KK, VV>
{
public CaseInsensitiveEntry(Map.Entry<KK, VV> entry)
{
super(entry);
}
public KK getKey()
{
KK superKey = super.getKey();
if (superKey instanceof CaseInsensitiveString)
{
return (KK) superKey.toString();
}
return superKey;
}
public VV setValue(VV value)
{
return (VV) map.put((K)super.getKey(), (V)value);
}
}
/**
* Internal 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.
*/
protected static final class CaseInsensitiveString implements Comparable
{
private final String caseInsensitiveString;
private final int hash;
protected CaseInsensitiveString(String string)
{
caseInsensitiveString = string;
hash = hashCodeIgnoreCase(string); // no new String created unlike .toLowerCase()
}
public String toString()
{
return caseInsensitiveString;
}
public int hashCode()
{
return hash;
}
public boolean equals(Object other)
{
if (other == this)
{
return true;
}
else if (other instanceof CaseInsensitiveString)
{
return hash == ((CaseInsensitiveString)other).hash &&
caseInsensitiveString.equalsIgnoreCase(((CaseInsensitiveString)other).caseInsensitiveString);
}
else if (other instanceof String)
{
return caseInsensitiveString.equalsIgnoreCase((String)other);
}
return false;
}
public int compareTo(Object o)
{
if (o instanceof CaseInsensitiveString)
{
CaseInsensitiveString other = (CaseInsensitiveString) o;
if (hash == other.hash)
{
return 0;
}
return caseInsensitiveString.compareToIgnoreCase(other.caseInsensitiveString);
}
else if (o instanceof String)
{
String other = (String)o;
return caseInsensitiveString.compareToIgnoreCase(other);
}
else
{ // Strings are less than non-Strings (come before)
return -1;
}
}
}
}