001/*-------------------------------------------------------------------------+ 002| | 003| Copyright 2005-2011 the ConQAT Project | 004| | 005| Licensed under the Apache License, Version 2.0 (the "License"); | 006| you may not use this file except in compliance with the License. | 007| 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.conqat.lib.commons.collections; 018 019import java.util.AbstractCollection; 020import java.util.AbstractSet; 021import java.util.ArrayList; 022import java.util.Collection; 023import java.util.HashMap; 024import java.util.Iterator; 025import java.util.List; 026import java.util.Map; 027import java.util.Set; 028 029/** 030 * Special implementation of a {@link Set} of {@link String}s, which allows 031 * storing strings in a case-insensitive manner. Two strings which only differ 032 * in case are handled as equal and can thus occur just once in the set (the 033 * first string added to the set is retained). 034 * <p> 035 * It also allows accessing the case-sensitive representation of a string that 036 * was stored utilizing the {@link #get(String)} method. I.e. storing string "A" 037 * in the set and calling set.get("a") will return the original representation 038 * "A". 039 * <p> 040 * This class does not inherit from {@link AbstractSet} or 041 * {@link AbstractCollection} as most provided operations have higher 042 * performance directly using the hash map instead of the iterator 043 * implementations. Moreover, comparison in {@link AbstractCollection} is 044 * performed with equals and the {@link CaseInsensitiveStringSet} violates this 045 * contract as it requires comparison after normalization with 046 * {@link #getMappingKey(String)}. 047 */ 048public class CaseInsensitiveStringSet implements Set<String> { 049 050 /** 051 * Mapping from case-insensitive lower-cased strings to case-sensitive ones. 052 */ 053 private Map<String, String> caseInsensitiveMapping = new HashMap<String, String>(); 054 055 /** Constructor. */ 056 public CaseInsensitiveStringSet() { 057 // default constructor. 058 } 059 060 /** Constructor. */ 061 public CaseInsensitiveStringSet(Collection<String> strings) { 062 addAll(strings); 063 } 064 065 /** 066 * Returns the case-sensitive representation of the given string, as it was 067 * stored with the call to {@link #add(String)} or <code>null</code> if the 068 * string is not stored in the {@link Set}. 069 */ 070 public String get(String string) { 071 return caseInsensitiveMapping.get(getMappingKey(string)); 072 } 073 074 /** 075 * Converts the string to a case-insensitive mapping key. This is 076 * lower-cased string representation. 077 */ 078 private static String getMappingKey(String string) { 079 if (string == null) { 080 return null; 081 } 082 return string.toLowerCase(); 083 } 084 085 /** {@inheritDoc} */ 086 @Override 087 public int size() { 088 return caseInsensitiveMapping.size(); 089 } 090 091 /** {@inheritDoc} */ 092 @Override 093 public boolean isEmpty() { 094 return caseInsensitiveMapping.isEmpty(); 095 } 096 097 /** {@inheritDoc} */ 098 @Override 099 public boolean contains(Object o) { 100 if (o instanceof String) { 101 String mappingKey = getMappingKey((String) o); 102 return caseInsensitiveMapping.containsKey(mappingKey); 103 } 104 return false; 105 } 106 107 /** {@inheritDoc} */ 108 @Override 109 public Iterator<String> iterator() { 110 return caseInsensitiveMapping.values().iterator(); 111 } 112 113 /** {@inheritDoc} */ 114 @Override 115 public Object[] toArray() { 116 return caseInsensitiveMapping.values().toArray(); 117 } 118 119 /** {@inheritDoc} */ 120 @Override 121 public <T> T[] toArray(T[] a) { 122 return caseInsensitiveMapping.values().toArray(a); 123 } 124 125 /** {@inheritDoc} */ 126 @Override 127 public boolean add(String e) { 128 if (!contains(e)) { 129 caseInsensitiveMapping.put(getMappingKey(e), e); 130 return true; 131 } 132 return false; 133 } 134 135 /** {@inheritDoc} */ 136 @Override 137 public boolean remove(Object o) { 138 if (o instanceof String) { 139 return caseInsensitiveMapping.remove(getMappingKey((String) o)) != null; 140 } 141 return false; 142 } 143 144 /** {@inheritDoc} */ 145 @Override 146 public boolean containsAll(Collection<?> c) { 147 for (Object o : c) { 148 if (!contains(o)) { 149 return false; 150 } 151 } 152 return true; 153 } 154 155 /** {@inheritDoc} */ 156 @Override 157 public boolean addAll(Collection<? extends String> c) { 158 boolean setChanged = false; 159 for (String s : c) { 160 setChanged |= add(s); 161 } 162 return setChanged; 163 } 164 165 /** {@inheritDoc} */ 166 @Override 167 public boolean retainAll(Collection<?> c) { 168 boolean setChanged = false; 169 170 CaseInsensitiveStringSet retain = new CaseInsensitiveStringSet(); 171 for (Object toRetain : c) { 172 if (toRetain instanceof String) { 173 retain.add((String) toRetain); 174 } 175 } 176 177 List<String> keys = new ArrayList<String>(caseInsensitiveMapping.keySet()); 178 179 for (String string : keys) { 180 if (!retain.contains(string)) { 181 setChanged |= remove(string); 182 } 183 } 184 185 return setChanged; 186 } 187 188 /** {@inheritDoc} */ 189 @Override 190 public boolean removeAll(Collection<?> c) { 191 boolean setChanged = false; 192 for (Object o : c) { 193 setChanged |= remove(o); 194 } 195 196 return setChanged; 197 } 198 199 /** {@inheritDoc} */ 200 @Override 201 public void clear() { 202 caseInsensitiveMapping.clear(); 203 } 204 205 /** {@inheritDoc} */ 206 @Override 207 public String toString() { 208 return caseInsensitiveMapping.values().toString(); 209 } 210 211}