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