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 com.atlassian.util.concurrent.TestUtil.serialize;
20  import static java.util.Arrays.asList;
21  import static org.junit.Assert.assertArrayEquals;
22  import static org.junit.Assert.assertEquals;
23  import static org.junit.Assert.assertFalse;
24  import static org.junit.Assert.assertNotNull;
25  import static org.junit.Assert.assertNull;
26  import static org.junit.Assert.assertTrue;
27  import static org.junit.Assert.fail;
28  
29  import java.util.ArrayList;
30  import java.util.Collection;
31  import java.util.Collections;
32  import java.util.HashMap;
33  import java.util.Iterator;
34  import java.util.Map;
35  import java.util.Map.Entry;
36  import java.util.concurrent.ConcurrentMap;
37  import java.util.concurrent.atomic.AtomicInteger;
38  import java.util.concurrent.atomic.AtomicReference;
39  
40  import org.junit.Test;
41  
42  public class CopyOnWriteMapTest {
43  
44      @Test
45      public void factoryCalledOnConstructor() {
46          final AtomicInteger count = new AtomicInteger();
47          final Map<String, String> init = MapBuilder.build("1", "o1", "2", "o2", "3", "o3");
48          final Map<String, String> map = new CopyOnWriteMap<String, String>(init) {
49              private static final long serialVersionUID = 8866224559807093002L;
50  
51              @Override
52              public <N extends Map<? extends String, ? extends String>> Map<String, String> copy(final N map) {
53                  count.getAndIncrement();
54                  return new HashMap<String, String>(map);
55              }
56          };
57          assertEquals(1, count.get());
58          assertEquals(3, map.size());
59          assertTrue(map.containsKey("2"));
60          assertTrue(map.containsValue("o3"));
61          assertEquals("o1", map.get("1"));
62      }
63  
64      @Test
65      public void factoryCalledOnWrite() {
66          final AtomicInteger count = new AtomicInteger();
67          final Map<String, String> map = new CopyOnWriteMap<String, String>() {
68              private static final long serialVersionUID = -3858713272422952372L;
69  
70              @Override
71              public <N extends Map<? extends String, ? extends String>> Map<String, String> copy(final N map) {
72                  count.getAndIncrement();
73                  return new HashMap<String, String>(map);
74              }
75          };
76  
77          assertEquals("should be called in ctor", 1, count.get());
78          map.put("test", "test");
79          assertEquals("should be called in put", 2, count.get());
80          assertEquals(1, map.size());
81          assertTrue(map.containsKey("test"));
82          assertTrue(map.containsValue("test"));
83          assertEquals("should not be called in reads", 2, count.get());
84          map.putAll(MapBuilder.build("1", "test1", "2", "test2", "3", "test3"));
85          assertEquals("should be called in putAll", 3, count.get());
86          assertEquals(4, map.size());
87          assertTrue(map.containsKey("1"));
88          assertTrue(map.containsValue("test3"));
89          map.remove("2");
90          assertEquals("should be called in remove", 4, count.get());
91          assertEquals(3, map.size());
92          assertFalse(map.containsValue("test2"));
93          map.clear();
94          assertEquals("should be called in clear", 5, count.get());
95          assertEquals(0, map.size());
96      }
97  
98      @Test
99      public void delegateHashMap() throws Exception {
100         final Map<String, String> map = MapBuilder.<String, String> builder().add("key", "value").toMap();
101         final CopyOnWriteMap<String, String> cowMap = CopyOnWriteMap.newHashMap(map);
102         assertEquals(map, cowMap);
103         assertEquals(map.hashCode(), cowMap.hashCode());
104         assertEquals(map.toString(), cowMap.toString());
105     }
106 
107     @Test
108     public void delegateKeySet() throws Exception {
109         final Map<String, String> map = MapBuilder.<String, String> builder().add("key", "value").toMap();
110         final CopyOnWriteMap<String, String> cowMap = CopyOnWriteMap.newHashMap(map);
111         assertEquals(map.keySet(), cowMap.keySet());
112         assertEquals(map.keySet().hashCode(), cowMap.keySet().hashCode());
113         assertEquals(map.keySet().toString(), cowMap.keySet().toString());
114     }
115 
116     @Test
117     public void delegateEqualityValues() throws Exception {
118         final Map<String, String> map = MapBuilder.<String, String> builder().add("key", "value").toMap();
119         final CopyOnWriteMap<String, String> cowMap = CopyOnWriteMap.newHashMap(map);
120         assertEquals(new ArrayList<String>(map.values()), new ArrayList<String>(cowMap.values()));
121         assertEquals(new ArrayList<String>(map.values()).hashCode(), new ArrayList<String>(cowMap.values()).hashCode());
122         assertEquals(map.values().toString(), cowMap.values().toString());
123     }
124 
125     @Test
126     public void delegateEntrySet() throws Exception {
127         final Map<String, String> map = MapBuilder.<String, String> builder().add("key", "value").toMap();
128         final CopyOnWriteMap<String, String> cowMap = CopyOnWriteMap.newHashMap(map);
129         assertEquals(map.entrySet(), cowMap.entrySet());
130         assertEquals(map.entrySet().hashCode(), cowMap.entrySet().hashCode());
131         assertEquals(map.entrySet().toString(), cowMap.entrySet().toString());
132     }
133 
134     @Test
135     public void delegateLinked() throws Exception {
136         final Map<String, String> map = MapBuilder.<String, String> builder().add("key", "value").toMap();
137         final CopyOnWriteMap<String, String> cowMap = CopyOnWriteMap.newLinkedMap(map);
138         assertEquals(map, cowMap);
139         assertEquals(map.hashCode(), cowMap.hashCode());
140         assertEquals(map.toString(), cowMap.toString());
141     }
142 
143     @Test
144     public void delegateKeySetLinked() throws Exception {
145         final Map<String, String> map = MapBuilder.<String, String> builder().add("key", "value").toMap();
146         final CopyOnWriteMap<String, String> cowMap = CopyOnWriteMap.newLinkedMap(map);
147         assertEquals(map.keySet(), cowMap.keySet());
148         assertEquals(map.keySet().hashCode(), cowMap.keySet().hashCode());
149         assertEquals(map.keySet().toString(), cowMap.keySet().toString());
150     }
151 
152     @Test
153     public void delegateValuesLinked() throws Exception {
154         final Map<String, String> map = MapBuilder.<String, String> builder().add("key", "value").toMap();
155         final CopyOnWriteMap<String, String> cowMap = CopyOnWriteMap.newLinkedMap(map);
156         assertEquals(new ArrayList<String>(map.values()), new ArrayList<String>(cowMap.values()));
157         assertEquals(new ArrayList<String>(map.values()).hashCode(), new ArrayList<String>(cowMap.values()).hashCode());
158         assertEquals(map.values().toString(), cowMap.values().toString());
159     }
160 
161     @Test
162     public void delegateEntrySetLinked() throws Exception {
163         final Map<String, String> map = MapBuilder.<String, String> builder().add("key", "value").toMap();
164         final CopyOnWriteMap<String, String> cowMap = CopyOnWriteMap.newLinkedMap(map);
165         assertEquals(map.entrySet(), cowMap.entrySet());
166         assertEquals(map.entrySet().hashCode(), cowMap.entrySet().hashCode());
167         assertEquals(map.entrySet().toString(), cowMap.entrySet().toString());
168     }
169 
170     @Test
171     public void putIfAbsentWorksIfAbsent() throws Exception {
172         final ConcurrentMap<String, String> map = CopyOnWriteMap.newHashMap(MapBuilder.<String, String> builder().add("key", "value").toMap());
173         assertNull(map.putIfAbsent("key2", "value2"));
174         assertEquals(2, map.size());
175         assertTrue(map.containsKey("key2"));
176         assertTrue(map.containsValue("value2"));
177     }
178 
179     @Test
180     public void putIfAbsentFailsIfPresent() throws Exception {
181         final ConcurrentMap<String, String> map = CopyOnWriteMap.newHashMap(MapBuilder.<String, String> builder().add("key", "value").add("key2",
182             "value2").toMap());
183         map.putIfAbsent("key2", "value3");
184         assertNotNull(map.putIfAbsent("key2", "value2"));
185         assertEquals(2, map.size());
186         assertTrue(map.containsKey("key2"));
187         assertTrue(map.containsValue("value2"));
188         assertFalse(map.containsValue("value3"));
189     }
190 
191     @Test
192     public void removeWorksIfValueSame() throws Exception {
193         final ConcurrentMap<String, String> map = CopyOnWriteMap.newHashMap(MapBuilder.<String, String> builder().add("key", "value").add("key2",
194             "value2").toMap());
195         assertTrue(map.remove("key2", "value2"));
196         assertEquals(1, map.size());
197         assertFalse(map.containsKey("key2"));
198         assertFalse(map.containsValue("value2"));
199     }
200 
201     @Test
202     public void removeFailsIfValueDifferent() throws Exception {
203         final ConcurrentMap<String, String> map = CopyOnWriteMap.newHashMap(MapBuilder.<String, String> builder().add("key", "value").add("key2",
204             "value2").toMap());
205         assertFalse(map.remove("key2", "value3"));
206         assertEquals(2, map.size());
207         assertTrue(map.containsKey("key2"));
208         assertTrue(map.containsValue("value2"));
209         assertFalse(map.containsValue("value3"));
210     }
211 
212     @Test
213     public void unconditionalReplaceWorksIfKeyMapped() throws Exception {
214         final ConcurrentMap<String, String> map = CopyOnWriteMap.newHashMap(MapBuilder.<String, String> builder().add("key", "value").add("key2",
215             "value2").toMap());
216         assertNotNull(map.replace("key2", "value3"));
217         assertEquals(2, map.size());
218         assertTrue(map.containsKey("key2"));
219         assertTrue(map.containsValue("value3"));
220         assertFalse(map.containsValue("value2"));
221     }
222 
223     @Test
224     public void unconditionalReplaceFailsIfKeyMissing() throws Exception {
225         final ConcurrentMap<String, String> map = CopyOnWriteMap.newHashMap(MapBuilder.<String, String> builder().add("key", "value").add("key2",
226             "value2").toMap());
227         assertNull(map.replace("key3", "value3"));
228         assertEquals(2, map.size());
229         assertTrue(map.containsKey("key2"));
230         assertTrue(map.containsValue("value2"));
231         assertFalse(map.containsKey("key3"));
232         assertFalse(map.containsValue("value3"));
233     }
234 
235     @Test
236     public void conditionalReplaceWorksIfKeyAndValueMapped() throws Exception {
237         final ConcurrentMap<String, String> map = CopyOnWriteMap.newHashMap(MapBuilder.<String, String> builder().add("key", "value").add("key2",
238             "value2").toMap());
239         assertTrue(map.replace("key2", "value2", "value3"));
240         assertEquals(2, map.size());
241         assertTrue(map.containsKey("key2"));
242         assertTrue(map.containsValue("value3"));
243         assertFalse(map.containsValue("value2"));
244     }
245 
246     @Test
247     public void conditionalReplaceWorksIfValueNull() throws Exception {
248         final ConcurrentMap<String, String> map = CopyOnWriteMap.newHashMap(MapBuilder.<String, String> builder().add("key", "value").add("key2",
249             null).toMap());
250         assertTrue(map.replace("key2", null, "value3"));
251         assertEquals(2, map.size());
252         assertTrue(map.containsKey("key2"));
253         assertTrue(map.containsValue("value3"));
254         assertFalse(map.containsValue(null));
255     }
256 
257     @Test
258     public void conditionalReplaceFailsIfValueDifferent() throws Exception {
259         final ConcurrentMap<String, String> map = CopyOnWriteMap.newHashMap(MapBuilder.<String, String> builder().add("key", "value").add("key2",
260             "value2").toMap());
261         assertFalse(map.replace("key2", "value3", "value4"));
262         assertEquals(2, map.size());
263         assertTrue(map.containsKey("key2"));
264         assertTrue(map.containsValue("value2"));
265         assertFalse(map.containsValue("value3"));
266         assertFalse(map.containsValue("value4"));
267     }
268 
269     @Test
270     public void conditionalReplaceFailsIfKeyMissing() throws Exception {
271         final Map<String, String> init = MapBuilder.<String, String> builder().add("key", "value").add("key2", "value2").toMap();
272         final ConcurrentMap<String, String> map = CopyOnWriteMap.newHashMap(init);
273         assertFalse(map.replace("key3", "value2", "value3"));
274         assertEquals(2, map.size());
275         assertTrue(map.containsKey("key2"));
276         assertTrue(map.containsValue("value2"));
277         assertFalse(map.containsKey("key3"));
278         assertFalse(map.containsValue("value3"));
279     }
280 
281     @Test
282     public void modifiableValues() throws Exception {
283         final AtomicInteger count = new AtomicInteger();
284         final Map<String, String> init = new MapBuilder<String, String>().add("test", "test").add("testing", "testing").add("sup", "tester").toMap();
285         final Map<String, String> map = new CopyOnWriteMap<String, String>(init) {
286             private static final long serialVersionUID = 3275978982528321604L;
287 
288             @Override
289             public <N extends Map<? extends String, ? extends String>> Map<String, String> copy(final N map) {
290                 count.getAndIncrement();
291                 return new HashMap<String, String>(map);
292             }
293         };
294         assertEquals(1, count.get());
295         final Collection<String> values = map.values();
296         try {
297             values.add("something");
298             fail("UnsupportedOp expected");
299         } catch (final UnsupportedOperationException ignore) {}
300         assertEquals(1, count.get());
301         try {
302             values.addAll(asList("one", "two", "three"));
303             fail("UnsupportedOp expected");
304         } catch (final UnsupportedOperationException ignore) {}
305         final Iterator<String> iterator = values.iterator();
306         assertTrue(iterator.hasNext());
307         assertNotNull(iterator.next());
308         try {
309             iterator.remove();
310             fail("UnsupportedOp expected");
311         } catch (final UnsupportedOperationException ignore) {}
312         assertEquals(1, count.get());
313         assertFalse(values.remove("blah"));
314         assertEquals("not modified if element not present to be removed", 1, count.get());
315         assertTrue(values.remove("test"));
316         assertEquals(2, count.get());
317         assertEquals(2, map.size());
318         assertFalse(values.retainAll(asList("testing", "tester")));
319         assertEquals(3, count.get());
320         assertEquals(2, map.size());
321         assertTrue(values.removeAll(asList("test", "testing")));
322         assertEquals(4, count.get());
323         assertEquals(1, map.size());
324         values.clear();
325         assertTrue(map.isEmpty());
326     }
327 
328     @Test
329     public void modifiableEntrySet() throws Exception {
330         final AtomicInteger count = new AtomicInteger();
331         final Map<String, String> init = new MapBuilder<String, String>().add("test", "test").add("testing", "testing").add("tester", "tester")
332             .toMap();
333         final Map<String, String> map = new CopyOnWriteMap<String, String>(init) {
334             private static final long serialVersionUID = -2882860445706454721L;
335 
336             @Override
337             public <N extends Map<? extends String, ? extends String>> Map<String, String> copy(final N map) {
338                 count.getAndIncrement();
339                 return new HashMap<String, String>(map);
340             }
341         };
342         assertEquals(1, count.get());
343         final Collection<Entry<String, String>> entries = map.entrySet();
344         class E implements Map.Entry<String, String> {
345             final String e;
346 
347             public E(final String e) {
348                 this.e = e;
349             }
350 
351             public String getKey() {
352                 return e;
353             }
354 
355             public String getValue() {
356                 return e;
357             }
358 
359             public String setValue(final String value) {
360                 throw new RuntimeException("should not be called, don't use UnsupportedOp here");
361             }
362         }
363 
364         try {
365             entries.add(new E("something"));
366             fail("UnsupportedOp expected");
367         } catch (final UnsupportedOperationException ignore) {}
368         assertEquals(1, count.get());
369         try {
370             entries.addAll(asList(new E("one"), new E("two"), new E("three")));
371             fail("UnsupportedOp expected");
372         } catch (final UnsupportedOperationException ignore) {}
373         final Iterator<Entry<String, String>> iterator = entries.iterator();
374         assertTrue(iterator.hasNext());
375         assertNotNull(iterator.next());
376         try {
377             iterator.remove();
378             fail("UnsupportedOp expected");
379         } catch (final UnsupportedOperationException ignore) {}
380         assertEquals(1, count.get());
381         assertFalse(entries.remove("blah"));
382         assertEquals("not modified if element not present to be removed", 1, count.get());
383         assertTrue(entries.remove(new E("test")));
384         assertEquals(2, count.get());
385         assertEquals(2, map.size());
386         assertFalse(entries.retainAll(asList(new E("testing"), new E("tester"))));
387         assertEquals(3, count.get());
388         assertEquals(2, map.size());
389         assertTrue(entries.removeAll(asList(new E("test"), new E("testing"))));
390         assertEquals(4, count.get());
391         assertEquals(1, map.size());
392         entries.clear();
393         assertTrue(map.isEmpty());
394     }
395 
396     @Test
397     public void modifiableKeySet() throws Exception {
398         final AtomicInteger count = new AtomicInteger();
399 
400         final Map<String, String> init = new MapBuilder<String, String>().add("test", "test").add("testing", "testing").add("tester", "tester")
401             .toMap();
402         final Map<String, String> map = new CopyOnWriteMap<String, String>(init) {
403             private static final long serialVersionUID = 7273654247572679525L;
404 
405             @Override
406             public <N extends Map<? extends String, ? extends String>> Map<String, String> copy(final N map) {
407                 count.getAndIncrement();
408                 return new HashMap<String, String>(map);
409             }
410         };
411         assertEquals(1, count.get());
412         final Collection<String> keys = map.keySet();
413         try {
414             keys.add("something");
415             fail("UnsupportedOp expected");
416         } catch (final UnsupportedOperationException ignore) {}
417         assertEquals(1, count.get());
418         try {
419             keys.addAll(asList("one", "two", "three"));
420             fail("UnsupportedOp expected");
421         } catch (final UnsupportedOperationException ignore) {}
422         final Iterator<String> iterator = keys.iterator();
423         assertTrue(iterator.hasNext());
424         assertNotNull(iterator.next());
425         try {
426             iterator.remove();
427             fail("UnsupportedOp expected");
428         } catch (final UnsupportedOperationException ignore) {}
429         assertEquals(1, count.get());
430         assertFalse(keys.remove("blah"));
431         assertEquals("not modified if element not present to be removed", 1, count.get());
432         assertTrue(keys.remove("test"));
433         assertEquals(2, count.get());
434         assertEquals(2, map.size());
435         assertFalse(keys.retainAll(asList("testing", "tester")));
436         assertEquals(3, count.get());
437         assertEquals(2, map.size());
438         assertTrue(keys.removeAll(asList("test", "testing")));
439         assertEquals(4, count.get());
440         assertEquals(1, map.size());
441         keys.clear();
442         assertTrue(map.isEmpty());
443     }
444 
445     @Test(expected = IllegalArgumentException.class)
446     public void nullMap() throws Exception {
447         new CopyOnWriteMap<String, String>(null) {
448             private static final long serialVersionUID = 4223850632932526917L;
449 
450             // /CLOVER:OFF
451             @Override
452             public <N extends Map<? extends String, ? extends String>> Map<String, String> copy(final N map) {
453                 return new HashMap<String, String>(map);
454             };
455             // /CLOVER:ON
456         };
457     }
458 
459     @Test(expected = IllegalArgumentException.class)
460     public void copyFunctionReturnsNull() throws Exception {
461         new CopyOnWriteMap<String, String>() {
462             private static final long serialVersionUID = 831716474176011289L;
463 
464             @Override
465             public <N extends Map<? extends String, ? extends String>> Map<String, String> copy(final N map) {
466                 return null;
467             };
468         };
469     }
470 
471     @Test
472     public void serializableHashMap() {
473         assertMutableMapSerializable(CopyOnWriteMap.<Object, Object> newHashMap());
474     }
475 
476     @Test
477     public void serializableLinkedMap() {
478         assertMutableMapSerializable(CopyOnWriteMap.<Object, Object> newLinkedMap());
479     }
480 
481     @Test
482     public void toStringTest() throws Exception {
483         final AtomicReference<Map<String, String>> ref = new AtomicReference<Map<String, String>>();
484         final CopyOnWriteMap<String, String> cowMap = new CopyOnWriteMap<String, String>() {
485             private static final long serialVersionUID = -17380087385174856L;
486 
487             @Override
488             protected <N extends Map<? extends String, ? extends String>> java.util.Map<String, String> copy(final N map) {
489                 ref.set(new HashMap<String, String>(map));
490                 return ref.get();
491             };
492         };
493         assertEquals(ref.get().toString(), cowMap.toString());
494     }
495 
496     @Test
497     public void isEmpty() throws Exception {
498         final CopyOnWriteMap<String, String> cowMap = CopyOnWriteMap.newHashMap();
499         assertTrue(cowMap.isEmpty());
500         assertTrue(cowMap.keySet().isEmpty());
501         assertTrue(cowMap.entrySet().isEmpty());
502         assertTrue(cowMap.values().isEmpty());
503         cowMap.put("1", "1");
504         assertFalse(cowMap.isEmpty());
505         assertFalse(cowMap.keySet().isEmpty());
506         assertFalse(cowMap.entrySet().isEmpty());
507         assertFalse(cowMap.values().isEmpty());
508     }
509 
510     @Test
511     public void equality() throws Exception {
512         final Map<String, String> init = new MapBuilder<String, String>().add("test", "test").add("testing", "testing").toMap();
513         final CopyOnWriteMap<String, String> map = CopyOnWriteMap.newHashMap(init);
514         assertEquals(init, map);
515         assertEquals(map, init);
516         assertEquals(init.hashCode(), map.hashCode());
517         assertEquals(map.hashCode(), init.hashCode());
518         assertEquals(init.keySet(), map.keySet());
519         assertEquals(map.keySet(), init.keySet());
520         assertEquals(init.entrySet(), map.entrySet());
521         assertEquals(map.entrySet(), init.entrySet());
522         assertFalse(init.values().equals(map.values()));
523         assertFalse(map.values().equals(init.values()));
524     }
525 
526     @Test
527     public void toArray() throws Exception {
528         final Map<String, String> init = new MapBuilder<String, String>().add("test", "test").add("testing", "testing").toMap();
529         final CopyOnWriteMap<String, String> map = CopyOnWriteMap.newHashMap(init);
530         assertArrayEquals(init.keySet().toArray(new String[2]), map.keySet().toArray(new String[2]));
531         assertArrayEquals(init.values().toArray(new String[2]), map.values().toArray(new String[2]));
532         assertArrayEquals(init.entrySet().toArray(new Map.Entry[2]), map.entrySet().toArray(new Map.Entry[2]));
533     }
534 
535     @Test
536     public void contains() throws Exception {
537         final Map<String, String> map = CopyOnWriteMap.newHashMap(MapBuilder.build("1", "o1", "2", "o2", "3", "o3"));
538         assertTrue(map.containsKey("2"));
539         assertTrue(map.containsValue("o2"));
540         assertTrue(map.keySet().contains("2"));
541         assertTrue(map.keySet().containsAll(asList(new String[] { "1", "2", "3" })));
542         assertTrue(map.values().contains("o2"));
543         assertTrue(map.values().containsAll(asList(new String[] { "o1", "o2", "o3" })));
544     }
545 
546     static void assertMutableMapSerializable(final Map<Object, Object> map) {
547         map.put("1", "one");
548         assertSerializable(map);
549         assertTrue(map.containsKey("1"));
550         assertTrue(map.containsValue("one"));
551         assertEquals("1", map.keySet().iterator().next());
552         assertEquals("one", map.values().iterator().next());
553         final Map.Entry<Object, Object> entry = map.entrySet().iterator().next();
554         assertEquals("1", entry.getKey());
555         assertEquals("one", entry.getValue());
556     }
557 
558     static void assertSerializable(final Map<?, ?> map) {
559         assertEquals(map, serialize(map));
560     }
561 }
562 
563 class MapBuilder<K, V> {
564     private final Map<K, V> map = new HashMap<K, V>();
565 
566     static <S> Map<S, S> build(final S... elements) {
567         if (elements.length % 2 != 0) {
568             throw new IllegalArgumentException("must have even number of elements: " + elements.length);
569         }
570         final MapBuilder<S, S> result = new MapBuilder<S, S>();
571         for (int i = 0; i < elements.length; i = i + 2) {
572             result.add(elements[i], elements[i + 1]);
573         }
574         return result.toMap();
575     }
576 
577     static <K, V> MapBuilder<K, V> builder() {
578         return new MapBuilder<K, V>();
579     }
580 
581     MapBuilder<K, V> add(final K key, final V value) {
582         map.put(key, value);
583         return this;
584     }
585 
586     Entry<K, V> entry(final K key, final V value) {
587         return new Entry<K, V>() {
588             public K getKey() {
589                 return key;
590             }
591 
592             public V getValue() {
593                 return value;
594             }
595 
596             public V setValue(final V arg0) {
597                 throw new UnsupportedOperationException();
598             };
599         };
600     }
601 
602     Map<K, V> toMap() {
603         return Collections.unmodifiableMap(new HashMap<K, V>(map));
604     }
605 }