package com.gmail.dpierron.calibre.cache;
/**
* This class is responsible for managing a cache of CachedFile
* objects.
*
* As well as providing the obvious support for adding/removing testing
* for such objects it also provides for the cache to be written
* to file at the end of a run and reloaded at the beginning of
* the next run. The main purpose of this is to avoid having
* to recalculate the CRC (which is an expensive operation) between
* runs if it can be avoided.
*
* NOTE: There should only ever be one instance of this class, so all
* global variables and methods are declared static
*/
import com.gmail.dpierron.calibre.gui.CatalogCallbackInterface;
import com.gmail.dpierron.tools.Helper;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.io.*;
import java.util.HashMap;
import java.util.Map;
public class CachedFileManager {
private final static Logger logger = LogManager.getLogger(CachedFileManager.class);
private static Map<String, CachedFile> cachedFilesMap = new HashMap<String, CachedFile>();
private static File cacheFile = null;
private final static String CALIBRE2OPDS_LOG_FILENAME = "c2o_cache";
private final static String CALIBRE2OPDS_LOG_FILENAME_OLD = "calibre2opds.cache";
private static long savedCount = 0;
private static long ignoredCount = 0;
public static void reset() {
cachedFilesMap = null; // Force release any currently assigned map
cachedFilesMap = new HashMap<String, CachedFile>();
}
/**
* Determine if an entry is already in the cache
*
* @param cf CachedFile object to check
* @return null if not present, object otherwise
*/
public static CachedFile inCache(CachedFile cf) {
if (cf == null) {
if (logger.isTraceEnabled()) logger.trace("inCache(cf) - unexpected null parameter");
return null;
}
CachedFile cf_result = cachedFilesMap.get(cf.getPath());
if (logger.isTraceEnabled()) logger.trace("inCache=" + (cf_result != null) + ": " + cf.getPath());
return cf_result;
}
/**
* Determine if an entry is already in the cache
*
* @param f File object to check
* @return null if not present, object otherwise
*/
public static CachedFile inCache(File f) {
if (f == null) {
if (logger.isTraceEnabled()) logger.trace("inCache(f) - unexpected null parameter");
return null;
}
CachedFile cf_result = cachedFilesMap.get(f.getPath());
if (logger.isTraceEnabled()) logger.trace("inCache=" + (cf_result != null) + ": " + f.getPath());
return cf_result;
}
/**
* Check the entry against the cache and if needed
* create a new entry.
*
* @param cf CachedFile object representing file
* @return A CachedFile object for the given path
*/
public static CachedFile addCachedFile(CachedFile cf) {
if (cf == null) {
if (logger.isTraceEnabled()) logger.trace("addCachedFile(cf) - unexpected null parameter");
return null;
}
CachedFile cf2 = inCache(cf);
if (cf2 == null) {
String path = cf.getPath();
cf2 = new CachedFile(path);
cachedFilesMap.put(path, cf2);
if (logger.isTraceEnabled()) logger.trace("Added CachedFile: " + path);
}
return cf2;
}
/**
* Check the entry against the cache and if needed
* create a new entry.
*
* @param f File object representing file
* @return A CachedFile object for the given path
*/
public static CachedFile addCachedFile(File f) {
if (f == null) {
if (logger.isTraceEnabled()) logger.trace("addCachedFile(f) - unexpected null parameter");
return null;
}
String path = f.getPath();
CachedFile cf = inCache(f);
if (cf == null) {
cf = new CachedFile(path);
cachedFilesMap.put(path, cf);
if (logger.isTraceEnabled()) logger.trace("Added file to cache: " + path);
}
return cf;
}
/**
*
* @param filename
* @return
*/
public static CachedFile addCachedFile (String filename) {
if (Helper.isNullOrEmpty(filename)) {
if (logger.isTraceEnabled()) logger.trace("addCachedFile(filename) - unexpected null parameter");
return null;
}
return addCachedFile(new File(filename));
}
/**
* Add a file to the file cache that is a source file
*
* @param parent Folder that will contain the file
* @param childname Filename
* @return CachedFile object corresponding to file
*/
public static CachedFile addCachedFile(File parent, String childname) {
if (Helper.isNullOrEmpty(parent) || Helper.isNullOrEmpty(childname)) {
if (logger.isTraceEnabled()) logger.trace("addCachedFile(parent,childname) - unexpected null parameter");
return null;
}
return addCachedFile(new File(parent, childname));
}
public static CachedFile addCachedFile(String parentname, File childfile) {
if (Helper.isNullOrEmpty(parentname) || Helper.isNullOrEmpty(childfile)) {
if (logger.isTraceEnabled()) logger.trace("addCachedFile(parent,childname) - unexpected null parameter");
return null;
}
return addCachedFile(new File(parentname, childfile.getName()));
}
/**
* Add a file to the cache
* @param parentname
* @param childname
* @return
*/
public static CachedFile addCachedFile(String parentname, String childname) {
if (Helper.isNullOrEmpty(parentname) || Helper.isNullOrEmpty(childname)) {
if (logger.isTraceEnabled()) logger.trace("addCachedFile(parentname,childname) - unexpected null parameter");
return null;
}
return addCachedFile(new File(parentname,childname));
}
/**
* Remove the entry from the cache (if it is present).
*
* @param f File object representing file
*/
public static void removeCachedFile(File f) {
String path = f.getPath();
if (cachedFilesMap.containsKey(path)) {
cachedFilesMap.remove(path);
if (logger.isTraceEnabled()) logger.trace("Remove CachedFile: " + path);
} else {
if (logger.isTraceEnabled()) logger.trace("Remove CachedFile (not found): " + path);
}
}
/**
* Remove the entry from the cache (if it is present).
*
* @param cf CachedFile object representing file
*/
public static void removeCachedFile(CachedFile cf) {
removeCachedFile((File)cf);
}
/**
* Set the location for any existing cache file
*
* @param cf Specify the folder to hold the cache
* This is normally the catalog sub-folder of the target folder
*/
public static void setCacheFolder(File cf) {
assert cf != null; // cf must not be null
cacheFile = new File(cf, CALIBRE2OPDS_LOG_FILENAME);
if (logger.isDebugEnabled()) logger.debug("CRC Cache file set to " + cacheFile.getPath());
// Check for old name, and if necessary rename to new style
File cacheFileOld = new File(cf, CALIBRE2OPDS_LOG_FILENAME_OLD);
if (cacheFileOld.exists()) {
if (logger.isDebugEnabled()) logger.debug("Cache file found with name " + CALIBRE2OPDS_LOG_FILENAME_OLD + ", rename to " + CALIBRE2OPDS_LOG_FILENAME);
if (cacheFileOld.renameTo(cacheFile)) {
if (logger.isDebugEnabled())logger.debug("Cache file renamed to " + CALIBRE2OPDS_LOG_FILENAME);
} else {
if (logger.isDebugEnabled()) logger.debug("ERROR: failed to rename cache file");
}
}
}
/**
* Save the current cache for potential later re-use
* You can specify a path that should beignored so that
* one can avoid saving objects for the TEMP area
*
* N.B. the setCacheFolder() call must have been used
*/
public static void saveCache(String pathToIgnore, CatalogCallbackInterface callback) {
// Check Cache folder has been set
if (logger.isDebugEnabled()) logger.debug("saveCache; pathToIgnore=" + pathToIgnore);
if (cacheFile == null) {
if (logger.isDebugEnabled()) logger.debug("Aborting saveCache() as cacheFile not set");
return;
}
savedCount = 0;
ignoredCount = 0;
long isDirectory = 0;
long pathMatch = 0;
long crcNotKnown = 0;
long notExists = 0 ;
long notUsed = 0;
long countChecked = 0;
ObjectOutputStream os = null;
BufferedOutputStream bs = null;
FileOutputStream fs = null;
long countPercent = cachedFilesMap.entrySet().size()/100; // Use to avoid too frequent GUI updates
if (countPercent == 0) countPercent=1; // Safety check for libraries where total books is less than 100
if (callback != null ) callback.setProgressMax(100);
deleteCache();
try {
try {
if (logger.isDebugEnabled()) logger.debug("STARTED Saving cacheFile entries to " + cacheFile.getPath());
// Open cache file (objects)
fs = new FileOutputStream(cacheFile); // Open File
assert fs != null: "saveCache: fs should never be null at this point";
bs = new BufferedOutputStream(fs,512 * 1024); // Add buffering
assert bs != null: "saveCache: bs should never be null at this point";
os = new ObjectOutputStream(bs); // Add object handling
assert os != null: "saveCache: os should never be null at this point";
// Write out the cache entries
CachedFile cf;
String key = null; // Force initialise to avoid later compile time warnings
for (Map.Entry<String, CachedFile> m : cachedFilesMap.entrySet()) {
// Only update GUI at 1% intervals (reduces overhead)
if ((countChecked % countPercent) == 0
&& callback != null) callback.incStepProgressIndicatorPosition();
countChecked++;
cf = m.getValue();
if (logger.isTraceEnabled()) key = m.getKey();
// We d not want to cache files that have unvalidated cache values.
if (! cf.isCachedValidated()) {
if (logger.isTraceEnabled()) logger.trace("saveCache: Not used. Not saving CachedFile " + key);
notUsed++;
ignoredCount++;
continue;
}
// No point in caching enrtries for fiels in the temporary area.
if (pathToIgnore != null && cf.getPath().startsWith(pathToIgnore)) {
if (logger.isTraceEnabled()) logger.trace("saveCache: PathtoIgnore matches Not saving CachedFile " + key);
pathMatch++;
ignoredCount++;
continue;
}
// We are only interested in caching entries for which the CRC is known
// as this is the expensive operation we do not want to do unnecessarily
if (!cf.isCrc()) {
if (logger.isTraceEnabled()) logger.trace("saveCache: CRC not known. Not saving CachedFile " + key);
crcNotKnown++;
ignoredCount++;
continue;
}
// No point in caching entries for non-existent files
if (!cf.exists()) {
if (logger.isTraceEnabled()) logger.trace("saveCache: Not exists. Not saving CachedFile " + key);
notExists++;
ignoredCount++;
continue;
}
// We do not bother with entries pointing at directories.
if (cf.isDirectory()) {
if (logger.isTraceEnabled()) logger.trace("saveCache: isDirectory Not saving CachedFile " + key);
isDirectory++;
ignoredCount++;
continue;
}
os.writeObject(cf);
if (logger.isTraceEnabled()) logger.trace("saveCache: Saved " + key);
savedCount++;
}
} finally {
try {
if (os != null) os.close();
if (bs != null) bs.close();
if (fs != null) fs.close();
} catch (Exception e) {
// Do nothing - we ignore an error at this point
// Having said that, an error here is a bit unexpected so lets log it when testing
logger.warn("saveCache: Unexpected error\n" + e);
}
}
} catch (IOException e) {
logger.warn("saveCache: Exception trying to write cache:\n" + e);
}
if (logger.isDebugEnabled()) {
logger.debug("saveCache: Cache Entries Saved: " + savedCount);
logger.debug("saveCache: Cache Entries Ignored: " + ignoredCount);
logger.debug("saveCache: isDirectory=" + isDirectory + ", notUsed=" + notUsed + ", notExists=" + notExists + ", crcNotKnown=" + crcNotKnown + ", pathMatch=" + pathMatch);
logger.debug("saveCache: COMPLETED Saving CRC cache to file " + cacheFile.getPath());
}
}
/**
* Initialize the cache if there is a saved one present
*
* N.B. the setCacheFolder() call must have been used
*/
public static void loadCache() {
reset(); // Reset cache to be empty
// Check cache folder has been specified
if (cacheFile == null) {
if (logger.isTraceEnabled()) logger.trace("Aborting loadCache() as cache folder not set");
return;
}
if (!cacheFile.exists()) {
if (logger.isDebugEnabled()) logger.debug("Exiting loadCache() as cache file not present");
return;
}
ObjectInputStream os = null;
FileInputStream fs = null;
BufferedInputStream bs = null;
long loadedCount = 0;
try {
if (logger.isDebugEnabled()) logger.debug("STARTED Loading CRC cache from file " + cacheFile.getPath());
// Open Cache file
fs = new FileInputStream(cacheFile); // Open file
assert fs != null : "loadCache: fs should never be null at this point";
bs = new BufferedInputStream(fs, 512 * 1024); // Add buffering
assert bs != null : "loadCache: bs should never be null at this point";
os = new ObjectInputStream(bs); // And now object handling
assert os != null : "loadCache: os should never be null at this point";
} catch (IOException e) {
logger.warn("loadCache: Aborting as cache file failed to open");
// Abort any cache loading
return;
}
// Read in entries from cache
CachedFile cf;
try {
for (; ; ) {
cf = (CachedFile) os.readObject();
String path = cf.getPath();
if (logger.isTraceEnabled()) logger.trace("Loaded cached object " + path);
loadedCount++;
CachedFile cf2 = inCache(cf);
if (cf2 == null) {
// Not in cache, so simply add it and
// set indicator that values not yet checked
addCachedFile(cf);
cf.clearCacheValidated();
cf.setChanged(true); // Assume changed unless we find otherwise
if (logger.isTraceEnabled()) logger.trace("added entry to cache");
} else {
// Already in cache (can this happen?), so we
// need to determine what values (if any) can
// be set in the entry already there.
if (logger.isDebugEnabled()) logger.debug("Entry already in cache - ignore cached entry for now");
}
}
} catch (ClassNotFoundException cnfe) {
logger.warn("Cache file not loaded +\n" + cnfe);
} catch (java.io.InvalidClassException ic) {
if (logger.isDebugEnabled()) logger.debug("Cache ignored as CachedFile class changed since it was created");
// Should just mean that CachedFile class was changed so old cache invalid
} catch (java.io.EOFException io) {
if (logger.isTraceEnabled()) logger.trace("End of Cache file encountered");
// Do nothing else - this is expected
} catch (IOException e) {
// This is to catch any currently unexpected error cnditions
logger.warn("Exception trying to read cache: " + e);
} catch (Exception e) {
logger.warn("Cache file not loaded +\n" + e);
} finally {
// Close cache file
try {
if (os != null)
os.close();
if (bs != null)
bs.close();
if (fs != null)
fs.close();
} catch (Exception e) {
// do nothing
}
}
if (logger.isDebugEnabled()) {
logger.debug("Cache Entries Loaded: " + loadedCount);
logger.debug("COMPLETED Loading CRC cache from file " + cacheFile.getPath());
}
}
/**
* Delete any existing cache file
*/
public static void deleteCache() {
if (cacheFile == null) {
if (logger.isDebugEnabled()) logger.debug("Aborting deleteCache() as cache folder not set");
return;
}
Helper.delete(cacheFile, false);
if (logger.isDebugEnabled()) logger.debug("Deleted CRC cache file " + cacheFile.getPath());
}
public static long getCacheSize() {
return cachedFilesMap.size();
}
public static long getSaveCount() {
return savedCount;
}
public static long getIgnoredCount() {
return ignoredCount;
}
}