TTLCache.java [src/csip/utils] Revision:   Date:
/*
 * $Id$
 *
 * This file is part of the Cloud Services Integration Platform (CSIP),
 * a Model-as-a-Service framework, API and application suite.
 *
 * 2012-2022, Olaf David and others, OMSLab, Colorado State University.
 *
 * OMSLab licenses this file to you under the MIT license.
 * See the LICENSE file in the project root for more information.
 */
package csip.utils;

import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;

/**
 * LRU + TTL cache.
 *
 * - The cache has a limited size/capacity - Each entry will have a ttl on put()
 * - evicted: if expired or capacity has reached.
 *
 * @author od
 */
public class TTLCache<K, V> {

  private static class Item<V> {

    long exptime;
    long ttl;

    V value;


    Item(long ttl, V value) {
      this.ttl = ttl;
      this.exptime = expire(System.currentTimeMillis(), ttl);
      this.value = value;
    }


    void updateExpiration(long systime) {
      this.exptime = expire(systime, ttl);
    }


    boolean isExpired(long systime) {
      return systime > exptime;
    }


    long getExpiration() {
      return exptime;
    }


    @Override
    public String toString() {
      return value.toString() + "[" + exptime + "]";
    }


    /**
     *
     * @param systime
     * @param ttl -1 means no expiration
     * @return
     */
    private static long expire(long systime, long ttl) {
      return ttl < 0 ? Long.MAX_VALUE : systime + ttl;
    }

  }

  Map<K, Item<V>> m = new LinkedHashMap<>();

  // the last key/values accessed.
  int size = 16;  // default 16 elements


  /**
   * The size of the cache
   * @param size the new size of the cache
   * @return this TTLCache instance
   */
  public TTLCache withSize(int size) {
    if (size < 0)
      throw new IllegalArgumentException("size >= 0!");
    
    this.size = size;
    prune();
    return this;
  }


  public V put(K key, V val, long ttl) {
    if (size == 0 || key == null)
      return null;
    
    Item<V> prev = m.put(key, new Item<V>(ttl, val));
    if (m.size() > size * 0.75)
      removeAllExpired();
    
    // nothing is expired, evict one if cache is full.
    prune();
    return prev == null ? null : prev.value;
  }


  public V get(K key) {
    if (size == 0 || key == null)
      return null;

    if (m.containsKey(key)) {
      Item<V> val = m.get(key);
      // remove it from wherever it is
      m.remove(key);
      long now = System.currentTimeMillis();
      if (!val.isExpired(now)) {
        // not expired, reset expiration
        val.updateExpiration(now);
        // put it back at the top
        m.put(key, val);
        return val.value;
      } else {
        // was expired, now removed.
        return null;
      }
    }
    return null;
  }


  public void clear() {
    m.clear();
  }


  public int getSize() {
    return size;
  }


  private void prune() {
    while (m.size() > size) {
      // evict element
      K first = m.keySet().iterator().next();
      m.remove(first);
    }
  }


  private void removeAllExpired() {
    long now = System.currentTimeMillis();
    Iterator<Item<V>> itr = m.values().iterator();
    while (itr.hasNext()) {
      if (itr.next().isExpired(now))
        itr.remove();
    }
  }

}