package com.limegroup.gnutella;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.HashSet;
import java.util.List;
import java.util.ArrayList;
import com.limegroup.gnutella.util.CommonUtils;
import com.limegroup.gnutella.util.ConverterObjectInputStream;
import com.limegroup.gnutella.util.IOUtils;
import com.limegroup.gnutella.util.ProcessingQueue;
import org.apache.commons.logging.LogFactory;
import org.apache.commons.logging.Log;
/**
* This class contains a systemwide URN cache that persists file URNs (hashes)
* across sessions.
*
* Modified by Gordon Mohr (2002/02/19): Added URN storage, calculation, caching
* Repackaged by Greg Bildson (2002/02/19): Moved to dedicated class.
*
* @see URN
*/
public final class UrnCache {
private static final Log LOG = LogFactory.getLog(UrnCache.class);
/**
* File where urns (currently SHA1 urns) for files are stored.
*/
private static final File URN_CACHE_FILE =
new File(CommonUtils.getUserSettingsDir(), "fileurns.cache");
/**
* Last good version of above.
*/
private static final File URN_CACHE_BACKUP_FILE =
new File(CommonUtils.getUserSettingsDir(), "fileurns.bak");
/**
* UrnCache instance variable. LOCKING: obtain UrnCache.class.
*/
private static UrnCache instance = null;
/**
* UrnCache container. LOCKING: obtain this. Although URN_MAP is static,
* UrnCache is a singleton, so obtaining UrnCache's monitor is sufficient--
* and slightly more convenient.
*/
private static final Map /* UrnSetKey -> HashSet */ URN_MAP = createMap();
/**
* The ProcessingQueue that Files are hashed in.
*/
private final ProcessingQueue QUEUE = new ProcessingQueue("Hasher");
/**
* The set of files that are pending hashing to the callbacks that are listening to them.
*/
private Map /* File -> List (of UrnCallback) */ pendingHashing = new HashMap();
/**
* Whether or not data is dirty since the last time we saved.
*/
private boolean dirty = false;
/**
* Returns the <tt>UrnCache</tt> instance.
*
* @return the <tt>UrnCache</tt> instance
*/
public static synchronized UrnCache instance() {
if (instance == null)
instance = new UrnCache();
return instance;
}
/**
* Create and initialize urn cache.
*/
private UrnCache() {
dirty = removeOldEntries(URN_MAP);
}
/**
* Calculates the given File's URN and caches it. The callback will
* be notified of the URNs. If they're already calculated, the callback
* will be notified immediately. Otherwise, it will be notified when hashing
* completes, fails, or is interrupted.
*/
public synchronized void calculateAndCacheUrns(File file, UrnCallback callback) {
Set urns = getUrns(file);
// TODO: If we ever create more URN types (other than SHA1)
// we cannot just check for size == 0, we must check for
// size == NUM_URNS_WE_WANT, and calculateUrns should only
// calculate the URN for the specific hash we still need.
if(!urns.isEmpty()) {
callback.urnsCalculated(file, urns);
} else {
if(LOG.isDebugEnabled())
LOG.debug("Adding: " + file + " to be hashed.");
List list = (List)pendingHashing.get(file);
if(list == null) {
list = new ArrayList(1);
pendingHashing.put(file, list);
}
list.add(callback);
QUEUE.add(new Processor(file));
}
}
/**
* Clears all callbacks that are owned by the given owner.
*/
public synchronized void clearPendingHashes(Object owner) {
if(LOG.isDebugEnabled())
LOG.debug("Clearing all pending hashes owned by: " + owner);
for(Iterator i = pendingHashing.entrySet().iterator(); i.hasNext(); ) {
Map.Entry next = (Map.Entry)i.next();
File f = (File)next.getKey();
List callbacks = (List)next.getValue();
for(int j = callbacks.size() - 1; j >= 0; j--) {
UrnCallback c = (UrnCallback)callbacks.get(j);
if(c.isOwner(owner))
callbacks.remove(j);
}
// if there's no more callbacks for this file, remove it.
if(callbacks.isEmpty())
i.remove();
}
}
/**
* Clears all callbacks for the given file that are owned by the given owner.
*/
public synchronized void clearPendingHashesFor(File file, Object owner) {
if(LOG.isDebugEnabled())
LOG.debug("Clearing all pending hashes for: " + file + ", owned by: " + owner);
List callbacks = (List)pendingHashing.get(file);
if(callbacks != null) {
for(int j = callbacks.size() - 1; j >= 0; j--) {
UrnCallback c = (UrnCallback)callbacks.get(j);
if(c.isOwner(owner))
callbacks.remove(j);
}
if(callbacks.isEmpty())
pendingHashing.remove(file);
}
}
/**
* Adds any URNs that can be locally calculated; may take a while to
* complete on large files. After calculation, the items are added
* for future remembering.
*
* @param file the <tt>File</tt> instance to calculate URNs for
* @return the new <tt>Set</tt> of calculated <tt>URN</tt> instances. If
* the calling thread is interrupted while executing this, returns an empty
* set.
*/
public Set calculateUrns(File file) throws IOException, InterruptedException {
Set set = new HashSet(1);
set.add(URN.createSHA1Urn(file));
return set;
}
/**
* Find any URNs remembered from a previous session for the specified
* <tt>File</tt> instance. The returned <tt>Set</tt> is
* guaranteed to be non-null, but it may be empty.
*
* @param file the <tt>File</tt> instance to look up URNs for
* @return a new <tt>Set</tt> containing any cached URNs for the
* speficied <tt>File</tt> instance, guaranteed to be non-null and
* unmodifiable, but possibly empty
*/
public synchronized Set getUrns(File file) {
// don't trust failed mod times
if (file.lastModified() == 0L)
return Collections.EMPTY_SET;
UrnSetKey key = new UrnSetKey(file);
// one or more "urn:" names for this file
Set cachedUrns = (Set)URN_MAP.get(key);
if(cachedUrns == null)
return Collections.EMPTY_SET;
return cachedUrns;
}
/**
* Removes any URNs that associated with a specified file.
*/
public synchronized void removeUrns(File f) {
UrnSetKey k = new UrnSetKey(f);
URN_MAP.remove(k);
dirty = true;
}
/**
* Add URNs for the specified <tt>FileDesc</tt> instance to URN_MAP.
*
* @param file the <tt>File</tt> instance containing URNs to store
*/
public synchronized void addUrns(File file, Set urns) {
UrnSetKey key = new UrnSetKey(file);
URN_MAP.put(key, Collections.unmodifiableSet(urns));
dirty = true;
}
/**
* Loads values from cache file, if available. If the cache file is
* not readable, tries the backup.
*/
private static Map createMap() {
Map result;
result = readMap(URN_CACHE_FILE);
if(result == null)
result = readMap(URN_CACHE_BACKUP_FILE);
if(result == null)
result = new HashMap();
return result;
}
/**
* Loads values from cache file, if available.
*/
private static Map readMap(File file) {
Map result;
ObjectInputStream ois = null;
try {
ois = new ConverterObjectInputStream(
new BufferedInputStream(
new FileInputStream(file)));
result = (Map)ois.readObject();
} catch(Throwable t) {
LOG.error("Unable to read UrnCache", t);
result = null;
} finally {
if(ois != null) {
try {
ois.close();
} catch(IOException e) {
// all we can do is try to close it
}
}
}
return result;
}
/**
* Removes any stale entries from the map so that they will automatically
* be replaced.
*
* @param map the <tt>Map</tt> to check
*/
private static boolean removeOldEntries(Map map) {
// discard outdated info
boolean dirty = false;
Iterator iter = map.keySet().iterator();
while (iter.hasNext()) {
Object next = iter.next();
if(next instanceof UrnSetKey) {
UrnSetKey key = (UrnSetKey)next;
if(key == null) continue;
// check to see if file still exists unmodified
File f = new File(key._path);
if (!f.exists() || f.lastModified() != key._modTime) {
dirty = true;
iter.remove();
}
} else {
dirty = true;
iter.remove();
}
}
return dirty;
}
/**
* Write cache so that we only have to calculate them once.
*/
public synchronized void persistCache() {
if(!dirty)
return;
//It's not ideal to hold a lock while writing to disk, but I doubt think
//it's a problem in practice.
URN_CACHE_FILE.renameTo(URN_CACHE_BACKUP_FILE);
ObjectOutputStream oos = null;
try {
oos = new ObjectOutputStream(
new BufferedOutputStream(new FileOutputStream(URN_CACHE_FILE)));
oos.writeObject(URN_MAP);
oos.flush();
} catch (IOException e) {
ErrorService.error(e);
} finally {
IOUtils.close(oos);
}
dirty = false;
}
private class Processor implements Runnable {
private final File file;
Processor(File f) {
file = f;
}
public void run() {
Set urns;
List callbacks;
synchronized(UrnCache.this) {
callbacks = (List)pendingHashing.remove(file);
urns = getUrns(file); // already calculated?
}
// If there was atleast one callback listening, try and send it out
// (which may involve calculating it).
if(callbacks != null && !callbacks.isEmpty()) {
// If not calculated, calculate OUTSIDE OF LOCK.
if(urns.isEmpty()) {
if(LOG.isDebugEnabled())
LOG.debug("Hashing file: " + file);
try {
urns = calculateUrns(file);
addUrns(file, urns);
} catch(IOException ignored) {
LOG.warn("Unable to calculate URNs", ignored);
} catch(InterruptedException ignored) {
LOG.warn("Unable to calculate URNs", ignored);
}
}
// Note that because we already removed this list from the Map,
// we don't need to synchronize while iterating over it, because
// nothing else can modify it now.
for(int i = 0; i < callbacks.size(); i++)
((UrnCallback)callbacks.get(i)).urnsCalculated(file, urns);
}
}
}
/**
* Private class for the key for the set of URNs for files.
*/
private static class UrnSetKey implements Serializable {
static final long serialVersionUID = -7183232365833531645L;
/**
* Constant for the file modification time.
* @serial
*/
transient long _modTime;
/**
* Constant for the file path.
* @serial
*/
transient String _path;
/**
* Constant cached hash code, since this class is used exclusively
* as a hash key.
* @serial
*/
transient int _hashCode;
/**
* Constructs a new <tt>UrnSetKey</tt> instance from the specified
* <tt>File</tt> instance.
*
* @param file the <tt>File</tt> instance to use in constructing the
* key
*/
UrnSetKey(File file) {
_modTime = file.lastModified();
_path = file.getAbsolutePath();
_hashCode = calculateHashCode();
}
/**
* Helper method to calculate the hash code.
*
* @return the hash code for this instance
*/
int calculateHashCode() {
int result = 17;
result = result*37 + (int)(_modTime ^(_modTime >>> 32));
result = result*37 + _path.hashCode();
return result;
}
/**
* Overrides Object.equals so that keys with equal paths and modification
* times will be considered equal.
*
* @param o the <tt>Object</tt> instance to compare for equality
* @return <tt>true</tt> if the specified object is the same instance
* as this object, or if it has the same modification time and the same
* path, otherwise returns <tt>false</tt>
*/
public boolean equals(Object o) {
if(this == o) return true;
if(!(o instanceof UrnSetKey)) return false;
UrnSetKey key = (UrnSetKey)o;
// note that the path is guaranteed to be non-null
return ((_modTime == key._modTime) &&
(_path.equals(key._path)));
}
/**
* Overrides Object.hashCode to meet the specification of Object.equals
* and to make this class functions properly as a hash key.
*
* @return the hash code for this instance
*/
public int hashCode() {
return _hashCode;
}
/**
* Serializes this instance.
*
* @serialData the modification time followed by the file path
*/
private void writeObject(ObjectOutputStream s)
throws IOException {
s.defaultWriteObject();
s.writeLong(_modTime);
s.writeObject(_path);
}
/**
* Deserializes this instance, restoring all invariants.
*/
private void readObject(ObjectInputStream s)
throws IOException, ClassNotFoundException {
s.defaultReadObject();
_modTime = s.readLong();
_path = (String)s.readObject();
_hashCode = calculateHashCode();
}
}
}