1
2
3
4
5
6
7
8
9
10
11
12
13
14
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
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
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 }