001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.commons.collections4.map;
018
019import java.io.IOException;
020import java.io.ObjectInputStream;
021import java.io.ObjectOutputStream;
022import java.io.Serializable;
023import java.util.Map;
024
025import org.apache.commons.collections4.BoundedMap;
026
027/**
028 * A <code>Map</code> implementation with a fixed maximum size which removes
029 * the least recently used entry if an entry is added when full.
030 * <p>
031 * The least recently used algorithm works on the get and put operations only.
032 * Iteration of any kind, including setting the value by iteration, does not
033 * change the order. Queries such as containsKey and containsValue or access
034 * via views also do not change the order.
035 * <p>
036 * A somewhat subtle ramification of the least recently used
037 * algorithm is that calls to {@link #get(Object)} stand a very good chance
038 * of modifying the map's iteration order and thus invalidating any
039 * iterators currently in use.  It is therefore suggested that iterations
040 * over an {@link LRUMap} instance access entry values only through a
041 * {@link org.apache.commons.collections4.MapIterator MapIterator} or {@link #entrySet()} iterator.
042 * <p>
043 * The map implements <code>OrderedMap</code> and entries may be queried using
044 * the bidirectional <code>OrderedMapIterator</code>. The order returned is
045 * least recently used to most recently used. Iterators from map views can
046 * also be cast to <code>OrderedIterator</code> if required.
047 * <p>
048 * All the available iterators can be reset back to the start by casting to
049 * <code>ResettableIterator</code> and calling <code>reset()</code>.
050 * <p>
051 * <strong>Note that LRUMap is not synchronized and is not thread-safe.</strong>
052 * If you wish to use this map from multiple threads concurrently, you must use
053 * appropriate synchronization. The simplest approach is to wrap this map
054 * using {@link java.util.Collections#synchronizedMap(Map)}. This class may throw
055 * <code>NullPointerException</code>'s when accessed by concurrent threads.
056 *
057 * @since 3.0 (previously in main package v1.0)
058 * @version $Id: LRUMap.html 972421 2015-11-14 20:00:04Z tn $
059 */
060public class LRUMap<K, V>
061        extends AbstractLinkedMap<K, V> implements BoundedMap<K, V>, Serializable, Cloneable {
062
063    /** Serialisation version */
064    private static final long serialVersionUID = -612114643488955218L;
065    /** Default maximum size */
066    protected static final int DEFAULT_MAX_SIZE = 100;
067
068    /** Maximum size */
069    private transient int maxSize;
070    /** Scan behaviour */
071    private boolean scanUntilRemovable;
072
073    /**
074     * Constructs a new empty map with a maximum size of 100.
075     */
076    public LRUMap() {
077        this(DEFAULT_MAX_SIZE, DEFAULT_LOAD_FACTOR, false);
078    }
079
080    /**
081     * Constructs a new, empty map with the specified maximum size.
082     *
083     * @param maxSize  the maximum size of the map
084     * @throws IllegalArgumentException if the maximum size is less than one
085     */
086    public LRUMap(final int maxSize) {
087        this(maxSize, DEFAULT_LOAD_FACTOR);
088    }
089
090    /**
091     * Constructs a new, empty map with the specified maximum size.
092     *
093     * @param maxSize  the maximum size of the map
094     * @param scanUntilRemovable  scan until a removeable entry is found, default false
095     * @throws IllegalArgumentException if the maximum size is less than one
096     * @since 3.1
097     */
098    public LRUMap(final int maxSize, final boolean scanUntilRemovable) {
099        this(maxSize, DEFAULT_LOAD_FACTOR, scanUntilRemovable);
100    }
101
102    /**
103     * Constructs a new, empty map with the specified initial capacity and
104     * load factor.
105     *
106     * @param maxSize  the maximum size of the map
107     * @param loadFactor  the load factor
108     * @throws IllegalArgumentException if the maximum size is less than one
109     * @throws IllegalArgumentException if the load factor is less than zero
110     */
111    public LRUMap(final int maxSize, final float loadFactor) {
112        this(maxSize, loadFactor, false);
113    }
114
115    /**
116     * Constructs a new, empty map with the specified initial capacity and
117     * load factor.
118     *
119     * @param maxSize  the maximum size of the ma
120     * @param loadFactor  the load factor
121     * @param scanUntilRemovable  scan until a removeable entry is found, default false
122     * @throws IllegalArgumentException if the maximum size is less than one
123     * @throws IllegalArgumentException if the load factor is less than zero
124     * @since 3.1
125     */
126    public LRUMap(final int maxSize, final float loadFactor, final boolean scanUntilRemovable) {
127        super(maxSize < 1 ? DEFAULT_CAPACITY : maxSize, loadFactor);
128        if (maxSize < 1) {
129            throw new IllegalArgumentException("LRUMap max size must be greater than 0");
130        }
131        this.maxSize = maxSize;
132        this.scanUntilRemovable = scanUntilRemovable;
133    }
134
135    /**
136     * Constructor copying elements from another map.
137     * <p>
138     * The maximum size is set from the map's size.
139     *
140     * @param map  the map to copy
141     * @throws NullPointerException if the map is null
142     * @throws IllegalArgumentException if the map is empty
143     */
144    public LRUMap(final Map<? extends K, ? extends V> map) {
145        this(map, false);
146    }
147
148    /**
149     * Constructor copying elements from another map.
150     * <p/>
151     * The maximum size is set from the map's size.
152     *
153     * @param map  the map to copy
154     * @param scanUntilRemovable  scan until a removeable entry is found, default false
155     * @throws NullPointerException if the map is null
156     * @throws IllegalArgumentException if the map is empty
157     * @since 3.1
158     */
159    public LRUMap(final Map<? extends K, ? extends V> map, final boolean scanUntilRemovable) {
160        this(map.size(), DEFAULT_LOAD_FACTOR, scanUntilRemovable);
161        putAll(map);
162    }
163
164    //-----------------------------------------------------------------------
165    /**
166     * Gets the value mapped to the key specified.
167     * <p>
168     * This operation changes the position of the key in the map to the
169     * most recently used position (first).
170     *
171     * @param key  the key
172     * @return the mapped value, null if no match
173     */
174    @Override
175    public V get(final Object key) {
176        final LinkEntry<K, V> entry = getEntry(key);
177        if (entry == null) {
178            return null;
179        }
180        moveToMRU(entry);
181        return entry.getValue();
182    }
183
184    //-----------------------------------------------------------------------
185    /**
186     * Moves an entry to the MRU position at the end of the list.
187     * <p>
188     * This implementation moves the updated entry to the end of the list.
189     *
190     * @param entry  the entry to update
191     */
192    protected void moveToMRU(final LinkEntry<K, V> entry) {
193        if (entry.after != header) {
194            modCount++;
195            // remove
196            if(entry.before == null) {
197                throw new IllegalStateException("Entry.before is null." +
198                    " Please check that your keys are immutable, and that you have used synchronization properly." +
199                    " If so, then please report this to dev@commons.apache.org as a bug.");
200            }
201            entry.before.after = entry.after;
202            entry.after.before = entry.before;
203            // add first
204            entry.after = header;
205            entry.before = header.before;
206            header.before.after = entry;
207            header.before = entry;
208        } else if (entry == header) {
209            throw new IllegalStateException("Can't move header to MRU" +
210                " (please report this to dev@commons.apache.org)");
211        }
212    }
213
214    /**
215     * Updates an existing key-value mapping.
216     * <p>
217     * This implementation moves the updated entry to the top of the list
218     * using {@link #moveToMRU(AbstractLinkedMap.LinkEntry)}.
219     *
220     * @param entry  the entry to update
221     * @param newValue  the new value to store
222     */
223    @Override
224    protected void updateEntry(final HashEntry<K, V> entry, final V newValue) {
225        moveToMRU((LinkEntry<K, V>) entry);  // handles modCount
226        entry.setValue(newValue);
227    }
228
229    /**
230     * Adds a new key-value mapping into this map.
231     * <p>
232     * This implementation checks the LRU size and determines whether to
233     * discard an entry or not using {@link #removeLRU(AbstractLinkedMap.LinkEntry)}.
234     * <p>
235     * From Commons Collections 3.1 this method uses {@link #isFull()} rather
236     * than accessing <code>size</code> and <code>maxSize</code> directly.
237     * It also handles the scanUntilRemovable functionality.
238     *
239     * @param hashIndex  the index into the data array to store at
240     * @param hashCode  the hash code of the key to add
241     * @param key  the key to add
242     * @param value  the value to add
243     */
244    @Override
245    protected void addMapping(final int hashIndex, final int hashCode, final K key, final V value) {
246        if (isFull()) {
247            LinkEntry<K, V> reuse = header.after;
248            boolean removeLRUEntry = false;
249            if (scanUntilRemovable) {
250                while (reuse != header && reuse != null) {
251                    if (removeLRU(reuse)) {
252                        removeLRUEntry = true;
253                        break;
254                    }
255                    reuse = reuse.after;
256                }
257                if (reuse == null) {
258                    throw new IllegalStateException(
259                        "Entry.after=null, header.after" + header.after + " header.before" + header.before +
260                        " key=" + key + " value=" + value + " size=" + size + " maxSize=" + maxSize +
261                        " Please check that your keys are immutable, and that you have used synchronization properly." +
262                        " If so, then please report this to dev@commons.apache.org as a bug.");
263                }
264            } else {
265                removeLRUEntry = removeLRU(reuse);
266            }
267
268            if (removeLRUEntry) {
269                if (reuse == null) {
270                    throw new IllegalStateException(
271                        "reuse=null, header.after=" + header.after + " header.before" + header.before +
272                        " key=" + key + " value=" + value + " size=" + size + " maxSize=" + maxSize +
273                        " Please check that your keys are immutable, and that you have used synchronization properly." +
274                        " If so, then please report this to dev@commons.apache.org as a bug.");
275                }
276                reuseMapping(reuse, hashIndex, hashCode, key, value);
277            } else {
278                super.addMapping(hashIndex, hashCode, key, value);
279            }
280        } else {
281            super.addMapping(hashIndex, hashCode, key, value);
282        }
283    }
284
285    /**
286     * Reuses an entry by removing it and moving it to a new place in the map.
287     * <p>
288     * This method uses {@link #removeEntry}, {@link #reuseEntry} and {@link #addEntry}.
289     *
290     * @param entry  the entry to reuse
291     * @param hashIndex  the index into the data array to store at
292     * @param hashCode  the hash code of the key to add
293     * @param key  the key to add
294     * @param value  the value to add
295     */
296    protected void reuseMapping(final LinkEntry<K, V> entry, final int hashIndex, final int hashCode,
297                                final K key, final V value) {
298        // find the entry before the entry specified in the hash table
299        // remember that the parameters (except the first) refer to the new entry,
300        // not the old one
301        try {
302            final int removeIndex = hashIndex(entry.hashCode, data.length);
303            final HashEntry<K, V>[] tmp = data;  // may protect against some sync issues
304            HashEntry<K, V> loop = tmp[removeIndex];
305            HashEntry<K, V> previous = null;
306            while (loop != entry && loop != null) {
307                previous = loop;
308                loop = loop.next;
309            }
310            if (loop == null) {
311                throw new IllegalStateException(
312                    "Entry.next=null, data[removeIndex]=" + data[removeIndex] + " previous=" + previous +
313                    " key=" + key + " value=" + value + " size=" + size + " maxSize=" + maxSize +
314                    " Please check that your keys are immutable, and that you have used synchronization properly." +
315                    " If so, then please report this to dev@commons.apache.org as a bug.");
316            }
317
318            // reuse the entry
319            modCount++;
320            removeEntry(entry, removeIndex, previous);
321            reuseEntry(entry, hashIndex, hashCode, key, value);
322            addEntry(entry, hashIndex);
323        } catch (final NullPointerException ex) {
324            throw new IllegalStateException(
325                    "NPE, entry=" + entry + " entryIsHeader=" + (entry==header) +
326                    " key=" + key + " value=" + value + " size=" + size + " maxSize=" + maxSize +
327                    " Please check that your keys are immutable, and that you have used synchronization properly." +
328                    " If so, then please report this to dev@commons.apache.org as a bug.");
329        }
330    }
331
332    /**
333     * Subclass method to control removal of the least recently used entry from the map.
334     * <p>
335     * This method exists for subclasses to override. A subclass may wish to
336     * provide cleanup of resources when an entry is removed. For example:
337     * <pre>
338     * protected boolean removeLRU(LinkEntry entry) {
339     *   releaseResources(entry.getValue());  // release resources held by entry
340     *   return true;  // actually delete entry
341     * }
342     * </pre>
343     * <p>
344     * Alternatively, a subclass may choose to not remove the entry or selectively
345     * keep certain LRU entries. For example:
346     * <pre>
347     * protected boolean removeLRU(LinkEntry entry) {
348     *   if (entry.getKey().toString().startsWith("System.")) {
349     *     return false;  // entry not removed from LRUMap
350     *   } else {
351     *     return true;  // actually delete entry
352     *   }
353     * }
354     * </pre>
355     * The effect of returning false is dependent on the scanUntilRemovable flag.
356     * If the flag is true, the next LRU entry will be passed to this method and so on
357     * until one returns false and is removed, or every entry in the map has been passed.
358     * If the scanUntilRemovable flag is false, the map will exceed the maximum size.
359     * <p>
360     * NOTE: Commons Collections 3.0 passed the wrong entry to this method.
361     * This is fixed in version 3.1 onwards.
362     *
363     * @param entry  the entry to be removed
364     * @return {@code true}
365     */
366    protected boolean removeLRU(final LinkEntry<K, V> entry) {
367        return true;
368    }
369
370    //-----------------------------------------------------------------------
371    /**
372     * Returns true if this map is full and no new mappings can be added.
373     *
374     * @return <code>true</code> if the map is full
375     */
376    public boolean isFull() {
377        return size >= maxSize;
378    }
379
380    /**
381     * Gets the maximum size of the map (the bound).
382     *
383     * @return the maximum number of elements the map can hold
384     */
385    public int maxSize() {
386        return maxSize;
387    }
388
389    /**
390     * Whether this LRUMap will scan until a removable entry is found when the
391     * map is full.
392     *
393     * @return true if this map scans
394     * @since 3.1
395     */
396    public boolean isScanUntilRemovable() {
397        return scanUntilRemovable;
398    }
399
400    //-----------------------------------------------------------------------
401    /**
402     * Clones the map without cloning the keys or values.
403     *
404     * @return a shallow clone
405     */
406    @Override
407    public LRUMap<K, V> clone() {
408        return (LRUMap<K, V>) super.clone();
409    }
410
411    /**
412     * Write the map out using a custom routine.
413     */
414    private void writeObject(final ObjectOutputStream out) throws IOException {
415        out.defaultWriteObject();
416        doWriteObject(out);
417    }
418
419    /**
420     * Read the map in using a custom routine.
421     */
422    private void readObject(final ObjectInputStream in) throws IOException, ClassNotFoundException {
423        in.defaultReadObject();
424        doReadObject(in);
425    }
426
427    /**
428     * Writes the data necessary for <code>put()</code> to work in deserialization.
429     *
430     * @param out  the output stream
431     * @throws IOException if an error occurs while writing to the stream
432     */
433    @Override
434    protected void doWriteObject(final ObjectOutputStream out) throws IOException {
435        out.writeInt(maxSize);
436        super.doWriteObject(out);
437    }
438
439    /**
440     * Reads the data necessary for <code>put()</code> to work in the superclass.
441     *
442     * @param in  the input stream
443     * @throws IOException if an error occurs while reading from the stream
444     * @throws ClassNotFoundException if an object read from the stream can not be loaded
445     */
446    @Override
447    protected void doReadObject(final ObjectInputStream in) throws IOException, ClassNotFoundException {
448        maxSize = in.readInt();
449        super.doReadObject(in);
450    }
451
452}