add SqliteDB.close(ResultSet) and use it
[org.ibex.mail.git] / src / org / ibex / mail / SqliteMailbox.java
1 package org.ibex.mail;
2
3 import org.ibex.util.*;
4 import org.ibex.io.Fountain;
5 import org.ibex.io.Stream;
6 import java.sql.Timestamp;
7 import java.sql.*;
8 import java.net.*;
9 import java.io.*;
10 import java.util.*;
11
12
13 public class SqliteMailbox extends Mailbox.Default implements MailTree {
14
15     public MailTree slash(String name, boolean create) {
16         String p = file.getAbsolutePath();
17         if (p.endsWith(".sqlite")) p = p.substring(0, p.length()-".sqlite".length());
18         return FileBasedMailbox.getFileBasedMailbox(p+"/"+name, create);
19     }
20     public String[]     children() { return new String[0]; }
21     public void         rmdir(String subdir) { throw new RuntimeException("invalid"); }
22     public void         rename(String subdir, MailTree newParent, String newName) { throw new RuntimeException("invalid"); }
23     public Mailbox      getMailbox() { return this; }
24
25
26     private Connection conn;
27     private static final String columns  =
28         "                                        messageid_,       from_,to_,date_,subject_,headers_,body_,flags_";
29     private static final String[] indexedColumns = new String[] {
30         "uid_",
31         "messageid_",
32         "flags_",
33         /*
34         "from_",
35         "to_",
36         "subject_",
37         "date_"
38         */
39     };
40     
41     /**
42      *  from http://www.sqlite.org/autoinc.html
43      *  "If a column has the type INTEGER PRIMARY KEY AUTOINCREMENT
44      *   then a slightly different ROWID selection algorithm is
45      *   used. The ROWID chosen for the new row is one larger than the
46      *   largest ROWID that has ever before existed in that same
47      *   table. If the table has never before contained any data, then
48      *   a ROWID of 1 is used. If the table has previously held a row
49      *   with the largest possible ROWID, then new INSERTs are not
50      *   allowed and any attempt to insert a new row will fail with an
51      *   SQLITE_FULL error.
52      */
53     private static final String columns_ =
54         "uid_ INTEGER PRIMARY KEY AUTOINCREMENT, messageid_,from_,to_,date_,subject_,headers_,body_,flags_";
55
56     private final int uidValidity;
57     private final File file;
58     private final SqliteDB db;
59
60     public  int uidValidity()  { return uidValidity; }
61
62     public String toString() { return file.getName(); }
63     public SqliteMailbox(String filename) throws SQLException {
64         ResultSet rs = null;
65         try {
66             this.file = new File(filename);
67             this.db = new SqliteDB(filename);
68             /*
69             Class.forName("org.sqlite.JDBC");
70             conn = DriverManager.getConnection("jdbc:sqlite:"+filename);
71             */
72             conn = db.getConnection();
73             conn.prepareStatement("create table if not exists uidvalidity (uidvalidity)").executeUpdate();
74             try {
75                 rs = conn.prepareStatement("select uidvalidity from uidvalidity").executeQuery();
76                 if (!rs.next()) {
77                     this.uidValidity = new Random().nextInt();
78                     PreparedStatement ps = conn.prepareStatement("insert into uidvalidity (uidvalidity) values (?)");
79                     ps.setInt(1, uidValidity);
80                     ps.executeUpdate();
81                 } else {
82                     this.uidValidity = rs.getInt(1);
83                 }
84                 conn.prepareStatement("create table if not exists 'mail' ("+columns_+")").executeUpdate();
85                 for(String name : indexedColumns)
86                     conn.prepareStatement("create index if not exists "+name+"index on mail("+name+");").executeUpdate();
87             } finally { db.close(rs); }
88         }
89         catch (SQLException e) { throw new RuntimeException(e); }
90         //catch (ClassNotFoundException e) { throw new RuntimeException(e); }
91     }
92
93     private HashMap<Integer,Integer> imapToUid = new HashMap<Integer,Integer>();
94     private HashMap<Integer,Integer> uidToImap = new HashMap<Integer,Integer>();
95     private boolean imapNumberCacheValid = false;
96     public void updateImapNumberCache() throws SQLException {
97         synchronized(this) {
98             Log.warn(this+"", "rebuilding imapNumberCache...");
99             imapToUid.clear();
100             uidToImap.clear();
101             PreparedStatement q = conn.prepareStatement("select uid_ from mail");
102             ResultSet rs = q.executeQuery();
103             try {
104                 int num = 1;
105                 while(rs.next()) {
106                     imapToUid.put(num, rs.getInt(1));
107                     uidToImap.put(rs.getInt(1), num);
108                     num++;
109                 }
110                 imapNumberCacheValid = true;
111             } finally { db.close(rs); }
112         }
113     }
114     public int queryImapNumberCache(int uid) throws SQLException {
115         synchronized(this) {
116             if (!imapNumberCacheValid) updateImapNumberCache();
117             Integer ret = uidToImap.get(uid);
118             if (ret == null) return -1;
119             return ret;
120         }
121     }
122     public int queryUidForImapNum(int imapNumber) throws SQLException {
123         synchronized(this) {
124             if (!imapNumberCacheValid) updateImapNumberCache();
125             Integer ret = imapToUid.get(imapNumber);
126             if (ret == null) return -1;
127             return ret;
128         }
129     }
130
131
132     public int maxuid() { return uidNext(); }
133     public int uidNext() {
134         try {
135             PreparedStatement q = conn.prepareStatement("select max(uid_) from mail");
136             ResultSet rs = q.executeQuery();
137             //if (!rs.next()) return -1;
138             try {
139                 if (!rs.next()) throw new RuntimeException("select max(uid_) returned no rows!");
140                 return rs.getInt(1)+1;
141             } finally { db.close(rs); }
142         } catch (Exception e) { throw new RuntimeException(e); }
143     }
144
145     public Mailbox.Iterator iterator() {
146         Log.warn(this, "performance warning: called iterator() on entire mailbox");
147         Log.printStackTrace(this, Log.WARN);
148         return new SqliteJdbcIterator();
149     }
150     private String set(int[] set, String arg) {
151         String whereClause = "";
152         boolean needsOr = false;
153         for(int i=0; i<set.length; i+=2) {
154             if (needsOr) whereClause += " or ";
155             whereClause += "(";
156             whereClause += arg+">=" + set[i];
157             whereClause += " and ";
158             while(i+2 < set.length && set[i+2] == (set[i+1]+1)) i += 2;
159             whereClause += arg+"<=" + set[i+1];
160             whereClause += ")";
161             needsOr = true;
162         }
163         return whereClause;
164     }
165     private String getWhereClause(Query q) throws UnsupportedQueryException {
166         String op;
167         switch(q.type) {
168             case Query.NOT:      return "not ("+getWhereClause(q.q[0])+")";
169             case Query.AND:      op = "and";
170             case Query.OR:       op = "or";
171                 {
172                     boolean add = false;
173                     StringBuffer sb = new StringBuffer();
174                     for(int i=0; i<q.q.length; i++) {
175                         if (add) sb.append(" " + op);
176                         sb.append(" (");
177                         sb.append(getWhereClause(q.q[i]));
178                         sb.append(")");
179                         add = true;
180                     }
181                     return sb.toString();
182                 }
183             case Query.ALL:      return "1=1";
184             case Query.UID:      return set(q.set, "uid_");
185             case Query.DELETED:  return "((flags_ & "+(Mailbox.Flag.DELETED)+")!=0)";
186             case Query.SEEN:     return "((flags_ & "+(Mailbox.Flag.SEEN)+")!=0)";
187             case Query.FLAGGED:  return "((flags_ & "+(Mailbox.Flag.FLAGGED)+")!=0)";
188             case Query.DRAFT:    return "((flags_ & "+(Mailbox.Flag.DRAFT)+")!=0)";
189             case Query.ANSWERED: return "((flags_ & "+(Mailbox.Flag.ANSWERED)+")!=0)";
190             case Query.RECENT:   return "((flags_ & "+(Mailbox.Flag.RECENT)+")!=0)";
191                 /*
192                 public static final int SENT       = 5;
193                 public static final int ARRIVAL    = 6;
194                 public static final int HEADER     = 7;
195                 public static final int SIZE       = 8;
196                 public static final int BODY       = 9;
197                 public static final int FULL       = 10;
198                 public static final int IMAPNUM    = 11;
199                 */
200
201             case Query.IMAPNUM: {
202                 try {
203                     // translate queries in terms of imap numbers into queries in terms of uids
204                     // RELIES ON THE FACT THAT UIDS ARE MONOTONICALLY INCREASING
205                     int[] set = new int[q.set==null ? 2 : q.set.length];
206                     if (q.set==null) { set[0] = q.min; set[1] = q.max; }
207                     else System.arraycopy(q.set, 0, set, 0, q.set.length);
208                     for(int i=0; i<set.length; i++) {
209                         int uid = queryUidForImapNum(set[i]);
210                         if (uid==-1) {
211                             Log.info(SqliteMailbox.class, "PROBLEM => resorting to superclass: " + q);
212                             throw new UnsupportedQueryException();
213                         }
214                         set[i] = uid;
215                     }
216                     return getWhereClause(Query.uid(set));
217                 } catch (SQLException e) {
218                     Log.error(this, e);
219                     Log.info(SqliteMailbox.class, "resorting to superclass: " + q);
220                     throw new UnsupportedQueryException();
221                 }
222             }
223
224             default: {
225                 Log.info(SqliteMailbox.class, "resorting to superclass: " + q);
226                 throw new UnsupportedQueryException();
227             }
228         }
229     }
230     private static class UnsupportedQueryException extends Exception { }
231     public Mailbox.Iterator iterator(Query q) {
232         try {
233             String whereClause = getWhereClause(q);
234             Log.info(this, "whereClause = " + whereClause);
235             return new SqliteJdbcIterator("where "+whereClause+";");
236         } catch (UnsupportedQueryException _) {
237             return super.iterator(q);
238         }
239     }
240     public int count(Query q) {
241         try {
242             String whereClause = getWhereClause(q);
243             Log.info(this, "whereClause = " + whereClause);
244             try {
245                 Log.warn("SQL", "select count(*) from mail where " + whereClause);
246                 ResultSet rs = conn.prepareStatement("select count(*) from mail where " + whereClause).executeQuery();
247                 try {
248                     rs.next();
249                     return rs.getInt(1);
250                 } finally { db.close(rs); }
251             } catch (Exception e) { throw new RuntimeException(e); }
252         } catch (UnsupportedQueryException _) {
253             return super.count(q);
254         }
255     }
256     public synchronized void insert(Message m, int flags) {
257         try {
258
259             /*
260             PreparedStatement query = conn.prepareStatement("select headers_,body_,flags_ from 'mail' where messageid_=?");
261             query.setString(1, m.messageid);
262             Log.warn("SQL", "select headers_,body_,flags_ from 'mail' where messageid_="+m.messageid);
263             ResultSet rs2 = query.executeQuery();
264             if (rs2.next()) {
265                 Message m2 = Message.newMessage(Fountain.Util.concat(Fountain.Util.create(rs2.getString(1)),
266                                                                      Fountain.Util.create("\r\n\r\n"),
267                                                                      Fountain.Util.create(rs2.getString(2))));
268                 StringBuffer s1 = new StringBuffer();
269                 m.getBody().getStream().transcribe(s1);
270                 StringBuffer s2 = new StringBuffer();
271                 m2.getBody().getStream().transcribe(s2);
272                 if (!s1.toString().equals(s2.toString())) {
273                     Log.error(this.toString(),
274                               "attempt to insert two messages with identical messageid ("+m.messageid+") but different bodies:\n"+
275                               "  (body length="+s1.length()+") "+m.summary()+"\n"+
276                               "  (body length="+s2.length()+") "+m2.summary()+"\n");
277                 } else {
278                     Log.warn(this.toString(),
279                              "silently dropping duplicate insert() [messageids and bodies match]: " + m.summary());
280                     return;
281                 }
282             }
283             */
284             PreparedStatement add =
285                 conn.prepareStatement("insert "+/*"or replace "+*/"into 'mail' ("+columns+") values (?,?,?,?,?,?,?,?)");
286             add.setString(1, m.messageid+"");
287             add.setString(2, m.from+"");
288             add.setString(3, m.to+"");
289             add.setString(4, m.date+"");
290             add.setString(5, m.subject+"");
291             add.setString(6, SqliteDB.streamToString(m.headers.getStream()));
292             add.setString(7, SqliteDB.streamToString(m.getBody().getStream()));
293             add.setInt   (8, flags);
294             add.executeUpdate();
295
296             // FIXME: be smarter here?
297             imapNumberCacheValid = false;
298         } catch (Exception e) { throw new RuntimeException(e); }
299     }
300
301     private class SqliteJdbcIterator implements Mailbox.Iterator {
302         // could be more efficient in a ton of ways
303         private ResultSet rs;
304         private int count  = 0;
305         private int flags;
306         private Message m  = null;
307         private int uid = -1;
308         private String whereClause;
309         public SqliteJdbcIterator() { this(""); }
310         public SqliteJdbcIterator(String whereClause) {
311             try {
312                 /*
313                 if (whereClause.equals(""))
314                     Log.warn(this, "performance warning: empty whereClause");
315                 */
316                 this.whereClause = whereClause;
317                 Log.warn("SQL", "select messageid_,uid_,flags_ from 'mail' "+whereClause);
318                 PreparedStatement query = conn.prepareStatement("select messageid_,uid_,flags_ from 'mail' "+whereClause);
319                 rs = query.executeQuery();
320             } catch (Exception e) { throw new RuntimeException(e); }
321         }
322         public Headers head()   {
323             if (m != null) return m.headers;
324             try {
325                 PreparedStatement query = conn.prepareStatement("select headers_,flags_ from 'mail' where uid_=?");
326                 query.setString(1, rs.getString(2));
327                 Log.warn("SQL", "select headers_,flags_ from 'mail' where uid_="+rs.getString(2));
328
329                 ResultSet rs2 = query.executeQuery();
330                 if (!rs2.next()) { Log.error("XXX", "should not happen"); return null; }
331                 flags = rs2.getInt(2);
332                 return new Headers(Fountain.Util.create(rs2.getString(1)));
333             } catch (Exception e) { throw new RuntimeException(e); }
334         }
335         public Message cur()    {
336             try {
337                 if (m!=null) return m;
338                 PreparedStatement query = conn.prepareStatement("select headers_,body_,flags_ from 'mail' where uid_=?");
339                 query.setString(1, rs.getString(2));
340                 Log.warn("SQL", "select headers_,body_,flags_ from 'mail' where uid_="+rs.getString(2));
341
342                 ResultSet rs2 = query.executeQuery();
343                 if (!rs2.next()) { Log.error("XXX", "should not happen"); return null; }
344                 m = Message.newMessage(Fountain.Util.concat(Fountain.Util.create(rs2.getString(1)),
345                                                             Fountain.Util.create("\r\n\r\n"),
346                                                             Fountain.Util.create(rs2.getString(2))));
347                 flags = rs2.getInt(3);
348                 return m;
349             } catch (Exception e) { throw new RuntimeException(e); }
350         }
351         public int     getFlags()   {
352             try { return rs.getInt("flags_"); } catch (Exception e) { throw new RuntimeException(e); }
353         }
354         public void    setFlags(int flags) {
355             try {
356                 int oldflags = rs.getInt("flags_");
357                 if (oldflags==flags) return;
358                 Log.info(this, "setflags (old="+oldflags+")" + "update mail set flags_="+(flags)+" where uid_="+uid()+"");
359                 if ((flags & Mailbox.Flag.DELETED) != 0) Log.printStackTrace("deletion", Log.WARN);
360                 PreparedStatement update = conn.prepareStatement("update mail set flags_=? where uid_=?");
361                 update.setInt(1, flags);
362                 update.setInt(2, uid());
363                 update.executeUpdate();
364             } catch (Exception e) { throw new RuntimeException(e); }
365         }
366         public boolean next()       {
367             try { m = null; uid = -1; count++;
368             boolean ret = rs.next();
369             return ret;
370             } catch (Exception e) { throw new RuntimeException(e); } }
371         public int     uid()        {
372             if (uid == -1)
373                 try { uid = rs.getInt("uid_"); } catch (Exception e) { throw new RuntimeException(e); }
374             return uid;
375         }
376         public int     imapNumber() {
377             if ("".equals(whereClause)) return count;
378             try { return queryImapNumberCache(uid()); } catch (SQLException s) { throw new RuntimeException(s); }
379         }
380         public int     nntpNumber() { return uid(); }
381         public void    delete()     {
382             try {
383                 Log.error("sqlite", "actually deleting message "+uid()+" "+head().get("subject"));
384                 Log.printStackTrace("sqlite", Log.ERROR);
385
386                 PreparedStatement update = conn.prepareStatement("delete from mail where uid_=?");
387                 update.setInt(1, uid());
388                 update.executeUpdate();
389
390                 // FIXME: be smarter here?
391                 imapNumberCacheValid = false;
392
393             } catch (Exception e) { throw new RuntimeException(e); }
394         }
395     }
396
397
398 }