View Javadoc
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 }