/*!
* 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.lang.reflect.Array;
import java.util.Arrays;
/**
* A fast linked-hashmap that avoids any unneccessay work. It is slightly slower than an ordinary hashmap but faster
* than a combined hashmap and list-index that would be needed to get this functionality on JDK 1.2. The map is as fast
* as the LinkedHashMap of JDK 1.4+.
*
* @author Thomas Morgner
* @noinspection ProtectedField
*/
public class LinkedMap implements Cloneable, Serializable {
/**
* A cache map entry class holding both the key and value and acting as member of a linked list.
*/
protected static final class MapEntry implements Serializable {
/**
* The precomputed hashkey of the key.
*/
protected final int hashKey;
/**
* The key object, which is never null and which never changes.
*/
protected final Object key;
/**
* The current value object (can be null).
*/
protected Object value;
/**
* The link to the previous entry in the list.
*/
protected MapEntry previous;
/**
* The link to the next entry in the list.
*/
protected MapEntry next;
/**
* The link to the next entry in the bucket that has the same hashkey.
*/
protected MapEntry collisionNext;
/**
* Creates a new map-entry for the given key and value.
*
* @param key the key, never null.
* @param hashKey the precomputed hashkey for the key.
* @param value the value, never null.
*/
protected MapEntry( final Object key, final int hashKey, final Object value ) {
if ( key == null ) {
throw new NullPointerException();
}
this.key = key;
this.hashKey = hashKey;
this.value = value;
}
/**
* Returns the previous entry in the list or null if this is the first entry.
*
* @return the previous entry.
*/
public MapEntry 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 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 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 next ) {
this.next = next;
}
/**
* Returns the current value.
*
* @return the value, never null.
*/
public Object getValue() {
return value;
}
/**
* Redefines the current value.
*
* @param value the value, never null.
*/
public void setValue( final Object value ) {
this.value = value;
}
/**
* Returns the next map-entry in the bucket. If more than one object maps into the same hash-bucket, this map stores
* the entries as linked list.
*
* @return the next entry.
*/
public MapEntry getCollisionNext() {
return collisionNext;
}
/**
* Defines the next map-entry in the bucket. If more than one object maps into the same hash-bucket, this map stores
* the entries as linked list.
*
* @param collisionNext the next entry.
*/
public void setCollisionNext( final MapEntry collisionNext ) {
if ( collisionNext == this ) {
throw new IllegalStateException();
}
this.collisionNext = collisionNext;
}
}
private static final int MAXIMUM_CAPACITY = 1 << 30;
private static final Object NULL_MARKER = new Object();
private int size;
private int mask;
private float loadFactor;
private int capacity;
private MapEntry[] backend;
private MapEntry firstEntry;
private MapEntry lastEntry;
/**
* Default constructor. Creates a map for 16 entries with a default load-factor of 0.75.
*/
public LinkedMap() {
this( 16, 0.75f );
}
/**
* Creates a new map with the given initial number of buckets and the given loadfactor. A load factor greater 1 will
* always cause hash-collisions, while lower loadfactors reduce the likelyhood of collisions.
*
* @param initialCapacity the initial capacity.
* @param loadFactor the load factor of the bucket-array.
*/
public LinkedMap( int initialCapacity, final float loadFactor ) {
if ( initialCapacity > MAXIMUM_CAPACITY ) {
initialCapacity = MAXIMUM_CAPACITY;
}
if ( loadFactor <= 0 || Float.isNaN( loadFactor ) ) {
throw new IllegalArgumentException( "Illegal load factor: " + loadFactor );
}
int capacity = 1;
int mask = 0;
while ( capacity < initialCapacity ) {
mask = ( mask << 1 ) | 1;
capacity <<= 1;
}
this.mask = mask;
this.loadFactor = loadFactor;
this.backend = new MapEntry[ capacity ];
this.capacity = (int) Math.ceil( capacity * loadFactor );
}
/**
* A helper to ensure that null-keys are maped into a special marker object.
*
* @param o the potential key.
* @return the null-marker.
*/
private static Object ensureKey( final Object o ) {
if ( o == null ) {
return NULL_MARKER;
}
return o;
}
/**
* Ensures that the hashcode produced by the key is sane. This does some bit-juggeling to avoid incorrect hashkey
* implementations.
*
* @param h the original hashcode.
* @return the cleaned hashcode.
*/
private static int cleanHash( int h ) {
h ^= ( h >>> 20 ) ^ ( h >>> 12 );
return h ^ ( h >>> 7 ) ^ ( h >>> 4 );
}
/**
* Ensures that the map contains enough space to store the next entry.
*/
private void ensureSize() {
final MapEntry[] backend = this.backend;
if ( size <= ( capacity ) ) {
return;
}
// expand ..
final MapEntry[] newBackend = new MapEntry[ ( backend.length << 1 ) ];
final int newMask = ( mask << 1 ) | 1;
transferEntry( newBackend, newMask );
this.mask = newMask;
this.backend = newBackend;
this.capacity = (int) Math.ceil( loadFactor * backend.length );
}
private void transferEntry( final MapEntry[] newBackend, final int newMask ) {
for ( int i = 0; i < this.backend.length; i++ ) {
MapEntry entry = this.backend[ i ];
while ( entry != null ) {
final MapEntry next = entry.collisionNext;
final int insertIndex = entry.hashKey & newMask;
entry.setCollisionNext( newBackend[ insertIndex ] );
newBackend[ insertIndex ] = entry;
entry = next;
}
}
for ( int i = 0; i < newBackend.length; i++ ) {
MapEntry mapEntry = newBackend[ i ];
while ( mapEntry != null ) {
final int insertIndex = mapEntry.hashKey & newMask;
if ( i != insertIndex ) {
throw new IllegalStateException();
}
mapEntry = mapEntry.collisionNext;
}
}
}
/**
* Returns the number of entries in the map.
*
* @return the number of entries.
*/
public int size() {
return size;
}
/**
* Stores the given value in the map using the provided key. Both key and value can be null.
*
* @param key the key.
* @param value the value to be stored under the key.
* @return the previous value stored under this key or null of the entry is new.
*/
public Object put( final Object key, final Object value ) {
final Object realKey = ensureKey( key );
final int hashKey = cleanHash( realKey.hashCode() );
ensureSize();
final int index = hashKey & mask;
final MapEntry existingEntry = backend[ index ];
if ( existingEntry == null ) {
final MapEntry entry = new MapEntry( realKey, hashKey, value );
addNewRecord( index, entry );
return null;
}
MapEntry colEntry = existingEntry;
while ( true ) {
// The root entry exists and matches the current key.
if ( colEntry.hashKey == hashKey &&
colEntry.key.equals( realKey ) ) {
// that means, we just have to update the value inside and move the entry to the last position
// in the list (to make it look like a remove/add operation.
return updateRecord( value, colEntry );
}
if ( colEntry.collisionNext == null ) {
// create a new entry in the backend-array ...
final MapEntry entry = new MapEntry( realKey, hashKey, value );
addCollisionRecord( index, entry );
return null;
}
colEntry = colEntry.collisionNext;
}
}
/**
* Updates an existing record and reinserts the record at the end of the linked list.
*
* @param value the new value value.
* @param colEntry the entry record that should be updated.
* @return the old value in the entry or null, if there is no old entry.
*/
private Object updateRecord( final Object value, final MapEntry colEntry ) {
final Object oldValue = colEntry.value;
// replace the value ..
colEntry.value = ( value );
// and reconnect the entry at the end of the queue ..
final MapEntry firstEntry = this.firstEntry;
final MapEntry lastEntry = this.lastEntry;
if ( lastEntry == colEntry ) {
// also covers the case where last = first = col
return oldValue;
}
if ( firstEntry == colEntry ) {
this.firstEntry = colEntry.next;
if ( this.firstEntry != null ) {
this.firstEntry.previous = null;
}
colEntry.previous = ( lastEntry );
colEntry.next = ( null );
lastEntry.next = ( colEntry );
this.lastEntry = colEntry;
asserta();
return oldValue;
}
final MapEntry prevEntry = colEntry.previous;
final MapEntry nextEntry = colEntry.next;
prevEntry.next = ( nextEntry );
nextEntry.previous = ( prevEntry );
colEntry.previous = ( lastEntry );
colEntry.next = ( null );
lastEntry.next = ( colEntry );
this.lastEntry = colEntry;
asserta();
return oldValue;
}
/**
* Adds a new map-entry to an already filled bucket.
*
* @param index where to add the new record in the map.
* @param entry the new entry to be added.
*/
private void addCollisionRecord( final int index, final MapEntry entry ) {
entry.setCollisionNext( backend[ index ] );
backend[ index ] = entry;
final MapEntry lastEntry = this.lastEntry;
entry.previous = ( lastEntry );
entry.next = ( null );
lastEntry.next = ( entry );
this.lastEntry = entry;
size += 1;
asserta();
}
/**
* Adds a completely new record to an previously empty bucket.
*
* @param index the index of the bucket to be updated.
* @param entry the new map-entry.
*/
private void addNewRecord( final int index, final MapEntry entry ) {
// thats easy ...
final MapEntry lastEntry = this.lastEntry;
if ( lastEntry == null ) {
firstEntry = entry;
} else {
entry.previous = ( lastEntry );
lastEntry.next = ( entry );
}
this.lastEntry = entry;
backend[ index ] = entry;
size += 1;
asserta();
}
/**
* Retrieves the object stored under the given key from the map.
*
* @param key the key for which a value should be located.
* @return the value or null, if the map did not contain a value for the key.
*/
public Object get( final Object key ) {
final Object realKey = ensureKey( key );
final int hashKey = cleanHash( realKey.hashCode() );
final int index = hashKey & mask;
final MapEntry existingEntry = backend[ index ];
if ( existingEntry == null ) {
return null;
}
MapEntry colEntry = existingEntry;
while ( colEntry != null ) {
// The root entry exists and matches the current key.
if ( colEntry.hashKey == hashKey && colEntry.key.equals( realKey ) ) {
return colEntry.value;
}
colEntry = colEntry.collisionNext;
}
return null;
}
/**
* Removes the object stored under the given key from the map.
*
* @param key the key for which a value should be located.
* @return the value or null, if the map did not contain a value for the key.
*/
public Object remove( final Object key ) {
final Object realKey = ensureKey( key );
final int hashKey = cleanHash( realKey.hashCode() );
final int index = hashKey & mask;
final MapEntry existingEntry = backend[ index ];
if ( existingEntry == null ) {
return null;
}
MapEntry prevEntry = null;
MapEntry colEntry = existingEntry;
while ( colEntry != null ) {
// The root entry exists and matches the current key.
if ( colEntry.hashKey == hashKey && colEntry.key.equals( realKey ) ) {
final Object value = colEntry.value;
if ( prevEntry == null ) {
// this is a root level entry ..
backend[ index ] = colEntry.collisionNext;
} else {
prevEntry.setCollisionNext( colEntry.collisionNext );
}
// now check the first and last entry ...
if ( firstEntry == lastEntry ) {
// there is ony one entry.
firstEntry = null;
lastEntry = null;
size -= 1;
asserta();
return value;
}
if ( firstEntry == colEntry ) {
final MapEntry nextfirstEntry = colEntry.next;
if ( nextfirstEntry != null ) {
nextfirstEntry.previous = ( null );
colEntry.next = null;
}
firstEntry = nextfirstEntry;
} else if ( lastEntry == colEntry ) {
final MapEntry nextLastEntry = colEntry.previous;
if ( nextLastEntry != null ) {
nextLastEntry.next = ( null );
colEntry.previous = null;
}
lastEntry = nextLastEntry;
}
if ( colEntry.previous != null ) {
colEntry.previous.next = colEntry.next;
}
if ( colEntry.next != null ) {
colEntry.next.previous = colEntry.previous;
}
size -= 1;
asserta();
return value;
}
prevEntry = colEntry;
colEntry = colEntry.collisionNext;
}
asserta();
return null;
}
private void asserta() {
if ( firstEntry == null ) {
return;
}
if ( firstEntry.previous != null ) {
throw new NullPointerException();
}
}
/**
* Checks, whether the map contains an entry for the key.
*
* @param key the key for which a value should be located.
* @return true if the map contains a value for the key, false otherwise.
*/
public boolean containsKey( final Object key ) {
final Object realKey = ensureKey( key );
final int hashKey = cleanHash( realKey.hashCode() );
final int index = hashKey & mask;
final MapEntry existingEntry = backend[ index ];
if ( existingEntry == null ) {
return false;
}
MapEntry colEntry = existingEntry;
while ( colEntry != null ) {
// The root entry exists and matches the current key.
if ( colEntry.hashKey == hashKey && colEntry.key.equals( realKey ) ) {
return true;
}
colEntry = colEntry.collisionNext;
}
return false;
}
/**
* Returns the keys used in this map as array. The keys are returned in the insertation order.
*
* @param data the object array that should receive the keys.
* @return the array filled with the keys.
*/
public Object[] keys( final Object[] data ) {
final Object[] list;
if ( data.length < size ) {
list = (Object[]) Array.newInstance( data.getClass().getComponentType(), size );
} else {
list = data;
}
int index = 0;
MapEntry entry = firstEntry;
while ( entry != null ) {
final Object o = entry.key;
if ( o == NULL_MARKER ) {
list[ index ] = ( null );
} else {
list[ index ] = ( o );
}
entry = entry.getNext();
index += 1;
}
return list;
}
/**
* Returns the keys used in this map as array. The keys are returned in the insertation order.
*
* @return the array filled with the keys.
*/
public Object[] keys() {
return keys( new Object[ size ] );
}
/**
* Returns the values used in this map as array. The values are returned in the insertation order.
*
* @return the array filled with the values.
*/
public Object[] values() {
return values( new Object[ size ] );
}
/**
* Returns the values used in this map as array. The values are returned in the insertation order.
*
* @param data the object array that should receive the values.
* @return the array filled with the values.
*/
public Object[] values( final Object[] data ) {
final Object[] list;
if ( data.length < size ) {
list = (Object[]) Array.newInstance( data.getClass().getComponentType(), size );
} else {
list = data;
}
int index = 0;
MapEntry entry = firstEntry;
while ( entry != null ) {
final Object o = entry.value;
list[ index ] = ( o );
entry = entry.getNext();
index += 1;
}
return list;
}
/**
* Clears the map and removes all map records.
*/
public void clear() {
if ( firstEntry == null ) {
return;
}
firstEntry = null;
lastEntry = null;
Arrays.fill( backend, null );
size = 0;
}
/**
* Clones this map.
*
* @return the cloned map.
* @throws CloneNotSupportedException
*/
public Object clone() throws CloneNotSupportedException {
final LinkedMap map = (LinkedMap) super.clone();
map.backend = backend.clone();
Arrays.fill( map.backend, null );
map.firstEntry = null;
map.lastEntry = null;
map.size = 0;
MapEntry entry = firstEntry;
while ( entry != null ) {
map.put( entry.key, entry.value );
entry = entry.getNext();
}
return map;
}
/**
* Checks whether this collection is empty.
*
* @return true, if the collection is empty, false otherwise.
*/
public boolean isEmpty() {
return size == 0;
}
}