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 * https://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.collections4.collection;
18
19 import java.util.Collection;
20 import java.util.HashMap;
21 import java.util.Iterator;
22 import java.util.Objects;
23 import java.util.function.Predicate;
24
25 import org.apache.commons.collections4.MultiMap;
26 import org.apache.commons.collections4.Transformer;
27 import org.apache.commons.collections4.map.MultiValueMap;
28
29 /**
30 * An IndexedCollection is a Map-like view onto a Collection. It accepts a
31 * keyTransformer to define how the keys are converted from the values.
32 * <p>
33 * Modifications made to this decorator modify the index as well as the
34 * decorated {@link Collection}. However, modifications to the underlying
35 * {@link Collection} will not update the index and it will get out of sync.
36 * </p>
37 * <p>
38 * If modification of the decorated {@link Collection} is unavoidable, then a
39 * call to {@link #reindex()} will update the index to the current contents of
40 * the {@link Collection}.
41 * </p>
42 *
43 * @param <K> The type of object in the index.
44 * @param <C> The type of object in the collection.
45 * @since 4.0
46 */
47 public class IndexedCollection<K, C> extends AbstractCollectionDecorator<C> {
48
49 // TODO: replace with MultiValuedMap
50
51 /** Serialization version */
52 private static final long serialVersionUID = -5512610452568370038L;
53
54 /**
55 * Creates an {@link IndexedCollection} for a non-unique index.
56 *
57 * @param <K> the index object type.
58 * @param <C> the collection type.
59 * @param coll The decorated {@link Collection}.
60 * @param keyTransformer The {@link Transformer} for generating index keys.
61 * @return The created {@link IndexedCollection}.
62 */
63 public static <K, C> IndexedCollection<K, C> nonUniqueIndexedCollection(final Collection<C> coll, final Transformer<C, K> keyTransformer) {
64 return new IndexedCollection<>(coll, keyTransformer, MultiValueMap.<K, C>multiValueMap(new HashMap<>()), false);
65 }
66
67 /**
68 * Creates an {@link IndexedCollection} for a unique index.
69 * <p>
70 * If an element is added, which maps to an existing key, an {@link IllegalArgumentException} will be thrown.
71 * </p>
72 *
73 * @param <K> the index object type.
74 * @param <C> the collection type.
75 * @param coll The decorated {@link Collection}.
76 * @param keyTransformer The {@link Transformer} for generating index keys.
77 * @return The created {@link IndexedCollection}.
78 */
79 public static <K, C> IndexedCollection<K, C> uniqueIndexedCollection(final Collection<C> coll, final Transformer<C, K> keyTransformer) {
80 return new IndexedCollection<>(coll, keyTransformer, MultiValueMap.<K, C>multiValueMap(new HashMap<>()), true);
81 }
82
83 /** The {@link Transformer} for generating index keys. */
84 private final Transformer<C, K> keyTransformer;
85
86 /** The map of indexes to collected objects. */
87 private final MultiMap<K, C> index;
88
89 /** The uniqueness constraint for the index. */
90 private final boolean uniqueIndex;
91
92 /**
93 * Creates a {@link IndexedCollection}.
94 *
95 * @param coll decorated {@link Collection}.
96 * @param keyTransformer {@link Transformer} for generating index keys.
97 * @param map map to use as index.
98 * @param uniqueIndex if the index shall enforce uniqueness of index keys.
99 */
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 }