/* * Copyright (C) 2009 The Guava Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.common.collect; import static org.mockito.Mockito.anyObject; import static org.mockito.Mockito.atLeast; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; import com.google.common.base.Function; import com.google.common.collect.testing.MapTestSuiteBuilder; import com.google.common.collect.testing.TestStringMapGenerator; import com.google.common.collect.testing.features.CollectionFeature; import com.google.common.collect.testing.features.CollectionSize; import com.google.common.collect.testing.features.MapFeature; import com.google.common.reflect.AbstractInvocationHandler; import com.google.common.reflect.Reflection; import com.google.common.testing.ArbitraryInstances; import com.google.common.testing.EqualsTester; import com.google.common.testing.ForwardingWrapperTester; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.Arrays; import java.util.Collection; import java.util.Iterator; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import junit.framework.Test; import junit.framework.TestCase; import junit.framework.TestSuite; /** * Unit test for {@link ForwardingMap}. * * @author Hayward Chan * @author Louis Wasserman */ public class ForwardingMapTest extends TestCase { static class StandardImplForwardingMap<K, V> extends ForwardingMap<K, V> { private final Map<K, V> backingMap; StandardImplForwardingMap(Map<K, V> backingMap) { this.backingMap = backingMap; } @Override protected Map<K, V> delegate() { return backingMap; } @Override public boolean containsKey(Object key) { return standardContainsKey(key); } @Override public boolean containsValue(Object value) { return standardContainsValue(value); } @Override public void putAll(Map<? extends K, ? extends V> map) { standardPutAll(map); } @Override public V remove(Object object) { return standardRemove(object); } @Override public boolean equals(Object object) { return standardEquals(object); } @Override public int hashCode() { return standardHashCode(); } @Override public Set<K> keySet() { return new StandardKeySet(); } @Override public Collection<V> values() { return new StandardValues(); } @Override public String toString() { return standardToString(); } @Override public Set<Entry<K, V>> entrySet() { return new StandardEntrySet() { @Override public Iterator<Entry<K, V>> iterator() { return delegate() .entrySet() .iterator(); } }; } @Override public void clear() { standardClear(); } @Override public boolean isEmpty() { return standardIsEmpty(); } } public static Test suite() { TestSuite suite = new TestSuite(); suite.addTestSuite(ForwardingMapTest.class); suite.addTest(MapTestSuiteBuilder.using(new TestStringMapGenerator() { @Override protected Map<String, String> create( Entry<String, String>[] entries) { Map<String, String> map = Maps.newLinkedHashMap(); for (Entry<String, String> entry : entries) { map.put(entry.getKey(), entry.getValue()); } return new StandardImplForwardingMap<String, String>(map); } }).named("ForwardingMap[LinkedHashMap] with standard implementations") .withFeatures(CollectionSize.ANY, MapFeature.ALLOWS_NULL_VALUES, MapFeature.ALLOWS_NULL_KEYS, MapFeature.ALLOWS_ANY_NULL_QUERIES, MapFeature.GENERAL_PURPOSE, CollectionFeature.SUPPORTS_ITERATOR_REMOVE, CollectionFeature.KNOWN_ORDER) .createTestSuite()); suite.addTest(MapTestSuiteBuilder.using(new TestStringMapGenerator() { @Override protected Map<String, String> create( Entry<String, String>[] entries) { ImmutableMap.Builder<String, String> builder = ImmutableMap.builder(); for (Entry<String, String> entry : entries) { builder.put(entry.getKey(), entry.getValue()); } return new StandardImplForwardingMap<String, String>(builder.build()); } }).named("ForwardingMap[ImmutableMap] with standard implementations") .withFeatures( CollectionSize.ANY, MapFeature.REJECTS_DUPLICATES_AT_CREATION, MapFeature.ALLOWS_ANY_NULL_QUERIES, CollectionFeature.KNOWN_ORDER) .createTestSuite()); return suite; } @SuppressWarnings({"rawtypes", "unchecked"}) public void testForwarding() { new ForwardingWrapperTester() .testForwarding(Map.class, new Function<Map, Map>() { @Override public Map apply(Map delegate) { return wrap(delegate); } }); } public void testEquals() { Map<Integer, String> map1 = ImmutableMap.of(1, "one"); Map<Integer, String> map2 = ImmutableMap.of(2, "two"); new EqualsTester() .addEqualityGroup(map1, wrap(map1), wrap(map1)) .addEqualityGroup(map2, wrap(map2)) .testEquals(); } public void testStandardEntrySet() throws InvocationTargetException { @SuppressWarnings("unchecked") final Map<String, Boolean> map = mock(Map.class); Map<String, Boolean> forward = new ForwardingMap<String, Boolean>() { @Override protected Map<String, Boolean> delegate() { return map; } @Override public Set<Entry<String, Boolean>> entrySet() { return new StandardEntrySet() { @Override public Iterator<Entry<String, Boolean>> iterator() { return Iterators.emptyIterator(); } }; } }; callAllPublicMethods(Set.class, forward.entrySet()); // These are the methods specified by StandardEntrySet verify(map, atLeast(0)).clear(); verify(map, atLeast(0)).containsKey(anyObject()); verify(map, atLeast(0)).get(anyObject()); verify(map, atLeast(0)).isEmpty(); verify(map, atLeast(0)).remove(anyObject()); verify(map, atLeast(0)).size(); verifyNoMoreInteractions(map); } public void testStandardKeySet() throws InvocationTargetException { @SuppressWarnings("unchecked") final Map<String, Boolean> map = mock(Map.class); Map<String, Boolean> forward = new ForwardingMap<String, Boolean>() { @Override protected Map<String, Boolean> delegate() { return map; } @Override public Set<String> keySet() { return new StandardKeySet(); } }; callAllPublicMethods(Set.class, forward.keySet()); // These are the methods specified by StandardKeySet verify(map, atLeast(0)).clear(); verify(map, atLeast(0)).containsKey(anyObject()); verify(map, atLeast(0)).isEmpty(); verify(map, atLeast(0)).remove(anyObject()); verify(map, atLeast(0)).size(); verify(map, atLeast(0)).entrySet(); verifyNoMoreInteractions(map); } public void testStandardValues() throws InvocationTargetException { @SuppressWarnings("unchecked") final Map<String, Boolean> map = mock(Map.class); Map<String, Boolean> forward = new ForwardingMap<String, Boolean>() { @Override protected Map<String, Boolean> delegate() { return map; } @Override public Collection<Boolean> values() { return new StandardValues(); } }; callAllPublicMethods(Collection.class, forward.values()); // These are the methods specified by StandardValues verify(map, atLeast(0)).clear(); verify(map, atLeast(0)).containsValue(anyObject()); verify(map, atLeast(0)).isEmpty(); verify(map, atLeast(0)).size(); verify(map, atLeast(0)).entrySet(); verifyNoMoreInteractions(map); } public void testToStringWithNullKeys() throws Exception { Map<String, String> hashmap = Maps.newHashMap(); hashmap.put("foo", "bar"); hashmap.put(null, "baz"); StandardImplForwardingMap<String, String> forwardingMap = new StandardImplForwardingMap<String, String>( Maps.<String, String>newHashMap()); forwardingMap.put("foo", "bar"); forwardingMap.put(null, "baz"); assertEquals(hashmap.toString(), forwardingMap.toString()); } public void testToStringWithNullValues() throws Exception { Map<String, String> hashmap = Maps.newHashMap(); hashmap.put("foo", "bar"); hashmap.put("baz", null); StandardImplForwardingMap<String, String> forwardingMap = new StandardImplForwardingMap<String, String>( Maps.<String, String>newHashMap()); forwardingMap.put("foo", "bar"); forwardingMap.put("baz", null); assertEquals(hashmap.toString(), forwardingMap.toString()); } private static <K, V> Map<K, V> wrap(final Map<K, V> delegate) { return new ForwardingMap<K, V>() { @Override protected Map<K, V> delegate() { return delegate; } }; } private static Object getDefaultValue(Class<?> returnType) { Object defaultValue = ArbitraryInstances.get(returnType); if (defaultValue != null) { return defaultValue; } if ("java.util.function.Predicate".equals(returnType.getCanonicalName()) || ("java.util.function.Consumer".equals(returnType.getCanonicalName()))) { // Generally, methods that accept java.util.function.* instances // don't like to get null values. We generate them dynamically // using Proxy so that we can have Java 7 compliant code. return Reflection.newProxy(returnType, new AbstractInvocationHandler() { @Override public Object handleInvocation(Object proxy, Method method, Object[] args) { // Crude, but acceptable until we can use Java 8. Other // methods have default implementations, and it is hard to // distinguish. if ("test".equals(method.getName()) || "accept".equals(method.getName())) { return getDefaultValue(method.getReturnType()); } throw new IllegalStateException("Unexpected " + method + " invoked on " + proxy); } }); } else { return null; } } private static <T> void callAllPublicMethods(Class<T> theClass, T object) throws InvocationTargetException { for (Method method : theClass.getMethods()) { Class<?>[] parameterTypes = method.getParameterTypes(); Object[] parameters = new Object[parameterTypes.length]; for (int i = 0; i < parameterTypes.length; i++) { parameters[i] = getDefaultValue(parameterTypes[i]); } try { try { method.invoke(object, parameters); } catch (InvocationTargetException ex) { try { throw ex.getCause(); } catch (UnsupportedOperationException unsupported) { // this is a legit exception } } } catch (Throwable cause) { throw new InvocationTargetException(cause, method + " with args: " + Arrays.toString(parameters)); } } } }