1 /*
2 * Licensed to the Apache Software Foundation (ASF) under one or more
3 * contributor license agreements. See the NOTICE file distributed with
4 * this work for additional information regarding copyright ownership.
5 * The ASF licenses this file to You under the Apache License, Version 2.0
6 * (the "License"); you may not use this file except in compliance with
7 * the License. You may obtain a copy of the License at
8 *
9 * http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
17 package org.apache.commons.collections4.map;
18
19 import static org.junit.jupiter.api.Assertions.assertEquals;
20 import static org.junit.jupiter.api.Assertions.assertFalse;
21 import static org.junit.jupiter.api.Assertions.assertNotNull;
22 import static org.junit.jupiter.api.Assertions.assertNull;
23 import static org.junit.jupiter.api.Assertions.assertThrows;
24 import static org.junit.jupiter.api.Assertions.assertTrue;
25 import static org.junit.jupiter.api.Assertions.fail;
26
27 import java.io.ByteArrayInputStream;
28 import java.io.ByteArrayOutputStream;
29 import java.io.IOException;
30 import java.io.ObjectInputStream;
31 import java.io.ObjectOutputStream;
32 import java.io.Serializable;
33 import java.lang.ref.WeakReference;
34 import java.util.ArrayList;
35 import java.util.Iterator;
36 import java.util.List;
37 import java.util.Map;
38 import java.util.function.Consumer;
39
40 import org.apache.commons.collections4.map.AbstractHashedMap.HashEntry;
41 import org.apache.commons.collections4.map.AbstractReferenceMap.ReferenceEntry;
42 import org.apache.commons.collections4.map.AbstractReferenceMap.ReferenceStrength;
43 import org.junit.jupiter.api.Test;
44
45 /**
46 * Tests for ReferenceMap.
47 *
48 * @param <K> the key type.
49 * @param <V> the value type.
50 */
51 public class ReferenceMapTest<K, V> extends AbstractIterableMapTest<K, V> {
52
53 private static final class AccessibleEntry<K, V> extends ReferenceEntry<K, V> {
54 final AbstractReferenceMap<K, V> parent;
55 final Consumer<V> consumer;
56
57 AccessibleEntry(final AbstractReferenceMap<K, V> parent, final HashEntry<K, V> next, final int hashCode, final K key, final V value, final Consumer<V> consumer) {
58 super(parent, next, hashCode, key, value);
59 this.parent = parent;
60 this.consumer = consumer;
61 }
62
63 @Override
64 protected void onPurge() {
65 if (parent.isValueType(ReferenceStrength.HARD)) {
66 consumer.accept(getValue());
67 }
68 }
69 }
70
71 @SuppressWarnings("unused")
72 private static void gc() {
73 try {
74 // trigger GC
75 final byte[][] tooLarge = new byte[1000000000][1000000000];
76 fail("you have too much RAM");
77 } catch (final OutOfMemoryError ex) {
78 System.gc(); // ignore
79 }
80 }
81
82 WeakReference<K> keyReference;
83
84 WeakReference<V> valueReference;
85
86 // public void testCreate() throws Exception {
87 // resetEmpty();
88 // writeExternalFormToDisk(
89 // (java.io.Serializable) map,
90 // "src/test/resources/data/test/ReferenceMap.emptyCollection.version4.obj");
91 // resetFull();
92 // writeExternalFormToDisk(
93 // (java.io.Serializable) map,
94 // "src/test/resources/data/test/ReferenceMap.fullCollection.version4.obj");
95 // }
96
97 @SuppressWarnings("unchecked")
98 public Map<K, V> buildRefMap() {
99 final K key = (K) new Object();
100 final V value = (V) new Object();
101
102 keyReference = new WeakReference<>(key);
103 valueReference = new WeakReference<>(value);
104
105 final Map<K, V> testMap = new ReferenceMap<>(ReferenceStrength.WEAK, ReferenceStrength.HARD, true);
106 testMap.put(key, value);
107
108 assertEquals(value, testMap.get(key), "In map");
109 assertNotNull(keyReference.get(), "Weak reference released early (1)");
110 assertNotNull(valueReference.get(), "Weak reference released early (2)");
111 return testMap;
112 }
113
114 /*
115 // Tests often fail because gc is uncontrollable
116
117 @Test
118 public void testPurge() {
119 ReferenceMap map = new ReferenceMap(ReferenceMap.WEAK, ReferenceMap.WEAK);
120 Object[] hard = new Object[10];
121 for (int i = 0; i < hard.length; i++) {
122 hard[i] = new Object();
123 map.put(hard[i], new Object());
124 }
125 gc();
126 assertTrue("map should be empty after purge of weak values", map.isEmpty());
127
128 for (int i = 0; i < hard.length; i++) {
129 map.put(new Object(), hard[i]);
130 }
131 gc();
132 assertTrue("map should be empty after purge of weak keys", map.isEmpty());
133
134 for (int i = 0; i < hard.length; i++) {
135 map.put(new Object(), hard[i]);
136 map.put(hard[i], new Object());
137 }
138
139 gc();
140 assertTrue("map should be empty after purge of weak keys and values", map.isEmpty());
141 }
142
143 @Test
144 public void testGetAfterGC() {
145 ReferenceMap map = new ReferenceMap(ReferenceMap.WEAK, ReferenceMap.WEAK);
146 for (int i = 0; i < 10; i++) {
147 map.put(Integer.valueOf(i), Integer.valueOf(i));
148 }
149
150 gc();
151 for (int i = 0; i < 10; i++) {
152 Integer I = Integer.valueOf(i);
153 assertTrue("map.containsKey should return false for GC'd element", !map.containsKey(I));
154 assertTrue("map.get should return null for GC'd element", map.get(I) == null);
155 }
156 }
157
158 @Test
159 public void testEntrySetIteratorAfterGC() {
160 ReferenceMap map = new ReferenceMap(ReferenceMap.WEAK, ReferenceMap.WEAK);
161 Object[] hard = new Object[10];
162 for (int i = 0; i < 10; i++) {
163 hard[i] = Integer.valueOf(10 + i);
164 map.put(Integer.valueOf(i), Integer.valueOf(i));
165 map.put(hard[i], hard[i]);
166 }
167
168 gc();
169 Iterator iterator = map.entrySet().iterator();
170 while (iterator.hasNext()) {
171 Map.Entry entry = (Map.Entry)iterator.next();
172 Integer key = (Integer)entry.getKey();
173 Integer value = (Integer)entry.getValue();
174 assertTrue("iterator should skip GC'd keys", key.intValue() >= 10);
175 assertTrue("iterator should skip GC'd values", value.intValue() >= 10);
176 }
177
178 }
179
180 @Test
181 public void testMapIteratorAfterGC() {
182 ReferenceMap map = new ReferenceMap(ReferenceMap.WEAK, ReferenceMap.WEAK);
183 Object[] hard = new Object[10];
184 for (int i = 0; i < 10; i++) {
185 hard[i] = Integer.valueOf(10 + i);
186 map.put(Integer.valueOf(i), Integer.valueOf(i));
187 map.put(hard[i], hard[i]);
188 }
189
190 gc();
191 MapIterator iterator = map.mapIterator();
192 while (iterator.hasNext()) {
193 Object key1 = iterator.next();
194 Integer key = (Integer) iterator.getKey();
195 Integer value = (Integer) iterator.getValue();
196 assertTrue("iterator keys should match", key == key1);
197 assertTrue("iterator should skip GC'd keys", key.intValue() >= 10);
198 assertTrue("iterator should skip GC'd values", value.intValue() >= 10);
199 }
200
201 }
202
203 @Test
204 public void testMapIteratorAfterGC2() {
205 ReferenceMap map = new ReferenceMap(ReferenceMap.WEAK, ReferenceMap.WEAK);
206 Object[] hard = new Object[10];
207 for (int i = 0; i < 10; i++) {
208 hard[i] = Integer.valueOf(10 + i);
209 map.put(Integer.valueOf(i), Integer.valueOf(i));
210 map.put(hard[i], hard[i]);
211 }
212
213 MapIterator iterator = map.mapIterator();
214 while (iterator.hasNext()) {
215 Object key1 = iterator.next();
216 gc();
217 Integer key = (Integer) iterator.getKey();
218 Integer value = (Integer) iterator.getValue();
219 assertTrue("iterator keys should match", key == key1);
220 assertTrue("iterator should skip GC'd keys", key.intValue() >= 10);
221 assertTrue("iterator should skip GC'd values", value.intValue() >= 10);
222 }
223
224 }
225 */
226
227 @Override
228 public String getCompatibilityVersion() {
229 return "4";
230 }
231 @Override
232 public boolean isAllowNullKey() {
233 return false;
234 }
235
236 @Override
237 public boolean isAllowNullValueGet() {
238 return true;
239 }
240
241 @Override
242 public boolean isAllowNullValuePut() {
243 return false;
244 }
245
246 @Override
247 public ReferenceMap<K, V> makeObject() {
248 return new ReferenceMap<>(ReferenceStrength.WEAK, ReferenceStrength.WEAK);
249 }
250
251 @Test
252 public void testCustomPurge() {
253 final List<Integer> expiredValues = new ArrayList<>();
254 @SuppressWarnings("unchecked")
255 final Consumer<Integer> consumer = (Consumer<Integer> & Serializable) expiredValues::add;
256 final Map<Integer, Integer> map = new ReferenceMap<Integer, Integer>(ReferenceStrength.WEAK, ReferenceStrength.HARD, false) {
257 private static final long serialVersionUID = 1L;
258
259 @Override
260 protected ReferenceEntry<Integer, Integer> createEntry(final HashEntry<Integer, Integer> next, final int hashCode, final Integer key, final Integer value) {
261 return new AccessibleEntry<>(this, next, hashCode, key, value, consumer);
262 }
263 };
264 for (int i = 100000; i < 100010; i++) {
265 map.put(Integer.valueOf(i), Integer.valueOf(i));
266 }
267 int iterations = 0;
268 int bytz = 2;
269 while (true) {
270 System.gc();
271 if (iterations++ > 50 || bytz < 0) {
272 fail("Max iterations reached before resource released.");
273 }
274 map.isEmpty();
275 if (!expiredValues.isEmpty()) {
276 break;
277 }
278 // create garbage:
279 @SuppressWarnings("unused")
280 final byte[] b = new byte[bytz];
281 bytz *= 2;
282 }
283 assertFalse(expiredValues.isEmpty(), "Value should be stored");
284 }
285
286 /**
287 * Test whether after serialization the "data" HashEntry array is the same size as the original.<p>
288 *
289 * See <a href="https://issues.apache.org/jira/browse/COLLECTIONS-599">COLLECTIONS-599: HashEntry array object naming data initialized with double the size during deserialization</a>
290 */
291 @Test
292 public void testDataSizeAfterSerialization() throws IOException, ClassNotFoundException {
293
294 final ReferenceMap<String, String> serializeMap = new ReferenceMap<>(ReferenceStrength.WEAK, ReferenceStrength.WEAK, true);
295 serializeMap.put("KEY", "VALUE");
296
297 final ByteArrayOutputStream baos = new ByteArrayOutputStream();
298 try (ObjectOutputStream out = new ObjectOutputStream(baos)) {
299 out.writeObject(serializeMap);
300 }
301
302 final ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
303 try (ObjectInputStream in = new ObjectInputStream(bais)) {
304 @SuppressWarnings("unchecked")
305 final ReferenceMap<String, String> deserializedMap = (ReferenceMap<String, String>) in.readObject();
306 assertEquals(1, deserializedMap.size());
307 assertEquals(serializeMap.data.length, deserializedMap.data.length);
308 }
309
310 }
311
312 /**
313 * Test whether remove is not removing last entry after calling hasNext.
314 * <p>
315 * See <a href="https://issues.apache.org/jira/browse/COLLECTIONS-802">COLLECTIONS-802: ReferenceMap iterator remove violates contract</a>
316 */
317 @Test
318 public void testIteratorLastEntryCanBeRemovedAfterHasNext() {
319 final ReferenceMap<Integer, Integer> map = new ReferenceMap<>();
320 map.put(1, 2);
321 final Iterator<Map.Entry<Integer, Integer>> iter = map.entrySet().iterator();
322 assertTrue(iter.hasNext());
323 iter.next();
324 // below line should not affect remove
325 assertFalse(iter.hasNext());
326 iter.remove();
327 assertTrue(map.isEmpty(), "Expect empty but have entry: " + map);
328 }
329
330 @Test
331 @SuppressWarnings("unchecked")
332 public void testNullHandling() {
333 resetFull();
334 assertNull(map.get(null));
335 assertFalse(map.containsKey(null));
336 assertFalse(map.containsValue(null));
337 assertNull(map.remove(null));
338 assertFalse(map.entrySet().contains(null));
339 assertFalse(map.containsKey(null));
340 assertFalse(map.containsValue(null));
341 assertThrows(NullPointerException.class, () -> map.put(null, null));
342 assertThrows(NullPointerException.class, () -> map.put((K) new Object(), null));
343 assertThrows(NullPointerException.class, () -> map.put(null, (V) new Object()));
344 }
345
346 /** Tests whether purge values setting works */
347 @Test
348 public void testPurgeValues() throws Exception {
349 // many thanks to Juozas Baliuka for suggesting this method
350 final Map<K, V> testMap = buildRefMap();
351
352 int iterations = 0;
353 int bytz = 2;
354 while (true) {
355 System.gc();
356 if (iterations++ > 50) {
357 fail("Max iterations reached before resource released.");
358 }
359 testMap.isEmpty();
360 if (keyReference.get() == null && valueReference.get() == null) {
361 break;
362
363 }
364 // create garbage:
365 @SuppressWarnings("unused")
366 final byte[] b = new byte[bytz];
367 bytz *= 2;
368 }
369 }
370
371 }