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    *      http://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.bidimap;
18  
19  import static org.junit.jupiter.api.Assertions.assertEquals;
20  import static org.junit.jupiter.api.Assertions.assertFalse;
21  import static org.junit.jupiter.api.Assertions.assertNull;
22  import static org.junit.jupiter.api.Assertions.assertSame;
23  import static org.junit.jupiter.api.Assertions.assertThrows;
24  import static org.junit.jupiter.api.Assertions.assertTrue;
25  
26  import java.util.Collection;
27  import java.util.HashMap;
28  import java.util.Iterator;
29  import java.util.Map;
30  import java.util.Set;
31  
32  import org.apache.commons.collections4.BidiMap;
33  import org.apache.commons.collections4.BulkTest;
34  import org.apache.commons.collections4.MapIterator;
35  import org.apache.commons.collections4.collection.AbstractCollectionTest;
36  import org.apache.commons.collections4.iterators.AbstractMapIteratorTest;
37  import org.apache.commons.collections4.map.AbstractIterableMapTest;
38  import org.junit.jupiter.api.Assumptions;
39  import org.junit.jupiter.api.Test;
40  
41  /**
42   * Abstract test class for {@link BidiMap} methods and contracts.
43   */
44  public abstract class AbstractBidiMapTest<K, V> extends AbstractIterableMapTest<K, V> {
45  
46      public class TestBidiMapEntrySet extends TestMapEntrySet {
47  
48          public TestBidiMapEntrySet() {
49          }
50  
51          @Test
52          public void testMapEntrySetIteratorEntrySetValueCrossCheck() {
53              final K key1 = getSampleKeys()[0];
54              final K key2 = getSampleKeys()[1];
55              final V newValue1 = getNewSampleValues()[0];
56              final V newValue2 = getNewSampleValues()[1];
57  
58              resetFull();
59              // explicitly get entries as sample values/keys are connected for some maps
60              // such as BeanMap
61              Iterator<Map.Entry<K, V>> it = TestBidiMapEntrySet.this.getCollection().iterator();
62              final Map.Entry<K, V> entry1 = getEntry(it, key1);
63              it = TestBidiMapEntrySet.this.getCollection().iterator();
64              final Map.Entry<K, V> entry2 = getEntry(it, key2);
65              Iterator<Map.Entry<K, V>> itConfirmed = TestBidiMapEntrySet.this.getConfirmed().iterator();
66              final Map.Entry<K, V> entryConfirmed1 = getEntry(itConfirmed, key1);
67              itConfirmed = TestBidiMapEntrySet.this.getConfirmed().iterator();
68              final Map.Entry<K, V> entryConfirmed2 = getEntry(itConfirmed, key2);
69              TestBidiMapEntrySet.this.verify();
70  
71              if (!isSetValueSupported()) {
72                  try {
73                      entry1.setValue(newValue1);
74                  } catch (final UnsupportedOperationException ex) {
75                  }
76                  return;
77              }
78  
79              // these checked in superclass
80              entry1.setValue(newValue1);
81              entryConfirmed1.setValue(newValue1);
82              entry2.setValue(newValue2);
83              entryConfirmed2.setValue(newValue2);
84  
85              // at this point
86              // key1=newValue1, key2=newValue2
87              try {
88                  entry2.setValue(newValue1);  // should remove key1
89              } catch (final IllegalArgumentException ex) {
90                  return;  // simplest way of dealing with tricky situation
91              }
92              entryConfirmed2.setValue(newValue1);
93              AbstractBidiMapTest.this.getConfirmed().remove(key1);
94              assertEquals(newValue1, entry2.getValue());
95              assertTrue(AbstractBidiMapTest.this.getMap().containsKey(entry2.getKey()));
96              assertTrue(AbstractBidiMapTest.this.getMap().containsValue(newValue1));
97              assertEquals(newValue1, AbstractBidiMapTest.this.getMap().get(entry2.getKey()));
98              assertFalse(AbstractBidiMapTest.this.getMap().containsKey(key1));
99              assertFalse(AbstractBidiMapTest.this.getMap().containsValue(newValue2));
100             TestBidiMapEntrySet.this.verify();
101 
102             // check for ConcurrentModification
103             it.next();  // if you fail here, maybe you should be throwing an IAE, see above
104             if (isRemoveSupported()) {
105                 it.remove();
106             }
107         }
108 
109     }
110 
111     public class TestBidiMapIterator extends AbstractMapIteratorTest<K, V> {
112 
113         public TestBidiMapIterator() {
114             super("TestBidiMapIterator");
115         }
116 
117         @Override
118         public V[] addSetValues() {
119             return AbstractBidiMapTest.this.getNewSampleValues();
120         }
121 
122         @Override
123         public Map<K, V> getConfirmedMap() {
124             // assumes makeFullMapIterator() called first
125             return AbstractBidiMapTest.this.getConfirmed();
126         }
127 
128         @Override
129         public BidiMap<K, V> getMap() {
130             // assumes makeFullMapIterator() called first
131             return AbstractBidiMapTest.this.getMap();
132         }
133 
134         @Override
135         public MapIterator<K, V> makeEmptyIterator() {
136             resetEmpty();
137             return AbstractBidiMapTest.this.getMap().mapIterator();
138         }
139 
140         @Override
141         public MapIterator<K, V> makeObject() {
142             resetFull();
143             return AbstractBidiMapTest.this.getMap().mapIterator();
144         }
145 
146         @Override
147         public boolean supportsRemove() {
148             return AbstractBidiMapTest.this.isRemoveSupported();
149         }
150 
151         @Override
152         public boolean supportsSetValue() {
153             return AbstractBidiMapTest.this.isSetValueSupported();
154         }
155 
156         @Override
157         public void verify() {
158             super.verify();
159             AbstractBidiMapTest.this.verify();
160         }
161 
162     }
163 
164     public class TestInverseBidiMap extends AbstractBidiMapTest<V, K> {
165 
166         final AbstractBidiMapTest<K, V> main;
167 
168         public TestInverseBidiMap(final AbstractBidiMapTest<K, V> main) {
169             this.main = main;
170         }
171 
172         @Override
173         public String getCompatibilityVersion() {
174             return main.getCompatibilityVersion();
175         }
176 
177         @Override
178         protected int getIterationBehaviour() {
179             return main.getIterationBehaviour();
180         }
181 
182         @Override
183         public V[] getSampleKeys() {
184             return main.getSampleValues();
185         }
186 
187         @Override
188         public K[] getSampleValues() {
189             return main.getSampleKeys();
190         }
191 
192         @Override
193         public boolean isAllowNullKey() {
194             return main.isAllowNullKey();
195         }
196 
197         @Override
198         public boolean isAllowNullValue() {
199             return main.isAllowNullValue();
200         }
201 
202         @Override
203         public boolean isPutAddSupported() {
204             return main.isPutAddSupported();
205         }
206 
207         @Override
208         public boolean isPutChangeSupported() {
209             return main.isPutChangeSupported();
210         }
211 
212         @Override
213         public boolean isRemoveSupported() {
214             return main.isRemoveSupported();
215         }
216 
217         @Override
218         public boolean isSetValueSupported() {
219             return main.isSetValueSupported();
220         }
221 
222         @Override
223         public BidiMap<V, K> makeFullMap() {
224             return main.makeFullMap().inverseBidiMap();
225         }
226 
227         @Override
228         public BidiMap<V, K> makeObject() {
229             return main.makeObject().inverseBidiMap();
230         }
231     }
232 
233     public AbstractBidiMapTest() {
234         super("Inverse");
235     }
236 
237     public AbstractBidiMapTest(final String testName) {
238         super(testName);
239     }
240 
241     public BulkTest bulkTestBidiMapIterator() {
242         return new TestBidiMapIterator();
243     }
244 
245     public BulkTest bulkTestInverseMap() {
246         return new TestInverseBidiMap(this);
247     }
248 
249     @Override
250     public BulkTest bulkTestMapEntrySet() {
251         return new TestBidiMapEntrySet();
252     }
253 
254     private void doTestGetKey(final BidiMap<?, ?> map, final Object key, final Object value) {
255         assertEquals(value, map.get(key), "Value not found for key.");
256         assertEquals(key, map.getKey(value), "Key not found for value.");
257     }
258 
259     /**
260      * Override as DualHashBidiMap didn't exist until version 3.
261      */
262     @Override
263     public String getCompatibilityVersion() {
264         return "4";
265     }
266 
267     /**
268      * {@inheritDoc}
269      */
270     @Override
271     public BidiMap<K, V> getMap() {
272         return (BidiMap<K, V>) super.getMap();
273     }
274 
275     /**
276      * Override to indicate to AbstractTestMap this is a BidiMap.
277      */
278     @Override
279     public boolean isAllowDuplicateValues() {
280         return false;
281     }
282 
283     /**
284      * Override to create a full {@code BidiMap} other than the default.
285      *
286      * @return a full {@code BidiMap} implementation.
287      */
288     @Override
289     public BidiMap<K, V> makeFullMap() {
290         return (BidiMap<K, V>) super.makeFullMap();
291     }
292 
293     /**
294      * Override to return the empty BidiMap.
295      */
296     @Override
297     public abstract BidiMap<K, V> makeObject();
298 
299     @SuppressWarnings("unchecked")
300     private <T> void modifyEntrySet(final BidiMap<?, T> map) {
301         // Gets first entry
302         final Map.Entry<?, T> entry = map.entrySet().iterator().next();
303 
304         // Gets key and value
305         final Object key = entry.getKey();
306         final Object oldValue = entry.getValue();
307 
308         // Sets new value
309         final Object newValue = "newValue";
310         entry.setValue((T) newValue);
311 
312         assertEquals(
313             newValue,
314             map.get(key),
315                 "Modifying entrySet did not affect underlying Map.");
316 
317         assertNull(
318             map.getKey(oldValue),
319                 "Modifying entrySet did not affect inverse Map.");
320     }
321 
322     private void remove(final BidiMap<?, ?> map, final Object key) {
323         final Object value = map.remove(key);
324         assertFalse(map.containsKey(key), "Key was not removed.");
325         assertNull(map.getKey(value), "Value was not removed.");
326     }
327 
328     private void removeByEntrySet(final BidiMap<?, ?> map, final Object key, final Object value) {
329         final Map<Object, Object> temp = new HashMap<>();
330         temp.put(key, value);
331         map.entrySet().remove(temp.entrySet().iterator().next());
332 
333         assertFalse(map.containsKey(key), "Key was not removed.");
334         assertFalse(map.containsValue(value), "Value was not removed.");
335 
336         assertFalse(map.inverseBidiMap().containsValue(key), "Key was not removed from inverse map.");
337         assertFalse(map.inverseBidiMap().containsKey(value), "Value was not removed from inverse map.");
338     }
339 
340     private void removeByKeySet(final BidiMap<?, ?> map, final Object key, final Object value) {
341         map.remove(key);
342 
343         assertFalse(map.containsKey(key), "Key was not removed.");
344         assertFalse(map.containsValue(value), "Value was not removed.");
345 
346         assertFalse(map.inverseBidiMap().containsValue(key), "Key was not removed from inverse map.");
347         assertFalse(map.inverseBidiMap().containsKey(value), "Value was not removed from inverse map.");
348     }
349 
350     private void removeValue(final BidiMap<?, ?> map, final Object value) {
351         final Object key = map.removeValue(value);
352         assertFalse(map.containsKey(key), "Key was not removed.");
353         assertNull(map.getKey(value), "Value was not removed.");
354     }
355 
356     @Test
357     public void testBidiClear() {
358         if (!isRemoveSupported()) {
359             assertThrows(UnsupportedOperationException.class, () -> makeFullMap().clear());
360             return;
361         }
362 
363         BidiMap<?, ?> map = makeFullMap();
364         map.clear();
365         assertTrue(map.isEmpty(), "Map was not cleared.");
366         assertTrue(map.inverseBidiMap().isEmpty(), "Inverse map was not cleared.");
367 
368         // Tests clear on inverse
369         map = makeFullMap().inverseBidiMap();
370         map.clear();
371         assertTrue(map.isEmpty(), "Map was not cleared.");
372         assertTrue(map.inverseBidiMap().isEmpty(), "Inverse map was not cleared.");
373     }
374 
375     // testGetKey
376     @Test
377     public void testBidiGetKey() {
378         doTestGetKey(makeFullMap(), getSampleKeys()[0], getSampleValues()[0]);
379     }
380 
381     @Test
382     public void testBidiGetKeyInverse() {
383         doTestGetKey(
384             makeFullMap().inverseBidiMap(),
385             getSampleValues()[0],
386             getSampleKeys()[0]);
387     }
388 
389     // testInverse
390     @Test
391     public void testBidiInverse() {
392         final BidiMap<K, V> map = makeFullMap();
393         final BidiMap<V, K> inverseMap = map.inverseBidiMap();
394 
395         assertSame(
396                 map,
397                 inverseMap.inverseBidiMap(),
398                 "Inverse of inverse is not equal to original.");
399 
400         assertEquals(
401                 getSampleKeys()[0],
402                 inverseMap.get(getSampleValues()[0]),
403                 "Value not found for key.");
404 
405         assertEquals(
406                 getSampleValues()[0],
407                 inverseMap.getKey(getSampleKeys()[0]),
408                 "Key not found for value.");
409     }
410 
411     @Test
412     public void testBidiKeySetValuesOrder() {
413         // Skip if collection is unordered
414         Assumptions.assumeFalse((getIterationBehaviour() & AbstractCollectionTest.UNORDERED) != 0);
415         resetFull();
416         final Iterator<K> keys = map.keySet().iterator();
417         final Iterator<V> values = map.values().iterator();
418         while (keys.hasNext() && values.hasNext()) {
419             final K key = keys.next();
420             final V value = values.next();
421             assertSame(map.get(key), value);
422         }
423         assertFalse(keys.hasNext());
424         assertFalse(values.hasNext());
425     }
426 
427     @Test
428     public void testBidiMapIteratorSet() {
429         final V newValue1 = getOtherValues()[0];
430         final V newValue2 = getOtherValues()[1];
431 
432         resetFull();
433         final BidiMap<K, V> bidi = getMap();
434         final MapIterator<K, V> it = bidi.mapIterator();
435         assertTrue(it.hasNext());
436         final K key1 = it.next();
437 
438         if (!isSetValueSupported()) {
439             assertThrows(UnsupportedOperationException.class, () -> it.setValue(newValue1));
440             return;
441         }
442 
443         it.setValue(newValue1);
444         confirmed.put(key1, newValue1);
445         assertSame(key1, it.getKey());
446         assertSame(newValue1, it.getValue());
447         assertTrue(bidi.containsKey(key1));
448         assertTrue(bidi.containsValue(newValue1));
449         assertEquals(newValue1, bidi.get(key1));
450         verify();
451 
452         it.setValue(newValue1);  // same value - should be OK
453         confirmed.put(key1, newValue1);
454         assertSame(key1, it.getKey());
455         assertSame(newValue1, it.getValue());
456         assertTrue(bidi.containsKey(key1));
457         assertTrue(bidi.containsValue(newValue1));
458         assertEquals(newValue1, bidi.get(key1));
459         verify();
460 
461         final K key2 = it.next();
462         it.setValue(newValue2);
463         confirmed.put(key2, newValue2);
464         assertSame(key2, it.getKey());
465         assertSame(newValue2, it.getValue());
466         assertTrue(bidi.containsKey(key2));
467         assertTrue(bidi.containsValue(newValue2));
468         assertEquals(newValue2, bidi.get(key2));
469         verify();
470 
471         // at this point
472         // key1=newValue1, key2=newValue2
473         assertThrows(IllegalArgumentException.class, () -> it.setValue(newValue1));  // should remove key1
474         // below code was previously never executed
475 //        confirmed.put(key2, newValue1);
476 //        AbstractBidiMapTest.this.getConfirmed().remove(key1);
477 //        assertEquals(newValue1, it.getValue());
478 //        assertTrue(bidi.containsKey(it.getKey()));
479 //        assertTrue(bidi.containsValue(newValue1));
480 //        assertEquals(newValue1, bidi.get(it.getKey()));
481 //        assertFalse(bidi.containsKey(key1));
482 //        assertFalse(bidi.containsValue(newValue2));
483 //        verify();
484 //
485 //        // check for ConcurrentModification
486 //        it.next();  // if you fail here, maybe you should be throwing an IAE, see above
487 //        if (isRemoveSupported()) {
488 //            it.remove();
489 //        }
490     }
491 
492     @Test
493     public void testBidiModifyEntrySet() {
494         if (!isSetValueSupported()) {
495             return;
496         }
497 
498         modifyEntrySet(makeFullMap());
499         modifyEntrySet(makeFullMap().inverseBidiMap());
500     }
501 
502     // BidiPut
503     @Test
504     @SuppressWarnings("unchecked")
505     public void testBidiPut() {
506         if (!isPutAddSupported() || !isPutChangeSupported()) {
507             return;
508         }
509 
510         final BidiMap<K, V> map = makeObject();
511         final BidiMap<V, K> inverse = map.inverseBidiMap();
512         assertEquals(0, map.size());
513         assertEquals(map.size(), inverse.size());
514 
515         map.put((K) "A", (V) "B");
516         assertEquals(1, map.size());
517         assertEquals(map.size(), inverse.size());
518         assertEquals("B", map.get("A"));
519         assertEquals("A", inverse.get("B"));
520 
521         map.put((K) "A", (V) "C");
522         assertEquals(1, map.size());
523         assertEquals(map.size(), inverse.size());
524         assertEquals("C", map.get("A"));
525         assertEquals("A", inverse.get("C"));
526 
527         map.put((K) "B", (V) "C");
528         assertEquals(1, map.size());
529         assertEquals(map.size(), inverse.size());
530         assertEquals("C", map.get("B"));
531         assertEquals("B", inverse.get("C"));
532 
533         map.put((K) "E", (V) "F");
534         assertEquals(2, map.size());
535         assertEquals(map.size(), inverse.size());
536         assertEquals("F", map.get("E"));
537         assertEquals("E", inverse.get("F"));
538     }
539 
540     @Test
541     public void testBidiRemove() {
542         if (!isRemoveSupported()) {
543             assertThrows(UnsupportedOperationException.class, () -> makeFullMap().remove(getSampleKeys()[0]));
544 
545             assertThrows(UnsupportedOperationException.class, () -> makeFullMap().removeValue(getSampleValues()[0]));
546 
547             return;
548         }
549 
550         remove(makeFullMap(), getSampleKeys()[0]);
551         remove(makeFullMap().inverseBidiMap(), getSampleValues()[0]);
552 
553         removeValue(makeFullMap(), getSampleValues()[0]);
554         removeValue(makeFullMap().inverseBidiMap(), getSampleKeys()[0]);
555 
556         assertNull(makeFullMap().removeValue("NotPresent"));
557     }
558 
559     @Test
560     public void testBidiRemoveByEntrySet() {
561         if (!isRemoveSupported()) {
562             return;
563         }
564 
565         removeByEntrySet(makeFullMap(), getSampleKeys()[0], getSampleValues()[0]);
566         removeByEntrySet(makeFullMap().inverseBidiMap(), getSampleValues()[0], getSampleKeys()[0]);
567     }
568 
569     @Test
570     public void testBidiRemoveByKeySet() {
571         if (!isRemoveSupported()) {
572             return;
573         }
574 
575         removeByKeySet(makeFullMap(), getSampleKeys()[0], getSampleValues()[0]);
576         removeByKeySet(makeFullMap().inverseBidiMap(), getSampleValues()[0], getSampleKeys()[0]);
577     }
578 
579     /**
580      * Verifies that {@link #map} is still equal to {@link #confirmed}.
581      * <p>
582      * This implementation checks the inverse map as well.
583      */
584     @Override
585     public void verify() {
586         verifyInverse();
587         super.verify();
588     }
589 
590     public void verifyInverse() {
591         assertEquals(map.size(), ((BidiMap<K, V>) map).inverseBidiMap().size());
592         final Map<K, V> map1 = new HashMap<>(map);
593         final Map<V, K> map2 = new HashMap<>(((BidiMap<K, V>) map).inverseBidiMap());
594         final Set<K> keys1 = map1.keySet();
595         final Set<V> keys2 = map2.keySet();
596         final Collection<V> values1 = map1.values();
597         final Collection<K> values2 = map2.values();
598         assertTrue(keys1.containsAll(values2));
599         assertTrue(values2.containsAll(keys1));
600         assertTrue(values1.containsAll(keys2));
601         assertTrue(keys2.containsAll(values1));
602     }
603 
604 }