001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. 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.apache.commons.collections4.collection; 018 019import java.util.Collection; 020import java.util.HashMap; 021import java.util.Iterator; 022import java.util.Objects; 023import java.util.function.Predicate; 024 025import org.apache.commons.collections4.MultiMap; 026import org.apache.commons.collections4.Transformer; 027import org.apache.commons.collections4.map.MultiValueMap; 028 029/** 030 * An IndexedCollection is a Map-like view onto a Collection. It accepts a 031 * keyTransformer to define how the keys are converted from the values. 032 * <p> 033 * Modifications made to this decorator modify the index as well as the 034 * decorated {@link Collection}. However, modifications to the underlying 035 * {@link Collection} will not update the index and it will get out of sync. 036 * </p> 037 * <p> 038 * If modification of the decorated {@link Collection} is unavoidable, then a 039 * call to {@link #reindex()} will update the index to the current contents of 040 * the {@link Collection}. 041 * </p> 042 * 043 * @param <K> the type of object in the index. 044 * @param <C> the type of object in the collection. 045 * @since 4.0 046 */ 047public class IndexedCollection<K, C> extends AbstractCollectionDecorator<C> { 048 049 // TODO: replace with MultiValuedMap 050 051 /** Serialization version */ 052 private static final long serialVersionUID = -5512610452568370038L; 053 054 /** 055 * Create an {@link IndexedCollection} for a non-unique index. 056 * 057 * @param <K> the index object type. 058 * @param <C> the collection type. 059 * @param coll the decorated {@link Collection}. 060 * @param keyTransformer the {@link Transformer} for generating index keys. 061 * @return the created {@link IndexedCollection}. 062 */ 063 public static <K, C> IndexedCollection<K, C> nonUniqueIndexedCollection(final Collection<C> coll, 064 final Transformer<C, K> keyTransformer) { 065 return new IndexedCollection<>(coll, keyTransformer, 066 MultiValueMap.<K, C>multiValueMap(new HashMap<>()), 067 false); 068 } 069 070 /** 071 * Create an {@link IndexedCollection} for a unique index. 072 * <p> 073 * If an element is added, which maps to an existing key, an {@link IllegalArgumentException} 074 * will be thrown. 075 * 076 * @param <K> the index object type. 077 * @param <C> the collection type. 078 * @param coll the decorated {@link Collection}. 079 * @param keyTransformer the {@link Transformer} for generating index keys. 080 * @return the created {@link IndexedCollection}. 081 */ 082 public static <K, C> IndexedCollection<K, C> uniqueIndexedCollection(final Collection<C> coll, 083 final Transformer<C, K> keyTransformer) { 084 return new IndexedCollection<>(coll, keyTransformer, 085 MultiValueMap.<K, C>multiValueMap(new HashMap<>()), 086 true); 087 } 088 089 /** The {@link Transformer} for generating index keys. */ 090 private final Transformer<C, K> keyTransformer; 091 092 /** The map of indexes to collected objects. */ 093 private final MultiMap<K, C> index; 094 095 /** The uniqueness constraint for the index. */ 096 private final boolean uniqueIndex; 097 098 /** 099 * Create a {@link IndexedCollection}. 100 * 101 * @param coll decorated {@link Collection} 102 * @param keyTransformer {@link Transformer} for generating index keys 103 * @param map map to use as index 104 * @param uniqueIndex if the index shall enforce uniqueness of index keys 105 */ 106 public IndexedCollection(final Collection<C> coll, final Transformer<C, K> keyTransformer, 107 final MultiMap<K, C> map, final boolean uniqueIndex) { 108 super(coll); 109 this.keyTransformer = keyTransformer; 110 this.index = map; 111 this.uniqueIndex = uniqueIndex; 112 reindex(); 113 } 114 115 /** 116 * {@inheritDoc} 117 * 118 * @throws IllegalArgumentException if the object maps to an existing key and the index 119 * enforces a uniqueness constraint 120 */ 121 @Override 122 public boolean add(final C object) { 123 final boolean added = super.add(object); 124 if (added) { 125 addToIndex(object); 126 } 127 return added; 128 } 129 130 @Override 131 public boolean addAll(final Collection<? extends C> coll) { 132 boolean changed = false; 133 for (final C c: coll) { 134 changed |= add(c); 135 } 136 return changed; 137 } 138 139 /** 140 * Provides checking for adding the index. 141 * 142 * @param object the object to index 143 * @throws IllegalArgumentException if the object maps to an existing key and the index 144 * enforces a uniqueness constraint 145 */ 146 private void addToIndex(final C object) { 147 final K key = keyTransformer.apply(object); 148 if (uniqueIndex && index.containsKey(key)) { 149 throw new IllegalArgumentException("Duplicate key in uniquely indexed collection."); 150 } 151 index.put(key, object); 152 } 153 154 @Override 155 public void clear() { 156 super.clear(); 157 index.clear(); 158 } 159 160 /** 161 * {@inheritDoc} 162 * <p> 163 * Note: uses the index for fast lookup 164 */ 165 @SuppressWarnings("unchecked") 166 @Override 167 public boolean contains(final Object object) { 168 return index.containsKey(keyTransformer.apply((C) object)); 169 } 170 171 /** 172 * {@inheritDoc} 173 * <p> 174 * Note: uses the index for fast lookup 175 */ 176 @Override 177 public boolean containsAll(final Collection<?> coll) { 178 for (final Object o : coll) { 179 if (!contains(o)) { 180 return false; 181 } 182 } 183 return true; 184 } 185 186 /** 187 * Gets the element associated with the given key. 188 * <p> 189 * In case of a non-unique index, this method will return the first 190 * value associated with the given key. To retrieve all elements associated 191 * with a key, use {@link #values(Object)}. 192 * 193 * @param key key to look up 194 * @return element found 195 * @see #values(Object) 196 */ 197 public C get(final K key) { 198 @SuppressWarnings("unchecked") // index is a MultiMap which returns a Collection 199 final Collection<C> coll = (Collection<C>) index.get(key); 200 return coll == null ? null : coll.iterator().next(); 201 } 202 203 /** 204 * Clears the index and re-indexes the entire decorated {@link Collection}. 205 */ 206 public void reindex() { 207 index.clear(); 208 for (final C c : decorated()) { 209 addToIndex(c); 210 } 211 } 212 213 @SuppressWarnings("unchecked") 214 @Override 215 public boolean remove(final Object object) { 216 final boolean removed = super.remove(object); 217 if (removed) { 218 removeFromIndex((C) object); 219 } 220 return removed; 221 } 222 223 @Override 224 public boolean removeAll(final Collection<?> coll) { 225 boolean changed = false; 226 for (final Object o : coll) { 227 changed |= remove(o); 228 } 229 return changed; 230 } 231 232 /** 233 * Removes an object from the index. 234 * 235 * @param object the object to remove 236 */ 237 private void removeFromIndex(final C object) { 238 index.remove(keyTransformer.apply(object)); 239 } 240 241 /** 242 * @since 4.4 243 */ 244 @Override 245 public boolean removeIf(final Predicate<? super C> filter) { 246 if (Objects.isNull(filter)) { 247 return false; 248 } 249 boolean changed = false; 250 final Iterator<C> it = iterator(); 251 while (it.hasNext()) { 252 if (filter.test(it.next())) { 253 it.remove(); 254 changed = true; 255 } 256 } 257 if (changed) { 258 reindex(); 259 } 260 return changed; 261 } 262 263 @Override 264 public boolean retainAll(final Collection<?> coll) { 265 final boolean changed = super.retainAll(coll); 266 if (changed) { 267 reindex(); 268 } 269 return changed; 270 } 271 272 /** 273 * Gets all elements associated with the given key. 274 * 275 * @param key key to look up 276 * @return a collection of elements found, or null if {@code contains(key) == false} 277 */ 278 @SuppressWarnings("unchecked") // index is a MultiMap which returns a Collection 279 public Collection<C> values(final K key) { 280 return (Collection<C>) index.get(key); 281 } 282 283}