001package votorola.a.diff.harvest.cache;
002
003import java.security.NoSuchAlgorithmException;
004import java.sql.PreparedStatement;
005import java.sql.ResultSet;
006import java.sql.SQLException;
007import java.util.logging.Logger;
008
009import javax.mail.internet.AddressException;
010
011import votorola.a.diff.harvest.Message;
012import votorola.a.voter.IDPair;
013import votorola.g.lang.ThreadRestricted;
014import votorola.g.lang.ThreadSafe;
015import votorola.g.lang.Warning;
016import votorola.g.logging.LoggerX;
017import votorola.g.sql.Database;
018
019/**
020 * The relational store for diff messages. This class automatically creates the
021 * necessary table if it does not exist and basically hides away the database
022 * layer. Note: It throws errors on double insertions.
023 */
024@ThreadSafe
025final public class DiffMessageTable {
026
027    /**
028     * This is only exposed as a quick hint for somebody trying to understand
029     * the database structure. This value might change.
030     */
031    @Warning("non-Api")
032    public static final String SCHEMA_NAME = "harvest";
033
034    /**
035     * This is only exposed as a quick hint for somebody trying to understand
036     * the database structure. This value might change.
037     */
038    @Warning("non-Api")
039    public static final String TABLE_NAME = "diff_messages";
040
041    private static final String TABLE = "\"" + SCHEMA_NAME + "\".\""
042            + TABLE_NAME + "\"";
043
044    /**
045     * Maximum rows in the result. This should match the maximum client fetch
046     * length.
047     */
048    @Warning("non-Api")
049    public static final int MAX_RESULTS = 200;
050
051    private final static Logger LOGGER = LoggerX.i(DiffMessageTable.class);
052
053    /**
054     * Main database object.
055     */
056    private final transient Database database;
057
058    /**
059     * Constructs a Table.
060     * 
061     * @param database
062     *            Database to connect to.
063     * @throws SQLException
064     * @throws NoSuchAlgorithmException
065     */
066    DiffMessageTable(final Database database) throws SQLException {
067        this.database = database;
068
069        synchronized (database) {
070            database.ensureSchema(SCHEMA_NAME);
071        }
072
073        statementKeyBase = getClass().getName() + ":" + SCHEMA_NAME + "/"
074                + TABLE_NAME + ".";
075    }
076
077    /**
078     * Statement key base for all queries.
079     */
080    private final transient String statementKeyBase;
081
082    /**
083     * Creates this table in the database.
084     * 
085     * @throws SQLException
086     */
087    void create() throws SQLException {
088        final String key = statementKeyBase + "create";
089        synchronized (database) {
090            PreparedStatement prepStatem = database.statementCache().get(key);
091            if (prepStatem == null) {
092                prepStatem = database
093                        .connection()
094                        .prepareStatement(
095                                "CREATE TABLE "
096                                        + TABLE
097                                        + " (id serial primary key"
098                                        + ", author varchar NOT NULL"
099                                        + ", addressee varchar NOT NULL"
100                                        + ", pollname varchar NOT NULL"
101                                        + ", content varchar NOT NULL"
102                                        + ", a integer NOT NULL"
103                                        + ", ar integer NOT NULL"
104                                        + ", b integer NOT NULL"
105                                        + ", br integer NOT NULL"
106                                        + ", selectand varchar NOT NULL"
107                                        + ", archive_url varchar NOT NULL"
108                                        + ", path varchar NOT NULL"
109                                        + ", sent_ts timestamp with time zone NOT NULL"
110                                        + ", UNIQUE(author,addressee,archive_url,path,sent_ts)"
111                                        + " )");
112
113                database.statementCache().put(key, prepStatem);
114            }
115            prepStatem.execute();
116        }
117    }
118
119    /**
120     * Returns the database in which this table is stored.
121     * 
122     * @return This objects database.
123     */
124    Database getDatabase() {
125        return database;
126    }
127
128    /**
129     * Drops this table from the database.
130     * 
131     * @throws SQLException
132     */
133    void drop() throws SQLException {
134        final String key = statementKeyBase + "drop";
135        synchronized (database) {
136            PreparedStatement prepStatem = database.statementCache().get(key);
137            if (prepStatem == null) {
138                prepStatem = database.connection().prepareStatement(
139                        "DROP TABLE " + TABLE);
140                database.statementCache().put(key, prepStatem);
141            }
142            prepStatem.execute();
143        }
144    }
145
146    /**
147     * Returns true if this table exists in the database; false otherwise.
148     * 
149     * @return whether database already exists or not.
150     * @throws SQLException
151     */
152    public boolean exists() throws SQLException {
153        final String key = statementKeyBase + "exists";
154        synchronized (database) {
155            PreparedStatement prepStatem = database.statementCache().get(key);
156            if (prepStatem == null) {
157                prepStatem = database.connection().prepareStatement(
158                        "SELECT * FROM " + TABLE);
159                prepStatem.setMaxRows(1);
160                database.statementCache().put(key, prepStatem);
161            }
162            try {
163                prepStatem.execute();
164            } catch (SQLException x) {
165                final String sqlState = x.getSQLState();
166                if ("3F000".equals(sqlState)) {
167                    return false; // 3F000 = [missing schema]
168                }
169
170                if ("42P01".equals(sqlState)) {
171                    return false; // 42P01 = UNDEFINED TABLE
172                }
173
174                throw x;
175            }
176        }
177        return true;
178    }
179
180    /**
181     * Query the table for the parameters. Order is: id, author, addressee,
182     * pollname, content, archive_url, path, sent_ts, a, ar, b, br, selectand
183     * 
184     * 
185     * @param poll
186     *            poll-name
187     * @param users
188     *            mailish-username of one or two users
189     * @param id
190     *            only fetch posts newer than id
191     * @return rs ResultSet with newest first.
192     * @throws SQLException
193     * @throws AddressException
194     */
195    @ThreadRestricted("rs holds lock on database")
196    public ResultSet get(final String poll, final String[] users, final int id)
197            throws SQLException, AddressException {
198
199        final StringBuilder whereScope = new StringBuilder();
200        String conj = " WHERE"; // conjunction
201        if (poll != null && !poll.isEmpty()) {
202            whereScope.append(conj).append(" pollname = ?");
203            conj = " AND";
204        }
205        if (users != null && users.length != 0) {
206            for (final String user : users) {
207                if (user.isEmpty()) {
208                    continue;
209                }
210                whereScope.append(conj)
211                        .append(" (addressee = ? OR author = ?)");
212                conj = " AND";
213            }
214        }
215        if (id > 0) {
216            whereScope.append(conj).append(" id > ?");
217            conj = " AND";
218        }
219
220        final String key = statementKeyBase + "getDiffMsgs"
221                + whereScope.toString();
222
223        PreparedStatement prepStatem = database.statementCache().get(key);
224
225        final String query = "SELECT id, author, addressee, pollname"
226                + ", content, archive_url, path, sent_ts" /* message */
227                + ", a, ar, b, br, selectand" /* difference */
228                + " FROM " + TABLE + whereScope.toString()
229                + " ORDER BY sent_ts DESC LIMIT " + MAX_RESULTS;
230        if (prepStatem == null) {
231            prepStatem = database.connection().prepareStatement(query);
232            database.statementCache().put(key, prepStatem);
233        }
234
235        int index = 1;
236        if (poll != null && !poll.isEmpty()) {
237            prepStatem.setString(index++, poll);
238        }
239        if (users != null && users.length != 0) {
240            for (final String user : users) {
241                if (user.isEmpty()) {
242                    continue;
243                }
244                final String email = IDPair.toInternetAddress(user)
245                        .getAddress();
246                prepStatem.setString(index++, email);
247                prepStatem.setString(index++, email);
248            }
249        }
250        if (id != 0) {
251            prepStatem.setInt(index++, id);
252        }
253
254        return prepStatem.executeQuery();
255    }
256
257    /**
258     * Stores a message. This throws an exception on double insert attempts on
259     * the primary key, which consists author,addresse,url and sentDate
260     * 
261     * @throws SQLException
262     */
263    void put(final AuthDiffMessage authMsg) throws SQLException {
264
265        synchronized (database) {
266            final String insertKey = statementKeyBase + "putDiffMsgI";
267            PreparedStatement s = database.statementCache().get(insertKey);
268            if (s == null) {
269                s = database
270                        .connection()
271                        .prepareStatement(
272                                "INSERT INTO "
273                                        + TABLE
274                                        + " (author"
275                                        + ", addressee, archive_url, path, pollname"
276                                        + ", content, a, ar"
277                                        + ", b, br, sent_ts, selectand"
278                                        + ")"
279                                        + " VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
280                database.statementCache().put(insertKey, s);
281            }
282            Message msg = authMsg.diffMessage().message();
283
284            s.setString(1, authMsg.author());
285            s.setString(2, authMsg.addressee());
286            s.setString(3, msg.mc().archiveUrl());
287            s.setString(4, msg.path());
288            s.setString(5, authMsg.diffMessage().draftPair().aCore()
289                    .pollName());
290            s.setString(6, msg.content());
291            {
292                // CWFIX obsolete form, e.g. by storing p.key() instead
293                final votorola.a.diff.DiffKeyParse parse =
294                  authMsg.diffMessage().draftPair().diffKeyParse();
295                final java.util.List<Integer> aPath = parse.aPath();
296                final java.util.List<Integer> bPath = parse.bPath();
297                final int aN = aPath.size();
298                final int bN = bPath.size();
299                if(aN > 2 || bN > 2) {
300                    throw new votorola.g.VotorolaRuntimeException(
301                      "new draft revision path too long for old Harvester" );
302                }
303
304                s.setInt(7, aPath.get(0));
305                s.setInt(8, aN > 1? aPath.get(1): -1);
306                s.setInt(9, bPath.get(0));
307                s.setInt(10, bN > 1? bPath.get(1): -1);
308            }
309            s.setTimestamp(11, new java.sql.Timestamp(msg.mc().sentDate()
310                    .getTime()));
311            s.setString(12, authMsg.selectand());
312            s.executeUpdate();
313
314        }
315    }
316
317    /**
318     * Remove all stored messages for an archive selected by its archive url.
319     * 
320     * @param archiveUrl
321     * @return number of removed messages
322     * @throws SQLException
323     */
324    public int removeArchive(final String archiveUrl) throws SQLException {
325        synchronized (database) {
326            final String removeKey = statementKeyBase + "remDiffMsgI";
327            PreparedStatement s = database.statementCache().get(removeKey);
328            if (s == null) {
329                s = database.connection().prepareStatement(
330                        "DELETE FROM " + TABLE + " where archive_url=?");
331                database.statementCache().put(removeKey, s);
332            }
333            s.setString(1, archiveUrl);
334
335            return s.executeUpdate();
336        }
337    }
338
339}