//
// DataMapCache.java - Implement a simple cache
//
// Some DataMaps simply return values we already have locally.
// Others read files, fetch URLs, or make requests to remote services,
// and thus mapping is expensive.
// So this provides a crude caching layer.
//
// The cache can be constrained to a certain size.
//
// A crude TTL value (in milliseconds) can be specified.
// We aim to keep cached data until response time plus the TTL value.
// Therefore if the request-reponse takes upto 5000ms, and the TTL value is
// 60000ms, a change in the underlying data will be active use after 65000ms.
//

package nyangau.jmsadapter;

//...simports:0:
import java.util.Set;
import java.util.Map;
import java.util.HashMap;
import java.util.LinkedList;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.Text;
//...e

public class DataMapCache extends DataMap
  {
//...sclass CacheEntry:2:
public class CacheEntry
  {
  public String src;
  public String dest;
  public long expiry;
  public CacheEntry(String s, String d, long e)
    { src = s; dest = d; expiry = e; }
  }
//...e
  protected Object oSync;
  protected Map<String,CacheEntry> ces;
  protected LinkedList<CacheEntry> deathRow;
  protected int ttl; // or -1 if no expiry
  protected int size;
  protected DataMap datamap;
//...sDataMapCache:2:
public DataMapCache(Node eDataMap)
  throws FlowProcessorException
  {
  super(eDataMap);
  NamedNodeMap nnmDataMap = eDataMap.getAttributes();
  Node aTtl = nnmDataMap.getNamedItem("ttl");
  ttl = ( aTtl != null ) ? Integer.parseInt(aTtl.getNodeValue()) : -1;
  Node aSize = nnmDataMap.getNamedItem("size");
  size = ( aSize != null ) ? Integer.parseInt(aSize.getNodeValue()) : 100;

  // Get the nested DataMap
  NodeList nl = eDataMap.getChildNodes();
  datamap = null;
  for ( int i = 0; i < nl.getLength(); i++ )
    {
    Node eDataMapNested = nl.item(i);
    if ( eDataMapNested.getNodeType() == Node.ELEMENT_NODE )
      {
      datamap = DataMap.newDataMap(eDataMapNested);
      break;
      }
    }
  if ( datamap == null )
    throw new FlowProcessorException("unexpected missing nested data map in data map "+name+", bad jmsadapter.xsd?");

  // Initialise an empty cache
  oSync = new Object();
  ces = new HashMap<String,CacheEntry>();
  deathRow = new LinkedList<CacheEntry>();
  }
//...e
//...smap:2:
public void map(Set<String> unmapped, Map<String,String> mapped)
  throws FlowProcessorException
  {
  // Make sure we are not using stale data
  if ( ttl != -1 )
    {
    long now = System.currentTimeMillis();
    synchronized ( oSync )
      {
      CacheEntry ce;
      while ( (ce = deathRow.getFirst()) != null && now > ce.expiry )
        {
        deathRow.removeFirst();
        ces.remove(ce.src);
        }
      }
    }

  // Satisfy what we can from the cache
  synchronized ( oSync )
    {
    if ( unmapped.size() < ces.size() )
      for ( String s : unmapped )
        {
        CacheEntry ce = ces.get(s);
        if ( ce != null )
          {
          mapped.put(s, ce.dest);
          unmapped.remove(s);
          }
        }
    else
      for ( String s : ces.keySet() )
        if ( unmapped.contains(s) )
          {
          CacheEntry ce = ces.get(s);
          mapped.put(s, ce.dest);
          unmapped.remove(s);
          }
    }

  // If some work still to do
  if ( ! unmapped.isEmpty() )
    {
    // Request the missing data from the nested MapStep below.
    // This could take a while to be satisfied
    // (which of course is why we bother to cache the results),
    // so notice that the oSync lock is not held while we do.
    Map<String,String> mappedNested = new HashMap<String,String>();
    datamap.map(unmapped, mappedNested);

    // Record returned values in the cache.
    // Note that concurrent map requests can mean that the data for a mapping
    // found to be missing earlier can now be present.
    // This is fine, we simply leave the existing value in the cache,
    // and we don't update the expiry time, as this would causes us to
    // need find the entry in deathRow (a slow operation) and move it to the
    // end, so as to preserve the monotonically increasing expiry times.
    long now = System.currentTimeMillis();
    long expiry = ( ttl != -1 ) ? now + ttl : Long.MAX_VALUE;
    synchronized ( oSync )
      {
      for ( String s : mappedNested.keySet() )
        if ( ces.get(s) == null )
          {
          CacheEntry ce = new CacheEntry(s, mappedNested.get(s), expiry);
          ces.put(s, ce);
          deathRow.addLast(ce);
          if ( ces.size() > size )
            {
            CacheEntry ce2 = deathRow.removeFirst();
            ces.remove(ce2.src);
            }
          }
      }

    // Satisfy what we now can from the data we got
    for ( String s : mappedNested.keySet() )
      if ( unmapped.contains(s) )
        {
        mapped.put(s, mappedNested.get(s));
        unmapped.remove(s);
        }
    }
  }
//...e
  }
