1 /* 2 * Licensed to the Apache Software Foundation (ASF) under one or more 3 * contributor license agreements. See the NOTICE file distributed with 4 * this work for additional information regarding copyright ownership. 5 * The ASF licenses this file to You under the Apache License, Version 2.0 6 * (the "License"); you may not use this file except in compliance with 7 * the License. You may obtain a copy of the License at 8 * 9 * http://www.apache.org/licenses/LICENSE-2.0 10 * 11 * Unless required by applicable law or agreed to in writing, software 12 * distributed under the License is distributed on an "AS IS" BASIS, 13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 * See the License for the specific language governing permissions and 15 * limitations under the License. 16 */ 17 package org.apache.commons.collections.map; 18 19 import java.util.AbstractCollection; 20 import java.util.ArrayList; 21 import java.util.Collection; 22 import java.util.HashMap; 23 import java.util.Iterator; 24 import java.util.Map; 25 import java.util.Set; 26 27 import org.apache.commons.collections.Factory; 28 import org.apache.commons.collections.FunctorException; 29 import org.apache.commons.collections.MultiMap; 30 import org.apache.commons.collections.iterators.EmptyIterator; 31 import org.apache.commons.collections.iterators.IteratorChain; 32 33 /** 34 * A MultiValueMap decorates another map, allowing it to have 35 * more than one value for a key. 36 * <p> 37 * A <code>MultiMap</code> is a Map with slightly different semantics. 38 * Putting a value into the map will add the value to a Collection at that key. 39 * Getting a value will return a Collection, holding all the values put to that key. 40 * <p> 41 * This implementation is a decorator, allowing any Map implementation 42 * to be used as the base. 43 * <p> 44 * In addition, this implementation allows the type of collection used 45 * for the values to be controlled. By default, an <code>ArrayList</code> 46 * is used, however a <code>Class</code> to instantiate may be specified, 47 * or a factory that returns a <code>Collection</code> instance. 48 * <p> 49 * <strong>Note that MultiValueMap is not synchronized and is not thread-safe.</strong> 50 * If you wish to use this map from multiple threads concurrently, you must use 51 * appropriate synchronization. This class may throw exceptions when accessed 52 * by concurrent threads without synchronization. 53 * 54 * @author James Carman 55 * @author Christopher Berry 56 * @author James Strachan 57 * @author Steve Downey 58 * @author Stephen Colebourne 59 * @author Julien Buret 60 * @author Serhiy Yevtushenko 61 * @version $Revision: 1713175 $ $Date: 2015-11-07 21:47:04 +0100 (Sat, 07 Nov 2015) $ 62 * @since Commons Collections 3.2 63 */ 64 public class MultiValueMap extends AbstractMapDecorator implements MultiMap { 65 66 /** The factory for creating value collections. */ 67 private final Factory collectionFactory; 68 /** The cached values. */ 69 private transient Collection values; 70 71 /** 72 * Creates a map which wraps the given map and 73 * maps keys to ArrayLists. 74 * 75 * @param map the map to wrap 76 */ decorate(Map map)77 public static MultiValueMap decorate(Map map) { 78 return new MultiValueMap(map, new ReflectionFactory(ArrayList.class)); 79 } 80 81 /** 82 * Creates a map which decorates the given <code>map</code> and 83 * maps keys to collections of type <code>collectionClass</code>. 84 * 85 * @param map the map to wrap 86 * @param collectionClass the type of the collection class 87 */ decorate(Map map, Class collectionClass)88 public static MultiValueMap decorate(Map map, Class collectionClass) { 89 return new MultiValueMap(map, new ReflectionFactory(collectionClass)); 90 } 91 92 /** 93 * Creates a map which decorates the given <code>map</code> and 94 * creates the value collections using the supplied <code>collectionFactory</code>. 95 * 96 * @param map the map to decorate 97 * @param collectionFactory the collection factory (must return a Collection object). 98 */ decorate(Map map, Factory collectionFactory)99 public static MultiValueMap decorate(Map map, Factory collectionFactory) { 100 return new MultiValueMap(map, collectionFactory); 101 } 102 103 //----------------------------------------------------------------------- 104 /** 105 * Creates a MultiValueMap based on a <code>HashMap</code> and 106 * storing the multiple values in an <code>ArrayList</code>. 107 */ MultiValueMap()108 public MultiValueMap() { 109 this(new HashMap(), new ReflectionFactory(ArrayList.class)); 110 } 111 112 /** 113 * Creates a MultiValueMap which decorates the given <code>map</code> and 114 * creates the value collections using the supplied <code>collectionFactory</code>. 115 * 116 * @param map the map to decorate 117 * @param collectionFactory the collection factory which must return a Collection instance 118 */ MultiValueMap(Map map, Factory collectionFactory)119 protected MultiValueMap(Map map, Factory collectionFactory) { 120 super(map); 121 if (collectionFactory == null) { 122 throw new IllegalArgumentException("The factory must not be null"); 123 } 124 this.collectionFactory = collectionFactory; 125 } 126 127 //----------------------------------------------------------------------- 128 /** 129 * Clear the map. 130 */ clear()131 public void clear() { 132 // If you believe that you have GC issues here, try uncommenting this code 133 // Set pairs = getMap().entrySet(); 134 // Iterator pairsIterator = pairs.iterator(); 135 // while (pairsIterator.hasNext()) { 136 // Map.Entry keyValuePair = (Map.Entry) pairsIterator.next(); 137 // Collection coll = (Collection) keyValuePair.getValue(); 138 // coll.clear(); 139 // } 140 getMap().clear(); 141 } 142 143 /** 144 * Removes a specific value from map. 145 * <p> 146 * The item is removed from the collection mapped to the specified key. 147 * Other values attached to that key are unaffected. 148 * <p> 149 * If the last value for a key is removed, <code>null</code> will be returned 150 * from a subsequant <code>get(key)</code>. 151 * 152 * @param key the key to remove from 153 * @param value the value to remove 154 * @return the value removed (which was passed in), null if nothing removed 155 */ remove(Object key, Object value)156 public boolean remove(Object key, Object value) { 157 Collection valuesForKey = getCollection(key); 158 if (valuesForKey == null) { 159 return false; 160 } 161 boolean removed = valuesForKey.remove(value); 162 if (removed == false) { 163 return false; 164 } 165 if (valuesForKey.isEmpty()) { 166 remove(key); 167 } 168 return true; 169 } 170 171 /** 172 * Checks whether the map contains the value specified. 173 * <p> 174 * This checks all collections against all keys for the value, and thus could be slow. 175 * 176 * @param value the value to search for 177 * @return true if the map contains the value 178 */ containsValue(Object value)179 public boolean containsValue(Object value) { 180 Set pairs = getMap().entrySet(); 181 if (pairs == null) { 182 return false; 183 } 184 Iterator pairsIterator = pairs.iterator(); 185 while (pairsIterator.hasNext()) { 186 Map.Entry keyValuePair = (Map.Entry) pairsIterator.next(); 187 Collection coll = (Collection) keyValuePair.getValue(); 188 if (coll.contains(value)) { 189 return true; 190 } 191 } 192 return false; 193 } 194 195 /** 196 * Adds the value to the collection associated with the specified key. 197 * <p> 198 * Unlike a normal <code>Map</code> the previous value is not replaced. 199 * Instead the new value is added to the collection stored against the key. 200 * 201 * @param key the key to store against 202 * @param value the value to add to the collection at the key 203 * @return the value added if the map changed and null if the map did not change 204 */ put(Object key, Object value)205 public Object put(Object key, Object value) { 206 boolean result = false; 207 Collection coll = getCollection(key); 208 if (coll == null) { 209 coll = createCollection(1); 210 result = coll.add(value); 211 if (coll.size() > 0) { 212 // only add if non-zero size to maintain class state 213 getMap().put(key, coll); 214 result = true; // map definitely changed 215 } 216 } else { 217 result = coll.add(value); 218 } 219 return (result ? value : null); 220 } 221 222 /** 223 * Override superclass to ensure that MultiMap instances are 224 * correctly handled. 225 * <p> 226 * If you call this method with a normal map, each entry is 227 * added using <code>put(Object,Object)</code>. 228 * If you call this method with a multi map, each entry is 229 * added using <code>putAll(Object,Collection)</code>. 230 * 231 * @param map the map to copy (either a normal or multi map) 232 */ putAll(Map map)233 public void putAll(Map map) { 234 if (map instanceof MultiMap) { 235 for (Iterator it = map.entrySet().iterator(); it.hasNext();) { 236 Map.Entry entry = (Map.Entry) it.next(); 237 Collection coll = (Collection) entry.getValue(); 238 putAll(entry.getKey(), coll); 239 } 240 } else { 241 for (Iterator it = map.entrySet().iterator(); it.hasNext();) { 242 Map.Entry entry = (Map.Entry) it.next(); 243 put(entry.getKey(), entry.getValue()); 244 } 245 } 246 } 247 248 /** 249 * Gets a collection containing all the values in the map. 250 * <p> 251 * This returns a collection containing the combination of values from all keys. 252 * 253 * @return a collection view of the values contained in this map 254 */ values()255 public Collection values() { 256 Collection vs = values; 257 return (vs != null ? vs : (values = new Values())); 258 } 259 260 /** 261 * Checks whether the collection at the specified key contains the value. 262 * 263 * @param value the value to search for 264 * @return true if the map contains the value 265 */ containsValue(Object key, Object value)266 public boolean containsValue(Object key, Object value) { 267 Collection coll = getCollection(key); 268 if (coll == null) { 269 return false; 270 } 271 return coll.contains(value); 272 } 273 274 /** 275 * Gets the collection mapped to the specified key. 276 * This method is a convenience method to typecast the result of <code>get(key)</code>. 277 * 278 * @param key the key to retrieve 279 * @return the collection mapped to the key, null if no mapping 280 */ getCollection(Object key)281 public Collection getCollection(Object key) { 282 return (Collection) getMap().get(key); 283 } 284 285 /** 286 * Gets the size of the collection mapped to the specified key. 287 * 288 * @param key the key to get size for 289 * @return the size of the collection at the key, zero if key not in map 290 */ size(Object key)291 public int size(Object key) { 292 Collection coll = getCollection(key); 293 if (coll == null) { 294 return 0; 295 } 296 return coll.size(); 297 } 298 299 /** 300 * Adds a collection of values to the collection associated with 301 * the specified key. 302 * 303 * @param key the key to store against 304 * @param values the values to add to the collection at the key, null ignored 305 * @return true if this map changed 306 */ putAll(Object key, Collection values)307 public boolean putAll(Object key, Collection values) { 308 if (values == null || values.size() == 0) { 309 return false; 310 } 311 boolean result = false; 312 Collection coll = getCollection(key); 313 if (coll == null) { 314 coll = createCollection(values.size()); 315 coll.addAll(values); 316 if (coll.size() > 0) { 317 // only add if non-zero size to maintain class state 318 getMap().put(key, coll); 319 result = true; // map definitely changed 320 } 321 } else { 322 result = coll.addAll(values); 323 } 324 return result; 325 } 326 327 /** 328 * Gets an iterator for the collection mapped to the specified key. 329 * 330 * @param key the key to get an iterator for 331 * @return the iterator of the collection at the key, empty iterator if key not in map 332 */ iterator(Object key)333 public Iterator iterator(Object key) { 334 if (!containsKey(key)) { 335 return EmptyIterator.INSTANCE; 336 } else { 337 return new ValuesIterator(key); 338 } 339 } 340 341 /** 342 * Gets the total size of the map by counting all the values. 343 * 344 * @return the total size of the map counting all values 345 */ totalSize()346 public int totalSize() { 347 int total = 0; 348 Collection values = getMap().values(); 349 for (Iterator it = values.iterator(); it.hasNext();) { 350 Collection coll = (Collection) it.next(); 351 total += coll.size(); 352 } 353 return total; 354 } 355 356 /** 357 * Creates a new instance of the map value Collection container 358 * using the factory. 359 * <p> 360 * This method can be overridden to perform your own processing 361 * instead of using the factory. 362 * 363 * @param size the collection size that is about to be added 364 * @return the new collection 365 */ createCollection(int size)366 protected Collection createCollection(int size) { 367 return (Collection) collectionFactory.create(); 368 } 369 370 //----------------------------------------------------------------------- 371 /** 372 * Inner class that provides the values view. 373 */ 374 private class Values extends AbstractCollection { iterator()375 public Iterator iterator() { 376 final IteratorChain chain = new IteratorChain(); 377 for (Iterator it = keySet().iterator(); it.hasNext();) { 378 chain.addIterator(new ValuesIterator(it.next())); 379 } 380 return chain; 381 } 382 size()383 public int size() { 384 return totalSize(); 385 } 386 clear()387 public void clear() { 388 MultiValueMap.this.clear(); 389 } 390 } 391 392 /** 393 * Inner class that provides the values iterator. 394 */ 395 private class ValuesIterator implements Iterator { 396 private final Object key; 397 private final Collection values; 398 private final Iterator iterator; 399 ValuesIterator(Object key)400 public ValuesIterator(Object key) { 401 this.key = key; 402 this.values = getCollection(key); 403 this.iterator = values.iterator(); 404 } 405 remove()406 public void remove() { 407 iterator.remove(); 408 if (values.isEmpty()) { 409 MultiValueMap.this.remove(key); 410 } 411 } 412 hasNext()413 public boolean hasNext() { 414 return iterator.hasNext(); 415 } 416 next()417 public Object next() { 418 return iterator.next(); 419 } 420 } 421 422 /** 423 * Inner class that provides a simple reflection factory. 424 */ 425 private static class ReflectionFactory implements Factory { 426 private final Class clazz; 427 ReflectionFactory(Class clazz)428 public ReflectionFactory(Class clazz) { 429 this.clazz = clazz; 430 } 431 create()432 public Object create() { 433 try { 434 return clazz.newInstance(); 435 } catch (Exception ex) { 436 throw new FunctorException("Cannot instantiate class: " + clazz, ex); 437 } 438 } 439 } 440 441 } 442