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.Collection;
021import java.util.HashMap;
022import java.util.HashSet;
023import java.util.Map;
024import java.util.Set;
025
026/**
027 * An implementation of a set whose values are case-insensitive strings. All operations match
028 * values in a case-insensitive manner. The original cases of values are retained, so the
029 * {@link #iterator() iterator()} method for example returns the originally values.
030 * <p>
031 * <strong>Note:</strong> The behavior of this class is undefined when wrapping a set that
032 * has keys that would result in duplicate case-insensitive values.
033 */
034public class CaseInsensitiveSet extends SetDecorator<String> {
035
036    /** Maps lowercase elements to the real string elements. */
037    private final Map<String, String> lc;
038
039    /**
040     * Constructs a new empty case-insensitive set. The backing set is a new {@link HashSet}
041     * with default initial capacity and load factor.
042     */
043    public CaseInsensitiveSet() {
044        super(new HashSet<String>());
045        lc = new HashMap<>();
046    }
047
048    /**
049     * Constructs a new case-insensitive set containing the elements in the specified
050     * collection. The {@code HashSet} is created with default load factor and an initial
051     * capacity sufficient to contain the elements in the specified collection.
052     *
053     * @param c the collection whose elements are to be placed into this set.
054     * @throws NullPointerException if the specified collection is {@code null}.
055     */
056    public CaseInsensitiveSet(Collection<String> c) {
057        super(c instanceof Set ? (Set<String>) c : new HashSet<>(c));
058        lc = new HashMap<>(c.size());
059        for (String e : c) {
060            lc.put(e.toLowerCase(), e);
061        }
062    }
063
064    /**
065     * Constructs a new, empty case-insensitive set; the backing {@code HashSet} instance has
066     * the specified initial capacity and the specified load factor.
067     *
068     * @param initialCapacity the initial capacity of the hash set.
069     * @param loadFactor the load factor of the hash set.
070     * @throws IllegalArgumentException if the initial capacity is less than zero, or if the load factor is nonpositive.
071     */
072    public CaseInsensitiveSet(int initialCapacity, float loadFactor) {
073        super(new HashSet<String>(initialCapacity, loadFactor));
074        lc = new HashMap<>(initialCapacity, loadFactor);
075    }
076
077    /**
078     * Constructs a new, empty case-insensitive set; the backing {@code HashSet} instance has
079     * the specified initial capacity and default load factor.
080     *
081     * @param initialCapacity the initial capacity of the hash set.
082     * @throws IllegalArgumentException if the initial capacity is less than zero.
083     */
084    public CaseInsensitiveSet(int initialCapacity) {
085        super(new HashSet<String>(initialCapacity));
086        lc = new HashMap<>(initialCapacity);
087    }
088
089    private Object translate(Object element) {
090        if (element instanceof String) {
091            String e = lc.get(((String) element).toLowerCase());
092            if (e != null) {
093                // found a mapped-equivalent
094                element = e;
095            }
096        }
097        return element;
098    }
099
100    @Override
101    public boolean contains(Object o) {
102        return super.contains(translate(o));
103    }
104
105    @Override
106    public boolean add(String e) {
107        if (contains(e)) {
108            return false;
109        }
110        lc.put(e.toLowerCase(), e);
111        return super.add(e);
112    }
113
114    @Override
115    public boolean remove(Object o) {
116        boolean removed = super.remove(translate(o));
117        if (o instanceof String) {
118            lc.remove(((String) o).toLowerCase());
119        }
120        return removed;
121    }
122
123    @Override
124    public boolean containsAll(Collection<?> c) {
125        for (Object o : c) {
126            if (!contains(o)) {
127                return false;
128            }
129        }
130        return true;
131    }
132
133    @Override
134    public boolean addAll(Collection<? extends String> c) {
135        boolean changed = false;
136        for (String e : c) {
137            if (add(e)) {
138                changed = true;
139            }
140        }
141        return changed;
142    }
143
144    @Override
145    public boolean retainAll(Collection<?> c) {
146        boolean changed = false;
147        for (String e : this) {
148            if (!c.contains(e)) {
149                remove(e);
150                changed = true;
151            }
152        }
153        return changed;
154    }
155
156    @Override
157    public boolean removeAll(Collection<?> c) {
158        boolean changed = false;
159        for (String e : this) {
160            if (c.contains(e)) {
161                remove(e);
162                changed = true;
163            }
164        }
165        return changed;
166    }
167
168    @Override
169    public void clear() {
170        lc.clear();
171        super.clear();
172    }
173}