001/*
002 * The contents of this file are subject to the terms of the Common Development and
003 * Distribution License (the License). You may not use this file except in compliance with the
004 * License.
005 *
006 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
007 * specific language governing permission and limitations under the License.
008 *
009 * When distributing Covered Software, include this CDDL Header Notice in each file and include
010 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
011 * Header, with the fields enclosed by brackets [] replaced by your own identifying
012 * information: "Portions Copyright [year] [name of copyright owner]".
013 *
014 * Copyright 2010–2011 ApexIdentity Inc.
015 * Portions Copyright 2011-2015 ForgeRock AS.
016 */
017
018package org.forgerock.http.util;
019
020import java.util.HashMap;
021import java.util.Map;
022
023import org.forgerock.util.MapDecorator;
024
025/**
026 * An implementation of a map whose keys are case-insensitive strings. All operations match
027 * keys in a case-insensitive manner. The original cases of keys are retained, so the
028 * {@link #keySet() keySet()} method for example returns the original keys.
029 * <p>
030 * <strong>Note:</strong> The behavior of this class is undefined when wrapping a map that
031 * has keys that would result in duplicate case-insensitive keys.
032 * @param <V> The type of the case insensitive map.
033 */
034public class CaseInsensitiveMap<V> extends MapDecorator<String, V> {
035
036    /** Maps lowercase keys to the real string keys. */
037    private final Map<String, String> lc = new HashMap<>();
038
039    /**
040     * Constructs a new empty case-insensitive map. The backing map is a new {@link HashMap}
041     * with default initial capacity and load factor.
042     */
043    public CaseInsensitiveMap() {
044        super(new HashMap<String, V>());
045    }
046
047    /**
048     * Wraps an existing map with a new case insensitive map.
049     *
050     * @param map the map to wrap with a new case insensitive map.
051     */
052    public CaseInsensitiveMap(Map<String, V> map) {
053        super(map);
054        sync();
055    }
056
057    /**
058     * Returns a case-insensitive key translated into its mapped equivalent. If its equivalent
059     * cannot be found, then the key is returned untouched.
060     */
061    private Object translate(Object key) {
062        if (key instanceof String) {
063            String k = lc.get(((String) key).toLowerCase());
064            if (k != null) {
065                // found a mapped-equivalent
066                key = k;
067            }
068        }
069        return key;
070    }
071
072    /**
073     * Synchronizes the keys of this case insensitive map and those of the map it is wrapping.
074     * This is necessary if the underlying map is modified directly and this map needs to be
075     * resynchronized.
076     */
077    public void sync() {
078        lc.clear();
079        for (String key : map.keySet()) {
080            lc.put(key.toLowerCase(), key);
081        }
082    }
083
084    @Override
085    public void clear() {
086        lc.clear();
087        super.clear();
088    }
089
090    @Override
091    public boolean containsKey(Object key) {
092        return super.containsKey(translate(key));
093    }
094
095    @Override
096    public V put(String key, V value) {
097        // remove potentially differently-cased key
098        V removed = remove(key);
099        lc.put(key.toLowerCase(), key);
100        super.put(key, value);
101        return removed;
102    }
103
104    @Override
105    public void putAll(Map<? extends String, ? extends V> m) {
106        for (String key : m.keySet()) {
107            put(key, m.get(key));
108        }
109    }
110
111    @Override
112    public V get(Object key) {
113        return super.get(translate(key));
114    }
115
116    @Override
117    public V remove(Object key) {
118        V removed = super.remove(translate(key));
119        if (key instanceof String) {
120            lc.remove(((String) key).toLowerCase());
121        }
122        return removed;
123    }
124}