/*
GNU GENERAL PUBLIC LICENSE
Copyright (C) 2006 The Lobo Project
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public
License as published by the Free Software Foundation; either
verion 2 of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
General Public License for more details.
You should have received a copy of the GNU General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
Contact info: lobochief@users.sourceforge.net
*/
/*
* Created on Jun 12, 2005
*/
package org.lobobrowser.store;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.URL;
import java.util.Arrays;
import java.util.List;
import java.util.jar.JarFile;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.lobobrowser.security.GenericLocalPermission;
import org.lobobrowser.util.LRUCache;
import org.lobobrowser.util.Strings;
import org.lobobrowser.util.Urls;
import org.lobobrowser.util.io.IORoutines;
/**
* @author J. H. S.
*/
public final class CacheManager implements Runnable {
private static final Logger logger = Logger.getLogger(CacheManager.class.getName());
private static final int AFTER_SWEEP_SLEEP = 5 * 60 * 1000;
private static final int INITIAL_SLEEP = 30 * 1000;
private static final int DELETE_TOLERANCE = 60 * 1000;
private static final long MAX_CACHE_SIZE = 100000000;
private final LRUCache transientCache = new LRUCache(1000000);
/**
*
*/
private CacheManager() {
super();
final Thread t = new Thread(this, "CacheManager");
t.setDaemon(true);
t.setPriority(Thread.MIN_PRIORITY);
t.start();
}
private static CacheManager instance;
public static CacheManager getInstance() {
final SecurityManager sm = System.getSecurityManager();
if (sm != null) {
sm.checkPermission(GenericLocalPermission.EXT_GENERIC);
}
if (instance == null) {
synchronized (CacheManager.class) {
if (instance == null) {
instance = new CacheManager();
}
}
}
return instance;
}
public void putTransient(final URL url, final Object value, final int approxSize) {
final String key = Urls.getNoRefForm(url);
synchronized (this.transientCache) {
this.transientCache.put(key, value, approxSize);
}
}
public Object getTransient(final URL url) {
final String key = Urls.getNoRefForm(url);
synchronized (this.transientCache) {
return this.transientCache.get(key);
}
}
public void removeTransient(final URL url) {
final String key = Urls.getNoRefForm(url);
synchronized (this.transientCache) {
this.transientCache.remove(key);
}
}
public void setMaxTransientCacheSize(final int approxMaxSize) {
synchronized (this.transientCache) {
this.transientCache.setApproxMaxSize(approxMaxSize);
}
}
public int getMaxTransientCacheSize() {
synchronized (this.transientCache) {
return this.transientCache.getApproxMaxSize();
}
}
public CacheInfo getTransientCacheInfo() {
long approxSize;
int numEntries;
List<?> entryInfo;
synchronized (this.transientCache) {
approxSize = this.transientCache.getApproxSize();
numEntries = this.transientCache.getNumEntries();
entryInfo = this.transientCache.getEntryInfoList();
}
return new CacheInfo(approxSize, numEntries, entryInfo);
}
public static void putPersistent(final URL url, final byte[] rawContent, final boolean isDecoration) throws IOException {
final File cacheFile = getCacheFile(url, isDecoration);
synchronized (getLock(cacheFile)) {
final File parent = cacheFile.getParentFile();
if ((parent != null) && !parent.exists()) {
parent.mkdirs();
}
final FileOutputStream fout = new FileOutputStream(cacheFile);
try {
fout.write(rawContent);
} finally {
fout.close();
}
}
}
public static byte[] getPersistent(final URL url, final boolean isDecoration) throws IOException {
// We don't return an InputStream because further synchronization
// would be needed to prevent concurrent writes into the file.
final File cacheFile = getCacheFile(url, isDecoration);
synchronized (getLock(cacheFile)) {
cacheFile.setLastModified(System.currentTimeMillis());
try {
return IORoutines.load(cacheFile);
} catch (final java.io.FileNotFoundException fnf) {
return null;
}
}
}
public static boolean removePersistent(final URL url, final boolean isDecoration) throws IOException {
final File cacheFile = getCacheFile(url, isDecoration);
synchronized (getLock(cacheFile)) {
return cacheFile.delete();
}
}
public static JarFile getJarFile(final URL url) throws java.io.IOException {
final File cacheFile = getCacheFile(url, false);
synchronized (getLock(cacheFile)) {
if (!cacheFile.exists()) {
if (Urls.isLocalFile(url)) {
return new JarFile(url.getFile());
}
throw new java.io.FileNotFoundException("JAR file cannot be obtained for a URL that is not cached locally: " + url + ".");
}
cacheFile.setLastModified(System.currentTimeMillis());
return new JarFile(cacheFile);
}
}
private static File getCacheFile(final URL url, final boolean isDecoration) throws IOException {
// Use file, not path, because query string matters in caching.
final String urlFile = url.getFile();
final String urlText = Urls.getNoRefForm(url);
final int lastSlashIdx = urlFile.lastIndexOf('/');
String simpleName = lastSlashIdx == -1 ? urlFile : urlFile.substring(lastSlashIdx + 1);
if (simpleName.length() > 16) {
simpleName = simpleName.substring(0, 16);
}
final String normalizedName = Strings.getJavaIdentifier(simpleName);
final String hash = Strings.getMD5(urlText);
String fileName = normalizedName + "_" + hash;
if (isDecoration) {
fileName += ".decor";
}
// TODO: Use lowercase hostname (for case-insensitive match)
return StorageManager.getInstance().getContentCacheFile(url.getHost(), fileName);
}
private static Object getLock(final File file) throws IOException {
return ("cm:" + file.getCanonicalPath()).intern();
}
/**
* Touches the cache file corresponding to the given URL and returns
* <code>true</code> if the file exists.
*/
public static boolean checkCacheFile(final URL url, final boolean isDecoration) throws IOException {
final File file = getCacheFile(url, isDecoration);
synchronized (getLock(file)) {
if (file.exists()) {
file.setLastModified(System.currentTimeMillis());
return true;
}
return false;
}
}
public void run() {
try {
Thread.sleep(INITIAL_SLEEP);
} catch (final InterruptedException ie) {
// ignore
}
for (;;) {
try {
this.sweepCache();
Thread.sleep(AFTER_SWEEP_SLEEP);
} catch (final Exception err) {
logger.log(Level.SEVERE, "run()", err);
try {
Thread.sleep(AFTER_SWEEP_SLEEP);
} catch (final java.lang.InterruptedException ie) {
// ignore
}
}
}
}
private static long getMaxCacheSize() {
return MAX_CACHE_SIZE;
}
private void sweepCache() throws Exception {
final CacheStoreInfo sinfo = this.getCacheStoreInfo();
if (logger.isLoggable(Level.INFO)) {
logger.info("sweepCache(): Cache size is " + sinfo.getLength() + " with a max of " + getMaxCacheSize()
+ ". The number of cache files is " + sinfo.getFileInfos().length + ".");
}
long oversize = sinfo.getLength() - getMaxCacheSize();
if (oversize > 0) {
final CacheFileInfo[] finfos = sinfo.getFileInfos();
// Sort in ascending order of modification
Arrays.sort(finfos);
final long okToDeleteBeforeThis = System.currentTimeMillis() - DELETE_TOLERANCE;
for (final CacheFileInfo finfo : finfos) {
try {
Thread.yield();
synchronized (getLock(finfo.getFile())) {
final long lastModified = finfo.getLastModified();
if (lastModified < okToDeleteBeforeThis) {
Thread.sleep(1);
final long time1 = System.currentTimeMillis();
finfo.delete();
final long time2 = System.currentTimeMillis();
if (logger.isLoggable(Level.INFO)) {
logger.info("sweepCache(): Removed " + finfo + " in " + (time2 - time1) + " ms.");
}
oversize -= finfo.getInitialLength();
if (oversize <= 0) {
break;
}
}
}
} catch (final Exception thrown) {
logger.log(Level.WARNING, "sweepCache()", thrown);
}
}
}
}
private CacheStoreInfo getCacheStoreInfo() throws IOException {
final CacheStoreInfo csinfo = new CacheStoreInfo();
final File cacheRoot = StorageManager.getInstance().getCacheRoot();
populateCacheStoreInfo(csinfo, cacheRoot);
return csinfo;
}
private void populateCacheStoreInfo(final CacheStoreInfo csinfo, final File directory) {
final File[] files = directory.listFiles();
if (files == null) {
// TODO: For large directories, java.nio.file.Files.newDirectoryStream() is supposedly faster.
logger.severe("populateCacheStoreInfo(): Unexpected: '" + directory + "' is not a directory.");
return;
}
if (files.length == 0) {
directory.delete();
}
for (final File file : files) {
Thread.yield();
if (file.isDirectory()) {
this.populateCacheStoreInfo(csinfo, file);
} else {
csinfo.addCacheFile(file);
}
}
}
}