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 * https://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 * Creates 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, final Transformer<C, K> keyTransformer) { 064 return new IndexedCollection<>(coll, keyTransformer, MultiValueMap.<K, C>multiValueMap(new HashMap<>()), false); 065 } 066 067 /** 068 * Creates an {@link IndexedCollection} for a unique index. 069 * <p> 070 * If an element is added, which maps to an existing key, an {@link IllegalArgumentException} will be thrown. 071 * </p> 072 * 073 * @param <K> the index object type. 074 * @param <C> the collection type. 075 * @param coll The decorated {@link Collection}. 076 * @param keyTransformer The {@link Transformer} for generating index keys. 077 * @return The created {@link IndexedCollection}. 078 */ 079 public static <K, C> IndexedCollection<K, C> uniqueIndexedCollection(final Collection<C> coll, final Transformer<C, K> keyTransformer) { 080 return new IndexedCollection<>(coll, keyTransformer, MultiValueMap.<K, C>multiValueMap(new HashMap<>()), true); 081 } 082 083 /** The {@link Transformer} for generating index keys. */ 084 private final Transformer<C, K> keyTransformer; 085 086 /** The map of indexes to collected objects. */ 087 private final MultiMap<K, C> index; 088 089 /** The uniqueness constraint for the index. */ 090 private final boolean uniqueIndex; 091 092 /** 093 * Creates a {@link IndexedCollection}. 094 * 095 * @param coll decorated {@link Collection}. 096 * @param keyTransformer {@link Transformer} for generating index keys. 097 * @param map map to use as index. 098 * @param uniqueIndex if the index shall enforce uniqueness of index keys. 099 */ 100 public IndexedCollection(final Collection<C> coll, final Transformer<C, K> keyTransformer, final MultiMap<K, C> map, final boolean uniqueIndex) { 101 super(coll); 102 this.keyTransformer = keyTransformer; 103 this.index = map; 104 this.uniqueIndex = uniqueIndex; 105 reindex(); 106 } 107 108 /** 109 * {@inheritDoc} 110 * 111 * @throws IllegalArgumentException if the object maps to an existing key and the index 112 * enforces a uniqueness constraint. 113 */ 114 @Override 115 public boolean add(final C object) { 116 final K key = toValidKey(object); 117 final boolean added = super.add(object); 118 if (added) { 119 index.put(key, object); 120 } 121 return added; 122 } 123 124 @Override 125 public boolean addAll(final Collection<? extends C> coll) { 126 boolean changed = false; 127 for (final C c: coll) { 128 changed |= add(c); 129 } 130 return changed; 131 } 132 133 /** 134 * Provides checking for adding the index. 135 * 136 * @param object The object to index. 137 * @throws IllegalArgumentException if the object maps to an existing key and the index 138 * enforces a uniqueness constraint. 139 */ 140 private void addToIndex(final C object) { 141 index.put(toValidKey(object), object); 142 } 143 144 @Override 145 public void clear() { 146 super.clear(); 147 index.clear(); 148 } 149 150 /** 151 * {@inheritDoc} 152 * <p> 153 * Note: uses the index for fast lookup. 154 * </p> 155 */ 156 @SuppressWarnings("unchecked") 157 @Override 158 public boolean contains(final Object object) { 159 return index.containsKey(keyTransformer.apply((C) object)); 160 } 161 162 /** 163 * {@inheritDoc} 164 * <p> 165 * Note: uses the index for fast lookup. 166 * </p> 167 */ 168 @Override 169 public boolean containsAll(final Collection<?> coll) { 170 return coll.stream().allMatch(this::contains); 171 } 172 173 /** 174 * Gets the element associated with the given key. 175 * <p> 176 * In case of a non-unique index, this method will return the first 177 * value associated with the given key. To retrieve all elements associated 178 * with a key, use {@link #values(Object)}. 179 * </p> 180 * 181 * @param key key to look up. 182 * @return element found. 183 * @see #values(Object) 184 */ 185 public C get(final K key) { 186 @SuppressWarnings("unchecked") // index is a MultiMap which returns a Collection 187 final Collection<C> coll = (Collection<C>) index.get(key); 188 return coll == null ? null : coll.iterator().next(); 189 } 190 191 /** 192 * Clears the index and re-indexes the entire decorated {@link Collection}. 193 */ 194 public void reindex() { 195 index.clear(); 196 decorated().forEach(this::addToIndex); 197 } 198 199 @SuppressWarnings("unchecked") 200 @Override 201 public boolean remove(final Object object) { 202 final boolean removed = super.remove(object); 203 if (removed) { 204 removeFromIndex((C) object); 205 } 206 return removed; 207 } 208 209 @Override 210 public boolean removeAll(final Collection<?> coll) { 211 boolean changed = false; 212 for (final Object o : coll) { 213 changed |= remove(o); 214 } 215 return changed; 216 } 217 218 /** 219 * Removes an object from the index. 220 * 221 * @param object The object to remove. 222 */ 223 private void removeFromIndex(final C object) { 224 index.removeMapping(keyTransformer.apply(object), object); 225 } 226 227 /** 228 * @since 4.4 229 */ 230 @Override 231 public boolean removeIf(final Predicate<? super C> filter) { 232 if (Objects.isNull(filter)) { 233 return false; 234 } 235 boolean changed = false; 236 final Iterator<C> it = iterator(); 237 while (it.hasNext()) { 238 if (filter.test(it.next())) { 239 it.remove(); 240 changed = true; 241 } 242 } 243 if (changed) { 244 reindex(); 245 } 246 return changed; 247 } 248 249 @Override 250 public boolean retainAll(final Collection<?> coll) { 251 final boolean changed = super.retainAll(coll); 252 if (changed) { 253 reindex(); 254 } 255 return changed; 256 } 257 258 private K toValidKey(final C object) { 259 final K key = keyTransformer.apply(object); 260 if (uniqueIndex && index.containsKey(key)) { 261 throw new IllegalArgumentException("Duplicate key in uniquely indexed collection."); 262 } 263 return key; 264 } 265 266 /** 267 * Gets all elements associated with the given key. 268 * 269 * @param key key to look up. 270 * @return A collection of elements found, or null if {@code contains(key) == false}. 271 */ 272 @SuppressWarnings("unchecked") // index is a MultiMap which returns a Collection. 273 public Collection<C> values(final K key) { 274 return (Collection<C>) index.get(key); 275 } 276 277}