View Javadoc
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 }