Package winterwell.utils.containers

Source Code of winterwell.utils.containers.Cache

package winterwell.utils.containers;

import java.io.BufferedWriter;
import java.io.File;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;

import winterwell.utils.io.FileUtils;
import winterwell.utils.time.Dt;
import winterwell.utils.time.RateCounter;
import winterwell.utils.time.Time;
import winterwell.utils.web.XStreamUtils;

/**
* A thread-safe in-memory cache which keeps the most recently used values.
* <p>
* Note: This is mostly a convenience wrapper for using {@link LinkedHashMap}
* with synchronized + RateCounters + crude persistence if you want it.
*
* @author daniel
* @testedby {@link CacheTest}
* @param <Key>
* @param <Value>
*/
public class Cache<Key, Value> extends AbstractMap2<Key, Value> {

  private final Map<Key, Value> backing;
  private RateCounter hitCounter;
  private RateCounter missCounter;

  private File persist;

  private Time persistAt;
  private Dt persistEvery;

  /**
   * Create a cache with the given capacity
   *
   * @param capacity
   */
  public Cache(final int capacity) {
    assert capacity > 0;
    // Hm: could we use ConcurrentHashMap somehow -- wouldn't it be faster?
    // 0.75 is the default load factor
    LinkedHashMap<Key, Value> map = new LinkedHashMap<Key, Value>(
        capacity + 1, .75F, true) {
      private static final long serialVersionUID = 1L;

      @Override
      public boolean removeEldestEntry(Map.Entry<Key, Value> eldest) {
        if (size() <= capacity)
          return false;
        boolean ok = preRemovalCheck(eldest.getKey(), eldest.getValue());
        return ok;
      }
    };
    backing = Collections.synchronizedMap(map);
  }

  /**
   * Convert the key into a canonical form. E.g. you might trim and lower-case
   * strings. The semantics of this are: If canonical(a) = canonical(b) then
   * get(a) = get(b).
   * <p>
   * This does nothing by default - override this as needed.
   *
   * @param key
   * @return canonical form of key.
   */
  public Key canonical(Key key) {
    return key;
  }

  /**
   * Drop everything from the cache.
   */
  @Override
  public final void clear() {
    backing.clear();
  }

  /**
   * @return the currently cached key => value mappings.
   */
  @Override
  public Set<Map.Entry<Key, Value>> entrySet() {
    return backing.entrySet();
  }

  /**
   * @param key
   * @return cached value or null
   */
  @Override
  public Value get(Object key) {
    Key k = canonical((Key) key);
    Value v = backing.get(k);
    if (v == null) {
      if (missCounter != null) {
        missCounter.plus(1);
      }
    } else {
      if (hitCounter != null) {
        hitCounter.plus(1);
      }
    }
    maybePersist();
    return v;
  }

  /**
   * Provides direct access to the backing map. For low-level convenience
   * only.
   */
  @Deprecated
  public Map<Key, Value> getBacking() {
    return backing;
  }

  /**
   * @see #setRateCounters(RateCounter, RateCounter)
   * @return null by default
   */
  public RateCounter getHitCounter() {
    return hitCounter;
  }

  /**
   * @see #setRateCounters(RateCounter, RateCounter)
   * @return null by default
   */
  public RateCounter getMissCounter() {
    return missCounter;
  }

  /**
   * @return the currently cached keys.
   */
  @Override
  public Set<Key> keySet() {
    return backing.keySet();
  }

  protected void maybePersist() {
    if (persist == null)
      return;
    if (System.currentTimeMillis() < persistAt.getTime())
      return;
    BufferedWriter out = FileUtils.getWriter(persist);
    XStreamUtils.serialiseToXml(out, new HashMap(backing));
    FileUtils.close(out);
    persistAt = persistAt.plus(persistEvery);
  }

  /**
   * Invoked when removing an entry due to capacity issues. This can be
   * over-ridden to implement custom behaviour. This is NOT called by
   * {@link #remove(Object)}.
   * <p>
   * NB1: preRemovalCheck must not poke at the cache itself -- in general
   * "work" should be done elsewhere. This is quite easy to do, as we found
   * out, if, for example, you have a cache inside a depot and want to
   * save-on-cache-removal.
   * <p>
   * NB2: Returning false will lead to the cache growing beyond its prescribed
   * capacity!
   *
   * @param key
   * @param value
   * @return true if the removal should go ahead.
   *
   */
  protected boolean preRemovalCheck(Key key, Value value) {
    return true;
  }

  @Override
  public final Value put(Key k, Value v) {
    Value old = backing.put(canonical(k), v);
    maybePersist();
    return old;
  }

  @Override
  public final Value remove(Object key) {
    Key k = canonical((Key) key);
    return backing.remove(k);
  }

  /**
   * Adds crude persistence support to the cache. Every so often (provided
   * get() or put() is being called), it will save the current mapping using
   * XStream.
   * <p>
   * If this is set while the cache is empty, then the persistence file will
   * be loaded, if it exists.
   *
   * @param persistHere
   * @param persistEvery
   * @return this
   */
  public Cache<Key, Value> setPersistence(File persistHere, Dt persistEvery) {
    assert persistHere.getParentFile().isDirectory() : persistHere;
    this.persist = persistHere;
    this.persistEvery = persistEvery;
    persistAt = new Time().plus(persistEvery);
    // load now?
    if (backing.isEmpty() && persist.exists()) {
      try {
        Map map = XStreamUtils
            .serialiseFromXml(FileUtils.read(persist));
        backing.putAll(map);
      } catch (Exception e) {
        // oh well -- it will be over-written by the next save
      }
    }
    return this;
  }

  /**
   * Counters are null be default. If set, they will count hits (requests that
   * are in cache) and misses (request we don't hold).
   *
   * @param hitCounter
   * @param missCounter
   * @return
   */
  public Cache<Key, Value> setRateCounters(RateCounter hitCounter,
      RateCounter missCounter) {
    this.hitCounter = hitCounter;
    this.missCounter = missCounter;
    return this;
  }

  /**
   * @return the current number of cached objects
   */
  @Override
  public final int size() {
    return backing.size();
  }

  /**
   * @return the currently cached values
   */
  @Override
  public Collection<Value> values() {
    return backing.values();
  }

}
TOP

Related Classes of winterwell.utils.containers.Cache

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.