OpenConcerto

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

svn://code.openconcerto.org/openconcerto

Rev

Rev 83 | Rev 144 | 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
 
83 ilm 16
import org.openconcerto.utils.cc.AbstractMapDecorator;
17
 
65 ilm 18
import java.util.AbstractCollection;
80 ilm 19
import java.util.Arrays;
65 ilm 20
import java.util.Collection;
21
import java.util.Collections;
22
import java.util.HashMap;
80 ilm 23
import java.util.HashSet;
65 ilm 24
import java.util.Iterator;
25
import java.util.Map;
80 ilm 26
import java.util.NoSuchElementException;
65 ilm 27
import java.util.Set;
28
 
29
/**
30
 * Allow to map keys to collections. This map always allow <code>null</code> items for mapped
31
 * collections, but it may restrict null collections.
32
 *
33
 * @author Sylvain
34
 *
35
 * @param <K> the type of keys maintained by this map
36
 * @param <C> the type of mapped collections
37
 * @param <V> the type of elements of the collections
38
 */
83 ilm 39
public abstract class CollectionMap2<K, C extends Collection<V>, V> extends AbstractMapDecorator<K, C> implements Cloneable, CollectionMap2Itf<K, C, V> {
65 ilm 40
 
41
    static final int DEFAULT_INITIAL_CAPACITY = 16;
42
 
43
    private static final String toStr(final Object o) {
44
        return o == null ? "null" : "'" + o + "'";
45
    }
46
 
47
    public static enum Mode {
48
        /**
49
         * Mapped collections cannot be <code>null</code>.
50
         */
51
        NULL_FORBIDDEN,
52
        /**
53
         * Mapped collections can be <code>null</code>, but some methods may throw
54
         * {@link NullPointerException}.
55
         *
56
         * @see CollectionMap2#addAll(Object, Collection)
57
         * @see CollectionMap2#removeAll(Object, Collection)
58
         */
59
        NULL_ALLOWED,
60
        /**
61
         * Mapped collections can be <code>null</code>, meaning every possible item. Thus no method
62
         * throws {@link NullPointerException}.
63
         */
64
        NULL_MEANS_ALL
65
    }
66
 
83 ilm 67
    static protected final Mode DEFAULT_MODE = Mode.NULL_FORBIDDEN;
68
    static private final Boolean DEFAULT_emptyCollSameAsNoColl = null;
65 ilm 69
 
70
    private final boolean emptyCollSameAsNoColl;
71
    private final Mode mode;
81 ilm 72
    private transient Collection<V> allValues = null;
65 ilm 73
 
74
    public CollectionMap2() {
75
        this(DEFAULT_MODE);
76
    }
77
 
78
    public CollectionMap2(final Mode mode) {
83 ilm 79
        this(mode, DEFAULT_emptyCollSameAsNoColl);
65 ilm 80
    }
81
 
83 ilm 82
    public CollectionMap2(final Map<K, C> delegate, final Mode mode) {
83
        this(delegate, mode, DEFAULT_emptyCollSameAsNoColl);
84
    }
85
 
65 ilm 86
    public CollectionMap2(final Mode mode, final Boolean emptyCollSameAsNoColl) {
87
        this(DEFAULT_INITIAL_CAPACITY, mode, emptyCollSameAsNoColl);
88
    }
89
 
83 ilm 90
    public CollectionMap2(final int initialCapacity) {
91
        this(initialCapacity, DEFAULT_MODE, DEFAULT_emptyCollSameAsNoColl);
65 ilm 92
    }
93
 
83 ilm 94
    public CollectionMap2(final int initialCapacity, final Mode mode, final Boolean emptyCollSameAsNoColl) {
95
        this(new HashMap<K, C>(initialCapacity), mode, emptyCollSameAsNoColl);
96
    }
97
 
98
    /**
99
     * Create a new instance with the passed delegate. The delegate is *not* cleared, this allows to
100
     * decorate an existing Map but it also means that the existing collections might not be the
101
     * exact same type as those returned by {@link #createCollection(Collection)}.
102
     *
103
     * @param delegate the map to use, it must not be modified afterwards.
104
     * @param mode how to handle null values.
105
     * @param emptyCollSameAsNoColl for {@link #getCollection(Object)} : whether the lack of an
106
     *        entry is the same as an entry with an empty collection, can be <code>null</code>.
107
     */
108
    public CollectionMap2(final Map<K, C> delegate, final Mode mode, final Boolean emptyCollSameAsNoColl) {
109
        super(delegate);
110
        if (mode == null)
111
            throw new NullPointerException("Null mode");
65 ilm 112
        this.mode = mode;
113
        this.emptyCollSameAsNoColl = emptyCollSameAsNoColl == null ? mode == Mode.NULL_MEANS_ALL : emptyCollSameAsNoColl;
83 ilm 114
        checkMode();
65 ilm 115
    }
116
 
83 ilm 117
    private final void checkMode() {
118
        assert this.mode != null : "Called too early";
119
        if (this.mode == Mode.NULL_FORBIDDEN && this.containsValue(null))
120
            throw new IllegalArgumentException("Null collection");
121
    }
122
 
123
    // ** copy constructors
124
 
125
    public CollectionMap2(final CollectionMap2<K, C, ? extends V> m) {
126
        this(CopyUtils.copy(m.getDelegate()), m);
127
    }
128
 
129
    public CollectionMap2(final Map<? extends K, ? extends Collection<? extends V>> m) {
130
        this(new HashMap<K, C>(m.size()), m);
131
    }
132
 
133
    /**
134
     * Create a new instance with the passed delegate and filling it with the passed map.
135
     *
136
     * @param delegate the map to use, it will be cleared and must not be modified afterwards.
137
     * @param m the values to put in this, if it's an instance of {@link CollectionMap2} the
138
     *        {@link #getMode() mode} and {@link #isEmptyCollSameAsNoColl()} will be copied as well.
139
     */
140
    public CollectionMap2(final Map<K, C> delegate, final Map<? extends K, ? extends Collection<? extends V>> m) {
65 ilm 141
        // don't use super(Map) since it doesn't copy the collections
142
        // also its type is more restrictive
83 ilm 143
        super(delegate);
65 ilm 144
        if (m instanceof CollectionMap2) {
145
            final CollectionMap2<?, ?, ?> collM = (CollectionMap2<?, ?, ?>) m;
146
            this.mode = collM.getMode();
147
            this.emptyCollSameAsNoColl = collM.isEmptyCollSameAsNoColl();
148
        } else {
149
            this.mode = DEFAULT_MODE;
150
            this.emptyCollSameAsNoColl = this.mode == Mode.NULL_MEANS_ALL;
151
        }
83 ilm 152
        // delegate might not contain the same instances of collections (i.e. LinkedList vs
153
        // ArrayList)
154
        this.clear();
65 ilm 155
        this.putAllCollections(m);
156
    }
157
 
83 ilm 158
    @Override
65 ilm 159
    public final Mode getMode() {
160
        return this.mode;
161
    }
162
 
83 ilm 163
    @Override
65 ilm 164
    public final boolean isEmptyCollSameAsNoColl() {
165
        return this.emptyCollSameAsNoColl;
166
    }
167
 
83 ilm 168
    public final C getNonNullIfMissing(final Object key) {
65 ilm 169
        return this.get(key, false, true);
170
    }
171
 
83 ilm 172
    @Override
173
    public final C getNonNull(final K key) {
65 ilm 174
        return this.get(key, false, false);
175
    }
176
 
83 ilm 177
    private final C getNonNullColl(final C res) {
65 ilm 178
        return res == null ? this.createCollection(Collections.<V> emptySet()) : res;
179
    }
180
 
83 ilm 181
    /**
182
     * Get the collection mapped to the passed key. Note : <code>get(key, true, true)</code> is
183
     * equivalent to <code>get(key)</code>.
184
     *
185
     * @param key the key whose associated value is to be returned.
186
     * @param nullIfMissing only relevant if the key isn't contained : if <code>true</code>
187
     *        <code>null</code> will be returned, otherwise an empty collection.
188
     * @param nullIfPresent only relevant if the key is mapped to <code>null</code> : if
189
     *        <code>true</code> <code>null</code> will be returned, otherwise an empty collection.
190
     * @return the non {@code null} value to which the specified key is mapped, otherwise
191
     *         {@code null} or empty collection depending on the other parameters.
192
     */
193
    @Override
194
    public final C get(final Object key, final boolean nullIfMissing, final boolean nullIfPresent) {
65 ilm 195
        if (nullIfMissing == nullIfPresent) {
196
            final C res = super.get(key);
197
            if (res != null || nullIfMissing && nullIfPresent) {
198
                return res;
199
            } else {
200
                assert !nullIfMissing && !nullIfPresent;
201
                return getNonNullColl(null);
202
            }
203
        } else if (nullIfMissing) {
204
            assert !nullIfPresent;
205
            if (!this.containsKey(key))
206
                return null;
207
            else
208
                return getNonNullColl(super.get(key));
209
        } else {
210
            assert !nullIfMissing && nullIfPresent;
211
            if (this.containsKey(key))
212
                return super.get(key);
213
            else
214
                return getNonNullColl(null);
215
        }
216
    }
217
 
83 ilm 218
    @Override
219
    public final C getCollection(final Object key) {
65 ilm 220
        return this.get(key, !this.isEmptyCollSameAsNoColl(), true);
221
    }
222
 
80 ilm 223
    /**
224
     * Returns a {@link Collection} view of all the values contained in this map. The collection is
225
     * backed by the map, so changes to the map are reflected in the collection, and vice-versa. If
226
     * the map is modified while an iteration over the collection is in progress (except through the
227
     * iterator's own <tt>remove</tt> operation), the results of the iteration are undefined. The
228
     * collection supports element removal, which removes the corresponding values from the map, via
229
     * the <tt>Iterator.remove</tt>, <tt>Collection.remove</tt>, <tt>removeAll</tt>,
230
     * <tt>retainAll</tt> and <tt>clear</tt> operations. Note that it doesn't remove entries only
231
     * values : keySet() doesn't change, use {@link #removeAllEmptyCollections()} and
232
     * {@link #removeAllNullCollections()} afterwards. It does not support the <tt>add</tt> or
233
     * <tt>addAll</tt> operations.
234
     *
235
     * @return a view all values in all entries, <code>null</code> collections are ignored.
236
     */
83 ilm 237
    @Override
80 ilm 238
    public Collection<V> allValues() {
239
        if (this.allValues == null)
240
            this.allValues = new AllValues();
241
        return this.allValues;
242
    }
243
 
244
    private final class AllValues extends AbstractCollection<V> {
245
        @Override
246
        public Iterator<V> iterator() {
247
            return new AllValuesIterator();
248
        }
249
 
250
        @Override
251
        public int size() {
252
            int compt = 0;
83 ilm 253
            for (final C c : values()) {
254
                if (c != null)
255
                    compt += c.size();
80 ilm 256
            }
257
            return compt;
258
        }
259
 
260
        // don't overload clear() to call Map.clear() as this would be incoherent with removeAll() :
261
        // this last method only removes values, resulting in empty and null collections
262
    }
263
 
264
    private final class AllValuesIterator implements Iterator<V> {
265
        private final Iterator<C> mapIterator;
266
        private Iterator<V> tempIterator;
267
 
268
        private AllValuesIterator() {
269
            this.mapIterator = values().iterator();
270
            this.tempIterator = null;
271
        }
272
 
273
        private boolean searchNextIterator() {
274
            // tempIterator == null initially and when a collection is null
275
            while (this.tempIterator == null || !this.tempIterator.hasNext()) {
276
                if (!this.mapIterator.hasNext()) {
277
                    return false;
278
                }
279
                final C nextCol = this.mapIterator.next();
280
                this.tempIterator = nextCol == null ? null : nextCol.iterator();
281
            }
282
            return true;
283
        }
284
 
285
        @Override
286
        public boolean hasNext() {
287
            return searchNextIterator();
288
        }
289
 
290
        @Override
291
        public V next() {
292
            // search next iterator if necessary
293
            if (!hasNext())
294
                throw new NoSuchElementException();
295
            return this.tempIterator.next();
296
        }
297
 
298
        @Override
299
        public void remove() {
300
            if (this.tempIterator == null)
301
                throw new IllegalStateException();
302
            this.tempIterator.remove();
303
        }
304
    }
305
 
65 ilm 306
    @Override
307
    public Set<Map.Entry<K, C>> entrySet() {
308
        if (getMode() == Mode.NULL_FORBIDDEN) {
83 ilm 309
            // prevent null insertion
65 ilm 310
            // MAYBE cache
311
            return new EntrySet(super.entrySet());
312
        } else {
313
            return super.entrySet();
314
        }
315
    }
316
 
317
    private final class EntrySet extends AbstractCollection<Map.Entry<K, C>> implements Set<Map.Entry<K, C>> {
318
 
319
        private final Set<Map.Entry<K, C>> delegate;
320
 
83 ilm 321
        public EntrySet(final Set<java.util.Map.Entry<K, C>> delegate) {
65 ilm 322
            super();
323
            this.delegate = delegate;
324
        }
325
 
326
        @Override
327
        public int size() {
328
            return this.delegate.size();
329
        }
330
 
331
        @Override
83 ilm 332
        public boolean contains(final Object o) {
65 ilm 333
            return this.delegate.contains(o);
334
        }
335
 
336
        @Override
83 ilm 337
        public boolean remove(final Object o) {
65 ilm 338
            return this.delegate.remove(o);
339
        }
340
 
341
        @Override
342
        public void clear() {
343
            this.delegate.clear();
344
        }
345
 
346
        @Override
347
        public Iterator<Map.Entry<K, C>> iterator() {
348
            return new Iterator<Map.Entry<K, C>>() {
349
 
350
                private final Iterator<Map.Entry<K, C>> delegateIter = EntrySet.this.delegate.iterator();
351
 
352
                @Override
353
                public boolean hasNext() {
354
                    return this.delegateIter.hasNext();
355
                }
356
 
357
                @Override
358
                public Map.Entry<K, C> next() {
359
                    final Map.Entry<K, C> delegate = this.delegateIter.next();
360
                    return new Map.Entry<K, C>() {
361
                        @Override
362
                        public K getKey() {
363
                            return delegate.getKey();
364
                        }
365
 
366
                        @Override
367
                        public C getValue() {
368
                            return delegate.getValue();
369
                        }
370
 
371
                        @Override
83 ilm 372
                        public C setValue(final C value) {
65 ilm 373
                            if (value == null)
374
                                throw new NullPointerException("Putting null collection for " + toStr(getKey()));
375
                            return delegate.setValue(value);
376
                        }
377
                    };
378
                }
379
 
380
                @Override
381
                public void remove() {
382
                    this.delegateIter.remove();
383
                }
384
            };
385
        }
386
 
387
        @Override
83 ilm 388
        public boolean equals(final Object o) {
65 ilm 389
            return this.delegate.equals(o);
390
        }
391
 
392
        @Override
393
        public int hashCode() {
394
            return this.delegate.hashCode();
395
        }
396
 
397
        @Override
83 ilm 398
        public boolean removeAll(final Collection<?> c) {
65 ilm 399
            return this.delegate.removeAll(c);
400
        }
401
    }
402
 
403
    @Override
83 ilm 404
    public final C put(final K key, final C value) {
65 ilm 405
        return this.putCollection(key, value);
406
    }
407
 
408
    // copy passed collection
83 ilm 409
    @Override
410
    public final C putCollection(final K key, final Collection<? extends V> value) {
65 ilm 411
        if (value == null && this.getMode() == Mode.NULL_FORBIDDEN)
412
            throw new NullPointerException("Putting null collection for " + toStr(key));
413
        return super.put(key, value == null ? null : createCollection(value));
414
    }
415
 
83 ilm 416
    public final C putCollection(final K key, final V... value) {
417
        return this.putCollection(key, Arrays.asList(value));
418
    }
419
 
420
    public void putAllCollections(final Map<? extends K, ? extends Collection<? extends V>> m) {
132 ilm 421
        this.putAllCollections(m, false);
422
    }
423
 
424
    public void putAllCollections(final Map<? extends K, ? extends Collection<? extends V>> m, final boolean removeEmptyCollections) {
65 ilm 425
        for (final Map.Entry<? extends K, ? extends Collection<? extends V>> e : m.entrySet()) {
132 ilm 426
            if (!removeEmptyCollections || !e.getValue().isEmpty())
427
                this.putCollection(e.getKey(), e.getValue());
65 ilm 428
        }
429
    }
430
 
431
    // ** add/remove collection
432
 
83 ilm 433
    @Override
434
    public final boolean add(final K k, final V v) {
435
        return this.addAll(k, Collections.singleton(v));
65 ilm 436
    }
437
 
83 ilm 438
    public final boolean addAll(final K k, final V... v) {
439
        return this.addAll(k, Arrays.asList(v));
80 ilm 440
    }
441
 
83 ilm 442
    @Override
443
    public final boolean addAll(final K k, final Collection<? extends V> v) {
65 ilm 444
        final boolean nullIsAll = getMode() == Mode.NULL_MEANS_ALL;
445
        if (v == null && !nullIsAll)
446
            throw new NullPointerException("Adding null collection for " + toStr(k));
83 ilm 447
        final boolean containsKey = this.containsKey(k);
448
        if (v == null) {
449
            return this.putCollection(k, v) != null;
450
        } else if (!containsKey) {
65 ilm 451
            this.putCollection(k, v);
83 ilm 452
            return true;
65 ilm 453
        } else {
454
            final C currentColl = this.get(k);
455
            if (nullIsAll && currentColl == null) {
456
                // ignore since we can't add something to everything
83 ilm 457
                return false;
65 ilm 458
            } else {
459
                // will throw if currentCol is null
83 ilm 460
                return currentColl.addAll(v);
65 ilm 461
            }
462
        }
463
    }
464
 
83 ilm 465
    @Override
466
    public final void merge(final Map<? extends K, ? extends Collection<? extends V>> mm) {
65 ilm 467
        for (final Map.Entry<? extends K, ? extends Collection<? extends V>> e : mm.entrySet()) {
468
            this.addAll(e.getKey(), e.getValue());
469
        }
470
    }
471
 
83 ilm 472
    @Override
473
    public final void mergeScalarMap(final Map<? extends K, ? extends V> scalarMap) {
81 ilm 474
        for (final Map.Entry<? extends K, ? extends V> e : scalarMap.entrySet()) {
475
            this.add(e.getKey(), e.getValue());
476
        }
477
    }
478
 
83 ilm 479
    @Override
480
    public final boolean remove(final K k, final V v) {
481
        return this.removeAll(k, Collections.singleton(v));
80 ilm 482
    }
483
 
83 ilm 484
    @Override
485
    public final boolean removeAll(final K k, final Collection<? extends V> v) {
486
        return this.removeAll(k, v, null);
65 ilm 487
    }
488
 
83 ilm 489
    private final boolean removeAll(final K k, final Collection<? extends V> v, final Iterator<Map.Entry<K, C>> iter) {
65 ilm 490
        boolean removeK = false;
83 ilm 491
        boolean modified = false;
65 ilm 492
        if (getMode() == Mode.NULL_MEANS_ALL) {
493
            if (v == null) {
494
                removeK = true;
495
            } else if (v.size() > 0) {
496
                final C currentColl = this.get(k);
497
                if (currentColl == null)
498
                    throw new IllegalStateException("Cannot remove from all for " + toStr(k));
83 ilm 499
                modified = currentColl.removeAll(v);
65 ilm 500
                if (currentColl.isEmpty())
501
                    removeK = true;
502
            }
503
        } else if (this.containsKey(k)) {
504
            final C currentColl = this.get(k);
505
            if (currentColl == null && v == null) {
506
                // since containsKey() and coll == null
507
                assert getMode() == Mode.NULL_ALLOWED;
508
                removeK = true;
83 ilm 509
                // since we just tested containsKey()
510
                modified = true;
65 ilm 511
            } else {
512
                if (v == null)
513
                    throw new NullPointerException("Removing null collection for " + toStr(k));
83 ilm 514
                modified = currentColl.removeAll(v);
65 ilm 515
                if (currentColl.isEmpty())
516
                    removeK = true;
517
            }
518
        }
83 ilm 519
        if (removeK) {
520
            if (iter == null) {
521
                modified |= this.containsKey(k);
65 ilm 522
                this.remove(k);
83 ilm 523
            } else {
65 ilm 524
                iter.remove();
83 ilm 525
                modified = true;
526
            }
527
        }
528
        return modified;
65 ilm 529
    }
530
 
83 ilm 531
    @Override
532
    public final boolean removeAll(final Map<? extends K, ? extends Collection<? extends V>> mm) {
533
        boolean modified = false;
65 ilm 534
        // iterate on this to allow mm.removeAll(mm)
535
        final Iterator<Map.Entry<K, C>> iter = this.entrySet().iterator();
536
        while (iter.hasNext()) {
537
            final Map.Entry<K, C> e = iter.next();
538
            final K key = e.getKey();
539
            if (mm.containsKey(key))
83 ilm 540
                modified |= this.removeAll(key, mm.get(key), iter);
65 ilm 541
        }
83 ilm 542
        return modified;
65 ilm 543
    }
544
 
83 ilm 545
    @Override
546
    public final boolean removeAllScalar(final Map<? extends K, ? extends V> m) {
547
        boolean modified = false;
81 ilm 548
        // incompatible types, allowing removal without ConcurrentModificationException
549
        assert m != this;
550
        for (final Map.Entry<? extends K, ? extends V> e : m.entrySet()) {
83 ilm 551
            modified |= this.remove(e.getKey(), e.getValue());
81 ilm 552
        }
83 ilm 553
        return modified;
81 ilm 554
    }
555
 
65 ilm 556
    // ** remove empty/null collections
557
 
83 ilm 558
    public final C removeIfEmpty(final K k) {
65 ilm 559
        final C v = this.get(k);
560
        if (v != null && v.isEmpty())
561
            return this.remove(k);
562
        else
563
            return null;
564
    }
565
 
83 ilm 566
    public final void removeIfNull(final K k) {
65 ilm 567
        if (this.get(k) == null)
568
            this.remove(k);
569
    }
570
 
83 ilm 571
    @Override
80 ilm 572
    public final Set<K> removeAllEmptyCollections() {
573
        return this.removeAll(true);
65 ilm 574
    }
575
 
83 ilm 576
    @Override
80 ilm 577
    public final Set<K> removeAllNullCollections() {
578
        return this.removeAll(false);
65 ilm 579
    }
580
 
80 ilm 581
    private final Set<K> removeAll(final boolean emptyOrNull) {
582
        final Set<K> removed = new HashSet<K>();
65 ilm 583
        final Iterator<Map.Entry<K, C>> iter = this.entrySet().iterator();
584
        while (iter.hasNext()) {
585
            final Map.Entry<K, C> e = iter.next();
586
            final C val = e.getValue();
80 ilm 587
            if ((emptyOrNull && val != null && val.isEmpty()) || (!emptyOrNull && val == null)) {
65 ilm 588
                iter.remove();
80 ilm 589
                removed.add(e.getKey());
590
            }
65 ilm 591
        }
80 ilm 592
        return removed;
65 ilm 593
    }
594
 
81 ilm 595
    public abstract C createCollection(Collection<? extends V> v);
65 ilm 596
 
597
    @Override
598
    public int hashCode() {
83 ilm 599
        if (this.mode == Mode.NULL_MEANS_ALL)
600
            return this.hashCodeExact();
601
        else
602
            return super.hashCode();
603
    }
604
 
605
    public int hashCodeExact() {
65 ilm 606
        final int prime = 31;
607
        int result = super.hashCode();
608
        result = prime * result + (this.emptyCollSameAsNoColl ? 1231 : 1237);
609
        result = prime * result + this.mode.hashCode();
610
        return result;
611
    }
612
 
83 ilm 613
    /**
614
     * Compares the specified object with this map for equality. Except for
615
     * {@link Mode#NULL_MEANS_ALL}, returns <tt>true</tt> if the given object is also a map and the
616
     * two maps represent the same mappings (as required by {@link Map}).
617
     * <code>NULL_MEANS_ALL</code> maps are tested using {@link #equalsExact(Object)}, meaning they
618
     * don't conform to the Map interface.
619
     *
620
     * @param obj object to be compared for equality with this map
621
     * @return <tt>true</tt> if the specified object is equal to this map
622
     * @see #equalsExact(Object)
623
     */
65 ilm 624
    @Override
83 ilm 625
    public final boolean equals(final Object obj) {
626
        return this.equals(obj, false);
627
    }
628
 
629
    /**
630
     * Compares the specified object with this map for complete equality. This method not only
631
     * checks for equality of values (as required by {@link Map}) but also the class and attributes.
632
     *
633
     * @param obj object to be compared for equality with this map
634
     * @return <tt>true</tt> if the specified object is exactly equal to this map.
635
     */
636
    public final boolean equalsExact(final Object obj) {
637
        return this.equals(obj, true);
638
    }
639
 
640
    private final boolean equals(final Object obj, final boolean forceExact) {
65 ilm 641
        if (this == obj)
642
            return true;
643
        if (!super.equals(obj))
644
            return false;
83 ilm 645
        assert obj != null;
646
        final CollectionMap2<?, ?, ?> other = obj instanceof CollectionMap2 ? (CollectionMap2<?, ?, ?>) obj : null;
647
        if (forceExact || this.mode == Mode.NULL_MEANS_ALL || (other != null && other.mode == Mode.NULL_MEANS_ALL)) {
648
            if (getClass() != obj.getClass())
649
                return false;
650
            // no need to test createCollection(), since values are tested by super.equals()
651
            return this.emptyCollSameAsNoColl == other.emptyCollSameAsNoColl && this.mode == other.mode && this.getDelegate().getClass() == other.getDelegate().getClass();
652
        } else {
653
            return true;
654
        }
65 ilm 655
    }
81 ilm 656
 
657
    @Override
83 ilm 658
    public CollectionMap2<K, C, V> clone() throws CloneNotSupportedException {
81 ilm 659
        @SuppressWarnings("unchecked")
660
        final CollectionMap2<K, C, V> result = (CollectionMap2<K, C, V>) super.clone();
661
        // allValues has a reference to this
662
        result.allValues = null;
663
        // clone each collection value
83 ilm 664
        for (final Map.Entry<K, C> entry : result.entrySet()) {
81 ilm 665
            final C coll = entry.getValue();
666
            entry.setValue(createCollection(coll));
667
        }
668
        return result;
669
    }
65 ilm 670
}