001/*
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements.  See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership.  The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License.  You may obtain a copy of the License at
009 *
010 *     http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing,
013 * software distributed under the License is distributed on an
014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015 * KIND, either express or implied.  See the License for the
016 * specific language governing permissions and limitations
017 * under the License.
018 */
019package org.apache.shiro.lang.util;
020
021import java.lang.ref.ReferenceQueue;
022import java.lang.ref.SoftReference;
023
024import java.util.ArrayList;
025import java.util.Collection;
026import java.util.Collections;
027import java.util.HashMap;
028import java.util.Map;
029import java.util.Queue;
030import java.util.Set;
031import java.util.concurrent.ConcurrentHashMap;
032import java.util.concurrent.ConcurrentLinkedQueue;
033import java.util.concurrent.locks.ReentrantLock;
034
035
036/**
037 * A <code><em>Soft</em>HashMap</code> is a memory-constrained map that stores its <em>values</em> in
038 * {@link SoftReference SoftReference}s.  (Contrast this with the JDK's
039 * {@link java.util.WeakHashMap WeakHashMap}, which uses weak references for its <em>keys</em>, which is of little value if you
040 * want the cache to auto-resize itself based on memory constraints).
041 * <p/>
042 * Having the values wrapped by soft references allows the cache to automatically reduce its size based on memory
043 * limitations and garbage collection.  This ensures that the cache will not cause memory leaks by holding strong
044 * references to all of its values.
045 * <p/>
046 * This class is a generics-enabled Map based on initial ideas from Heinz Kabutz's and Sydney Redelinghuys's
047 * <a href="http://www.javaspecialists.eu/archive/Issue015.html">publicly posted version (with their approval)</a>, with
048 * continued modifications.
049 * <p/>
050 * This implementation is thread-safe and usable in concurrent environments.
051 *
052 * @param <K> K
053 * @param <V> V
054 * @since 1.0
055 */
056public class SoftHashMap<K, V> implements Map<K, V> {
057
058    /**
059     * The default value of the RETENTION_SIZE attribute, equal to 100.
060     */
061    private static final int DEFAULT_RETENTION_SIZE = 100;
062
063    /**
064     * The internal HashMap that will hold the SoftReference.
065     */
066    private final Map<K, SoftValue<V, K>> map;
067
068    /**
069     * The number of strong references to hold internally, that is, the number of instances to prevent
070     * from being garbage collected automatically (unlike other soft references).
071     */
072    private final int retentionSize;
073
074    /**
075     * The FIFO list of strong references (not to be garbage collected), order of last access.
076     */
077    //guarded by 'strongReferencesLock'
078    private final Queue<V> strongReferences;
079    private final ReentrantLock strongReferencesLock;
080
081    /**
082     * Reference queue for cleared SoftReference objects.
083     */
084    private final ReferenceQueue<? super V> queue;
085
086    /**
087     * Creates a new SoftHashMap with a default retention size size of
088     * {@link #DEFAULT_RETENTION_SIZE DEFAULT_RETENTION_SIZE} (100 entries).
089     *
090     * @see #SoftHashMap(int)
091     */
092    public SoftHashMap() {
093        this(DEFAULT_RETENTION_SIZE);
094    }
095
096    /**
097     * Creates a new SoftHashMap with the specified retention size.
098     * <p/>
099     * The retention size (n) is the total number of most recent entries in the map that will be strongly referenced
100     * (i.e.'retained') to prevent them from being eagerly garbage collected.  That is, the point of a SoftHashMap is to
101     * allow the garbage collector to remove as many entries from this map as it desires, but there will always be (n)
102     * elements retained after a GC due to the strong references.
103     * <p/>
104     * Note that in a highly concurrent environments the exact total number of strong references may differ slightly
105     * than the actual <code>retentionSize</code> value.  This number is intended to be a best-effort retention low
106     * water mark.
107     *
108     * @param retentionSize the total number of most recent entries in the map that will be strongly referenced
109     *                      (retained), preventing them from being eagerly garbage collected by the JVM.
110     */
111    @SuppressWarnings({"unchecked"})
112    public SoftHashMap(int retentionSize) {
113        super();
114        this.retentionSize = Math.max(0, retentionSize);
115        queue = new ReferenceQueue<V>();
116        strongReferencesLock = new ReentrantLock();
117        map = new ConcurrentHashMap<K, SoftValue<V, K>>();
118        strongReferences = new ConcurrentLinkedQueue<V>();
119    }
120
121    /**
122     * Creates a {@code SoftHashMap} backed by the specified {@code source}, with a default retention
123     * size of {@link #DEFAULT_RETENTION_SIZE DEFAULT_RETENTION_SIZE} (100 entries).
124     *
125     * @param source the backing map to populate this {@code SoftHashMap}
126     * @see #SoftHashMap(java.util.Map, int)
127     */
128    public SoftHashMap(Map<K, V> source) {
129        this(DEFAULT_RETENTION_SIZE);
130        putAll(source);
131    }
132
133    /**
134     * Creates a {@code SoftHashMap} backed by the specified {@code source}, with the specified retention size.
135     * <p/>
136     * The retention size (n) is the total number of most recent entries in the map that will be strongly referenced
137     * (i.e.'retained') to prevent them from being eagerly garbage collected.  That is, the point of a SoftHashMap is to
138     * allow the garbage collector to remove as many entries from this map as it desires, but there will always be (n)
139     * elements retained after a GC due to the strong references.
140     * <p/>
141     * Note that in a highly concurrent environments the exact total number of strong references may differ slightly
142     * than the actual <code>retentionSize</code> value.  This number is intended to be a best-effort retention low
143     * water mark.
144     *
145     * @param source        the backing map to populate this {@code SoftHashMap}
146     * @param retentionSize the total number of most recent entries in the map that will be strongly referenced
147     *                      (retained), preventing them from being eagerly garbage collected by the JVM.
148     */
149    public SoftHashMap(Map<K, V> source, int retentionSize) {
150        this(retentionSize);
151        putAll(source);
152    }
153
154    public V get(Object key) {
155        processQueue();
156
157        V result = null;
158        SoftValue<V, K> value = map.get(key);
159
160        if (value != null) {
161            //unwrap the 'real' value from the SoftReference
162            result = value.get();
163            if (result == null) {
164                //The wrapped value was garbage collected, so remove this entry from the backing map:
165                //noinspection SuspiciousMethodCalls
166                map.remove(key);
167            } else {
168                //Add this value to the beginning of the strong reference queue (FIFO).
169                addToStrongReferences(result);
170            }
171        }
172        return result;
173    }
174
175    private void addToStrongReferences(V result) {
176        strongReferencesLock.lock();
177        try {
178            strongReferences.add(result);
179            trimStrongReferencesIfNecessary();
180        } finally {
181            strongReferencesLock.unlock();
182        }
183
184    }
185
186    //Guarded by the strongReferencesLock in the addToStrongReferences method
187
188    private void trimStrongReferencesIfNecessary() {
189        //trim the strong ref queue if necessary:
190        while (strongReferences.size() > retentionSize) {
191            strongReferences.poll();
192        }
193    }
194
195    /**
196     * Traverses the ReferenceQueue and removes garbage-collected SoftValue objects from the backing map
197     * by looking them up using the SoftValue.key data member.
198     */
199    @SuppressWarnings({"unchecked", "SuspiciousMethodCalls"})
200    private void processQueue() {
201        SoftValue sv;
202        while ((sv = (SoftValue) queue.poll()) != null) {
203            // we can access private data!
204            map.remove(sv.key);
205        }
206    }
207
208    public boolean isEmpty() {
209        processQueue();
210        return map.isEmpty();
211    }
212
213    public boolean containsKey(Object key) {
214        processQueue();
215        return map.containsKey(key);
216    }
217
218    public boolean containsValue(Object value) {
219        processQueue();
220        Collection values = values();
221        return values != null && values.contains(value);
222    }
223
224    public void putAll(Map<? extends K, ? extends V> m) {
225        if (m == null || m.isEmpty()) {
226            processQueue();
227            return;
228        }
229        for (Map.Entry<? extends K, ? extends V> entry : m.entrySet()) {
230            put(entry.getKey(), entry.getValue());
231        }
232    }
233
234    public Set<K> keySet() {
235        processQueue();
236        return map.keySet();
237    }
238
239    @SuppressWarnings("unchecked")
240    public Collection<V> values() {
241        processQueue();
242        Collection<K> keys = map.keySet();
243        if (keys.isEmpty()) {
244            return Collections.EMPTY_SET;
245        }
246        Collection<V> values = new ArrayList<V>(keys.size());
247        for (K key : keys) {
248            V v = get(key);
249            if (v != null) {
250                values.add(v);
251            }
252        }
253        return values;
254    }
255
256    /**
257     * Creates a new entry, but wraps the value in a SoftValue instance to enable auto garbage collection.
258     */
259    public V put(K key, V value) {
260        // throw out garbage collected values first
261        processQueue();
262        SoftValue<V, K> sv = new SoftValue<V, K>(value, key, queue);
263        SoftValue<V, K> previous = map.put(key, sv);
264        addToStrongReferences(value);
265        return previous != null ? previous.get() : null;
266    }
267
268    public V remove(Object key) {
269        // throw out garbage collected values first
270        processQueue();
271        SoftValue<V, K> raw = map.remove(key);
272        return raw != null ? raw.get() : null;
273    }
274
275    public void clear() {
276        strongReferencesLock.lock();
277        try {
278            strongReferences.clear();
279        } finally {
280            strongReferencesLock.unlock();
281        }
282        // throw out garbage collected values
283        processQueue();
284        map.clear();
285    }
286
287    public int size() {
288        // throw out garbage collected values first
289        processQueue();
290        return map.size();
291    }
292
293    @SuppressWarnings("unchecked")
294    public Set<Map.Entry<K, V>> entrySet() {
295        // throw out garbage collected values first
296        processQueue();
297        Collection<K> keys = map.keySet();
298        if (keys.isEmpty()) {
299            //noinspection unchecked
300            return Collections.EMPTY_SET;
301        }
302
303        Map<K, V> kvPairs = new HashMap<K, V>(keys.size());
304        for (K key : keys) {
305            V v = get(key);
306            if (v != null) {
307                kvPairs.put(key, v);
308            }
309        }
310        return kvPairs.entrySet();
311    }
312
313    /**
314     * We define our own subclass of SoftReference which contains
315     * not only the value but also the key to make it easier to find
316     * the entry in the HashMap after it's been garbage collected.
317     */
318    private static final class SoftValue<V, K> extends SoftReference<V> {
319
320        private final K key;
321
322        /**
323         * Constructs a new instance, wrapping the value, key, and queue, as
324         * required by the superclass.
325         *
326         * @param value the map value
327         * @param key   the map key
328         * @param queue the soft reference queue to poll to determine if the entry had been reaped by the GC.
329         */
330        private SoftValue(V value, K key, ReferenceQueue<? super V> queue) {
331            super(value, queue);
332            this.key = key;
333        }
334
335    }
336}