/*! * This program is free software; you can redistribute it and/or modify it under the * terms of the GNU Lesser General Public License, version 2.1 as published by the Free Software * Foundation. * * You should have received a copy of the GNU Lesser General Public License along with this * program; if not, you can obtain a copy at http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html * or from the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. * * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. * See the GNU Lesser General Public License for more details. * * Copyright (c) 2002-2013 Pentaho Corporation.. All rights reserved. */ package org.pentaho.reporting.libraries.base.util; import java.io.Serializable; import java.util.HashMap; /** * A Least-Frequently-Used Map. * <p/> * This is not a real map in the sense of the Java-Collections-API. This is a slimmed down version of a * Least-Frequently-Used map with no unnecessary extra stuff like iterators or other costly but rarely used * java.util.Collections features. The cache does not accept null-keys, and any attempt to store null-values will yield * an error. * <p/> * To remove a couple of ugly checks and thus improving performance, this map enforces a minimum size of 3 items. * * @author Thomas Morgner */ public class LFUMap<K, V> implements Serializable, Cloneable { /** * A cache map entry class holding both the key and value and acting as member of a linked list. */ private static class MapEntry<K, V> { private K key; private V value; private MapEntry<K, V> previous; private MapEntry<K, V> next; /** * Creates a new map-entry for the given key and value. * * @param key the key, never null. * @param value the value, never null. */ protected MapEntry( final K key, final V value ) { if ( key == null ) { throw new NullPointerException(); } if ( value == null ) { throw new NullPointerException(); } this.key = key; this.value = value; } /** * Returns the entry's key. * * @return the key. */ public K getKey() { return key; } /** * Returns the previous entry in the list or null if this is the first entry. * * @return the previous entry. */ public MapEntry<K, V> getPrevious() { return previous; } /** * Redefines the previous entry in the list or null if this is the first entry. * * @param previous the previous entry. */ public void setPrevious( final MapEntry<K, V> previous ) { this.previous = previous; } /** * Returns the next entry in the list or null if this is the last entry. * * @return the next entry. */ public MapEntry<K, V> getNext() { return next; } /** * Redefines the next entry in the list or null if this is the last entry. * * @param next the next entry. */ public void setNext( final MapEntry<K, V> next ) { this.next = next; } /** * Returns the current value. * * @return the value, never null. */ public V getValue() { return value; } /** * Redefines the current value. * * @param value the value, never null. */ public void setValue( final V value ) { if ( value == null ) { throw new NullPointerException(); } this.value = value; } } private HashMap<K, MapEntry<K, V>> map; private MapEntry<K, V> first; private MapEntry<K, V> last; private int cacheSize; /** * Creates a new LFU-Map with a maximum size of <code>cacheSize</code> entries. * * @param cacheSize the maximum number of elements this map will be able to store. */ public LFUMap( final int cacheSize ) { // having at least 3 entries saves me a lot of coding and thus gives more performance .. this.cacheSize = Math.max( 3, cacheSize ); this.map = new HashMap<K, MapEntry<K, V>>( cacheSize ); } public void clear() { this.map.clear(); this.first = null; this.last = null; } /** * Return the entry for the given key. Any successful lookup moves the entry to the top of the list. * * @param key the lookup key. * @return the value stored for the key or null. */ public V get( final K key ) { if ( key == null ) { throw new NullPointerException(); } if ( first == null ) { // the cache is empty, so there is no way how we can have a result return null; } if ( first == last ) { // single entry does not even need to hit the cache .. if ( first.getKey().equals( key ) ) { return first.getValue(); } return null; } final MapEntry<K, V> metrics = map.get( key ); if ( metrics == null ) { // no such key .. return null; } final MapEntry<K, V> prev = metrics.getPrevious(); if ( prev == null ) { // already the first value return metrics.getValue(); } final MapEntry<K, V> next = metrics.getNext(); if ( next == null ) { // metrics is last entry // prev will be the new last entry prev.setNext( null ); last = prev; metrics.setPrevious( null ); metrics.setNext( first ); first.setPrevious( metrics ); first = metrics; return metrics.getValue(); } // in the middle .. remove from the chain next.setPrevious( prev ); prev.setNext( next ); // and add it at the top .. metrics.setPrevious( null ); metrics.setNext( first ); first.setPrevious( metrics ); first = metrics; return metrics.getValue(); } /** * Puts the given value into the map using the specified non-null key. The new entry is added as first entry in the * list of recently used values. * * @param key the key. * @param value the value. */ public void put( final K key, final V value ) { if ( key == null ) { throw new NullPointerException(); } if ( first == null ) { if ( value == null ) { return; } first = new MapEntry<K, V>( key, value ); last = first; map.put( key, first ); return; } if ( value == null ) { remove( key ); return; } if ( first.getKey().equals( key ) ) { // no need to do actual work .. first.setValue( value ); return; } final MapEntry<K, V> entry = map.get( key ); if ( entry == null ) { // check, whether the backend can carry another entry .. if ( ( 1 + map.size() ) >= cacheSize ) { // remove the last entry map.remove( last.getKey() ); final MapEntry<K, V> previous = last.getPrevious(); last.setNext( null ); last.setPrevious( null ); previous.setNext( null ); last = previous; } // now add this entry as first one .. final MapEntry<K, V> cacheEntry = new MapEntry<K, V>( key, value ); first.setPrevious( cacheEntry ); cacheEntry.setNext( first ); map.put( key, cacheEntry ); first = cacheEntry; return; } // replace an existing value .. entry.setValue( value ); if ( entry == first ) { // already the first one .. // should not happen, we have checked that ... // map.put(key, entry); throw new IllegalStateException( "Duplicate return?" ); } if ( entry == last ) { // prev is now the new last entry .. final MapEntry<K, V> previous = last.getPrevious(); previous.setNext( null ); last = previous; first.setPrevious( entry ); entry.setNext( first ); entry.setPrevious( null ); first = entry; return; } final MapEntry<K, V> previous = entry.getPrevious(); final MapEntry<K, V> next = entry.getNext(); // next cannot be null, else 'entry' would be the last entry, and we checked that already .. previous.setNext( next ); next.setPrevious( previous ); first.setPrevious( entry ); entry.setNext( first ); entry.setPrevious( null ); first = entry; } /** * Removes the entry for the given key. * * @param key the key for which an entry should be removed. */ public void remove( final K key ) { if ( key == null ) { throw new NullPointerException(); } if ( first == null ) { return; } final MapEntry<K, V> entry = map.remove( key ); if ( entry == null ) { return; } if ( entry == first ) { final MapEntry<K, V> nextEntry = first.getNext(); if ( nextEntry == null ) { first = null; last = null; entry.setNext( null ); entry.setPrevious( null ); return; } first = nextEntry; nextEntry.setPrevious( null ); entry.setNext( null ); entry.setPrevious( null ); return; } if ( entry == last ) { final MapEntry<K, V> prev = last.getPrevious(); // prev cannot be null, else first would be the same as last prev.setNext( null ); last = prev; entry.setNext( null ); entry.setPrevious( null ); return; } final MapEntry<K, V> previous = entry.getPrevious(); final MapEntry<K, V> next = entry.getNext(); // next cannot be null, else 'entry' would be the last entry, and we checked that already .. previous.setNext( next ); next.setPrevious( previous ); entry.setNext( null ); entry.setPrevious( null ); } /** * Returns the number of items in this map. * * @return the number of items in the map. */ public int size() { return map.size(); } /** * Checks whether this map is empty. * * @return true, if the map is empty, false otherwise. */ public boolean isEmpty() { return first == null; } /** * Returns the defined maximum size. * * @return the defines maximum size. */ public int getMaximumSize() { return cacheSize; } /** * Validates the map's internal datastructures. There should be no need to call this method manually. */ public void validate() { if ( first == null ) { return; } if ( first.getPrevious() != null ) { throw new IllegalStateException(); } if ( this.last.getNext() != null ) { throw new IllegalStateException(); } int counter = 0; MapEntry<K, V> p = null; MapEntry<K, V> entryFromStart = first; while ( entryFromStart != null ) { if ( entryFromStart.getPrevious() != p ) { throw new IllegalStateException(); } p = entryFromStart; entryFromStart = entryFromStart.getNext(); counter += 1; } if ( counter != size() ) { throw new IllegalStateException(); } int fromEndCounter = 0; MapEntry<K, V> n = null; MapEntry<K, V> entryFromEnd = this.last; while ( entryFromEnd != null ) { if ( entryFromEnd.getNext() != n ) { throw new IllegalStateException(); } n = entryFromEnd; entryFromEnd = entryFromEnd.getPrevious(); fromEndCounter += 1; } if ( n != first ) { throw new IllegalStateException(); } if ( fromEndCounter != size() ) { throw new IllegalStateException(); } if ( size() > cacheSize ) { throw new IllegalStateException(); } } public Object clone() throws CloneNotSupportedException { final LFUMap<K, V> map = (LFUMap<K, V>) super.clone(); map.map = (HashMap<K, MapEntry<K, V>>) this.map.clone(); map.map.clear(); MapEntry<K, V> entry = first; while ( entry != null ) { final K key = entry.getKey(); final V value = entry.getValue(); map.put( key, value ); entry = entry.getNext(); } return map; } }