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