added Prevayler-based cache to FileBasedMailbox
[org.ibex.mail.git] / src / org / ibex / mail / target / FileBasedMailbox.java
1 package org.ibex.mail.target;
2 import org.prevayler.*;
3 import org.ibex.mail.*;
4 import org.ibex.util.*;
5 import org.ibex.io.*;
6 import java.io.*;
7 import java.nio.*;
8 import java.nio.channels.*;
9 import java.net.*;
10 import java.util.*;
11 import java.text.*;
12
13 // FIXME: we can omit UIDNEXT!
14 // FIXME use directory date/time as UIDNEXT and file date/time as UID; need to 'correct' file date/time after changes
15
16 /** An exceptionally crude implementation of Mailbox relying on POSIXy filesystem semantics */
17 public class FileBasedMailbox extends Mailbox.Default implements Serializable {
18
19     public String toString() { return "[FileBasedMailbox " + path + "]"; }
20     private static final char slash = File.separatorChar;
21     private static final WeakHashMap<String,FileBasedMailbox> instances = new WeakHashMap<String,FileBasedMailbox>();
22     public Mailbox slash(String name, boolean create) { return getFileBasedMailbox(path + slash + name, create); }
23     public static FileBasedMailbox getFileBasedMailbox(String path, boolean create) {
24         try {
25             FileBasedMailbox ret = instances.get(path);
26             if (ret == null) {
27                 if (!create && !new File(path).exists()) return null;
28                 instances.put(path, ret = new FileBasedMailbox(path));
29             }
30             return ret;
31         } catch (Exception e) {
32             Log.error(FileBasedMailbox.class, e);
33             return null;
34         }
35     }
36
37
38     // Instance //////////////////////////////////////////////////////////////////////////////
39
40     private String path;
41     private FileLock lock;
42     private Prevayler prevayler;
43     private LinkedList<CacheEntry> cache;
44     private int uidNext;
45
46     public static class CacheEntry implements Serializable {
47         public MIME.Headers headers;
48         public String path;
49         public boolean seen;
50         private String name() { return path.indexOf(slash) == -1 ? path : path.substring(path.lastIndexOf(slash)+1); }
51         public boolean seen() { return name().substring(name().indexOf('.')+1).indexOf('s') != -1; }
52         public int uid() { return Integer.parseInt(name().substring(0, name().indexOf('.'))); }
53         public synchronized void seen(boolean seen) {
54             String newpath = path.substring(0, path.lastIndexOf('.') + 1) + (seen ? "s" : "");
55             new File(path).renameTo(new File(newpath));
56             path = newpath;
57         }
58         public CacheEntry(File f) throws IOException {
59             path = f.getAbsolutePath();
60             headers = new MIME.Headers(new Stream(new FileInputStream(f)), true);
61         }
62     }        
63
64     private FileBasedMailbox(String path) throws MailException, IOException, ClassNotFoundException {
65         new File(this.path = path).mkdirs();
66         new File(path + slash + ".cache").mkdirs();
67         lock = new RandomAccessFile(this.path + slash + ".cache" + slash + "lock", "rw").getChannel().tryLock();
68         if (lock == null) throw new IOException("unable to lock FileBasedMailbox");
69         prevayler = PrevaylerFactory.createPrevayler(new LinkedList<CacheEntry>(), path + slash + ".cache");
70         cache = (LinkedList<CacheEntry>)prevayler.prevalentSystem();
71         for(int i=0; i<cache.size(); i++) uidNext = Math.max(uidNext, cache.get(i).uid()+1);
72         if (cache.size() > 0) return;
73
74         Log.warn(this, "rebuilding cache for " + path);
75         String[] files = new File(path).list(new FilenameFilter() {
76                 public boolean accept(File f, String s) {
77                     return s.substring(1).indexOf('.') != -1 && !s.equals("..");
78                 } });
79         for(int i=0; i<files.length; i++) {
80             CacheEntry ce = new CacheEntry(new File(path + slash + files[i]));
81             prevayler.execute(new Rebuild(ce));
82             uidNext = Math.max(uidNext, ce.uid()+1);
83         }
84         prevayler.takeSnapshot();
85     }
86
87     private static class Rebuild implements Transaction {
88         public final CacheEntry entry;
89         public Rebuild(CacheEntry entry) { this.entry = entry; }
90         public void executeOn(Object c, Date now) { synchronized(c) { ((LinkedList<CacheEntry>)c).add(entry); } }
91     }
92
93     public Mailbox.Iterator  iterator()           { return new Iterator(); }
94     public int               uidValidity()        { return (int)(new File(path).lastModified() & 0xffffffL); }
95     public int               uidNext()            { return uidNext(false); }
96     public int               uidNext(boolean inc) { return inc ? uidNext++ : uidNext; }
97     public synchronized void add(Message message) { add(message, Mailbox.Flag.RECENT); }
98
99     public String[] children() {
100         Vec vec = new Vec();
101         String[] list = new File(path).list();
102         for(int i=0; i<list.length; i++) if ((new File(path + slash + list[i]).isDirectory())) vec.addElement(list[i]);
103         return (String[])vec.copyInto(new String[vec.size()]);
104     }
105
106     public synchronized void add(Message message, int flags) {
107         Log.info(path, message.summary());
108         try {
109             final String name = path + slash + uidNext(true) + "." +
110                 ((flags & Mailbox.Flag.SEEN) == Mailbox.Flag.SEEN ? "s" : "");
111             File target = new File(name);
112             File f = new File(target.getCanonicalPath() + "-");
113             FileOutputStream fo = new FileOutputStream(f);
114             Stream stream = new Stream(fo);
115             if (message.envelope != null) {
116                 stream.println("X-org.ibex.mail.headers.envelope.From: " + message.envelope.from);
117                 stream.println("X-org.ibex.mail.headers.envelope.To: " + message.envelope.to);
118             }
119             message.dump(stream);
120             fo.close();
121             f.renameTo(new File(name));
122             CacheEntry entry = new CacheEntry(new File(name));
123             prevayler.execute(new Rebuild(entry));
124         } catch (IOException e) { throw new MailException.IOException(e); }
125     }
126
127     private class Iterator extends Mailbox.Default.Iterator implements Serializable {
128         int cur = -1;
129         private CacheEntry entry() { synchronized(cache) { return cache.get(cur); } }
130         public MIME.Headers head() { return done() ? null : entry().headers; }
131         public boolean done() { synchronized(cache) { if (cur >= cache.size()) return true; } return false; }
132         public boolean next() { cur++; return !done(); }
133         public boolean seen() { return done() ? false : entry().seen(); }
134         public int num() { return cur+1; }  // EUDORA insists that message numbers start at 1, not 0
135         public int uid() { return entry().uid(); }
136         public Message cur() {
137             if (done()) return null;
138             try {
139                 File file = new File(entry().path);
140                 FileInputStream fis = null;
141                 try {
142                     fis = new FileInputStream(file);
143                     return new Message(new Stream(fis), new Message.Envelope(null, null, new Date(file.lastModified())));
144                 } finally { if (fis != null) fis.close(); }
145             } catch (IOException e) { throw new MailException.IOException(e);
146             } catch (Message.Malformed e) { throw new MailException(e.getMessage()); }
147         }
148         public void seen(final boolean on) {
149             if (done()) return;
150             final CacheEntry entry = entry();
151             prevayler.execute(new Transaction() { public void executeOn(Object c, Date d) { entry.seen(on); } });
152         }
153         public void delete() {
154             if (done()) return;
155             final CacheEntry entry = entry();
156             prevayler.execute(new Transaction() {
157                     public void executeOn(Object c, Date d) {
158                             new File(entry.path).delete();
159                             cache.remove(entry);
160                     }});
161         }
162     }
163 }