// Copyright (c) 1999 Dustin Sallings <dustin@spy.net> package net.spy.cache; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.Serializable; import java.lang.ref.SoftReference; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.AbstractMap; import java.util.HashSet; import java.util.Iterator; import java.util.Map; import java.util.Set; import net.spy.log.Logger; import net.spy.log.LoggerFactory; import net.spy.util.CloseUtil; import net.spy.util.SpyUtil; /** * Simple local disk caching. * * This is used for terribly simple caches with no expiration dates on * objects. Things go in and they stay in. */ public class DiskCache extends AbstractMap<Serializable, Serializable> { // Base directory for hashing final String basedir; private final LRUCache<Serializable, SoftReference<Serializable>> lruCache; private static final int DEFAULT_LRU_CACHE_SIZE=100; private transient Logger logger = null; /** * Get an DiskObject using the given directory. */ public DiskCache(String base) { this(base, DEFAULT_LRU_CACHE_SIZE); } /** * Get a DiskCache using the given directory with a backing LRU cache * of the specified size. * * @param base the base directory for the disk cache * @param lruCacheSize the size of the LRU cache holding recently accessed * objects */ public DiskCache(String base, int lruCacheSize) { super(); this.basedir=base; lruCache=new LRUCache<Serializable, SoftReference<Serializable>>(lruCacheSize); } /** * Get the base directory which this cache is watching. */ public String getBaseDir() { return(basedir); } // Get the path of an object for the given key private String getPath(Object key) { MessageDigest md=null; try { md=MessageDigest.getInstance("SHA"); } catch(NoSuchAlgorithmException e) { throw new AssertionError("There's no SHA?"); } if(key instanceof String) { String k=(String)key; md.update(k.getBytes()); } else { String tmpkey=key.getClass().getName() + key.hashCode(); md.update(tmpkey.getBytes()); } String hashed=SpyUtil.byteAToHexString(md.digest()); String base=basedir+"/"+hashed.substring(0,2); String path=base+"/"+hashed; File f=new File(base); if(!f.isDirectory()) { f.mkdirs(); } return(path); } /** * Store an object in the cache. * * @param k object key * @param v value * @return the old object stored in that location (if any) */ @Override public Serializable put(Serializable k, Serializable v) { Serializable rv=get(k); String pathto=getPath(k); FileOutputStream ostream=null; ObjectOutputStream p=null; try { ostream = new FileOutputStream(pathto); p = new ObjectOutputStream(ostream); p.writeObject(k); p.writeObject(v); p.flush(); } catch(IOException e) { throw new RuntimeException("Error storing object", e); } finally { CloseUtil.close(p); CloseUtil.close(ostream); } return(rv); } /** * Get an object from the cache. * * @return the object, or null if there's no such object */ @Override public Serializable get(Object key) { Serializable rv=null; if(key==null) { throw new NullPointerException("Name not provided"); } rv=(Serializable) lruCache.get(key); if(rv==null) { rv=(Serializable) getFromDiskCache(key); lruCache.put((Serializable) key, new SoftReference<Serializable>(rv)); } return(rv); } private Logger getLogger() { if(logger==null) { logger=LoggerFactory.getLogger(getClass()); } return(logger); } private Object getFromDiskCache(Object key) { Object rv=null; FileInputStream istream=null; ObjectInputStream p=null; try { istream = new FileInputStream(getPath(key)); p = new ObjectInputStream(istream); Object storedKey = p.readObject(); Object o = p.readObject(); if(!key.equals(storedKey)) { throw new Exception("Key value did not match (" + storedKey + " != " + key + ")"); } rv=o; } catch(FileNotFoundException e) { if(getLogger().isDebugEnabled()) { getLogger().debug("File not found loading disk cache", e); } } catch(Exception e) { getLogger().warn("Error getting ``%s'' from disk cache", key, e); } finally { CloseUtil.close(p); CloseUtil.close(istream); } return(rv); } @Override public Set<Map.Entry<Serializable, Serializable>> entrySet() { Set<Map.Entry<Serializable, Serializable>> rv=null; try { rv=new WalkerDiskCacheRanger(); } catch(ClassNotFoundException e) { throw new RuntimeException("Error getting set", e); } catch(IOException e) { throw new RuntimeException("Error getting set", e); } return(rv); } private class WalkerDiskCacheRanger extends HashSet<Map.Entry<Serializable, Serializable>> { public WalkerDiskCacheRanger() throws IOException, ClassNotFoundException { super(); init(new File(basedir)); } private void init(File f) throws IOException, ClassNotFoundException { if(f.isDirectory()) { // If it's a directory, recurse File[] stuff=f.listFiles(); for(int i=0; i<stuff.length; i++) { init(stuff[i]); } } else { // Regular file, open it and read it FileInputStream istream = null; ObjectInputStream p = null; Serializable key=null; try { istream = new FileInputStream(f); p = new ObjectInputStream(istream); key=(Serializable) p.readObject(); } finally { CloseUtil.close(p); CloseUtil.close(istream); } // Add the new entry. add(new E(key, f)); } } /** * Get an iterator. */ @Override public Iterator<Entry<Serializable, Serializable>> iterator() { return(new I(super.iterator())); } } // Iterator implementation private static class I extends Object implements Iterator<Entry<Serializable, Serializable>> { private final Iterator<Map.Entry<Serializable, Serializable>> i; private E current=null; private boolean begun=false; // Instatiate the iterator over the default iterator implementation public I(Iterator<Map.Entry<Serializable, Serializable>> it) { super(); i=it; } /** * Get the next object. */ public E next() { begun=true; current=(E) i.next(); return(current); } /** * True if there's another entry. */ public boolean hasNext() { return(i.hasNext()); } /** * Remove the given object. */ public void remove() { if(begun==false) { throw new IllegalStateException("Have not yet begun walking."); } File f=current.getPath(); f.delete(); i.remove(); } } // Map entry implementation private static class E extends Object implements Map.Entry<Serializable, Serializable> { final File path; final Serializable k; public E(Serializable key, File p) { super(); k=key; path=p; } /** * True if two objects are equal. */ @Override public boolean equals(Object o) { boolean rv=false; if(o instanceof E) { E e=(E)o; rv=e.k.equals(k); } return(rv); } /** * Get the path to the file. */ public File getPath() { return(path); } /** * Get the key. */ public Serializable getKey() { return(k); } /** * Get the hash code. */ @Override public int hashCode() { return(k.hashCode()); } /** * Get the value. */ public Serializable getValue() { Serializable rv=null; FileInputStream istream=null; ObjectInputStream p=null; try { istream = new FileInputStream(path); p = new ObjectInputStream(istream); Object key=p.readObject(); assert(key.equals(k)); rv=(Serializable) p.readObject(); } catch(IOException e) { throw new RuntimeException("Error getting object",e); } catch(ClassNotFoundException e) { throw new RuntimeException("Error getting object",e); } finally { CloseUtil.close(p); CloseUtil.close(istream); } return(rv); } /** * Not implemented. */ public Serializable setValue(Serializable o) { throw new UnsupportedOperationException("Can't set here."); } } }