1   /**
2    * Copyright 2008 Atlassian Pty Ltd 
3    * 
4    * Licensed under the Apache License, Version 2.0 (the "License"); 
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at 
7    * 
8    *     http://www.apache.org/licenses/LICENSE-2.0 
9    * 
10   * Unless required by applicable law or agreed to in writing, software 
11   * distributed under the License is distributed on an "AS IS" BASIS, 
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 
13   * See the License for the specific language governing permissions and 
14   * limitations under the License.
15   */
16  
17  package com.atlassian.util.concurrent;
18  
19  import static java.util.Arrays.asList;
20  import static org.junit.Assert.assertArrayEquals;
21  import static org.junit.Assert.assertEquals;
22  import static org.junit.Assert.assertFalse;
23  import static org.junit.Assert.assertNotNull;
24  import static org.junit.Assert.assertTrue;
25  import static org.junit.Assert.fail;
26  
27  import java.io.ByteArrayInputStream;
28  import java.io.ByteArrayOutputStream;
29  import java.io.ObjectInputStream;
30  import java.io.ObjectOutputStream;
31  import java.util.ArrayList;
32  import java.util.Collection;
33  import java.util.Collections;
34  import java.util.HashMap;
35  import java.util.Iterator;
36  import java.util.Map;
37  import java.util.Map.Entry;
38  import java.util.concurrent.atomic.AtomicInteger;
39  import java.util.concurrent.atomic.AtomicReference;
40  
41  import org.junit.Test;
42  
43  public class CopyOnWriteMapTest {
44  
45      @Test
46      public void factoryCalledOnConstructor() {
47          final AtomicInteger count = new AtomicInteger();
48          final Map<String, String> init = MapBuilder.build("1", "o1", "2", "o2", "3", "o3");
49          final Map<String, String> map = new CopyOnWriteMap<String, String>(init) {
50              private static final long serialVersionUID = 8866224559807093002L;
51  
52              @Override
53              public <N extends Map<? extends String, ? extends String>> Map<String, String> copy(final N map) {
54                  count.getAndIncrement();
55                  return new HashMap<String, String>(map);
56              }
57          };
58          assertEquals(1, count.get());
59          assertEquals(3, map.size());
60          assertTrue(map.containsKey("2"));
61          assertTrue(map.containsValue("o3"));
62          assertEquals("o1", map.get("1"));
63      }
64  
65      @Test
66      public void factoryCalledOnWrite() {
67          final AtomicInteger count = new AtomicInteger();
68          final Map<String, String> map = new CopyOnWriteMap<String, String>() {
69              private static final long serialVersionUID = -3858713272422952372L;
70  
71              @Override
72              public <N extends Map<? extends String, ? extends String>> Map<String, String> copy(final N map) {
73                  count.getAndIncrement();
74                  return new HashMap<String, String>(map);
75              }
76          };
77  
78          assertEquals("should be called in ctor", 1, count.get());
79          map.put("test", "test");
80          assertEquals("should be called in put", 2, count.get());
81          assertEquals(1, map.size());
82          assertTrue(map.containsKey("test"));
83          assertTrue(map.containsValue("test"));
84          assertEquals("should not be called in reads", 2, count.get());
85          map.putAll(MapBuilder.build("1", "test1", "2", "test2", "3", "test3"));
86          assertEquals("should be called in putAll", 3, count.get());
87          assertEquals(4, map.size());
88          assertTrue(map.containsKey("1"));
89          assertTrue(map.containsValue("test3"));
90          map.remove("2");
91          assertEquals("should be called in remove", 4, count.get());
92          assertEquals(3, map.size());
93          assertFalse(map.containsValue("test2"));
94          map.clear();
95          assertEquals("should be called in clear", 5, count.get());
96          assertEquals(0, map.size());
97      }
98  
99      @Test
100     public void delegateHashMap() throws Exception {
101         final Map<String, String> map = MapBuilder.<String, String> builder().add("key", "value").toMap();
102         final CopyOnWriteMap<String, String> cowMap = CopyOnWriteMap.newHashMap(map);
103         assertEquals(map, cowMap);
104         assertEquals(map.hashCode(), cowMap.hashCode());
105         assertEquals(map.toString(), cowMap.toString());
106     }
107 
108     @Test
109     public void delegateKeySet() throws Exception {
110         final Map<String, String> map = MapBuilder.<String, String> builder().add("key", "value").toMap();
111         final CopyOnWriteMap<String, String> cowMap = CopyOnWriteMap.newHashMap(map);
112         assertEquals(map.keySet(), cowMap.keySet());
113         assertEquals(map.keySet().hashCode(), cowMap.keySet().hashCode());
114         assertEquals(map.keySet().toString(), cowMap.keySet().toString());
115     }
116 
117     @Test
118     public void delegateEqualityValues() throws Exception {
119         final Map<String, String> map = MapBuilder.<String, String> builder().add("key", "value").toMap();
120         final CopyOnWriteMap<String, String> cowMap = CopyOnWriteMap.newHashMap(map);
121         assertEquals(new ArrayList<String>(map.values()), new ArrayList<String>(cowMap.values()));
122         assertEquals(new ArrayList<String>(map.values()).hashCode(), new ArrayList<String>(cowMap.values()).hashCode());
123         assertEquals(map.values().toString(), cowMap.values().toString());
124     }
125 
126     @Test
127     public void delegateEntrySet() throws Exception {
128         final Map<String, String> map = MapBuilder.<String, String> builder().add("key", "value").toMap();
129         final CopyOnWriteMap<String, String> cowMap = CopyOnWriteMap.newHashMap(map);
130         assertEquals(map.entrySet(), cowMap.entrySet());
131         assertEquals(map.entrySet().hashCode(), cowMap.entrySet().hashCode());
132         assertEquals(map.entrySet().toString(), cowMap.entrySet().toString());
133     }
134 
135     @Test
136     public void delegateLinked() throws Exception {
137         final Map<String, String> map = MapBuilder.<String, String> builder().add("key", "value").toMap();
138         final CopyOnWriteMap<String, String> cowMap = CopyOnWriteMap.newLinkedMap(map);
139         assertEquals(map, cowMap);
140         assertEquals(map.hashCode(), cowMap.hashCode());
141         assertEquals(map.toString(), cowMap.toString());
142     }
143 
144     @Test
145     public void delegateKeySetLinked() throws Exception {
146         final Map<String, String> map = MapBuilder.<String, String> builder().add("key", "value").toMap();
147         final CopyOnWriteMap<String, String> cowMap = CopyOnWriteMap.newLinkedMap(map);
148         assertEquals(map.keySet(), cowMap.keySet());
149         assertEquals(map.keySet().hashCode(), cowMap.keySet().hashCode());
150         assertEquals(map.keySet().toString(), cowMap.keySet().toString());
151     }
152 
153     @Test
154     public void delegateValuesLinked() throws Exception {
155         final Map<String, String> map = MapBuilder.<String, String> builder().add("key", "value").toMap();
156         final CopyOnWriteMap<String, String> cowMap = CopyOnWriteMap.newLinkedMap(map);
157         assertEquals(new ArrayList<String>(map.values()), new ArrayList<String>(cowMap.values()));
158         assertEquals(new ArrayList<String>(map.values()).hashCode(), new ArrayList<String>(cowMap.values()).hashCode());
159         assertEquals(map.values().toString(), cowMap.values().toString());
160     }
161 
162     @Test
163     public void delegateEntrySetLinked() throws Exception {
164         final Map<String, String> map = MapBuilder.<String, String> builder().add("key", "value").toMap();
165         final CopyOnWriteMap<String, String> cowMap = CopyOnWriteMap.newLinkedMap(map);
166         assertEquals(map.entrySet(), cowMap.entrySet());
167         assertEquals(map.entrySet().hashCode(), cowMap.entrySet().hashCode());
168         assertEquals(map.entrySet().toString(), cowMap.entrySet().toString());
169     }
170 
171     @Test
172     public void modifiableValues() throws Exception {
173         final AtomicInteger count = new AtomicInteger();
174         final Map<String, String> init = new MapBuilder<String, String>().add("test", "test").add("testing", "testing").add("sup", "tester").toMap();
175         final Map<String, String> map = new CopyOnWriteMap<String, String>(init) {
176             private static final long serialVersionUID = 3275978982528321604L;
177 
178             @Override
179             public <N extends Map<? extends String, ? extends String>> Map<String, String> copy(final N map) {
180                 count.getAndIncrement();
181                 return new HashMap<String, String>(map);
182             }
183         };
184         assertEquals(1, count.get());
185         final Collection<String> values = map.values();
186         try {
187             values.add("something");
188             fail("UnsupportedOp expected");
189         } catch (final UnsupportedOperationException ignore) {}
190         assertEquals(1, count.get());
191         try {
192             values.addAll(asList("one", "two", "three"));
193             fail("UnsupportedOp expected");
194         } catch (final UnsupportedOperationException ignore) {}
195         final Iterator<String> iterator = values.iterator();
196         assertTrue(iterator.hasNext());
197         assertNotNull(iterator.next());
198         try {
199             iterator.remove();
200             fail("UnsupportedOp expected");
201         } catch (final UnsupportedOperationException ignore) {}
202         assertEquals(1, count.get());
203         assertFalse(values.remove("blah"));
204         assertEquals("not modified if element not present to be removed", 1, count.get());
205         assertTrue(values.remove("test"));
206         assertEquals(2, count.get());
207         assertEquals(2, map.size());
208         assertFalse(values.retainAll(asList("testing", "tester")));
209         assertEquals(3, count.get());
210         assertEquals(2, map.size());
211         assertTrue(values.removeAll(asList("test", "testing")));
212         assertEquals(4, count.get());
213         assertEquals(1, map.size());
214         values.clear();
215         assertTrue(map.isEmpty());
216     }
217 
218     @Test
219     public void modifiableEntrySet() throws Exception {
220         final AtomicInteger count = new AtomicInteger();
221         final Map<String, String> init = new MapBuilder<String, String>().add("test", "test").add("testing", "testing").add("tester", "tester")
222             .toMap();
223         final Map<String, String> map = new CopyOnWriteMap<String, String>(init) {
224             private static final long serialVersionUID = -2882860445706454721L;
225 
226             @Override
227             public <N extends Map<? extends String, ? extends String>> Map<String, String> copy(final N map) {
228                 count.getAndIncrement();
229                 return new HashMap<String, String>(map);
230             }
231         };
232         assertEquals(1, count.get());
233         final Collection<Entry<String, String>> entries = map.entrySet();
234         class E implements Map.Entry<String, String> {
235             final String e;
236 
237             public E(final String e) {
238                 this.e = e;
239             }
240 
241             public String getKey() {
242                 return e;
243             }
244 
245             public String getValue() {
246                 return e;
247             }
248 
249             public String setValue(final String value) {
250                 throw new RuntimeException("should not be called, don't use UnsupportedOp here");
251             }
252         }
253 
254         try {
255             entries.add(new E("something"));
256             fail("UnsupportedOp expected");
257         } catch (final UnsupportedOperationException ignore) {}
258         assertEquals(1, count.get());
259         try {
260             entries.addAll(asList(new E("one"), new E("two"), new E("three")));
261             fail("UnsupportedOp expected");
262         } catch (final UnsupportedOperationException ignore) {}
263         final Iterator<Entry<String, String>> iterator = entries.iterator();
264         assertTrue(iterator.hasNext());
265         assertNotNull(iterator.next());
266         try {
267             iterator.remove();
268             fail("UnsupportedOp expected");
269         } catch (final UnsupportedOperationException ignore) {}
270         assertEquals(1, count.get());
271         assertFalse(entries.remove("blah"));
272         assertEquals("not modified if element not present to be removed", 1, count.get());
273         assertTrue(entries.remove(new E("test")));
274         assertEquals(2, count.get());
275         assertEquals(2, map.size());
276         assertFalse(entries.retainAll(asList(new E("testing"), new E("tester"))));
277         assertEquals(3, count.get());
278         assertEquals(2, map.size());
279         assertTrue(entries.removeAll(asList(new E("test"), new E("testing"))));
280         assertEquals(4, count.get());
281         assertEquals(1, map.size());
282         entries.clear();
283         assertTrue(map.isEmpty());
284     }
285 
286     @Test
287     public void modifiableKeySet() throws Exception {
288         final AtomicInteger count = new AtomicInteger();
289 
290         final Map<String, String> init = new MapBuilder<String, String>().add("test", "test").add("testing", "testing").add("tester", "tester")
291             .toMap();
292         final Map<String, String> map = new CopyOnWriteMap<String, String>(init) {
293             private static final long serialVersionUID = 7273654247572679525L;
294 
295             @Override
296             public <N extends Map<? extends String, ? extends String>> Map<String, String> copy(final N map) {
297                 count.getAndIncrement();
298                 return new HashMap<String, String>(map);
299             }
300         };
301         assertEquals(1, count.get());
302         final Collection<String> keys = map.keySet();
303         try {
304             keys.add("something");
305             fail("UnsupportedOp expected");
306         } catch (final UnsupportedOperationException ignore) {}
307         assertEquals(1, count.get());
308         try {
309             keys.addAll(asList("one", "two", "three"));
310             fail("UnsupportedOp expected");
311         } catch (final UnsupportedOperationException ignore) {}
312         final Iterator<String> iterator = keys.iterator();
313         assertTrue(iterator.hasNext());
314         assertNotNull(iterator.next());
315         try {
316             iterator.remove();
317             fail("UnsupportedOp expected");
318         } catch (final UnsupportedOperationException ignore) {}
319         assertEquals(1, count.get());
320         assertFalse(keys.remove("blah"));
321         assertEquals("not modified if element not present to be removed", 1, count.get());
322         assertTrue(keys.remove("test"));
323         assertEquals(2, count.get());
324         assertEquals(2, map.size());
325         assertFalse(keys.retainAll(asList("testing", "tester")));
326         assertEquals(3, count.get());
327         assertEquals(2, map.size());
328         assertTrue(keys.removeAll(asList("test", "testing")));
329         assertEquals(4, count.get());
330         assertEquals(1, map.size());
331         keys.clear();
332         assertTrue(map.isEmpty());
333     }
334 
335     @Test(expected = IllegalArgumentException.class)
336     public void nullMap() throws Exception {
337         new CopyOnWriteMap<String, String>(null) {
338             private static final long serialVersionUID = 4223850632932526917L;
339 
340             // /CLOVER:OFF
341             @Override
342             public <N extends Map<? extends String, ? extends String>> Map<String, String> copy(final N map) {
343                 return new HashMap<String, String>(map);
344             };
345             // /CLOVER:ON
346         };
347     }
348 
349     @Test(expected = IllegalArgumentException.class)
350     public void copyFunctionReturnsNull() throws Exception {
351         new CopyOnWriteMap<String, String>() {
352             private static final long serialVersionUID = 831716474176011289L;
353 
354             @Override
355             public <N extends Map<? extends String, ? extends String>> Map<String, String> copy(final N map) {
356                 return null;
357             };
358         };
359     }
360 
361     @Test
362     public void serializableHashMap() {
363         final CopyOnWriteMap<Object, Object> map = CopyOnWriteMap.newHashMap();
364         assertMutableMapSerializable(map);
365     }
366 
367     @Test
368     public void serializableLinkedMap() {
369         final CopyOnWriteMap<Object, Object> map = CopyOnWriteMap.newLinkedMap();
370         assertMutableMapSerializable(map);
371     }
372 
373     @Test
374     public void toStringTest() throws Exception {
375         final AtomicReference<Map<String, String>> ref = new AtomicReference<Map<String, String>>();
376         final CopyOnWriteMap<String, String> cowMap = new CopyOnWriteMap<String, String>() {
377             private static final long serialVersionUID = -17380087385174856L;
378 
379             @Override
380             protected <N extends Map<? extends String, ? extends String>> java.util.Map<String, String> copy(final N map) {
381                 ref.set(new HashMap<String, String>(map));
382                 return ref.get();
383             };
384         };
385         assertEquals(ref.get().toString(), cowMap.toString());
386     }
387 
388     @Test
389     public void isEmpty() throws Exception {
390         final CopyOnWriteMap<String, String> cowMap = CopyOnWriteMap.newHashMap();
391         assertTrue(cowMap.isEmpty());
392         assertTrue(cowMap.keySet().isEmpty());
393         assertTrue(cowMap.entrySet().isEmpty());
394         assertTrue(cowMap.values().isEmpty());
395         cowMap.put("1", "1");
396         assertFalse(cowMap.isEmpty());
397         assertFalse(cowMap.keySet().isEmpty());
398         assertFalse(cowMap.entrySet().isEmpty());
399         assertFalse(cowMap.values().isEmpty());
400     }
401 
402     @Test
403     public void equality() throws Exception {
404         final Map<String, String> init = new MapBuilder<String, String>().add("test", "test").add("testing", "testing").toMap();
405         final CopyOnWriteMap<String, String> map = CopyOnWriteMap.newHashMap(init);
406         assertEquals(init, map);
407         assertEquals(map, init);
408         assertEquals(init.hashCode(), map.hashCode());
409         assertEquals(map.hashCode(), init.hashCode());
410         assertEquals(init.keySet(), map.keySet());
411         assertEquals(map.keySet(), init.keySet());
412         assertEquals(init.entrySet(), map.entrySet());
413         assertEquals(map.entrySet(), init.entrySet());
414         assertFalse(init.values().equals(map.values()));
415         assertFalse(map.values().equals(init.values()));
416     }
417 
418     @Test
419     public void toArray() throws Exception {
420         final Map<String, String> init = new MapBuilder<String, String>().add("test", "test").add("testing", "testing").toMap();
421         final CopyOnWriteMap<String, String> map = CopyOnWriteMap.newHashMap(init);
422         assertArrayEquals(init.keySet().toArray(new String[2]), map.keySet().toArray(new String[2]));
423         assertArrayEquals(init.values().toArray(new String[2]), map.values().toArray(new String[2]));
424         assertArrayEquals(init.entrySet().toArray(new Map.Entry[2]), map.entrySet().toArray(new Map.Entry[2]));
425     }
426 
427     @Test
428     public void contains() throws Exception {
429         final Map<String, String> map = CopyOnWriteMap.newHashMap(MapBuilder.build("1", "o1", "2", "o2", "3", "o3"));
430         assertTrue(map.containsKey("2"));
431         assertTrue(map.containsValue("o2"));
432         assertTrue(map.keySet().contains("2"));
433         assertTrue(map.keySet().containsAll(asList(new String[] { "1", "2", "3" })));
434         assertTrue(map.values().contains("o2"));
435         assertTrue(map.values().containsAll(asList(new String[] { "o1", "o2", "o3" })));
436     }
437 
438     static void assertMutableMapSerializable(final Map<Object, Object> map) {
439         map.put("1", "one");
440         assertSerializable(map);
441         assertTrue(map.containsKey("1"));
442         assertTrue(map.containsValue("one"));
443         assertEquals("1", map.keySet().iterator().next());
444         assertEquals("one", map.values().iterator().next());
445         final Map.Entry<Object, Object> entry = map.entrySet().iterator().next();
446         assertEquals("1", entry.getKey());
447         assertEquals("one", entry.getValue());
448     }
449 
450     static void assertSerializable(final Map<?, ?> map) {
451         final ByteArrayOutputStream bytes = new ByteArrayOutputStream();
452         try {
453             new ObjectOutputStream(bytes).writeObject(map);
454             new ObjectInputStream(new ByteArrayInputStream(bytes.toByteArray())).readObject();
455         } catch (final Exception e) {
456             throw new RuntimeException(e);
457         }
458     }
459 }
460 
461 class MapBuilder<K, V> {
462     private final Map<K, V> map = new HashMap<K, V>();
463 
464     static <S> Map<S, S> build(final S... elements) {
465         if (elements.length % 2 != 0) {
466             throw new IllegalArgumentException("must have even number of elements: " + elements.length);
467         }
468         final MapBuilder<S, S> result = new MapBuilder<S, S>();
469         for (int i = 0; i < elements.length; i = i + 2) {
470             result.add(elements[i], elements[i + 1]);
471         }
472         return result.toMap();
473     }
474 
475     static <K, V> MapBuilder<K, V> builder() {
476         return new MapBuilder<K, V>();
477     }
478 
479     MapBuilder<K, V> add(final K key, final V value) {
480         map.put(key, value);
481         return this;
482     }
483 
484     Entry<K, V> entry(final K key, final V value) {
485         return new Entry<K, V>() {
486             public K getKey() {
487                 return key;
488             }
489 
490             public V getValue() {
491                 return value;
492             }
493 
494             public V setValue(final V arg0) {
495                 throw new UnsupportedOperationException();
496             };
497         };
498     }
499 
500     Map<K, V> toMap() {
501         return Collections.unmodifiableMap(new HashMap<K, V>(map));
502     }
503 }