LimitedHashMap.java
/*
* GovWay - A customizable API Gateway
* https://govway.org
*
* Copyright (c) 2005-2024 Link.it srl (https://link.it).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 3, as published by
* the Free Software Foundation.
*
* 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
package org.openspcoop2.utils.cache;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import org.openspcoop2.utils.date.DateManager;
/**
* LimitedHashMap
*
* @author Luigi Buoncristiani (Kali.Blu@gmail.com)
* @author $Author$
* @version $Rev$, $Date$
*/
public class LimitedHashMap<K,V> extends ConcurrentHashMap<K, V> {
private static final long serialVersionUID = 1L;
private int maxSize = -1;
private int maxLifeTime = -1;
private LimitedHashMap.ElementDateInfo<K> elementInfos[] = null;
private int startIx = -1;
private int insIx = -1;
private org.openspcoop2.utils.Semaphore semaphore = null;
private static class ElementDateInfo<K> {
private final long timeMillis;
private final K key;
ElementDateInfo( long timeMillis, K key ) {
this.timeMillis = timeMillis;
this.key = key;
}
public long getTimeMillis() {
return this.timeMillis;
}
public K getKey() {
return this.key;
}
}
public LimitedHashMap( String cacheName, int maxSize, int maxLifeTime ) {
super();
initElementInfos( cacheName, maxSize, maxLifeTime );
}
public LimitedHashMap( String cacheName, int maxSize, int maxLifeTime, int initialCapacity ) {
super( initialCapacity );
initElementInfos( cacheName, maxSize, maxLifeTime );
}
public LimitedHashMap( String cacheName, int maxSize, int maxLifeTime, Map<? extends K, ? extends V> m ) {
super( m );
initElementInfos( cacheName, maxSize, maxLifeTime );
}
public LimitedHashMap( String cacheName, int maxSize, int maxLifeTime, int initialCapacity, float loadFactor ) {
super( initialCapacity, loadFactor );
initElementInfos( cacheName, maxSize, maxLifeTime );
}
public LimitedHashMap( String cacheName, int maxSize, int maxLifeTime, int initialCapacity, float loadFactor, int concurrencyLevel ) {
super( initialCapacity, loadFactor, concurrencyLevel );
initElementInfos( cacheName, maxSize, maxLifeTime );
}
/*
public LimitedHashMap() {
this( null, -1, -1 );
}
public LimitedHashMap( int initialCapacity ) {
this( null, -1, -1, initialCapacity );
}
public LimitedHashMap( Map<? extends K, ? extends V> m ) {
this( null, -1, -1, m );
}
public LimitedHashMap( int initialCapacity, float loadFactor ) {
this( null, -1, -1, initialCapacity, loadFactor );
}
public LimitedHashMap( int initialCapacity, float loadFactor, int concurrencyLevel ) {
this( null, -1, -1, initialCapacity, loadFactor, concurrencyLevel );
}
*/
@SuppressWarnings("unchecked")
private void initElementInfos( String cacheName, int maxSize, int maxLifeTime ) {
if ( maxSize <= 0 && maxLifeTime > 0 )
throw new IllegalArgumentException( "Cannot use maxLifeTime without maxSize" );
this.maxSize = maxSize;
this.maxLifeTime = maxLifeTime;
if ( maxSize > 0 ) {
this.elementInfos = new ElementDateInfo[ maxSize ];
this.startIx = 0;
this.insIx = 0;
}
this.semaphore = new org.openspcoop2.utils.Semaphore(cacheName!=null ? "LimitedHashMap_"+cacheName : "LimitedHashMap");
}
private int previousCircular( int ix ) {
if ( ix == 0 )
return (this.maxSize - 1);
return ix - 1;
}
private int nextCircular( int ix ) {
if ( ix == (this.maxSize - 1) )
return 0;
return ix + 1;
}
private void decrementInsIx() {
this.insIx = previousCircular( this.insIx );
}
private void incrementStartIx() {
this.startIx = nextCircular( this.startIx );
}
private int posSize() {
if ( this.insIx < 0 )
return 0;
if ( this.startIx == this.insIx )
return ( this.elementInfos[this.startIx] != null ? this.maxSize : 0 );
if ( this.startIx < this.insIx )
return this.insIx - this.startIx;
return ( (this.maxSize + this.insIx) - this.startIx );
}
private void cleanUpExtraTime() {
if ( this.insIx < 0 || this.startIx < 0 )
return;
long timeMillis = DateManager.getTimeMillis() - (this.maxLifeTime * 1000);
while ( posSize() > 0 ) {
ElementDateInfo<K> oldestInfo = this.elementInfos[ this.startIx ];
if ( oldestInfo.getTimeMillis() > timeMillis )
break;
K curKey = oldestInfo.getKey();
var removedValue = super.remove( curKey );
if ( removedValue == null )
throw new RuntimeException( "fail to remove by cleanUp: " + curKey );
removeElementInfo( curKey );
}
}
private void syncCleanUpExtraTime() {
if ( this.insIx < 0 )
return;
if(posSize()<=0) {
return;
}
long timeMillis = DateManager.getTimeMillis() - (this.maxLifeTime * 1000);
ElementDateInfo<K> oldestInfo = this.elementInfos[ this.startIx ];
if ( oldestInfo!=null && oldestInfo.getTimeMillis() <= timeMillis ) {
this.semaphore.acquireThrowRuntime("cleanUpExtraTime");
try {
cleanUpExtraTime();
}finally {
this.semaphore.release("cleanUpExtraTime");
}
}
}
private void addElementInfo( K key ) {
ElementDateInfo<K> elInfo = new ElementDateInfo<K>( DateManager.getTimeMillis(), key );
if ( this.insIx >= 0 ) {
int posIx = this.insIx;
this.insIx = (this.insIx + 1) % this.maxSize;
ElementDateInfo<K> startElInfo = this.elementInfos[this.startIx];
if ( posIx == this.startIx && startElInfo != null ) {
var removedValue = super.remove( startElInfo.getKey() );
if ( removedValue == null )
throw new RuntimeException( "fail to remove by add: " + startElInfo.getKey() + " adding " + key );
incrementStartIx();
}
this.elementInfos[ posIx ] = elInfo;
} else
throw new RuntimeException( "Invalid insIx: not initialized" );
}
private void removeElementInfo( K key ) {
int posIx = -1;
int size = posSize();
for ( int ix = this.startIx, jx = 0; posIx < 0 && jx < size; jx++ ) {
if ( this.elementInfos[ix] != null && this.elementInfos[ix].getKey().equals( key ) )
posIx = ix;
ix = nextCircular(ix);
}
if ( posIx < 0 )
return;
int prevInsIx = previousCircular( this.insIx );
if ( posIx == prevInsIx ) {
this.elementInfos[ prevInsIx ] = null;
decrementInsIx();
} else
if ( posIx == this.startIx ) {
this.elementInfos[ posIx ] = null;
incrementStartIx();
} else
if ( posIx < this.startIx ) {
if ( posIx < prevInsIx ) {
System.arraycopy( this.elementInfos, posIx + 1, this.elementInfos, posIx, prevInsIx - posIx );
this.elementInfos[ prevInsIx ] = null;
decrementInsIx();
} else
throw new RuntimeException( "Invalid index position: " + posIx +
" (startIx: " + this.startIx + ") (insIx: " + this.insIx );
} else {
if ( posIx < this.insIx ) {
if ( this.startIx < this.insIx ) {
System.arraycopy( this.elementInfos, posIx + 1, this.elementInfos, posIx, prevInsIx - posIx );
this.elementInfos[ prevInsIx ] = null;
decrementInsIx();
} else
throw new RuntimeException( "Invalid index position: " + posIx +
" (startIx: " + this.startIx + ") (insIx: " + this.insIx );
} else {
System.arraycopy( this.elementInfos, posIx + 1, this.elementInfos, posIx, this.maxSize - 1 - posIx );
if ( this.insIx > 0 ) {
this.elementInfos[ this.maxSize - 1 ] = this.elementInfos[0];
// TBK
if ( prevInsIx > 0 )
System.arraycopy( this.elementInfos, 1, this.elementInfos, 0, prevInsIx );
}
this.elementInfos[ prevInsIx ] = null;
decrementInsIx();
}
}
}
private void updateElementInfo( K key ) {
removeElementInfo( key );
addElementInfo( key );
}
@Override
public V get( Object key ) {
if ( this.maxLifeTime > 0 )
syncCleanUpExtraTime();
return super.get( key );
}
@Override
public V put( K key, V value ) {
this.semaphore.acquireThrowRuntime("put");
try {
if ( this.maxLifeTime > 0 )
cleanUpExtraTime();
var res = super.put( key, value );
if ( this.maxSize > 0 ) {
if ( res == null )
addElementInfo( key );
else
updateElementInfo( key );
}
return res;
}finally {
this.semaphore.release("put");
}
}
@SuppressWarnings("unchecked")
@Override
public V remove( Object key ) {
this.semaphore.acquireThrowRuntime("remove");
try {
if ( this.maxLifeTime > 0 )
cleanUpExtraTime();
var res = super.remove( key );
if ( res != null )
removeElementInfo( (K)key );
return res;
}finally {
this.semaphore.release("remove");
}
}
}