package com.gmail.dpierron.calibre.cache; /** * This class is intended to allow the amount of physical I/O * to be reduced to a minimum during a run by caching as much * information as is practical. This means that the main * application logic does not need to worry too much about * making redundant calls on file status type information * as doing so will not cause real calls to be made on the * underlying file system unless it is really needed. * * It is also intended that this type of object can be cached * between runs, so special support is provided for this. * * NOTE: It is important that the main program removes the * cached file entry (or updates/invalidates the cached * information) if it writes to the file or deletes the * file. If not then unexpected actions can occur. */ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.util.zip.Adler32; import java.util.zip.CheckedInputStream; public class CachedFile extends File { private final static Logger logger = LogManager.getLogger(CachedFile.class); private long privateLastModified; private long privateLength; private long privateCrc; // A -ve value indicates invalid CRC; private final static long CRC_NOT_SET = -1; // Flags indicating state of entry // Noee: Using bits in more memory efficient than using boolean types. // This can mount up with the number of these objects that are created. final static short FLAG_ALL_CLEAR = 0x0000; final static short FLAG_EXISTS = 0x0001; final static short FLAG_EXISTS_CHECKED = 0x0002; final static short FLAG_MODIFIED_CHECKED = 0x0004; final static short FLAG_LENGTH_CHECKED = 0x0008; final static short FLAG_IS_DIRECTORY = 0x0010; final static short FLAG_IS_DIRECTORY_CHECKED = 0x0020; final static short FLAG_CRC_CALCED = 0x0040; final static short FLAG_TARGET_FILE = 0x0080; final static short FLAG_CACHED_VALUES_CHECKED = 0x0100; final static short FLAG_IS_CHANGED = 0x02000; // If set to clear then can assume file is unchanged so target cioy is OK private short flags = FLAG_ALL_CLEAR; // Constructors mirror those for the File class public CachedFile(String pathname) { super(pathname); if (logger.isTraceEnabled()) logger.trace("new CachedFile: " + getAbsolutePath()); resetCached(); } /** * Helper routine to set flags bits state */ private void setFlags (boolean b, int f) { if (b == true) flags |= f; // Set flag bits else flags &= ~f; // Clear flag bits; } /** * Helper routine to check if specified kags set * @param f * @return */ private boolean isFlags( int f) { return ((flags & f) == f); } /** * Reset all cached information to defaults. */ private void resetCached() { setFlags(false, FLAG_CACHED_VALUES_CHECKED + FLAG_TARGET_FILE); clearCachedInformation(); } /** * Use this when we have just created a new file. * This is pribarliy when new image files are generated, * so we want to force any cached files to be re-read. */ public void clearCachedInformation() { privateLastModified = 0; privateCrc = CRC_NOT_SET; setFlags(false, FLAG_CRC_CALCED + FLAG_MODIFIED_CHECKED + FLAG_EXISTS_CHECKED + FLAG_LENGTH_CHECKED + FLAG_EXISTS + FLAG_IS_DIRECTORY + FLAG_IS_DIRECTORY_CHECKED + FLAG_CACHED_VALUES_CHECKED); // Reset to say cache only entry setFlags(true, FLAG_IS_CHANGED); // We always assume a file is changed until explicitly told otherwise } /** * If this instance has not been used since * it was loaded from cache, check which * stored values are still relevant. */ private void checkCachedValues() { // If it is not a cached entry then simply return if (isFlags(FLAG_CACHED_VALUES_CHECKED)) { return; } if (logger.isTraceEnabled()) logger.trace("Check Cached data for " + getPath()); if (! isFlags(FLAG_EXISTS_CHECKED)) { setFlags(super.exists(), FLAG_EXISTS); setFlags(true, FLAG_EXISTS_CHECKED); if (! isFlags (FLAG_EXISTS)) { // Cached entry for non-existent file needs resetting if (logger.isTraceEnabled()) logger.trace("File does not exist - reset to defaults"); resetCached(); return; } } if (! isFlags(FLAG_LENGTH_CHECKED)) { long l = super.length(); if (privateLength != l) { if (logger.isTraceEnabled()) logger.trace("privateLength not matched - CRC needs recalculating"); clearCachedCrc(); } privateLength = l; } if (! isFlags(FLAG_MODIFIED_CHECKED)) { long d = super.lastModified(); if (privateLastModified != d) { if (logger.isTraceEnabled()) logger.trace("date not matched - CRC needs recalculating"); clearCachedCrc(); } privateLastModified = d; } if (!isFlags(FLAG_CRC_CALCED) && privateCrc != CRC_NOT_SET) { if (logger.isTraceEnabled()) logger.trace("CRC assumed valid"); setFlags(true, FLAG_CRC_CALCED); } if ( ! isFlags(FLAG_IS_DIRECTORY_CHECKED)) { setFlags(super.isDirectory(), FLAG_IS_DIRECTORY); } setFlags(true, FLAG_MODIFIED_CHECKED + FLAG_LENGTH_CHECKED + FLAG_IS_DIRECTORY_CHECKED + FLAG_CACHED_VALUES_CHECKED); } /** * * Set the privateExists cached values. * * CAUTION: Set with great care as this will stop an actual check * againstthe underlying file system. Should only be used * after successfully copying a file * @param exists * @param lastModified * @param length * @param crc * @param isDirectory */ public void setCachedValues(boolean exists, long lastModified, long length, long crc, boolean isDirectory) { setFlags(exists, FLAG_EXISTS); privateLastModified = lastModified; privateLength = length; privateCrc = crc; setFlags((crc != -1), FLAG_CRC_CALCED); setFlags(isDirectory, FLAG_IS_DIRECTORY); setFlags(true, FLAG_EXISTS_CHECKED + FLAG_LENGTH_CHECKED + FLAG_MODIFIED_CHECKED + FLAG_IS_DIRECTORY_CHECKED + FLAG_CACHED_VALUES_CHECKED); } /** * @return privateExists status, using cached value if already known */ @Override public boolean exists() { checkCachedValues(); return isFlags(FLAG_EXISTS); } /** * Check modified date of the cached file object * Use cached value if known, otherwise get real value * * @return last modified sate */ @Override public long lastModified() { checkCachedValues(); return privateLastModified; } /** * Get the Length of the file * Use cached value if known, otherwise get real value and cache it * * @return File privateLength */ @Override public long length() { checkCachedValues(); return privateLength; } @Override public boolean isDirectory() { checkCachedValues(); return isFlags(FLAG_IS_DIRECTORY); } @Override public boolean delete() { resetCached(); return super.delete(); } /** * Get the CRC for the given file. * * If already known the cached value is returned, * and if not then it is calculated (and then cached) * * Note that Adler32 is used as it is significantly faster than CRC32 * Apparently there is a slight risk of a false match. but for this * purpose we can live with that, and the chance is anyway very small. * * @return The CRC value */ public long getCrc() { checkCachedValues(); if (! isFlags(FLAG_CRC_CALCED)) { // See i conditions for used cached value are met if ((privateCrc != CRC_NOT_SET) && isFlags(FLAG_LENGTH_CHECKED + FLAG_MODIFIED_CHECKED)) { setFlags(true, FLAG_CRC_CALCED); } else { // Calculate the CRC for this file. CheckedInputStream cis = null; try { cis = new CheckedInputStream(new FileInputStream(super.getPath()), new Adler32()); byte[] buf = new byte[128]; while (cis.read(buf) >= 0) { } privateCrc = cis.getChecksum().getValue(); } catch (IOException e) { logger.error("ERROR: Failed trying to calculate CRC for " + super.getAbsolutePath() + "\n" + e.toString()); clearCachedCrc(); } finally { setFlags((privateCrc != CRC_NOT_SET), FLAG_CRC_CALCED); } // Close file ignoring any errors try { if (cis != null) cis.close(); } catch (IOException e) { // Do nothing } if (logger.isTraceEnabled()) logger.trace("calcCrc=" + isFlags(FLAG_CRC_CALCED) + " (privateCrc=" + privateCrc + "): " + getAbsolutePath()); } } if (logger.isTraceEnabled()) logger.trace("getCrc=" + privateCrc + ": " + getAbsolutePath()); return privateCrc; } /** * Find out if privateCrc value known(and not simply cached). * Can be used to find out if the privateCrc can be retrieved * via getCrc() without forcing it be calculated * * @return True if yes, false if not. */ public boolean isCrc() { return isFlags(FLAG_CACHED_VALUES_CHECKED & FLAG_CRC_CALCED); } /** * Clear any cached CRC value to invalidate it */ private void clearCachedCrc() { privateCrc = CRC_NOT_SET; setFlags(false, FLAG_CRC_CALCED); } /** * Set to status for whether file is on target rather than source * * @param b True if it is, false otherwise */ public void setTarget(boolean b) { setFlags(b, FLAG_TARGET_FILE); if (logger.isTraceEnabled()) logger.trace("setTarget(" + isFlags(FLAG_TARGET_FILE) + "): " + getAbsolutePath()); } /** * Indicate whether entry is cached data or file system data * * @return true if cached data */ public boolean isCachedValidated() { if (logger.isTraceEnabled()) logger.trace("isCachedValidated=" + isFlags(FLAG_CACHED_VALUES_CHECKED) + ": " + getAbsolutePath()); return isFlags (FLAG_CACHED_VALUES_CHECKED); } public void clearCacheValidated() { setFlags(false, FLAG_EXISTS_CHECKED + FLAG_LENGTH_CHECKED + FLAG_MODIFIED_CHECKED + FLAG_CACHED_VALUES_CHECKED); } /** * Return status to say whether file has changed since last run * of calibre2opds. For backwards compatibility default is true. * * This method is intended to support a future optimisation where * we try and calculate whether pages have chnged without actually * generating them. * * @return */ public boolean isChanged() { return isFlags(FLAG_IS_CHANGED); } /** * Set to status to say whether file has changed since last run * of calibre2opds. For backwards compatibility default is true. * * This method is intended to support a future optimisation where * we try and calculate whether pages have chnged without actually * generating them. * * @param b True if it is, false otherwise */ public void setChanged (boolean b) { setFlags(b, FLAG_IS_CHANGED); if (logger.isTraceEnabled()) logger.trace("setChanged(" + isFlags(FLAG_IS_CHANGED) + "): " + getAbsolutePath()); } }