package org.itsnat.droid.impl.browser;
import org.itsnat.droid.impl.util.MiscUtil;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.SortedMap;
import java.util.TreeMap;
/**
* Created by Jose on 18/06/2015.
*/
public class HttpFileCache
{
private long maxCacheSize;
private long currentCacheSize = 0;
private Map<String,FileCached> mapByUrl = new HashMap<String,FileCached>();
private SortedMap<String,FileCached> mapByLastAccess = new TreeMap<String,FileCached>();
public HttpFileCache(long maxCacheSize)
{
this.maxCacheSize = maxCacheSize;
}
public void setMaxCacheSize(long maxCacheSize)
{
this.maxCacheSize = maxCacheSize;
}
public synchronized FileCached get(String url)
{
FileCached file = mapByUrl.get(url);
if (file == null) return null;
long prevLastAccess = file.getLastAccessTimestamp();
long currentLastAccess = file.updateLastAccessTimestamp();
if (currentLastAccess != prevLastAccess) // por si acaso ocurre dos accesos del mismo archivo en el mismo milisegundo
{
FileCached file2 = mapByLastAccess.remove(genLastAccessKey(prevLastAccess,file));
if (file2 != file) throw MiscUtil.internalError(); // file2 en teoria DEBE ser igual que file, pero por si acaso...
mapByLastAccess.put(genLastAccessKey(currentLastAccess,file), file);
}
return file;
}
public synchronized void put(FileCached file)
{
FileCached oldFile = mapByUrl.get(file.getUrl()); // La intencion es eliminarlo no necesitamos actualizar el LastAccessTimestamp y registrarlo y por tanto no llamamos a get(url)
if (oldFile != null)
removeInternal(oldFile); // asi aseguramos que dos put a la vez con el mismo FileCached solo inserten un FileCached no dos
if (mapByUrl.put(file.getUrl(),file) != null) throw MiscUtil.internalError();
long lastAccess = file.getLastAccessTimestamp();
if (mapByLastAccess.put(genLastAccessKey(lastAccess,file), file) != null) throw MiscUtil.internalError();
currentCacheSize += file.getContentByteArray().length;
if (currentCacheSize > maxCacheSize)
cleanLessUsedFilesToMaxCacheSize();
}
public synchronized void remove(FileCached file)
{
FileCached oldFile = get(file.getUrl());
if (oldFile == null) return;
removeInternal(file); // asi aseguramos que dos remove a la vez con el mismo FileCached solo elimine uno y el otro no falle
}
private void removeInternal(FileCached file)
{
if (mapByUrl.remove(file.getUrl()) != file) throw MiscUtil.internalError();
long lastAccess = file.getLastAccessTimestamp();
if (mapByLastAccess.remove(genLastAccessKey(lastAccess,file)) != file) throw MiscUtil.internalError();
currentCacheSize -= file.getContentByteArray().length;
}
private void cleanLessUsedFilesToMaxCacheSize()
{
for(Iterator<Map.Entry<String,FileCached>> it = mapByLastAccess.entrySet().iterator(); it.hasNext(); )
{
if (currentCacheSize <= maxCacheSize) break;
Map.Entry<String,FileCached> entry = it.next();
FileCached file = entry.getValue();
currentCacheSize -= file.getContentByteArray().length;
mapByUrl.remove(file.getUrl());
it.remove();
}
}
private String genLastAccessKey(long lastAccess,FileCached file)
{
return lastAccess + "-" + file.getUrl();
}
static class FileCached
{
private String url;
private long lastModified;
private byte[] contentByteArray;
private long lastAccessTimestamp;
public FileCached(String url, long lastModified, byte[] contentByteArray)
{
this.url = url;
this.lastModified = lastModified;
this.contentByteArray = contentByteArray;
this.lastAccessTimestamp = System.currentTimeMillis();
}
public String getUrl()
{
return url;
}
public long getLastModified()
{
return lastModified;
}
public byte[] getContentByteArray()
{
return contentByteArray;
}
private long getLastAccessTimestamp()
{
return lastAccessTimestamp;
}
private long updateLastAccessTimestamp()
{
return this.lastAccessTimestamp = System.currentTimeMillis();
}
}
}