• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2009 The Guava Authors
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.google.common.collect;
18 
19 import static java.lang.reflect.Modifier.STATIC;
20 import static org.mockito.ArgumentMatchers.any;
21 import static org.mockito.Mockito.atLeast;
22 import static org.mockito.Mockito.mock;
23 import static org.mockito.Mockito.verify;
24 import static org.mockito.Mockito.verifyNoMoreInteractions;
25 
26 import com.google.common.base.Function;
27 import com.google.common.collect.testing.MapTestSuiteBuilder;
28 import com.google.common.collect.testing.TestStringMapGenerator;
29 import com.google.common.collect.testing.features.CollectionFeature;
30 import com.google.common.collect.testing.features.CollectionSize;
31 import com.google.common.collect.testing.features.MapFeature;
32 import com.google.common.reflect.AbstractInvocationHandler;
33 import com.google.common.reflect.Parameter;
34 import com.google.common.reflect.Reflection;
35 import com.google.common.reflect.TypeToken;
36 import com.google.common.testing.ArbitraryInstances;
37 import com.google.common.testing.EqualsTester;
38 import com.google.common.testing.ForwardingWrapperTester;
39 import java.lang.reflect.InvocationTargetException;
40 import java.lang.reflect.Method;
41 import java.util.Arrays;
42 import java.util.Collection;
43 import java.util.Iterator;
44 import java.util.Map;
45 import java.util.Map.Entry;
46 import java.util.Set;
47 import junit.framework.Test;
48 import junit.framework.TestCase;
49 import junit.framework.TestSuite;
50 
51 /**
52  * Unit test for {@link ForwardingMap}.
53  *
54  * @author Hayward Chan
55  * @author Louis Wasserman
56  */
57 public class ForwardingMapTest extends TestCase {
58   static class StandardImplForwardingMap<K, V> extends ForwardingMap<K, V> {
59     private final Map<K, V> backingMap;
60 
StandardImplForwardingMap(Map<K, V> backingMap)61     StandardImplForwardingMap(Map<K, V> backingMap) {
62       this.backingMap = backingMap;
63     }
64 
65     @Override
delegate()66     protected Map<K, V> delegate() {
67       return backingMap;
68     }
69 
70     @Override
containsKey(Object key)71     public boolean containsKey(Object key) {
72       return standardContainsKey(key);
73     }
74 
75     @Override
containsValue(Object value)76     public boolean containsValue(Object value) {
77       return standardContainsValue(value);
78     }
79 
80     @Override
putAll(Map<? extends K, ? extends V> map)81     public void putAll(Map<? extends K, ? extends V> map) {
82       standardPutAll(map);
83     }
84 
85     @Override
remove(Object object)86     public V remove(Object object) {
87       return standardRemove(object);
88     }
89 
90     @Override
equals(Object object)91     public boolean equals(Object object) {
92       return standardEquals(object);
93     }
94 
95     @Override
hashCode()96     public int hashCode() {
97       return standardHashCode();
98     }
99 
100     @Override
keySet()101     public Set<K> keySet() {
102       return new StandardKeySet();
103     }
104 
105     @Override
values()106     public Collection<V> values() {
107       return new StandardValues();
108     }
109 
110     @Override
toString()111     public String toString() {
112       return standardToString();
113     }
114 
115     @Override
entrySet()116     public Set<Entry<K, V>> entrySet() {
117       return new StandardEntrySet() {
118         @Override
119         public Iterator<Entry<K, V>> iterator() {
120           return delegate().entrySet().iterator();
121         }
122       };
123     }
124 
125     @Override
clear()126     public void clear() {
127       standardClear();
128     }
129 
130     @Override
isEmpty()131     public boolean isEmpty() {
132       return standardIsEmpty();
133     }
134   }
135 
136   public static Test suite() {
137     TestSuite suite = new TestSuite();
138 
139     suite.addTestSuite(ForwardingMapTest.class);
140     suite.addTest(
141         MapTestSuiteBuilder.using(
142                 new TestStringMapGenerator() {
143 
144                   @Override
145                   protected Map<String, String> create(Entry<String, String>[] entries) {
146                     Map<String, String> map = Maps.newLinkedHashMap();
147                     for (Entry<String, String> entry : entries) {
148                       map.put(entry.getKey(), entry.getValue());
149                     }
150                     return new StandardImplForwardingMap<>(map);
151                   }
152                 })
153             .named("ForwardingMap[LinkedHashMap] with standard implementations")
154             .withFeatures(
155                 CollectionSize.ANY,
156                 MapFeature.ALLOWS_NULL_VALUES,
157                 MapFeature.ALLOWS_NULL_KEYS,
158                 MapFeature.ALLOWS_ANY_NULL_QUERIES,
159                 MapFeature.GENERAL_PURPOSE,
160                 CollectionFeature.SUPPORTS_ITERATOR_REMOVE,
161                 CollectionFeature.KNOWN_ORDER)
162             .createTestSuite());
163     suite.addTest(
164         MapTestSuiteBuilder.using(
165                 new TestStringMapGenerator() {
166 
167                   @Override
168                   protected Map<String, String> create(Entry<String, String>[] entries) {
169                     ImmutableMap.Builder<String, String> builder = ImmutableMap.builder();
170                     for (Entry<String, String> entry : entries) {
171                       builder.put(entry.getKey(), entry.getValue());
172                     }
173                     return new StandardImplForwardingMap<>(builder.build());
174                   }
175                 })
176             .named("ForwardingMap[ImmutableMap] with standard implementations")
177             .withFeatures(
178                 CollectionSize.ANY,
179                 MapFeature.REJECTS_DUPLICATES_AT_CREATION,
180                 MapFeature.ALLOWS_ANY_NULL_QUERIES,
181                 CollectionFeature.KNOWN_ORDER)
182             .createTestSuite());
183 
184     return suite;
185   }
186 
187   @SuppressWarnings({"rawtypes", "unchecked"})
188   public void testForwarding() {
189     new ForwardingWrapperTester()
190         .testForwarding(
191             Map.class,
192             new Function<Map, Map>() {
193               @Override
194               public Map apply(Map delegate) {
195                 return wrap(delegate);
196               }
197             });
198   }
199 
200   public void testEquals() {
201     Map<Integer, String> map1 = ImmutableMap.of(1, "one");
202     Map<Integer, String> map2 = ImmutableMap.of(2, "two");
203     new EqualsTester()
204         .addEqualityGroup(map1, wrap(map1), wrap(map1))
205         .addEqualityGroup(map2, wrap(map2))
206         .testEquals();
207   }
208 
209   public void testStandardEntrySet() throws InvocationTargetException {
210     @SuppressWarnings("unchecked")
211     final Map<String, Boolean> map = mock(Map.class);
212 
213     Map<String, Boolean> forward =
214         new ForwardingMap<String, Boolean>() {
215           @Override
216           protected Map<String, Boolean> delegate() {
217             return map;
218           }
219 
220           @Override
221           public Set<Entry<String, Boolean>> entrySet() {
222             return new StandardEntrySet() {
223               @Override
224               public Iterator<Entry<String, Boolean>> iterator() {
225                 return Iterators.emptyIterator();
226               }
227             };
228           }
229         };
230     callAllPublicMethods(new TypeToken<Set<Entry<String, Boolean>>>() {}, forward.entrySet());
231 
232     // These are the methods specified by StandardEntrySet
233     verify(map, atLeast(0)).clear();
234     verify(map, atLeast(0)).containsKey(any());
235     verify(map, atLeast(0)).get(any());
236     verify(map, atLeast(0)).isEmpty();
237     verify(map, atLeast(0)).remove(any());
238     verify(map, atLeast(0)).size();
239     verifyNoMoreInteractions(map);
240   }
241 
242   public void testStandardKeySet() throws InvocationTargetException {
243     @SuppressWarnings("unchecked")
244     final Map<String, Boolean> map = mock(Map.class);
245 
246     Map<String, Boolean> forward =
247         new ForwardingMap<String, Boolean>() {
248           @Override
249           protected Map<String, Boolean> delegate() {
250             return map;
251           }
252 
253           @Override
254           public Set<String> keySet() {
255             return new StandardKeySet();
256           }
257         };
258     callAllPublicMethods(new TypeToken<Set<String>>() {}, forward.keySet());
259 
260     // These are the methods specified by StandardKeySet
261     verify(map, atLeast(0)).clear();
262     verify(map, atLeast(0)).containsKey(any());
263     verify(map, atLeast(0)).isEmpty();
264     verify(map, atLeast(0)).remove(any());
265     verify(map, atLeast(0)).size();
266     verify(map, atLeast(0)).entrySet();
267     verifyNoMoreInteractions(map);
268   }
269 
270   public void testStandardValues() throws InvocationTargetException {
271     @SuppressWarnings("unchecked")
272     final Map<String, Boolean> map = mock(Map.class);
273 
274     Map<String, Boolean> forward =
275         new ForwardingMap<String, Boolean>() {
276           @Override
277           protected Map<String, Boolean> delegate() {
278             return map;
279           }
280 
281           @Override
282           public Collection<Boolean> values() {
283             return new StandardValues();
284           }
285         };
286     callAllPublicMethods(new TypeToken<Collection<Boolean>>() {}, forward.values());
287 
288     // These are the methods specified by StandardValues
289     verify(map, atLeast(0)).clear();
290     verify(map, atLeast(0)).containsValue(any());
291     verify(map, atLeast(0)).isEmpty();
292     verify(map, atLeast(0)).size();
293     verify(map, atLeast(0)).entrySet();
294     verifyNoMoreInteractions(map);
295   }
296 
297   public void testToStringWithNullKeys() throws Exception {
298     Map<String, String> hashmap = Maps.newHashMap();
299     hashmap.put("foo", "bar");
300     hashmap.put(null, "baz");
301 
302     StandardImplForwardingMap<String, String> forwardingMap =
303         new StandardImplForwardingMap<>(Maps.<String, String>newHashMap());
304     forwardingMap.put("foo", "bar");
305     forwardingMap.put(null, "baz");
306 
307     assertEquals(hashmap.toString(), forwardingMap.toString());
308   }
309 
310   public void testToStringWithNullValues() throws Exception {
311     Map<String, String> hashmap = Maps.newHashMap();
312     hashmap.put("foo", "bar");
313     hashmap.put("baz", null);
314 
315     StandardImplForwardingMap<String, String> forwardingMap =
316         new StandardImplForwardingMap<>(Maps.<String, String>newHashMap());
317     forwardingMap.put("foo", "bar");
318     forwardingMap.put("baz", null);
319 
320     assertEquals(hashmap.toString(), forwardingMap.toString());
321   }
322 
323   private static <K, V> Map<K, V> wrap(final Map<K, V> delegate) {
324     return new ForwardingMap<K, V>() {
325       @Override
326       protected Map<K, V> delegate() {
327         return delegate;
328       }
329     };
330   }
331 
332   private static final ImmutableMap<String, String> JUF_METHODS =
333       ImmutableMap.of(
334           "java.util.function.Predicate", "test",
335           "java.util.function.Consumer", "accept",
336           "java.util.function.IntFunction", "apply");
337 
338   private static Object getDefaultValue(final TypeToken<?> type) {
339     Class<?> rawType = type.getRawType();
340     Object defaultValue = ArbitraryInstances.get(rawType);
341     if (defaultValue != null) {
342       return defaultValue;
343     }
344 
345     final String typeName = rawType.getCanonicalName();
346     if (JUF_METHODS.containsKey(typeName)) {
347       // Generally, methods that accept java.util.function.* instances
348       // don't like to get null values.  We generate them dynamically
349       // using Proxy so that we can have Java 7 compliant code.
350       return Reflection.newProxy(
351           rawType,
352           new AbstractInvocationHandler() {
353             @Override
354             public Object handleInvocation(Object proxy, Method method, Object[] args) {
355               // Crude, but acceptable until we can use Java 8.  Other
356               // methods have default implementations, and it is hard to
357               // distinguish.
358               if (method.getName().equals(JUF_METHODS.get(typeName))) {
359                 return getDefaultValue(type.method(method).getReturnType());
360               }
361               throw new IllegalStateException("Unexpected " + method + " invoked on " + proxy);
362             }
363           });
364     } else {
365       return null;
366     }
367   }
368 
369   private static <T> void callAllPublicMethods(TypeToken<T> type, T object)
370       throws InvocationTargetException {
371     for (Method method : type.getRawType().getMethods()) {
372       if ((method.getModifiers() & STATIC) != 0) {
373         continue;
374       }
375       ImmutableList<Parameter> parameters = type.method(method).getParameters();
376       Object[] args = new Object[parameters.size()];
377       for (int i = 0; i < parameters.size(); i++) {
378         args[i] = getDefaultValue(parameters.get(i).getType());
379       }
380       try {
381         try {
382           method.invoke(object, args);
383         } catch (InvocationTargetException ex) {
384           try {
385             throw ex.getCause();
386           } catch (UnsupportedOperationException unsupported) {
387             // this is a legit exception
388           }
389         }
390       } catch (Throwable cause) {
391         throw new InvocationTargetException(cause, method + " with args: " + Arrays.toString(args));
392       }
393     }
394   }
395 }
396