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 org.junit.Assert.assertEquals;
20  import static org.junit.Assert.assertFalse;
21  import static org.junit.Assert.assertNotNull;
22  import static org.junit.Assert.assertTrue;
23  import static org.junit.Assert.fail;
24  
25  import org.junit.Test;
26  
27  import java.util.ArrayList;
28  import java.util.Arrays;
29  import java.util.Collection;
30  import java.util.Collections;
31  import java.util.HashMap;
32  import java.util.Iterator;
33  import java.util.Map;
34  import java.util.Map.Entry;
35  import java.util.concurrent.atomic.AtomicInteger;
36  
37  public class CopyOnWriteMapTest {
38  
39      @Test public void factoryCalledOnConstructor() {
40          final AtomicInteger count = new AtomicInteger();
41          final Map<String, String> init = MapBuilder.build("1", "o1", "2", "o2", "3", "o3");
42          final Map<String, String> map = new CopyOnWriteMap<String, String>(init) {
43              @Override public <N extends Map<? extends String, ? extends String>> Map<String, String> copy(final N map) {
44                  count.getAndIncrement();
45                  return new HashMap<String, String>(map);
46              }
47          };
48          assertEquals(1, count.get());
49          assertEquals(3, map.size());
50          assertTrue(map.containsKey("2"));
51          assertTrue(map.containsValue("o3"));
52          assertEquals("o1", map.get("1"));
53      }
54  
55      @Test public void factoryCalledOnWrite() {
56          final AtomicInteger count = new AtomicInteger();
57          final Map<String, String> map = new CopyOnWriteMap<String, String>() {
58              @Override public <N extends Map<? extends String, ? extends String>> Map<String, String> copy(final N map) {
59                  count.getAndIncrement();
60                  return new HashMap<String, String>(map);
61              }
62          };
63  
64          assertEquals("should be called in ctor", 1, count.get());
65          map.put("test", "test");
66          assertEquals("should be called in put", 2, count.get());
67          assertEquals(1, map.size());
68          assertTrue(map.containsKey("test"));
69          assertTrue(map.containsValue("test"));
70          assertEquals("should not be called in reads", 2, count.get());
71          map.putAll(MapBuilder.build("1", "test1", "2", "test2", "3", "test3"));
72          assertEquals("should be called in putAll", 3, count.get());
73          assertEquals(4, map.size());
74          assertTrue(map.containsKey("1"));
75          assertTrue(map.containsValue("test3"));
76          map.remove("2");
77          assertEquals("should be called in remove", 4, count.get());
78          assertEquals(3, map.size());
79          assertFalse(map.containsValue("test2"));
80          map.clear();
81          assertEquals("should be called in clear", 5, count.get());
82          assertEquals(0, map.size());
83      }
84  
85      @Test public void hashAndEquality() throws Exception {
86          final Map<String, String> map = MapBuilder.<String, String> builder().add("key", "value").toMap();
87          final CopyOnWriteMap<String, String> cowMap = CopyOnWriteMaps.newHashMap(map);
88          assertEquals(map, cowMap);
89          assertEquals(map.hashCode(), cowMap.hashCode());
90      }
91  
92      @Test public void hashAndEqualityKeySet() throws Exception {
93          final Map<String, String> map = MapBuilder.<String, String> builder().add("key", "value").toMap();
94          final CopyOnWriteMap<String, String> cowMap = CopyOnWriteMaps.newHashMap(map);
95          assertEquals(map.keySet(), cowMap.keySet());
96          assertEquals(map.keySet().hashCode(), cowMap.keySet().hashCode());
97      }
98  
99      @Test public void hashAndEqualityValues() throws Exception {
100         final Map<String, String> map = MapBuilder.<String, String> builder().add("key", "value").toMap();
101         final CopyOnWriteMap<String, String> cowMap = CopyOnWriteMaps.newHashMap(map);
102         assertEquals(new ArrayList<String>(map.values()), new ArrayList<String>(cowMap.values()));
103         assertEquals(new ArrayList<String>(map.values()).hashCode(), new ArrayList<String>(cowMap.values()).hashCode());
104     }
105 
106     @Test public void hashAndEqualityEntrySet() throws Exception {
107         final Map<String, String> map = MapBuilder.<String, String> builder().add("key", "value").toMap();
108         final CopyOnWriteMap<String, String> cowMap = CopyOnWriteMaps.newHashMap(map);
109         assertEquals(map.entrySet(), cowMap.entrySet());
110         assertEquals(map.entrySet().hashCode(), cowMap.entrySet().hashCode());
111     }
112 
113     @Test public void modifiableValues() throws Exception {
114         final AtomicInteger count = new AtomicInteger();
115         final Map<String, String> init = new MapBuilder<String, String>().add("test", "test").add("testing", "testing").toMap();
116         final Map<String, String> map = new CopyOnWriteMap<String, String>(init) {
117             @Override public <N extends Map<? extends String, ? extends String>> Map<String, String> copy(final N map) {
118                 count.getAndIncrement();
119                 return new HashMap<String, String>(map);
120             }
121         };
122         assertEquals(1, count.get());
123         final Collection<String> values = map.values();
124         try {
125             values.add("something");
126             fail("UnsupportedOp expected");
127         }
128         catch (final UnsupportedOperationException ignore) {}
129         assertEquals(1, count.get());
130         try {
131             values.addAll(Arrays.asList("one", "two", "three"));
132             fail("UnsupportedOp expected");
133         }
134         catch (final UnsupportedOperationException ignore) {}
135         final Iterator<String> iterator = values.iterator();
136         assertTrue(iterator.hasNext());
137         assertNotNull(iterator.next());
138         try {
139             iterator.remove();
140             fail("UnsupportedOp expected");
141         }
142         catch (final UnsupportedOperationException ignore) {}
143         assertEquals(1, count.get());
144         assertFalse(values.remove("blah"));
145         assertEquals("not modified if element not present to be removed", 1, count.get());
146         assertTrue(values.remove("test"));
147         assertEquals(2, count.get());
148         assertEquals(1, map.size());
149         assertFalse(values.retainAll(Arrays.asList("testing")));
150         assertEquals(3, count.get());
151         assertEquals(1, map.size());
152         assertTrue(values.removeAll(Arrays.asList("test", "blah", "testing")));
153         assertEquals(4, count.get());
154         assertEquals(0, map.size());
155     }
156 
157     @Test public void modifiableEntrySet() throws Exception {
158         final AtomicInteger count = new AtomicInteger();
159         final Map<String, String> init = new MapBuilder<String, String>().add("test", "test").add("testing", "testing").toMap();
160         final Map<String, String> map = new CopyOnWriteMap<String, String>(init) {
161             @Override public <N extends Map<? extends String, ? extends String>> Map<String, String> copy(final N map) {
162                 count.getAndIncrement();
163                 return new HashMap<String, String>(map);
164             }
165         };
166         assertEquals(1, count.get());
167         final Collection<Entry<String, String>> keys = map.entrySet();
168         class E implements Map.Entry<String, String> {
169             final String e;
170 
171             public E(final String e) {
172                 this.e = e;
173             }
174 
175             public String getKey() {
176                 return e;
177             }
178 
179             public String getValue() {
180                 return e;
181             }
182 
183             public String setValue(final String value) {
184                 throw new RuntimeException("should not be called, don't use UnsupportedOp here");
185             }
186         }
187 
188         try {
189             keys.add(new E("something"));
190             fail("UnsupportedOp expected");
191         }
192         catch (final UnsupportedOperationException ignore) {}
193         assertEquals(1, count.get());
194         try {
195             keys.addAll(Arrays.asList(new E("one"), new E("two"), new E("three")));
196             fail("UnsupportedOp expected");
197         }
198         catch (final UnsupportedOperationException ignore) {}
199         final Iterator<Entry<String, String>> iterator = keys.iterator();
200         assertTrue(iterator.hasNext());
201         assertNotNull(iterator.next());
202         try {
203             iterator.remove();
204             fail("UnsupportedOp expected");
205         }
206         catch (final UnsupportedOperationException ignore) {}
207         assertEquals(1, count.get());
208         assertFalse(keys.remove("blah"));
209         assertEquals("not modified if element not present to be removed", 1, count.get());
210         assertTrue(keys.remove(new E("test")));
211         assertEquals(2, count.get());
212         assertEquals(1, map.size());
213         assertFalse(keys.retainAll(Arrays.asList(new E("testing"))));
214         assertEquals(3, count.get());
215         assertEquals(1, map.size());
216         assertTrue(keys.removeAll(Arrays.asList(new E("test"), new E("blah"), new E("testing"))));
217         assertEquals(4, count.get());
218         assertEquals(0, map.size());
219     }
220 
221     @Test public void modifiableKeySet() throws Exception {
222         final AtomicInteger count = new AtomicInteger();
223 
224         final Map<String, String> init = new MapBuilder<String, String>().add("test", "test").add("testing", "testing").toMap();
225         final Map<String, String> map = new CopyOnWriteMap<String, String>(init) {
226             @Override public <N extends Map<? extends String, ? extends String>> Map<String, String> copy(final N map) {
227                 count.getAndIncrement();
228                 return new HashMap<String, String>(map);
229             }
230         };
231         assertEquals(1, count.get());
232         final Collection<String> keys = map.keySet();
233         try {
234             keys.add("something");
235             fail("UnsupportedOp expected");
236         }
237         catch (final UnsupportedOperationException ignore) {}
238         assertEquals(1, count.get());
239         try {
240             keys.addAll(Arrays.asList("one", "two", "three"));
241             fail("UnsupportedOp expected");
242         }
243         catch (final UnsupportedOperationException ignore) {}
244         final Iterator<String> iterator = keys.iterator();
245         assertTrue(iterator.hasNext());
246         assertNotNull(iterator.next());
247         try {
248             iterator.remove();
249             fail("UnsupportedOp expected");
250         }
251         catch (final UnsupportedOperationException ignore) {}
252         assertEquals(1, count.get());
253         assertFalse(keys.remove("blah"));
254         assertEquals("not modified if element not present to be removed", 1, count.get());
255         assertTrue(keys.remove("test"));
256         assertEquals(2, count.get());
257         assertEquals(1, map.size());
258         assertFalse(keys.retainAll(Arrays.asList("testing")));
259         assertEquals(3, count.get());
260         assertEquals(1, map.size());
261         assertTrue(keys.removeAll(Arrays.asList("test", "blah", "testing")));
262         assertEquals(4, count.get());
263         assertEquals(0, map.size());
264     }
265 
266     @Test public void nullMap() throws Exception {
267         try {
268             new CopyOnWriteMap<String, String>(null) {
269                 @Override public <N extends Map<? extends String, ? extends String>> Map<String, String> copy(final N map) {
270                     return new HashMap<String, String>(map);
271                 };
272             };
273             fail("Should have thrown IllegalArgumentEx");
274         }
275         catch (final IllegalArgumentException ignore) {}
276     }
277 
278     @Test public void copyFunctionReturnsNull() throws Exception {
279         try {
280             new CopyOnWriteMap<String, String>() {
281                 @Override public <N extends Map<? extends String, ? extends String>> Map<String, String> copy(final N map) {
282                     return null;
283                 };
284             };
285             fail("Should have thrown IllegalArgumentEx");
286         }
287         catch (final IllegalArgumentException ignore) {}
288     }
289 }
290 
291 class MapBuilder<K, V> {
292     private final Map<K, V> map = new HashMap<K, V>();
293 
294     static <S> Map<S, S> build(final S... elements) {
295         if (elements.length % 2 != 0) {
296             throw new IllegalArgumentException("must have even number of elements: " + elements.length);
297         }
298         final MapBuilder<S, S> result = new MapBuilder<S, S>();
299         for (int i = 0; i < elements.length; i = i + 2) {
300             result.add(elements[i], elements[i + 1]);
301         }
302         return result.toMap();
303     }
304 
305     static <K, V> MapBuilder<K, V> builder() {
306         return new MapBuilder<K, V>();
307     }
308 
309     MapBuilder<K, V> add(final K key, final V value) {
310         map.put(key, value);
311         return this;
312     }
313 
314     Entry<K, V> entry(final K key, final V value) {
315         return new Entry<K, V>() {
316             public K getKey() {
317                 return key;
318             }
319 
320             public V getValue() {
321                 return value;
322             }
323 
324             public V setValue(final V arg0) {
325                 throw new UnsupportedOperationException();
326             };
327         };
328     }
329 
330     Map<K, V> toMap() {
331         return Collections.unmodifiableMap(new HashMap<K, V>(map));
332     }
333 }