Skip to content

Commit 4aad918

Browse files
authored
Merge pull request #6 from Andremoniy/Issue249-jdk17
Issue249 jdk17 - I am merging this so that I can review it properly. I may revert afterwards if it fails review.
2 parents 0adc3b7 + 3839005 commit 4aad918

2 files changed

Lines changed: 644 additions & 0 deletions

File tree

Lines changed: 355 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,355 @@
1+
package org.hamcrest.collection;
2+
3+
import org.hamcrest.Description;
4+
import org.hamcrest.Matcher;
5+
import org.hamcrest.TypeSafeDiagnosingMatcher;
6+
7+
import java.lang.reflect.Array;
8+
import java.lang.reflect.Constructor;
9+
import java.util.*;
10+
11+
/**
12+
* Matches if collection is truly unmodifiable
13+
*/
14+
public class IsUnmodifiableCollection<E> extends TypeSafeDiagnosingMatcher<Collection<? extends E>> {
15+
16+
private static final Map<Class, Object> DEFAULT_COLLECTIONS = new HashMap<>();
17+
private static final Set<String> KNOWN_UNMODIFIABLE_COLLECTIONS = new HashSet<>();
18+
private static final Set<String> KNOWN_MODIFIABLE_COLLECTIONS = new HashSet<>();
19+
20+
static {
21+
final List<String> list = Arrays.asList("a", "b", "c");
22+
DEFAULT_COLLECTIONS.put(Collection.class, list);
23+
DEFAULT_COLLECTIONS.put(List.class, list);
24+
DEFAULT_COLLECTIONS.put(Set.class, new HashSet<>(list));
25+
26+
KNOWN_UNMODIFIABLE_COLLECTIONS.add("java.util.ImmutableCollections");
27+
KNOWN_UNMODIFIABLE_COLLECTIONS.add("java.util.Collections$Unmodifiable");
28+
29+
KNOWN_MODIFIABLE_COLLECTIONS.add("java.util.Arrays$ArrayList");
30+
}
31+
32+
/**
33+
* Creates matcher that matches when collection is truly unmodifiable
34+
*/
35+
public static <E> Matcher<Collection<? extends E>> isUnmodifiable() {
36+
return new IsUnmodifiableCollection<>();
37+
}
38+
39+
@SuppressWarnings("unchecked")
40+
@Override
41+
protected boolean matchesSafely(final Collection collection, final Description mismatchDescription) {
42+
final Class<? extends Collection> collectionClass = collection.getClass();
43+
String collectionClassName = collectionClass.getName();
44+
for (String knownUnmodifiableCollection : KNOWN_UNMODIFIABLE_COLLECTIONS) {
45+
if (collectionClassName.startsWith(knownUnmodifiableCollection)) {
46+
return true;
47+
}
48+
}
49+
for (String knownModifiableCollection : KNOWN_MODIFIABLE_COLLECTIONS) {
50+
if (collectionClassName.startsWith(knownModifiableCollection)) {
51+
mismatchDescription.appendText(collectionClassName + " is a known modifiable collection");
52+
return false;
53+
}
54+
}
55+
final Collection item = getInstanceOfType(collectionClass, collection);
56+
if (item == null) {
57+
throw failedToInstantiateItem(collectionClass, null);
58+
}
59+
final Object testObject = new Object();
60+
final Set<Object> singletonList = Collections.singleton(testObject);
61+
62+
if (collection instanceof List) {
63+
// This is an operation on the original collection, but it is safe, since it sets the same element
64+
List originalList = (List) collection;
65+
if (checkMethod_set(originalList, mismatchDescription)) return false;
66+
67+
List copiedList = (List) item;
68+
if (checkMethod_listIterator_remove(copiedList, mismatchDescription)) return false;
69+
if (checkMethod_listIterator_set(copiedList, testObject, mismatchDescription)) return false;
70+
if (checkMethod_listIterator_add(copiedList, testObject, mismatchDescription)) return false;
71+
if (checkMethod_listIterator_index(copiedList, mismatchDescription)) return false;
72+
if (checkMethod_add_index(copiedList, testObject, mismatchDescription)) return false;
73+
if (checkMethod_add_all_index(copiedList, singletonList, mismatchDescription)) return false;
74+
if (checkMethod_remove_index(copiedList, mismatchDescription)) return false;
75+
}
76+
77+
if (checkMethod_add(item, testObject, mismatchDescription)) return false;
78+
if (checkMethod_add_all(item, singletonList, mismatchDescription)) return false;
79+
if (checkMethod_remove(item, testObject, mismatchDescription)) return false;
80+
if (checkMethod_remove_all(item, singletonList, mismatchDescription)) return false;
81+
if (checkMethod_retail_all(item, singletonList, mismatchDescription)) return false;
82+
if (checkMethod_clear(item, mismatchDescription)) return false;
83+
if (checkMethod_iterator(item, mismatchDescription)) return false;
84+
85+
return true;
86+
}
87+
88+
private boolean checkMethod_iterator(Collection item, Description mismatchDescription) {
89+
try {
90+
Iterator iterator = item.iterator();
91+
iterator.remove();
92+
mismatchDescription.appendText("was able to remove an element from the iterator");
93+
return true;
94+
} catch (Exception ignore) {
95+
}
96+
return false;
97+
}
98+
99+
private boolean checkMethod_clear(Collection item, Description mismatchDescription) {
100+
try {
101+
item.clear();
102+
mismatchDescription.appendText("was able to clear the collection");
103+
return true;
104+
} catch (Exception ignore) {
105+
}
106+
return false;
107+
}
108+
109+
private boolean checkMethod_retail_all(Collection item, Set<Object> singletonList, Description mismatchDescription) {
110+
try {
111+
item.retainAll(singletonList);
112+
mismatchDescription.appendText("was able to call retainAll on the collection");
113+
return true;
114+
} catch (Exception ignore) {
115+
}
116+
return false;
117+
}
118+
119+
private boolean checkMethod_remove_all(Collection item, Set<Object> singletonList, Description mismatchDescription) {
120+
try {
121+
item.removeAll(singletonList);
122+
mismatchDescription.appendText("was able to call removeAll on the collection");
123+
return true;
124+
} catch (Exception ignore) {
125+
}
126+
return false;
127+
}
128+
129+
private boolean checkMethod_remove(Collection item, Object testObject, Description mismatchDescription) {
130+
try {
131+
item.remove(testObject);
132+
mismatchDescription.appendText("was able to call remove a value from the collection");
133+
return true;
134+
} catch (Exception ignore) {
135+
}
136+
return false;
137+
}
138+
139+
private boolean checkMethod_remove_index(List item, Description mismatchDescription) {
140+
try {
141+
item.remove(0);
142+
mismatchDescription.appendText("was able to call remove by index from the collection");
143+
return true;
144+
} catch (Exception ignore) {
145+
}
146+
return false;
147+
}
148+
149+
private boolean checkMethod_add_all(Collection item, Set<Object> singletonList, Description mismatchDescription) {
150+
try {
151+
item.addAll(singletonList);
152+
mismatchDescription.appendText("was able to perform addAll on the collection");
153+
return true;
154+
} catch (Exception ignore) {
155+
}
156+
return false;
157+
}
158+
159+
private boolean checkMethod_add_all_index(List item, Set<Object> singletonList, Description mismatchDescription) {
160+
try {
161+
item.addAll(0, singletonList);
162+
mismatchDescription.appendText("was able to perform addAll by index on the collection");
163+
return true;
164+
} catch (Exception ignore) {
165+
}
166+
return false;
167+
}
168+
169+
private boolean checkMethod_add(Collection item, Object testObject, Description mismatchDescription) {
170+
try {
171+
item.add(testObject);
172+
mismatchDescription.appendText("was able to add a value into the collection");
173+
return true;
174+
} catch (Exception ignore) {
175+
}
176+
return false;
177+
}
178+
179+
private boolean checkMethod_add_index(List item, Object testObject, Description mismatchDescription) {
180+
try {
181+
item.add(0, testObject);
182+
mismatchDescription.appendText("was able to add a value into the list by index");
183+
return true;
184+
} catch (Exception ignore) {
185+
}
186+
return false;
187+
}
188+
189+
private boolean checkMethod_listIterator_remove(List item, Description mismatchDescription) {
190+
List list = item;
191+
try {
192+
ListIterator iterator = list.listIterator();
193+
iterator.remove();
194+
mismatchDescription.appendText("was able to remove an element from the list iterator");
195+
return true;
196+
} catch (Exception ignore) {
197+
}
198+
return false;
199+
}
200+
201+
private boolean checkMethod_listIterator_set(List item, Object testObject, Description mismatchDescription) {
202+
List list = item;
203+
try {
204+
ListIterator iterator = list.listIterator();
205+
iterator.next();
206+
iterator.set(testObject);
207+
mismatchDescription.appendText("was able to set element on the list iterator");
208+
return true;
209+
} catch (Exception ignore) {
210+
}
211+
return false;
212+
}
213+
214+
private boolean checkMethod_listIterator_add(List item, Object testObject, Description mismatchDescription) {
215+
List list = item;
216+
try {
217+
ListIterator iterator = list.listIterator();
218+
iterator.next();
219+
iterator.add(testObject);
220+
mismatchDescription.appendText("was able to add element on the list iterator");
221+
return true;
222+
} catch (Exception ignore) {
223+
}
224+
return false;
225+
}
226+
227+
private boolean checkMethod_listIterator_index(List item, Description mismatchDescription) {
228+
List list = item;
229+
try {
230+
Iterator iterator = list.listIterator(0);
231+
iterator.remove();
232+
mismatchDescription.appendText("was able to remove an element from the list iterator with index");
233+
return true;
234+
} catch (Exception ignore) {
235+
}
236+
return false;
237+
}
238+
239+
private boolean checkMethod_set(List list, Description mismatchDescription) {
240+
if (list.size() > 0) {
241+
try {
242+
list.set(0, list.get(0));
243+
mismatchDescription.appendText("was able to set an element of the collection");
244+
return true;
245+
} catch (Exception ignore) {
246+
}
247+
}
248+
return false;
249+
}
250+
251+
@SuppressWarnings("unchecked")
252+
private <T> T getInstanceOfType(final Class clazz, Collection collection) {
253+
if (clazz.isArray()) {
254+
return (T) Array.newInstance(clazz, 0);
255+
}
256+
257+
if (clazz.isPrimitive()) {
258+
if (Byte.TYPE.isAssignableFrom(clazz)) {
259+
return (T) Byte.valueOf((byte) 1);
260+
}
261+
if (Short.TYPE.isAssignableFrom(clazz)) {
262+
return (T) Short.valueOf((short) 1);
263+
}
264+
if (Integer.TYPE.isAssignableFrom(clazz)) {
265+
return (T) Integer.valueOf(1);
266+
}
267+
if (Long.TYPE.isAssignableFrom(clazz)) {
268+
return (T) Long.valueOf(1L);
269+
}
270+
if (Float.TYPE.isAssignableFrom(clazz)) {
271+
return (T) Float.valueOf(1L);
272+
}
273+
if (Double.TYPE.isAssignableFrom(clazz)) {
274+
return (T) Double.valueOf(1L);
275+
}
276+
if (Boolean.TYPE.isAssignableFrom(clazz)) {
277+
return (T) Boolean.valueOf(true);
278+
}
279+
if (Character.TYPE.isAssignableFrom(clazz)) {
280+
return (T) Character.valueOf(' ');
281+
}
282+
}
283+
284+
if (clazz.isInterface()) {
285+
Object defaultCollection = DEFAULT_COLLECTIONS.get(clazz);
286+
if (defaultCollection != null) {
287+
return (T) defaultCollection;
288+
}
289+
return null;
290+
}
291+
292+
// For the most part of implementations there probably won't be any default constructor
293+
final Constructor<?>[] declaredConstructors = clazz.getDeclaredConstructors();
294+
295+
Constructor<?> constructorForCollection = findConstructorForCollection(declaredConstructors);
296+
297+
Exception lastException = null;
298+
if (constructorForCollection != null) {
299+
try {
300+
return (T) constructorForCollection.newInstance(collection);
301+
} catch (Exception e) {
302+
lastException = e;
303+
}
304+
}
305+
306+
// First take constructor with fewer number of arguments
307+
Arrays.sort(declaredConstructors, new Comparator<Constructor<?>>() {
308+
@Override
309+
public int compare(Constructor<?> o1, Constructor<?> o2) {
310+
return Integer.compare(o2.getParameterTypes().length, o1.getParameterTypes().length);
311+
}
312+
});
313+
314+
for (Constructor<?> declaredConstructor : declaredConstructors) {
315+
try {
316+
declaredConstructor.setAccessible(true);
317+
} catch (Exception ignore) {
318+
// Since Java 17 it is impossible to make jdk* classes accessible without manipulation with modules:
319+
// module java.base does not "opens java.util" to unnamed module
320+
}
321+
final int parametersNumber = declaredConstructor.getParameterTypes().length;
322+
323+
Object[] arguments = new Object[parametersNumber];
324+
for (int argumentIndex = 0; argumentIndex < arguments.length; argumentIndex++) {
325+
arguments[argumentIndex] = getInstanceOfType(declaredConstructor.getParameterTypes()[argumentIndex], collection);
326+
}
327+
try {
328+
return (T) declaredConstructor.newInstance(arguments);
329+
} catch (Exception e) {
330+
lastException = e;
331+
}
332+
333+
}
334+
throw failedToInstantiateItem(clazz, lastException);
335+
}
336+
337+
private Constructor<?> findConstructorForCollection(Constructor<?>[] declaredConstructors) {
338+
for (Constructor<?> constructor : declaredConstructors) {
339+
if (constructor.getParameterTypes().length == 1 && constructor.getParameterTypes()[0].isAssignableFrom(Collection.class)) {
340+
return constructor;
341+
}
342+
}
343+
return null;
344+
}
345+
346+
private <T> IllegalStateException failedToInstantiateItem(Class<T> clazz, Exception e) {
347+
return new IllegalStateException("Failed to create an instance of <" + clazz + "> class.", e);
348+
}
349+
350+
@Override
351+
public void describeTo(Description description) {
352+
description.appendText("Expected to be unmodifiable collection, but ");
353+
}
354+
355+
}

0 commit comments

Comments
 (0)