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