OpenConcerto

Dépôt officiel du code source de l'ERP OpenConcerto
sonarqube

svn://code.openconcerto.org/openconcerto

Rev

Rev 80 | Rev 83 | Go to most recent revision | Details | Compare with Previous | Last modification | View Log | RSS feed

Rev Author Line No. Line
65 ilm 1
/*
2
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
3
 *
4
 * Copyright 2011 OpenConcerto, by ILM Informatique. All rights reserved.
5
 *
6
 * The contents of this file are subject to the terms of the GNU General Public License Version 3
7
 * only ("GPL"). You may not use this file except in compliance with the License. You can obtain a
8
 * copy of the License at http://www.gnu.org/licenses/gpl-3.0.html See the License for the specific
9
 * language governing permissions and limitations under the License.
10
 *
11
 * When distributing the software, include this License Header Notice in each file.
12
 */
13
 
14
 package org.openconcerto.utils;
15
 
16
import java.util.AbstractCollection;
80 ilm 17
import java.util.Arrays;
65 ilm 18
import java.util.Collection;
19
import java.util.Collections;
20
import java.util.HashMap;
80 ilm 21
import java.util.HashSet;
65 ilm 22
import java.util.Iterator;
23
import java.util.Map;
80 ilm 24
import java.util.NoSuchElementException;
65 ilm 25
import java.util.Set;
26
 
27
/**
28
 * Allow to map keys to collections. This map always allow <code>null</code> items for mapped
29
 * collections, but it may restrict null collections.
30
 *
31
 * @author Sylvain
32
 *
33
 * @param <K> the type of keys maintained by this map
34
 * @param <C> the type of mapped collections
35
 * @param <V> the type of elements of the collections
36
 */
37
public abstract class CollectionMap2<K, C extends Collection<V>, V> extends HashMap<K, C> {
38
 
39
    static final int DEFAULT_INITIAL_CAPACITY = 16;
40
 
41
    private static final String toStr(final Object o) {
42
        return o == null ? "null" : "'" + o + "'";
43
    }
44
 
45
    public static enum Mode {
46
        /**
47
         * Mapped collections cannot be <code>null</code>.
48
         */
49
        NULL_FORBIDDEN,
50
        /**
51
         * Mapped collections can be <code>null</code>, but some methods may throw
52
         * {@link NullPointerException}.
53
         *
54
         * @see CollectionMap2#addAll(Object, Collection)
55
         * @see CollectionMap2#removeAll(Object, Collection)
56
         */
57
        NULL_ALLOWED,
58
        /**
59
         * Mapped collections can be <code>null</code>, meaning every possible item. Thus no method
60
         * throws {@link NullPointerException}.
61
         */
62
        NULL_MEANS_ALL
63
    }
64
 
65
    static private final Mode DEFAULT_MODE = Mode.NULL_FORBIDDEN;
66
 
67
    private final boolean emptyCollSameAsNoColl;
68
    private final Mode mode;
81 ilm 69
    private transient Collection<V> allValues = null;
65 ilm 70
 
71
    public CollectionMap2() {
72
        this(DEFAULT_MODE);
73
    }
74
 
75
    public CollectionMap2(final Mode mode) {
76
        this(mode, null);
77
    }
78
 
79
    public CollectionMap2(final Mode mode, final Boolean emptyCollSameAsNoColl) {
80
        this(DEFAULT_INITIAL_CAPACITY, mode, emptyCollSameAsNoColl);
81
    }
82
 
83
    public CollectionMap2(int initialCapacity) {
84
        this(initialCapacity, DEFAULT_MODE, null);
85
    }
86
 
87
    public CollectionMap2(int initialCapacity, final Mode mode, final Boolean emptyCollSameAsNoColl) {
88
        super(initialCapacity);
89
        this.mode = mode;
90
        this.emptyCollSameAsNoColl = emptyCollSameAsNoColl == null ? mode == Mode.NULL_MEANS_ALL : emptyCollSameAsNoColl;
91
    }
92
 
93
    public CollectionMap2(Map<? extends K, ? extends Collection<? extends V>> m) {
94
        // don't use super(Map) since it doesn't copy the collections
95
        // also its type is more restrictive
96
        super(m.size());
97
        if (m instanceof CollectionMap2) {
98
            final CollectionMap2<?, ?, ?> collM = (CollectionMap2<?, ?, ?>) m;
99
            this.mode = collM.getMode();
100
            this.emptyCollSameAsNoColl = collM.isEmptyCollSameAsNoColl();
101
        } else {
102
            this.mode = DEFAULT_MODE;
103
            this.emptyCollSameAsNoColl = this.mode == Mode.NULL_MEANS_ALL;
104
        }
105
        this.putAllCollections(m);
106
    }
107
 
108
    public final Mode getMode() {
109
        return this.mode;
110
    }
111
 
112
    public final boolean isEmptyCollSameAsNoColl() {
113
        return this.emptyCollSameAsNoColl;
114
    }
115
 
116
    public final C getNonNullIfMissing(Object key) {
117
        return this.get(key, false, true);
118
    }
119
 
120
    public final C getNonNull(K key) {
121
        return this.get(key, false, false);
122
    }
123
 
124
    private final C getNonNullColl(C res) {
125
        return res == null ? this.createCollection(Collections.<V> emptySet()) : res;
126
    }
127
 
128
    public final C get(Object key, final boolean nullIfMissing, final boolean nullIfPresent) {
129
        if (nullIfMissing == nullIfPresent) {
130
            final C res = super.get(key);
131
            if (res != null || nullIfMissing && nullIfPresent) {
132
                return res;
133
            } else {
134
                assert !nullIfMissing && !nullIfPresent;
135
                return getNonNullColl(null);
136
            }
137
        } else if (nullIfMissing) {
138
            assert !nullIfPresent;
139
            if (!this.containsKey(key))
140
                return null;
141
            else
142
                return getNonNullColl(super.get(key));
143
        } else {
144
            assert !nullIfMissing && nullIfPresent;
145
            if (this.containsKey(key))
146
                return super.get(key);
147
            else
148
                return getNonNullColl(null);
149
        }
150
    }
151
 
152
    public final C getCollection(Object key) {
153
        return this.get(key, !this.isEmptyCollSameAsNoColl(), true);
154
    }
155
 
80 ilm 156
    /**
157
     * Returns a {@link Collection} view of all the values contained in this map. The collection is
158
     * backed by the map, so changes to the map are reflected in the collection, and vice-versa. If
159
     * the map is modified while an iteration over the collection is in progress (except through the
160
     * iterator's own <tt>remove</tt> operation), the results of the iteration are undefined. The
161
     * collection supports element removal, which removes the corresponding values from the map, via
162
     * the <tt>Iterator.remove</tt>, <tt>Collection.remove</tt>, <tt>removeAll</tt>,
163
     * <tt>retainAll</tt> and <tt>clear</tt> operations. Note that it doesn't remove entries only
164
     * values : keySet() doesn't change, use {@link #removeAllEmptyCollections()} and
165
     * {@link #removeAllNullCollections()} afterwards. It does not support the <tt>add</tt> or
166
     * <tt>addAll</tt> operations.
167
     *
168
     * @return a view all values in all entries, <code>null</code> collections are ignored.
169
     */
170
    public Collection<V> allValues() {
171
        if (this.allValues == null)
172
            this.allValues = new AllValues();
173
        return this.allValues;
174
    }
175
 
176
    private final class AllValues extends AbstractCollection<V> {
177
        @Override
178
        public Iterator<V> iterator() {
179
            return new AllValuesIterator();
180
        }
181
 
182
        @Override
183
        public boolean isEmpty() {
184
            return !iterator().hasNext();
185
        }
186
 
187
        @Override
188
        public int size() {
189
            int compt = 0;
190
            final Iterator<V> it = iterator();
191
            while (it.hasNext()) {
192
                it.next();
193
                compt++;
194
            }
195
            return compt;
196
        }
197
 
198
        // don't overload clear() to call Map.clear() as this would be incoherent with removeAll() :
199
        // this last method only removes values, resulting in empty and null collections
200
    }
201
 
202
    private final class AllValuesIterator implements Iterator<V> {
203
        private final Iterator<C> mapIterator;
204
        private Iterator<V> tempIterator;
205
 
206
        private AllValuesIterator() {
207
            this.mapIterator = values().iterator();
208
            this.tempIterator = null;
209
        }
210
 
211
        private boolean searchNextIterator() {
212
            // tempIterator == null initially and when a collection is null
213
            while (this.tempIterator == null || !this.tempIterator.hasNext()) {
214
                if (!this.mapIterator.hasNext()) {
215
                    return false;
216
                }
217
                final C nextCol = this.mapIterator.next();
218
                this.tempIterator = nextCol == null ? null : nextCol.iterator();
219
            }
220
            return true;
221
        }
222
 
223
        @Override
224
        public boolean hasNext() {
225
            return searchNextIterator();
226
        }
227
 
228
        @Override
229
        public V next() {
230
            // search next iterator if necessary
231
            if (!hasNext())
232
                throw new NoSuchElementException();
233
            return this.tempIterator.next();
234
        }
235
 
236
        @Override
237
        public void remove() {
238
            if (this.tempIterator == null)
239
                throw new IllegalStateException();
240
            this.tempIterator.remove();
241
        }
242
    }
243
 
65 ilm 244
    @Override
245
    public Set<Map.Entry<K, C>> entrySet() {
246
        if (getMode() == Mode.NULL_FORBIDDEN) {
247
            // MAYBE cache
248
            return new EntrySet(super.entrySet());
249
        } else {
250
            return super.entrySet();
251
        }
252
    }
253
 
254
    private final class EntrySet extends AbstractCollection<Map.Entry<K, C>> implements Set<Map.Entry<K, C>> {
255
 
256
        private final Set<Map.Entry<K, C>> delegate;
257
 
258
        public EntrySet(Set<java.util.Map.Entry<K, C>> delegate) {
259
            super();
260
            this.delegate = delegate;
261
        }
262
 
263
        @Override
264
        public int size() {
265
            return this.delegate.size();
266
        }
267
 
268
        @Override
269
        public boolean contains(Object o) {
270
            return this.delegate.contains(o);
271
        }
272
 
273
        @Override
274
        public boolean remove(Object o) {
275
            return this.delegate.remove(o);
276
        }
277
 
278
        @Override
279
        public void clear() {
280
            this.delegate.clear();
281
        }
282
 
283
        @Override
284
        public Iterator<Map.Entry<K, C>> iterator() {
285
            return new Iterator<Map.Entry<K, C>>() {
286
 
287
                private final Iterator<Map.Entry<K, C>> delegateIter = EntrySet.this.delegate.iterator();
288
 
289
                @Override
290
                public boolean hasNext() {
291
                    return this.delegateIter.hasNext();
292
                }
293
 
294
                @Override
295
                public Map.Entry<K, C> next() {
296
                    final Map.Entry<K, C> delegate = this.delegateIter.next();
297
                    return new Map.Entry<K, C>() {
298
                        @Override
299
                        public K getKey() {
300
                            return delegate.getKey();
301
                        }
302
 
303
                        @Override
304
                        public C getValue() {
305
                            return delegate.getValue();
306
                        }
307
 
308
                        @Override
309
                        public C setValue(C value) {
310
                            if (value == null)
311
                                throw new NullPointerException("Putting null collection for " + toStr(getKey()));
312
                            return delegate.setValue(value);
313
                        }
314
                    };
315
                }
316
 
317
                @Override
318
                public void remove() {
319
                    this.delegateIter.remove();
320
                }
321
            };
322
        }
323
 
324
        @Override
325
        public boolean equals(Object o) {
326
            return this.delegate.equals(o);
327
        }
328
 
329
        @Override
330
        public int hashCode() {
331
            return this.delegate.hashCode();
332
        }
333
 
334
        @Override
335
        public boolean removeAll(Collection<?> c) {
336
            return this.delegate.removeAll(c);
337
        }
338
    }
339
 
340
    @Override
341
    public final C put(K key, C value) {
342
        return this.putCollection(key, value);
343
    }
344
 
345
    // copy passed collection
346
    public final C putCollection(K key, Collection<? extends V> value) {
347
        if (value == null && this.getMode() == Mode.NULL_FORBIDDEN)
348
            throw new NullPointerException("Putting null collection for " + toStr(key));
349
        return super.put(key, value == null ? null : createCollection(value));
350
    }
351
 
352
    public void putAllCollections(Map<? extends K, ? extends Collection<? extends V>> m) {
353
        for (final Map.Entry<? extends K, ? extends Collection<? extends V>> e : m.entrySet()) {
354
            this.putCollection(e.getKey(), e.getValue());
355
        }
356
    }
357
 
358
    // ** add/remove collection
359
 
360
    public final void add(K k, V v) {
361
        this.addAll(k, Collections.singleton(v));
362
    }
363
 
80 ilm 364
    public final void addAll(K k, V... v) {
365
        this.addAll(k, Arrays.asList(v));
366
    }
367
 
65 ilm 368
    public final void addAll(K k, Collection<? extends V> v) {
369
        final boolean nullIsAll = getMode() == Mode.NULL_MEANS_ALL;
370
        if (v == null && !nullIsAll)
371
            throw new NullPointerException("Adding null collection for " + toStr(k));
372
        if (v == null || !this.containsKey(k)) {
373
            this.putCollection(k, v);
374
        } else {
375
            final C currentColl = this.get(k);
376
            if (nullIsAll && currentColl == null) {
377
                // ignore since we can't add something to everything
378
            } else {
379
                // will throw if currentCol is null
380
                currentColl.addAll(v);
381
            }
382
        }
383
    }
384
 
81 ilm 385
    public final void merge(Map<? extends K, ? extends Collection<? extends V>> mm) {
65 ilm 386
        for (final Map.Entry<? extends K, ? extends Collection<? extends V>> e : mm.entrySet()) {
387
            this.addAll(e.getKey(), e.getValue());
388
        }
389
    }
390
 
81 ilm 391
    public final void mergeScalarMap(Map<? extends K, ? extends V> scalarMap) {
392
        for (final Map.Entry<? extends K, ? extends V> e : scalarMap.entrySet()) {
393
            this.add(e.getKey(), e.getValue());
394
        }
395
    }
396
 
80 ilm 397
    public final void remove(K k, V v) {
398
        this.removeAll(k, Collections.singleton(v));
399
    }
400
 
65 ilm 401
    public final void removeAll(K k, Collection<? extends V> v) {
402
        this.removeAll(k, v, null);
403
    }
404
 
405
    private final void removeAll(K k, Collection<? extends V> v, final Iterator<Map.Entry<K, C>> iter) {
406
        boolean removeK = false;
407
        if (getMode() == Mode.NULL_MEANS_ALL) {
408
            if (v == null) {
409
                removeK = true;
410
            } else if (v.size() > 0) {
411
                final C currentColl = this.get(k);
412
                if (currentColl == null)
413
                    throw new IllegalStateException("Cannot remove from all for " + toStr(k));
414
                currentColl.removeAll(v);
415
                if (currentColl.isEmpty())
416
                    removeK = true;
417
            }
418
        } else if (this.containsKey(k)) {
419
            final C currentColl = this.get(k);
420
            if (currentColl == null && v == null) {
421
                // since containsKey() and coll == null
422
                assert getMode() == Mode.NULL_ALLOWED;
423
                removeK = true;
424
            } else {
425
                if (v == null)
426
                    throw new NullPointerException("Removing null collection for " + toStr(k));
427
                currentColl.removeAll(v);
428
                if (currentColl.isEmpty())
429
                    removeK = true;
430
            }
431
        }
432
        if (removeK)
433
            if (iter == null)
434
                this.remove(k);
435
            else
436
                iter.remove();
437
    }
438
 
439
    public final void removeAll(Map<? extends K, ? extends Collection<? extends V>> mm) {
440
        // iterate on this to allow mm.removeAll(mm)
441
        final Iterator<Map.Entry<K, C>> iter = this.entrySet().iterator();
442
        while (iter.hasNext()) {
443
            final Map.Entry<K, C> e = iter.next();
444
            final K key = e.getKey();
445
            if (mm.containsKey(key))
446
                this.removeAll(key, mm.get(key), iter);
447
        }
448
    }
449
 
81 ilm 450
    public final void removeAllScalar(Map<? extends K, ? extends V> m) {
451
        // incompatible types, allowing removal without ConcurrentModificationException
452
        assert m != this;
453
        for (final Map.Entry<? extends K, ? extends V> e : m.entrySet()) {
454
            this.remove(e.getKey(), e.getValue());
455
        }
456
    }
457
 
65 ilm 458
    // ** remove empty/null collections
459
 
460
    public final C removeIfEmpty(K k) {
461
        final C v = this.get(k);
462
        if (v != null && v.isEmpty())
463
            return this.remove(k);
464
        else
465
            return null;
466
    }
467
 
468
    public final void removeIfNull(K k) {
469
        if (this.get(k) == null)
470
            this.remove(k);
471
    }
472
 
80 ilm 473
    public final Set<K> removeAllEmptyCollections() {
474
        return this.removeAll(true);
65 ilm 475
    }
476
 
80 ilm 477
    public final Set<K> removeAllNullCollections() {
478
        return this.removeAll(false);
65 ilm 479
    }
480
 
80 ilm 481
    private final Set<K> removeAll(final boolean emptyOrNull) {
482
        final Set<K> removed = new HashSet<K>();
65 ilm 483
        final Iterator<Map.Entry<K, C>> iter = this.entrySet().iterator();
484
        while (iter.hasNext()) {
485
            final Map.Entry<K, C> e = iter.next();
486
            final C val = e.getValue();
80 ilm 487
            if ((emptyOrNull && val != null && val.isEmpty()) || (!emptyOrNull && val == null)) {
65 ilm 488
                iter.remove();
80 ilm 489
                removed.add(e.getKey());
490
            }
65 ilm 491
        }
80 ilm 492
        return removed;
65 ilm 493
    }
494
 
81 ilm 495
    public abstract C createCollection(Collection<? extends V> v);
65 ilm 496
 
497
    @Override
498
    public int hashCode() {
499
        final int prime = 31;
500
        int result = super.hashCode();
501
        result = prime * result + (this.emptyCollSameAsNoColl ? 1231 : 1237);
502
        result = prime * result + this.mode.hashCode();
503
        return result;
504
    }
505
 
506
    @Override
507
    public boolean equals(Object obj) {
508
        if (this == obj)
509
            return true;
510
        if (!super.equals(obj))
511
            return false;
512
        if (getClass() != obj.getClass())
513
            return false;
514
        // no need to test createCollection(), since values are tested by super.equals()
515
        final CollectionMap2<?, ?, ?> other = (CollectionMap2<?, ?, ?>) obj;
516
        return this.emptyCollSameAsNoColl == other.emptyCollSameAsNoColl && this.mode == other.mode;
517
    }
81 ilm 518
 
519
    @Override
520
    public CollectionMap2<K, C, V> clone() {
521
        @SuppressWarnings("unchecked")
522
        final CollectionMap2<K, C, V> result = (CollectionMap2<K, C, V>) super.clone();
523
        // allValues has a reference to this
524
        result.allValues = null;
525
        // clone each collection value
526
        for (Map.Entry<K, C> entry : result.entrySet()) {
527
            final C coll = entry.getValue();
528
            entry.setValue(createCollection(coll));
529
        }
530
        return result;
531
    }
65 ilm 532
}