LimitedHashMap.java

  1. /*
  2.  * GovWay - A customizable API Gateway
  3.  * https://govway.org
  4.  *
  5.  * Copyright (c) 2005-2025 Link.it srl (https://link.it).
  6.  *
  7.  * This program is free software: you can redistribute it and/or modify
  8.  * it under the terms of the GNU General Public License version 3, as published by
  9.  * the Free Software Foundation.
  10.  *
  11.  * This program is distributed in the hope that it will be useful,
  12.  * but WITHOUT ANY WARRANTY; without even the implied warranty of
  13.  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  14.  * GNU General Public License for more details.
  15.  *
  16.  * You should have received a copy of the GNU General Public License
  17.  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
  18.  *
  19.  */

  20. package org.openspcoop2.utils.cache;

  21. import java.util.Map;
  22. import java.util.concurrent.ConcurrentHashMap;

  23. import org.openspcoop2.utils.SemaphoreLock;
  24. import org.openspcoop2.utils.date.DateManager;

  25. /**
  26.  * LimitedHashMap
  27.  *
  28.  * @author Luigi Buoncristiani (Kali.Blu@gmail.com)
  29.  * @author $Author$
  30.  * @version $Rev$, $Date$
  31.  */
  32. public class LimitedHashMap<K,V> extends ConcurrentHashMap<K, V> {
  33.     private static final long serialVersionUID = 1L;

  34.     private int maxSize = -1;
  35.     private int maxLifeTime = -1;
  36.     private LimitedHashMap.ElementDateInfo<K> elementInfos[] = null;
  37.     private int startIx = -1;
  38.     private int insIx = -1;
  39.     private org.openspcoop2.utils.Semaphore semaphore = null;
  40.    
  41.     private static class ElementDateInfo<K> {
  42.         private final long timeMillis;
  43.         private final K key;
  44.         ElementDateInfo( long timeMillis, K key ) {
  45.             this.timeMillis = timeMillis;
  46.             this.key = key;
  47.         }
  48.         public long getTimeMillis() {
  49.             return this.timeMillis;
  50.         }
  51.         public K getKey() {
  52.             return this.key;
  53.         }
  54.     }

  55.     public LimitedHashMap( String cacheName, int maxSize, int maxLifeTime ) {
  56.         super();
  57.         initElementInfos( cacheName, maxSize, maxLifeTime );
  58.     }
  59.     public LimitedHashMap( String cacheName, int maxSize, int maxLifeTime, int initialCapacity ) {
  60.         super( initialCapacity );
  61.         initElementInfos( cacheName, maxSize, maxLifeTime );
  62.     }
  63.     public LimitedHashMap( String cacheName, int maxSize, int maxLifeTime, Map<? extends K, ? extends V> m ) {
  64.         super( m );
  65.         initElementInfos( cacheName, maxSize, maxLifeTime );
  66.     }
  67.     public LimitedHashMap( String cacheName, int maxSize, int maxLifeTime, int initialCapacity, float loadFactor ) {
  68.         super( initialCapacity, loadFactor );
  69.         initElementInfos( cacheName, maxSize, maxLifeTime );
  70.     }
  71.     public LimitedHashMap( String cacheName, int maxSize, int maxLifeTime, int initialCapacity, float loadFactor, int concurrencyLevel ) {
  72.         super( initialCapacity, loadFactor, concurrencyLevel );
  73.         initElementInfos( cacheName, maxSize, maxLifeTime );
  74.     }

  75.     /*
  76.     public LimitedHashMap() {
  77.         this( null, -1, -1 );
  78.     }
  79.     public LimitedHashMap( int initialCapacity ) {
  80.         this( null, -1, -1, initialCapacity );
  81.     }
  82.     public LimitedHashMap( Map<? extends K, ? extends V> m ) {
  83.         this( null, -1, -1, m );
  84.     }
  85.     public LimitedHashMap( int initialCapacity, float loadFactor ) {
  86.         this( null, -1, -1, initialCapacity, loadFactor );
  87.     }
  88.     public LimitedHashMap( int initialCapacity, float loadFactor, int concurrencyLevel ) {
  89.         this( null, -1, -1, initialCapacity, loadFactor, concurrencyLevel );
  90.     }
  91.     */

  92.     @SuppressWarnings("unchecked")
  93.     private void initElementInfos( String cacheName, int maxSize, int maxLifeTime ) {
  94.         if ( maxSize <= 0 && maxLifeTime > 0 )
  95.             throw new IllegalArgumentException( "Cannot use maxLifeTime without maxSize" );
  96.         this.maxSize = maxSize;
  97.         this.maxLifeTime = maxLifeTime;
  98.         if ( maxSize > 0 ) {
  99.             this.elementInfos = new ElementDateInfo[ maxSize ];
  100.             this.startIx = 0;
  101.             this.insIx = 0;
  102.         }
  103.         this.semaphore = new org.openspcoop2.utils.Semaphore(cacheName!=null ? "LimitedHashMap_"+cacheName : "LimitedHashMap");
  104.     }

  105.     private int previousCircular( int ix ) {
  106.         if ( ix == 0 )
  107.             return (this.maxSize - 1);
  108.         return ix - 1;
  109.     }

  110.     private int nextCircular( int ix ) {
  111.         if ( ix == (this.maxSize - 1) )
  112.             return 0;
  113.         return ix + 1;
  114.     }

  115.     private void decrementInsIx() {
  116.         this.insIx = previousCircular( this.insIx );
  117.     }

  118.     private void incrementStartIx() {
  119.         this.startIx = nextCircular( this.startIx );
  120.     }

  121.     private int posSize() {
  122.         if ( this.insIx < 0 )
  123.             return 0;
  124.         if ( this.startIx == this.insIx )
  125.             return ( this.elementInfos[this.startIx] != null ? this.maxSize : 0 );
  126.         if ( this.startIx < this.insIx )
  127.             return this.insIx - this.startIx;
  128.         return ( (this.maxSize + this.insIx) - this.startIx );
  129.     }

  130.     private void cleanUpExtraTime() {
  131.         if ( this.insIx < 0 || this.startIx < 0 )
  132.             return;
  133.         long timeMillis = DateManager.getTimeMillis() - (this.maxLifeTime * 1000);
  134.         while ( posSize() > 0 ) {
  135.             ElementDateInfo<K> oldestInfo = this.elementInfos[ this.startIx ];
  136.             if ( oldestInfo.getTimeMillis() > timeMillis )
  137.                 break;
  138.             K curKey = oldestInfo.getKey();
  139.             var removedValue = super.remove( curKey );
  140.             if ( removedValue == null )
  141.                 throw new RuntimeException( "fail to remove by cleanUp: " + curKey );
  142.             removeElementInfo( curKey );
  143.         }
  144.     }
  145.     private void syncCleanUpExtraTime() {
  146.         if ( this.insIx < 0 )
  147.             return;
  148.         if(posSize()<=0) {
  149.             return;
  150.         }
  151.         long timeMillis = DateManager.getTimeMillis() - (this.maxLifeTime * 1000);
  152.         ElementDateInfo<K> oldestInfo = this.elementInfos[ this.startIx ];
  153.         if ( oldestInfo!=null && oldestInfo.getTimeMillis() <= timeMillis ) {

  154.             SemaphoreLock lock = this.semaphore.acquireThrowRuntime("cleanUpExtraTime");
  155.             try {
  156.                 cleanUpExtraTime();
  157.             }finally {
  158.                 this.semaphore.release(lock, "cleanUpExtraTime");
  159.             }
  160.            
  161.         }
  162.     }

  163.     private void addElementInfo( K key ) {
  164.         ElementDateInfo<K> elInfo = new ElementDateInfo<K>( DateManager.getTimeMillis(), key );
  165.         if ( this.insIx >= 0 ) {
  166.             int posIx = this.insIx;
  167.             this.insIx = (this.insIx + 1) % this.maxSize;
  168.             ElementDateInfo<K> startElInfo = this.elementInfos[this.startIx];
  169.             if ( posIx == this.startIx && startElInfo != null ) {
  170.                 var removedValue = super.remove( startElInfo.getKey() );
  171.                 if ( removedValue == null )
  172.                     throw new RuntimeException( "fail to remove by add: " + startElInfo.getKey() + " adding " + key );
  173.                 incrementStartIx();
  174.             }
  175.             this.elementInfos[ posIx ] = elInfo;
  176.         } else
  177.             throw new RuntimeException( "Invalid insIx: not initialized" );
  178.     }

  179.     private void removeElementInfo( K key ) {
  180.         int posIx = -1;
  181.         int size = posSize();
  182.         for ( int ix = this.startIx, jx = 0; posIx < 0 && jx < size; jx++ ) {
  183.             if ( this.elementInfos[ix] != null && this.elementInfos[ix].getKey().equals( key ) )
  184.                 posIx = ix;
  185.             ix = nextCircular(ix);
  186.         }
  187.         if ( posIx < 0 )
  188.             return;

  189.         int prevInsIx = previousCircular( this.insIx );
  190.         if ( posIx == prevInsIx ) {
  191.             this.elementInfos[ prevInsIx ] = null;
  192.             decrementInsIx();
  193.         } else
  194.         if ( posIx == this.startIx ) {
  195.             this.elementInfos[ posIx ] = null;
  196.             incrementStartIx();
  197.         } else
  198.         if ( posIx < this.startIx ) {
  199.             if ( posIx < prevInsIx ) {
  200.                 System.arraycopy( this.elementInfos, posIx + 1, this.elementInfos, posIx, prevInsIx - posIx );
  201.                 this.elementInfos[ prevInsIx ] = null;
  202.                 decrementInsIx();
  203.             } else
  204.                 throw new RuntimeException( "Invalid index position: " + posIx +
  205.                                             " (startIx: " + this.startIx + ") (insIx: " + this.insIx );
  206.         } else {
  207.             if ( posIx < this.insIx ) {
  208.                 if ( this.startIx < this.insIx ) {
  209.                     System.arraycopy( this.elementInfos, posIx + 1, this.elementInfos, posIx, prevInsIx - posIx );
  210.                     this.elementInfos[ prevInsIx ] = null;
  211.                     decrementInsIx();
  212.                 } else
  213.                     throw new RuntimeException( "Invalid index position: " + posIx +
  214.                                                 " (startIx: " + this.startIx + ") (insIx: " + this.insIx );
  215.             } else {
  216.                 System.arraycopy( this.elementInfos, posIx + 1, this.elementInfos, posIx, this.maxSize - 1 - posIx );
  217.                 if ( this.insIx > 0 ) {
  218.                     this.elementInfos[ this.maxSize - 1 ] = this.elementInfos[0];
  219.                     // TBK
  220.                     if ( prevInsIx > 0 )
  221.                         System.arraycopy( this.elementInfos, 1, this.elementInfos, 0, prevInsIx );
  222.                 }
  223.                 this.elementInfos[ prevInsIx ] = null;
  224.                 decrementInsIx();
  225.             }
  226.         }
  227.     }

  228.     private void updateElementInfo( K key ) {
  229.         removeElementInfo( key );
  230.         addElementInfo( key );
  231.     }

  232.     @Override
  233.     public V get( Object key ) {
  234.         if ( this.maxLifeTime > 0 )
  235.             syncCleanUpExtraTime();
  236.         return super.get( key );
  237.     }

  238.     @Override
  239.     public V put( K key, V value ) {
  240.         SemaphoreLock lock = this.semaphore.acquireThrowRuntime("put");
  241.         try {
  242.             if ( this.maxLifeTime > 0 )
  243.                 cleanUpExtraTime();
  244.            
  245.             var res = super.put( key, value );
  246.             if ( this.maxSize > 0 ) {
  247.                 if ( res == null )
  248.                     addElementInfo( key );
  249.                 else
  250.                     updateElementInfo( key );
  251.             }
  252.             return res;
  253.         }finally {
  254.             this.semaphore.release(lock, "put");
  255.         }
  256.     }

  257.     @SuppressWarnings("unchecked")
  258.     @Override
  259.     public V remove( Object key ) {
  260.         SemaphoreLock lock = this.semaphore.acquireThrowRuntime("remove");
  261.         try {
  262.             if ( this.maxLifeTime > 0 )
  263.                 cleanUpExtraTime();
  264.             var res = super.remove( key );
  265.             if ( res != null )
  266.                 removeElementInfo( (K)key );
  267.             return res;
  268.         }finally {
  269.             this.semaphore.release(lock, "remove");
  270.         }
  271.     }

  272. }