FileBasedMailbox overhaul
authoradam <adam@megacz.com>
Fri, 22 Oct 2004 06:52:45 +0000 (06:52 +0000)
committeradam <adam@megacz.com>
Fri, 22 Oct 2004 06:52:45 +0000 (06:52 +0000)
darcs-hash:20041022065245-5007d-1bf708691025151a3263877c7312c518f8938eb2.gz

src/org/ibex/mail/target/FileBasedMailbox.java

index ae42112..50cce12 100644 (file)
@@ -10,22 +10,21 @@ import java.net.*;
 import java.util.*;
 import java.text.*;
 
-// FIXME: we can omit UIDNEXT!
-// FIXME use directory date/time as UIDNEXT and file date/time as UID; need to 'correct' file date/time after changes
-
 /** An exceptionally crude implementation of Mailbox relying on POSIXy filesystem semantics */
-public class FileBasedMailbox extends Mailbox.Default implements Serializable {
+public class FileBasedMailbox extends Mailbox.Default {
 
-    public String toString() { return "[FileBasedMailbox " + path + "]"; }
+    public String toString() { return "[FileBasedMailbox " + path.getAbsolutePath() + "]"; }
     private static final char slash = File.separatorChar;
     private static final WeakHashMap<String,FileBasedMailbox> instances = new WeakHashMap<String,FileBasedMailbox>();
-    public Mailbox slash(String name, boolean create) { return getFileBasedMailbox(path + slash + name, create); }
+    public Mailbox slash(String name, boolean create) { return getFileBasedMailbox(path.getAbsolutePath()+slash+name, create); }
+
+    // FIXME: should be a File()
     public static synchronized FileBasedMailbox getFileBasedMailbox(String path, boolean create) {
         try {
             FileBasedMailbox ret = instances.get(path);
             if (ret == null) {
-                if (!create && !new File(path).exists()) return null;
-                instances.put(path, ret = new FileBasedMailbox(path));
+                if (!create && !(new File(path).exists())) return null;
+                instances.put(path, ret = new FileBasedMailbox(new File(path)));
             }
             return ret;
         } catch (Exception e) {
@@ -37,103 +36,145 @@ public class FileBasedMailbox extends Mailbox.Default implements Serializable {
 
     // Instance //////////////////////////////////////////////////////////////////////////////
 
-    private String path;
-    private transient FileLock lock;
-    private transient Prevayler prevayler;
-    private transient ArrayList<CacheEntry> cache;
-    private transient int uidValidity;
-    private transient int uidNext;
-
-    public static class CacheEntry implements Serializable {
-        public MIME.Headers headers;
-        public String path;
-        public boolean seen;
-        private String name() { return path.indexOf(slash) == -1 ? path : path.substring(path.lastIndexOf(slash)+1); }
-        public boolean seen() { return name().substring(name().indexOf('.')+1).indexOf('s') != -1; }
-        public int uid() { return Integer.parseInt(name().substring(0, name().indexOf('.'))); }
-        public synchronized void seen(boolean seen) {
-            String newpath = path.substring(0, path.lastIndexOf('.') + 1) + (seen ? "s" : "");
-            new File(path).renameTo(new File(newpath));
-            path = newpath;
+    private File path;
+    private FileLock lock;
+    private Prevayler prevayler;
+    private Cache cache;
+
+    public static class Cache implements Serializable {
+        public            final int uidValidity = new Random().nextInt();
+        private           final Hashtable<String,Entry> byname = new Hashtable<String,Entry>();
+        private transient final Hashtable<Integer,Entry> byuid = new Hashtable<Integer,Entry>();
+        public            final File dir;
+        private                 int uidNext = 0;
+
+        public Cache(File dir) { this.dir = dir; }
+
+        public void init(Prevayler prevayler) throws IOException {
+            dir.mkdirs();
+            Log.info(this, "initializing maildir " + dir.getAbsolutePath());
+            boolean invalid = false;
+            for(String s : byname.keySet()) create(prevayler, s);
+            for(String file : dir.list())
+                if (file.charAt(0)!='.' && !(new File(dir.getAbsolutePath() + slash + file).isDirectory()))
+                    create(prevayler, file);
+            Log.info(this, "  done initializing maildir " + dir.getAbsolutePath());
+            prevayler.takeSnapshot();
         }
-        public CacheEntry(File f) throws IOException {
-            path = f.getAbsolutePath();
-            headers = new MIME.Headers(new Stream(new FileInputStream(f)), true);
+
+        public synchronized void create(Prevayler prevayler, String name) throws IOException {
+            if (get(name) != null) return;
+            new Entry(this, prevayler, name);
         }
-    }        
-
-    private FileBasedMailbox(String path) throws MailException, IOException, ClassNotFoundException {
-        new File(this.path = path).mkdirs();
-        new File(path + slash + ".cache").mkdirs();
-        File uidValidityFile = new File(path + slash + ".uidValidity");
-        if (!uidValidityFile.exists()) uidValidityFile.createNewFile();
-        uidValidity = (int)(uidValidityFile.lastModified() & 0xffffffff);
-        lock = new RandomAccessFile(this.path + slash + ".cache" + slash + "lock", "rw").getChannel().tryLock();
-        if (lock == null) throw new IOException("unable to lock FileBasedMailbox");
-        prevayler = PrevaylerFactory.createPrevayler(new ArrayList<CacheEntry>(), path + slash + ".cache");
-        cache = (ArrayList<CacheEntry>)prevayler.prevalentSystem();
-        for(int i=0; i<cache.size(); i++) if (cache.get(i) != null) uidNext = Math.max(uidNext, cache.get(i).uid()+1);
-        if (cache.size() == 0) {
-            Log.warn(this, "rebuilding cache for " + path);
-            String[] files = new File(path).list(new FilenameFilter() {
-                    public boolean accept(File f, String s) {
-                        return s.substring(1).indexOf('.') != -1 && !s.equals("..");
-                    } });
-            long last = System.currentTimeMillis();
-            CacheEntry[] entries = new CacheEntry[files.length];
-            for(int i=0; i<files.length; i++) {
-                if (System.currentTimeMillis() - last > 1000 * 1) {
-                    Log.warn(this, "rebuilding cache for " + path + ": " + Math.ceil((((float)i)/((float)files.length)) * 100)+ "%");
-                    last = System.currentTimeMillis();
-                }
-                CacheEntry ce = new CacheEntry(new File(path + slash + files[i]));
-                entries[i] = ce;
-                uidNext = Math.max(uidNext, ce.uid()+1);
+
+        public synchronized int size() { return byname.size(); }
+        public synchronized int uidNext(boolean increment) { return increment ? uidNext++ : uidNext; }
+        public synchronized Entry get(int uid) { return byuid.get(uid); }
+        public synchronized Entry get(String name) { return byname.get(name); }
+
+        public static class Entry implements Serializable {
+            public final MIME.Headers headers;
+            public final String name;
+            private int uid;
+            private boolean seen;
+            
+            public Entry(Cache cache, Prevayler prevayler, String name) throws IOException {
+                seen = name.endsWith(".s");
+                this.name = seen ? name.substring(0, name.length() - 2) : name;
+                headers = new MIME.Headers(new Stream(new FileInputStream(cache.dir.getAbsolutePath()+slash+name)), true);
+                prevayler.execute(new Transaction() {
+                        public void executeOn(Object o, Date now) {
+                            Cache cache = (Cache)o;
+                            synchronized(cache) {
+                                Entry.this.uid = cache.uidNext(true);
+                                cache.byuid.put(Entry.this.uid, Entry.this);
+                                cache.byname.put(Entry.this.name, Entry.this);
+                            } } });
+            }
+
+            public int uid() { return uid; }
+            public boolean seen() { return seen; }
+            public void seen(Cache cache, boolean seen) {
+                String base = cache.dir.getAbsolutePath() + slash + name;
+                File target = new File(base + (seen?".s":""));
+                if (target.exists()) return;
+                new File(base + (seen?"":".s")).renameTo(target);
+            }
+            public void delete(Cache cache) {
+                String base = cache.dir.getAbsolutePath() + slash + name;
+                File target = new File(base);
+                if (!target.exists()) target = new File(base + ".s");
+                if (target.exists()) target.delete();
+            }
+            public Message message(Cache cache) { try {
+                String base = cache.dir.getAbsolutePath() + slash + name;
+                File target = new File(base);
+                if (!target.exists()) target = new File(base + ".s");
+                FileInputStream fis = null;
+                try {
+                    fis = new FileInputStream(target);
+                    return new Message(new Stream(fis), new Message.Envelope(null, null, new Date(target.lastModified())));
+                } finally { if (fis != null) fis.close(); }
+            } catch (IOException e) { throw new MailException.IOException(e);
+            } catch (Message.Malformed e) { throw new MailException(e.getMessage()); }
             }
-            prevayler.execute(new Rebuild(entries));
         }
-        Log.warn(this, "taking snapshot for " + path);
-        prevayler.takeSnapshot();
     }
 
-    private static class Rebuild implements Transaction {
-        public final CacheEntry[] entries;
-        public Rebuild(CacheEntry entry) { this.entries = new CacheEntry[] { entry }; }
-        public Rebuild(CacheEntry[] entries) { this.entries = entries; }
-        public void executeOn(Object c, Date now) {
-            synchronized(c) {
-                for(int i=0; i<entries.length; i++) {
-                    ((ArrayList<CacheEntry>)c).add(entries[i]);
-                }
-            } }
+    private static void rmDashRf(File f) throws IOException {
+        if (!f.isDirectory()) { f.delete(); return; }
+        String[] children = f.list();
+        for(int i=0; i<children.length; i++) rmDashRf(new File(f.getAbsolutePath() + slash + children[i]));
+        f.delete();
     }
 
-    public Mailbox.Iterator  iterator()           { return new Iterator(); }
+    private FileBasedMailbox(File path) throws MailException, IOException, ClassNotFoundException {
+        this.path = path;
+        path.mkdirs();
 
-    public int               uidValidity()        { return uidValidity; }
+        // acquire lock
+        lock = new RandomAccessFile(this.path.getAbsolutePath() + slash + ".lock", "rw").getChannel().tryLock();
+        if (lock == null) throw new IOException("unable to lock FileBasedMailbox");
+
+        File cacheDir = new File(path.getAbsolutePath() + slash + ".cache");
+        try {
+            prevayler = PrevaylerFactory.createPrevayler(new Cache(cacheDir), cacheDir.getAbsolutePath());
+            cache = (Cache)prevayler.prevalentSystem();
+        } catch (Throwable t) {
+            Log.warn(this, "error while attempting to reconstitute a FileBasedMailbox.cache:");
+            Log.warn(this, t);
+            Log.warn(this, "discarding cache...");
+            rmDashRf(cacheDir);
+            prevayler = PrevaylerFactory.createPrevayler(new Cache(cacheDir), cacheDir.getAbsolutePath());
+            cache = (Cache)prevayler.prevalentSystem();
+        }
+        cache.init(prevayler);
+    }
 
-    public int               uidNext()            { return uidNext(false); }
-    public int               uidNext(boolean inc) { return inc ? uidNext++ : uidNext; }
-    public synchronized void add(Message message) { add(message, Mailbox.Flag.RECENT); }
 
+    public Mailbox.Iterator  iterator()           { return new Iterator(); }
+    public int               uidValidity()        { return cache.uidValidity; }
+    public synchronized void add(Message message) { add(message, Mailbox.Flag.RECENT); }
     public String[] children() {
         Vec vec = new Vec();
-        String[] list = new File(path).list();
+        String[] list = path.list();
         for(int i=0; i<list.length; i++) {
-            File f = new File(path + slash + list[i]);
-            if (f.isDirectory() && f.getName().charAt(0) != '.')
-                vec.addElement(list[i]);
+            File f = new File(path.getAbsolutePath() + slash + list[i]);
+            if (f.isDirectory() && f.getName().charAt(0) != '.') vec.addElement(list[i]);
         }
         return (String[])vec.copyInto(new String[vec.size()]);
     }
 
+    public int uidNext() { return cache.uidNext(false); }
     public synchronized void add(Message message, int flags) {
         Log.info(path, message.summary());
         try {
-           final String name = path + slash + uidNext(true) + "." +
+           final String name = path.getAbsolutePath() + slash + cache.uidNext(true) + "." +
                 ((flags & Mailbox.Flag.SEEN) == Mailbox.Flag.SEEN ? "s" : "");
             File target = new File(name);
             File f = new File(target.getCanonicalPath() + "-");
+            if (f.exists() || target.exists())
+                throw new RuntimeException("aieeee!!!! target of add() already exists: " + target.getAbsolutePath());
             FileOutputStream fo = new FileOutputStream(f);
             Stream stream = new Stream(fo);
             if (message.envelope != null) {
@@ -143,49 +184,33 @@ public class FileBasedMailbox extends Mailbox.Default implements Serializable {
             message.dump(stream);
             fo.close();
             f.renameTo(new File(name));
-            CacheEntry entry = new CacheEntry(new File(name));
-            prevayler.execute(new Rebuild(entry));
+            new Cache.Entry(cache, prevayler, name);
         } catch (IOException e) { throw new MailException.IOException(e); }
     }
 
     private class Iterator extends Mailbox.Default.Iterator implements Serializable {
         int cur = -1;
-        private CacheEntry entry() { synchronized(cache) { return cache.get(cur); } }
+        private Cache.Entry entry() { return cache.get(cur); }
         public MIME.Headers head() { return done() ? null : entry().headers; }
-        public boolean done() { synchronized(cache) { if (cur >= cache.size()) return true; } return false; }
+        public boolean done() { return cur >= cache.size(); }
         public boolean next() { cur++; return !done(); }
         public boolean seen() { return done() ? false : entry().seen(); }
         public int num() { return cur+1; }  // EUDORA insists that message numbers start at 1, not 0
         public int uid() { return done() ? -1 : entry().uid(); }
-        public Message cur() {
-            if (done()) return null;
-            try {
-                String where = entry().path;
-                if (!new File(where).exists()) where = where.substring(0, where.lastIndexOf('.')+1);
-                File file = new File(where);
-                FileInputStream fis = null;
-                try {
-                    fis = new FileInputStream(file);
-                    return new Message(new Stream(fis), new Message.Envelope(null, null, new Date(file.lastModified())));
-                } finally { if (fis != null) fis.close(); }
-            } catch (IOException e) { throw new MailException.IOException(e);
-            } catch (Message.Malformed e) { throw new MailException(e.getMessage()); }
-        }
-        public void seen(final boolean on) {
+        public Message cur() { return done() ? null : entry().message(cache); }
+        public void seen(final boolean seen) {
             if (done()) return;
-            final int ptr = this.cur;
+            final int cur = this.cur;
             prevayler.execute(new Transaction() {
                     public void executeOn(Object c, Date d) {
-                        ((ArrayList<CacheEntry>)c).get(ptr).seen(on); } });
+                        ((Cache)c).get(cur).seen((Cache)c, seen); } });
         }
         public void delete() {
             if (done()) return;
-            final CacheEntry entry = entry();
+            final int cur = this.cur;
             prevayler.execute(new Transaction() {
                     public void executeOn(Object c, Date d) {
-                            new File(entry.path).delete();
-                            if (cache.contains(entry)) { cache.remove(entry); }
-                    }});
+                        ((Cache)c).get(cur).delete((Cache)c); } });
        }
     }
 }