diff --git a/hamcrest/src/main/java/org/hamcrest/collection/IsUnmodifiableCollection.java b/hamcrest/src/main/java/org/hamcrest/collection/IsUnmodifiableCollection.java new file mode 100644 index 00000000..5f72ca7f --- /dev/null +++ b/hamcrest/src/main/java/org/hamcrest/collection/IsUnmodifiableCollection.java @@ -0,0 +1,355 @@ +package org.hamcrest.collection; + +import org.hamcrest.Description; +import org.hamcrest.Matcher; +import org.hamcrest.TypeSafeDiagnosingMatcher; + +import java.lang.reflect.Array; +import java.lang.reflect.Constructor; +import java.util.*; + +/** + * Matches if collection is truly unmodifiable + */ +public class IsUnmodifiableCollection extends TypeSafeDiagnosingMatcher> { + + private static final Map DEFAULT_COLLECTIONS = new HashMap<>(); + private static final Set KNOWN_UNMODIFIABLE_COLLECTIONS = new HashSet<>(); + private static final Set KNOWN_MODIFIABLE_COLLECTIONS = new HashSet<>(); + + static { + final List list = Arrays.asList("a", "b", "c"); + DEFAULT_COLLECTIONS.put(Collection.class, list); + DEFAULT_COLLECTIONS.put(List.class, list); + DEFAULT_COLLECTIONS.put(Set.class, new HashSet<>(list)); + + KNOWN_UNMODIFIABLE_COLLECTIONS.add("java.util.ImmutableCollections"); + KNOWN_UNMODIFIABLE_COLLECTIONS.add("java.util.Collections$Unmodifiable"); + + KNOWN_MODIFIABLE_COLLECTIONS.add("java.util.Arrays$ArrayList"); + } + + /** + * Creates matcher that matches when collection is truly unmodifiable + */ + public static Matcher> isUnmodifiable() { + return new IsUnmodifiableCollection<>(); + } + + @SuppressWarnings("unchecked") + @Override + protected boolean matchesSafely(final Collection collection, final Description mismatchDescription) { + final Class collectionClass = collection.getClass(); + String collectionClassName = collectionClass.getName(); + for (String knownUnmodifiableCollection : KNOWN_UNMODIFIABLE_COLLECTIONS) { + if (collectionClassName.startsWith(knownUnmodifiableCollection)) { + return true; + } + } + for (String knownModifiableCollection : KNOWN_MODIFIABLE_COLLECTIONS) { + if (collectionClassName.startsWith(knownModifiableCollection)) { + mismatchDescription.appendText(collectionClassName + " is a known modifiable collection"); + return false; + } + } + final Collection item = getInstanceOfType(collectionClass, collection); + if (item == null) { + throw failedToInstantiateItem(collectionClass, null); + } + final Object testObject = new Object(); + final Set singletonList = Collections.singleton(testObject); + + if (collection instanceof List) { + // This is an operation on the original collection, but it is safe, since it sets the same element + List originalList = (List) collection; + if (checkMethod_set(originalList, mismatchDescription)) return false; + + List copiedList = (List) item; + if (checkMethod_listIterator_remove(copiedList, mismatchDescription)) return false; + if (checkMethod_listIterator_set(copiedList, testObject, mismatchDescription)) return false; + if (checkMethod_listIterator_add(copiedList, testObject, mismatchDescription)) return false; + if (checkMethod_listIterator_index(copiedList, mismatchDescription)) return false; + if (checkMethod_add_index(copiedList, testObject, mismatchDescription)) return false; + if (checkMethod_add_all_index(copiedList, singletonList, mismatchDescription)) return false; + if (checkMethod_remove_index(copiedList, mismatchDescription)) return false; + } + + if (checkMethod_add(item, testObject, mismatchDescription)) return false; + if (checkMethod_add_all(item, singletonList, mismatchDescription)) return false; + if (checkMethod_remove(item, testObject, mismatchDescription)) return false; + if (checkMethod_remove_all(item, singletonList, mismatchDescription)) return false; + if (checkMethod_retail_all(item, singletonList, mismatchDescription)) return false; + if (checkMethod_clear(item, mismatchDescription)) return false; + if (checkMethod_iterator(item, mismatchDescription)) return false; + + return true; + } + + private boolean checkMethod_iterator(Collection item, Description mismatchDescription) { + try { + Iterator iterator = item.iterator(); + iterator.remove(); + mismatchDescription.appendText("was able to remove an element from the iterator"); + return true; + } catch (Exception ignore) { + } + return false; + } + + private boolean checkMethod_clear(Collection item, Description mismatchDescription) { + try { + item.clear(); + mismatchDescription.appendText("was able to clear the collection"); + return true; + } catch (Exception ignore) { + } + return false; + } + + private boolean checkMethod_retail_all(Collection item, Set singletonList, Description mismatchDescription) { + try { + item.retainAll(singletonList); + mismatchDescription.appendText("was able to call retainAll on the collection"); + return true; + } catch (Exception ignore) { + } + return false; + } + + private boolean checkMethod_remove_all(Collection item, Set singletonList, Description mismatchDescription) { + try { + item.removeAll(singletonList); + mismatchDescription.appendText("was able to call removeAll on the collection"); + return true; + } catch (Exception ignore) { + } + return false; + } + + private boolean checkMethod_remove(Collection item, Object testObject, Description mismatchDescription) { + try { + item.remove(testObject); + mismatchDescription.appendText("was able to call remove a value from the collection"); + return true; + } catch (Exception ignore) { + } + return false; + } + + private boolean checkMethod_remove_index(List item, Description mismatchDescription) { + try { + item.remove(0); + mismatchDescription.appendText("was able to call remove by index from the collection"); + return true; + } catch (Exception ignore) { + } + return false; + } + + private boolean checkMethod_add_all(Collection item, Set singletonList, Description mismatchDescription) { + try { + item.addAll(singletonList); + mismatchDescription.appendText("was able to perform addAll on the collection"); + return true; + } catch (Exception ignore) { + } + return false; + } + + private boolean checkMethod_add_all_index(List item, Set singletonList, Description mismatchDescription) { + try { + item.addAll(0, singletonList); + mismatchDescription.appendText("was able to perform addAll by index on the collection"); + return true; + } catch (Exception ignore) { + } + return false; + } + + private boolean checkMethod_add(Collection item, Object testObject, Description mismatchDescription) { + try { + item.add(testObject); + mismatchDescription.appendText("was able to add a value into the collection"); + return true; + } catch (Exception ignore) { + } + return false; + } + + private boolean checkMethod_add_index(List item, Object testObject, Description mismatchDescription) { + try { + item.add(0, testObject); + mismatchDescription.appendText("was able to add a value into the list by index"); + return true; + } catch (Exception ignore) { + } + return false; + } + + private boolean checkMethod_listIterator_remove(List item, Description mismatchDescription) { + List list = item; + try { + ListIterator iterator = list.listIterator(); + iterator.remove(); + mismatchDescription.appendText("was able to remove an element from the list iterator"); + return true; + } catch (Exception ignore) { + } + return false; + } + + private boolean checkMethod_listIterator_set(List item, Object testObject, Description mismatchDescription) { + List list = item; + try { + ListIterator iterator = list.listIterator(); + iterator.next(); + iterator.set(testObject); + mismatchDescription.appendText("was able to set element on the list iterator"); + return true; + } catch (Exception ignore) { + } + return false; + } + + private boolean checkMethod_listIterator_add(List item, Object testObject, Description mismatchDescription) { + List list = item; + try { + ListIterator iterator = list.listIterator(); + iterator.next(); + iterator.add(testObject); + mismatchDescription.appendText("was able to add element on the list iterator"); + return true; + } catch (Exception ignore) { + } + return false; + } + + private boolean checkMethod_listIterator_index(List item, Description mismatchDescription) { + List list = item; + try { + Iterator iterator = list.listIterator(0); + iterator.remove(); + mismatchDescription.appendText("was able to remove an element from the list iterator with index"); + return true; + } catch (Exception ignore) { + } + return false; + } + + private boolean checkMethod_set(List list, Description mismatchDescription) { + if (list.size() > 0) { + try { + list.set(0, list.get(0)); + mismatchDescription.appendText("was able to set an element of the collection"); + return true; + } catch (Exception ignore) { + } + } + return false; + } + + @SuppressWarnings("unchecked") + private T getInstanceOfType(final Class clazz, Collection collection) { + if (clazz.isArray()) { + return (T) Array.newInstance(clazz, 0); + } + + if (clazz.isPrimitive()) { + if (Byte.TYPE.isAssignableFrom(clazz)) { + return (T) Byte.valueOf((byte) 1); + } + if (Short.TYPE.isAssignableFrom(clazz)) { + return (T) Short.valueOf((short) 1); + } + if (Integer.TYPE.isAssignableFrom(clazz)) { + return (T) Integer.valueOf(1); + } + if (Long.TYPE.isAssignableFrom(clazz)) { + return (T) Long.valueOf(1L); + } + if (Float.TYPE.isAssignableFrom(clazz)) { + return (T) Float.valueOf(1L); + } + if (Double.TYPE.isAssignableFrom(clazz)) { + return (T) Double.valueOf(1L); + } + if (Boolean.TYPE.isAssignableFrom(clazz)) { + return (T) Boolean.valueOf(true); + } + if (Character.TYPE.isAssignableFrom(clazz)) { + return (T) Character.valueOf(' '); + } + } + + if (clazz.isInterface()) { + Object defaultCollection = DEFAULT_COLLECTIONS.get(clazz); + if (defaultCollection != null) { + return (T) defaultCollection; + } + return null; + } + + // For the most part of implementations there probably won't be any default constructor + final Constructor[] declaredConstructors = clazz.getDeclaredConstructors(); + + Constructor constructorForCollection = findConstructorForCollection(declaredConstructors); + + Exception lastException = null; + if (constructorForCollection != null) { + try { + return (T) constructorForCollection.newInstance(collection); + } catch (Exception e) { + lastException = e; + } + } + + // First take constructor with fewer number of arguments + Arrays.sort(declaredConstructors, new Comparator>() { + @Override + public int compare(Constructor o1, Constructor o2) { + return Integer.compare(o2.getParameterTypes().length, o1.getParameterTypes().length); + } + }); + + for (Constructor declaredConstructor : declaredConstructors) { + try { + declaredConstructor.setAccessible(true); + } catch (Exception ignore) { + // Since Java 17 it is impossible to make jdk* classes accessible without manipulation with modules: + // module java.base does not "opens java.util" to unnamed module + } + final int parametersNumber = declaredConstructor.getParameterTypes().length; + + Object[] arguments = new Object[parametersNumber]; + for (int argumentIndex = 0; argumentIndex < arguments.length; argumentIndex++) { + arguments[argumentIndex] = getInstanceOfType(declaredConstructor.getParameterTypes()[argumentIndex], collection); + } + try { + return (T) declaredConstructor.newInstance(arguments); + } catch (Exception e) { + lastException = e; + } + + } + throw failedToInstantiateItem(clazz, lastException); + } + + private Constructor findConstructorForCollection(Constructor[] declaredConstructors) { + for (Constructor constructor : declaredConstructors) { + if (constructor.getParameterTypes().length == 1 && constructor.getParameterTypes()[0].isAssignableFrom(Collection.class)) { + return constructor; + } + } + return null; + } + + private IllegalStateException failedToInstantiateItem(Class clazz, Exception e) { + return new IllegalStateException("Failed to create an instance of <" + clazz + "> class.", e); + } + + @Override + public void describeTo(Description description) { + description.appendText("Expected to be unmodifiable collection, but "); + } + +} diff --git a/hamcrest/src/test/java/org/hamcrest/collection/IsUnmodifiableCollectionTest.java b/hamcrest/src/test/java/org/hamcrest/collection/IsUnmodifiableCollectionTest.java new file mode 100644 index 00000000..9aabcd88 --- /dev/null +++ b/hamcrest/src/test/java/org/hamcrest/collection/IsUnmodifiableCollectionTest.java @@ -0,0 +1,289 @@ +package org.hamcrest.collection; + +import org.hamcrest.AbstractMatcherTest; +import org.hamcrest.Matcher; + +import java.util.*; + +import static org.hamcrest.collection.IsUnmodifiableCollection.isUnmodifiable; + +public class IsUnmodifiableCollectionTest extends AbstractMatcherTest { + + private static final String SET_INT_INDEX_E_ELEMENT = "set(int index, E element)"; + private static final String ADD_E_E = "add(E e)"; + private static final String ADD_INT_INDEX_E_ELEMENT = "add(int index, E element)"; + private static final String REMOVE_INT_INDEX = "remove(int index)"; + private static final String REMOVE_OBJECT_O = "remove(Object o)"; + private static final String ADD_ALL_COLLECTION_EXTENDS_E_C = "addAll(Collection c)"; + private static final String ADD_ALL_INT_INDEX_COLLECTION_EXTENDS_E_C = "addAll(int index, Collection c)"; + private static final String REMOVE_ALL_COLLECTION_C = "removeAll(Collection c)"; + private static final String RETAIN_ALL_COLLECTION_C = "retainAll(Collection c)"; + private static final String CLEAR = "clear()"; + private static final List ERROR_CONDITIONS = Arrays.asList( + new String[]{"was able to add element on the list iterator", SET_INT_INDEX_E_ELEMENT}, + new String[]{"was able to perform addAll by index on the collection", SET_INT_INDEX_E_ELEMENT, ADD_INT_INDEX_E_ELEMENT}, + new String[]{"was able to call remove by index from the collection", SET_INT_INDEX_E_ELEMENT, ADD_INT_INDEX_E_ELEMENT, ADD_ALL_INT_INDEX_COLLECTION_EXTENDS_E_C}, + new String[]{"was able to add a value into the collection", SET_INT_INDEX_E_ELEMENT, ADD_INT_INDEX_E_ELEMENT, ADD_ALL_INT_INDEX_COLLECTION_EXTENDS_E_C, REMOVE_INT_INDEX}, + new String[]{"was able to perform addAll on the collection", SET_INT_INDEX_E_ELEMENT, ADD_INT_INDEX_E_ELEMENT, ADD_ALL_INT_INDEX_COLLECTION_EXTENDS_E_C, REMOVE_INT_INDEX, ADD_E_E}, + new String[]{"was able to call remove a value from the collection", SET_INT_INDEX_E_ELEMENT, ADD_INT_INDEX_E_ELEMENT, ADD_ALL_INT_INDEX_COLLECTION_EXTENDS_E_C, REMOVE_INT_INDEX, ADD_E_E, ADD_ALL_COLLECTION_EXTENDS_E_C}, + new String[]{"was able to call removeAll on the collection", SET_INT_INDEX_E_ELEMENT, ADD_INT_INDEX_E_ELEMENT, ADD_ALL_INT_INDEX_COLLECTION_EXTENDS_E_C, REMOVE_INT_INDEX, ADD_E_E, ADD_ALL_COLLECTION_EXTENDS_E_C, REMOVE_OBJECT_O}, + new String[]{"was able to call retainAll on the collection", SET_INT_INDEX_E_ELEMENT, ADD_INT_INDEX_E_ELEMENT, ADD_ALL_INT_INDEX_COLLECTION_EXTENDS_E_C, REMOVE_INT_INDEX, ADD_E_E, ADD_ALL_COLLECTION_EXTENDS_E_C, REMOVE_OBJECT_O, REMOVE_ALL_COLLECTION_C}, + new String[]{"was able to clear the collection", SET_INT_INDEX_E_ELEMENT, ADD_INT_INDEX_E_ELEMENT, ADD_ALL_INT_INDEX_COLLECTION_EXTENDS_E_C, REMOVE_INT_INDEX, ADD_E_E, ADD_ALL_COLLECTION_EXTENDS_E_C, REMOVE_OBJECT_O, REMOVE_ALL_COLLECTION_C, RETAIN_ALL_COLLECTION_C}, + new String[]{null, SET_INT_INDEX_E_ELEMENT, ADD_INT_INDEX_E_ELEMENT, ADD_ALL_INT_INDEX_COLLECTION_EXTENDS_E_C, REMOVE_INT_INDEX, ADD_E_E, ADD_ALL_COLLECTION_EXTENDS_E_C, REMOVE_OBJECT_O, REMOVE_ALL_COLLECTION_C, RETAIN_ALL_COLLECTION_C, CLEAR} + ); + + @Override + protected Matcher createMatcher() { + return isUnmodifiable(); + } + + public void testMatchesUnmodifiableList() { + assertMatches("truly unmodifiable list", isUnmodifiable(), Collections.unmodifiableList(Collections.emptyList())); + } + + public void testMatchesUnmodifiableCustomList() { + class CustomUnmodifiableList implements List { + + private List list; + + public CustomUnmodifiableList(List list) { + this.list = Collections.unmodifiableList(list); + } + + @Override + public int size() { + return list.size(); + } + + @Override + public boolean isEmpty() { + return list.isEmpty(); + } + + @Override + public boolean contains(Object o) { + return list.contains(o); + } + + @Override + public Iterator iterator() { + return list.iterator(); + } + + @Override + public Object[] toArray() { + return list.toArray(); + } + + @Override + public T[] toArray(T[] a) { + return list.toArray(a); + } + + @Override + public boolean add(E e) { + return list.add(e); + } + + @Override + public boolean remove(Object o) { + return list.remove(o); + } + + @Override + public boolean containsAll(Collection c) { + return list.containsAll(c); + } + + @Override + public boolean addAll(Collection c) { + return list.addAll(c); + } + + @Override + public boolean addAll(int index, Collection c) { + return list.addAll(index, c); + } + + @Override + public boolean removeAll(Collection c) { + return list.removeAll(c); + } + + @Override + public boolean retainAll(Collection c) { + return list.retainAll(c); + } + + @Override + public void clear() { + list.clear(); + } + + @Override + public E get(int index) { + return list.get(index); + } + + @Override + public E set(int index, E element) { + return list.set(index, element); + } + + @Override + public void add(int index, E element) { + list.add(index, element); + } + + @Override + public E remove(int index) { + return list.remove(index); + } + + @Override + public int indexOf(Object o) { + return list.indexOf(o); + } + + @Override + public int lastIndexOf(Object o) { + return list.lastIndexOf(o); + } + + @Override + public ListIterator listIterator() { + return list.listIterator(); + } + + @Override + public ListIterator listIterator(int index) { + return list.listIterator(index); + } + + @Override + public List subList(int fromIndex, int toIndex) { + return list.subList(fromIndex, toIndex); + } + } + assertMatches("truly unmodifiable list", isUnmodifiable(), new CustomUnmodifiableList<>(Arrays.asList(1, 2, 3))); + } + + public void testMatchesUnmodifiableSet() { + assertMatches("truly unmodifiable set", isUnmodifiable(), Collections.unmodifiableSet(Collections.emptySet())); + } + + public void testMatchesUnmodifiableCollection() { + assertMatches("truly unmodifiable collection", isUnmodifiable(), Collections.unmodifiableCollection(Arrays.asList(1, 2, 3))); + } + + public void testMismatchesArrayList() { + assertMismatchDescription("was able to add a value into the list by index", isUnmodifiable(), new ArrayList<>()); + } + + public void testMismatchesArraysList() { + assertMismatchDescription("java.util.Arrays$ArrayList is a known modifiable collection", isUnmodifiable(), Arrays.asList(1, 2, 3)); + } + + public void testMismatchesHashSet() { + assertMismatchDescription("was able to add a value into the collection", isUnmodifiable(), new HashSet<>()); + } + + public void testMismatches() { + for (String[] errorCondition : ERROR_CONDITIONS) { + String[] unsupportedMethods = new String[errorCondition.length - 1]; + System.arraycopy(errorCondition, 1, unsupportedMethods, 0, unsupportedMethods.length); + ArrayListWrapper arrayListWrapper = new ArrayListWrapper<>(Arrays.asList(1, 2, 3), unsupportedMethods); + String error = errorCondition[0]; + if (error != null) { + assertMismatchDescription( + error, + isUnmodifiable(), + arrayListWrapper + ); + } else { + assertMatches("truly unmodifiable collection", isUnmodifiable(), arrayListWrapper); + } + } + } + + static class ArrayListWrapper extends ArrayList { + private final Set unsupportedMethods; + + @SuppressWarnings("unused") // Used by reflection + public ArrayListWrapper(Collection c) { + super(c); + if (c instanceof ArrayListWrapper) { + this.unsupportedMethods = new HashSet<>(((ArrayListWrapper) c).unsupportedMethods); + } else { + throw new IllegalStateException(); + } + } + + public ArrayListWrapper(List list, String... unsupportedMethods) { + super(list); + this.unsupportedMethods = new HashSet<>(Arrays.asList(unsupportedMethods)); + } + + @Override + public E set(int index, E element) { + if (unsupportedMethods.contains(SET_INT_INDEX_E_ELEMENT)) throw new UnsupportedOperationException(); + return super.set(index, element); + } + + @Override + public boolean add(E e) { + if (unsupportedMethods.contains(ADD_E_E)) throw new UnsupportedOperationException(); + return super.add(e); + } + + @Override + public void add(int index, E element) { + if (unsupportedMethods.contains(ADD_INT_INDEX_E_ELEMENT)) throw new UnsupportedOperationException(); + super.add(index, element); + } + + @Override + public E remove(int index) { + if (unsupportedMethods.contains(REMOVE_INT_INDEX)) throw new UnsupportedOperationException(); + return super.remove(index); + } + + @Override + public boolean remove(Object o) { + if (unsupportedMethods.contains(REMOVE_OBJECT_O)) throw new UnsupportedOperationException(); + return super.remove(o); + } + + @Override + public void clear() { + if (unsupportedMethods.contains(CLEAR)) throw new UnsupportedOperationException(); + super.clear(); + } + + @Override + public boolean addAll(Collection c) { + if (unsupportedMethods.contains(ADD_ALL_COLLECTION_EXTENDS_E_C)) throw new UnsupportedOperationException(); + return super.addAll(c); + } + + @Override + public boolean addAll(int index, Collection c) { + if (unsupportedMethods.contains(ADD_ALL_INT_INDEX_COLLECTION_EXTENDS_E_C)) + throw new UnsupportedOperationException(); + return super.addAll(index, c); + } + + @Override + public boolean removeAll(Collection c) { + if (unsupportedMethods.contains(REMOVE_ALL_COLLECTION_C)) throw new UnsupportedOperationException(); + return super.removeAll(c); + } + + @Override + public boolean retainAll(Collection c) { + if (unsupportedMethods.contains(RETAIN_ALL_COLLECTION_C)) throw new UnsupportedOperationException(); + return super.retainAll(c); + } + } + +}